From 1d4ab1339a8b49a6ac406c66bb697fe17c6726b5 Mon Sep 17 00:00:00 2001 From: Yvan Brunel <41630728+YBTopaz8@users.noreply.github.com> Date: Thu, 19 Dec 2024 12:27:40 -0500 Subject: [PATCH] feat: Upgrade target framework from NET Standard 2.0 to .NET 6.0 (#393) BREAKING CHANGE: This release requires .NET 6.0 or later and removes compatibility with NET Standard 2.0; Xamarin developers should migrate to .NET MAUI to use this version of the Parse SDK; Unity developers should use the previous SDK version until Unity supports .NET. --- .github/workflows/ci.yml | 13 +- Parse.Tests/ACLTests.cs | 162 +- Parse.Tests/AnalyticsControllerTests.cs | 266 ++- Parse.Tests/AnalyticsTests.cs | 177 +- Parse.Tests/CloudControllerTests.cs | 169 +- Parse.Tests/CloudTests.cs | 120 +- Parse.Tests/CommandTests.cs | 291 +-- Parse.Tests/ConfigTests.cs | 76 +- Parse.Tests/ConversionTests.cs | 38 +- Parse.Tests/CurrentUserControllerTests.cs | 332 +-- Parse.Tests/DecoderTests.cs | 343 ++-- Parse.Tests/EncoderTests.cs | 389 ++-- Parse.Tests/FileControllerTests.cs | 152 +- Parse.Tests/FileStateTests.cs | 55 +- Parse.Tests/FileTests.cs | 102 +- Parse.Tests/GeoPointTests.cs | 199 +- Parse.Tests/InstallationIdControllerTests.cs | 178 +- Parse.Tests/InstallationTests.cs | 299 ++- Parse.Tests/JsonTests.cs | 542 ++--- Parse.Tests/LateInitializerTests.cs | 63 +- Parse.Tests/MoqExtensions.cs | 27 +- Parse.Tests/ObjectCoderTests.cs | 45 +- Parse.Tests/ObjectControllerTests.cs | 607 ++---- Parse.Tests/ObjectStateTests.cs | 269 ++- Parse.Tests/ObjectTests.cs | 994 +++++---- Parse.Tests/Parse.Tests.csproj | 19 +- Parse.Tests/ProgressTests.cs | 95 +- Parse.Tests/PushEncoderTests.cs | 69 +- Parse.Tests/PushStateTests.cs | 49 +- Parse.Tests/PushTests.cs | 217 +- Parse.Tests/RelationTests.cs | 29 +- Parse.Tests/SessionControllerTests.cs | 226 +- Parse.Tests/SessionTests.cs | 260 +-- Parse.Tests/UserControllerTests.cs | 324 +-- Parse.Tests/UserTests.cs | 918 ++------- Parse.sln | 14 +- .../Control/IParseFieldOperation.cs | 79 +- .../Infrastructure/CustomServiceHub.cs | 55 +- .../Infrastructure/Data/IParseDataDecoder.cs | 23 +- .../Execution/IParseCommandRunner.cs | 25 +- .../Infrastructure/Execution/IWebClient.cs | 27 +- .../Infrastructure/ICacheController.cs | 69 +- .../Infrastructure/ICustomServiceHub.cs | 9 +- .../Abstractions/Infrastructure/IDataCache.cs | 43 +- .../Infrastructure/IDataTransferLevel.cs | 9 +- .../IDiskFileCacheController.cs | 35 +- .../Infrastructure/IEnvironmentData.cs | 35 +- .../Infrastructure/IHostManifestData.cs | 41 +- .../Infrastructure/IJsonConvertible.cs | 19 +- .../Infrastructure/IMetadataController.cs | 27 +- .../Infrastructure/IMutableServiceHub.cs | 53 +- .../IRelativeCacheLocationGenerator.cs | 17 +- .../Infrastructure/IServerConnectionData.cs | 47 +- .../Infrastructure/IServiceHub.cs | 81 +- .../Infrastructure/IServiceHubCloner.cs | 9 +- .../Infrastructure/IServiceHubComposer.cs | 13 +- .../Infrastructure/IServiceHubMutator.cs | 33 +- .../Analytics/IParseAnalyticsController.cs | 43 +- .../IParseAuthenticationProvider.cs | 57 +- .../Cloud/IParseCloudCodeController.cs | 17 +- .../IParseConfigurationController.cs | 23 +- .../IParseCurrentConfigurationController.cs | 49 +- .../Platform/Files/IParseFileController.cs | 9 +- .../IParseCurrentInstallationController.cs | 7 +- .../Installations/IParseInstallationCoder.cs | 16 +- .../IParseInstallationController.cs | 35 +- .../IParseInstallationDataFinalizer.cs | 29 +- .../Platform/Objects/IObjectState.cs | 24 +- .../Objects/IParseObjectClassController.cs | 25 +- .../Objects/IParseObjectController.cs | 17 +- .../Objects/IParseObjectCurrentController.cs | 81 +- .../Push/IParsePushChannelsController.cs | 11 +- .../Platform/Push/IParsePushController.cs | 9 +- .../Abstractions/Platform/Push/IPushState.cs | 23 +- .../Platform/Queries/IParseQueryController.cs | 13 +- .../Sessions/IParseSessionController.cs | 15 +- .../Users/IParseCurrentUserController.cs | 11 +- .../Platform/Users/IParseUserController.cs | 20 +- .../AbsoluteCacheLocationMutator.cs | 45 +- Parse/Infrastructure/CacheController.cs | 515 +++-- .../ConcurrentUserServiceHubCloner.cs | 22 +- .../Control/ParseAddOperation.cs | 117 +- .../Control/ParseAddUniqueOperation.cs | 144 +- .../Control/ParseDeleteOperation.cs | 42 +- .../Control/ParseFieldOperations.cs | 58 +- .../Control/ParseIncrementOperation.cs | 212 +- .../Control/ParseRelationOperation.cs | 121 +- .../Control/ParseRemoveOperation.cs | 55 +- .../Control/ParseSetOperation.cs | 52 +- Parse/Infrastructure/Data/NoObjectsEncoder.cs | 22 +- Parse/Infrastructure/Data/ParseDataDecoder.cs | 90 +- Parse/Infrastructure/Data/ParseDataEncoder.cs | 260 ++- Parse/Infrastructure/Data/ParseObjectCoder.cs | 161 +- .../Data/PointerOrLocalIdEncoder.cs | 39 +- Parse/Infrastructure/DataTransferLevel.cs | 17 +- Parse/Infrastructure/EnvironmentData.cs | 49 +- .../Infrastructure/Execution/ParseCommand.cs | 78 +- .../Execution/ParseCommandRunner.cs | 269 ++- .../Execution/UniversalWebClient.cs | 245 ++- Parse/Infrastructure/Execution/WebRequest.cs | 37 +- Parse/Infrastructure/HostManifestData.cs | 108 +- ...fierBasedRelativeCacheLocationGenerator.cs | 70 +- .../LateInitializedMutableServiceHub.cs | 267 ++- ...dataBasedRelativeCacheLocationGenerator.cs | 50 +- Parse/Infrastructure/MetadataController.cs | 23 +- Parse/Infrastructure/MetadataMutator.cs | 30 +- Parse/Infrastructure/MutableServiceHub.cs | 119 +- .../Infrastructure/OrchestrationServiceHub.cs | 57 +- .../Infrastructure/ParseClassNameAttribute.cs | 29 +- Parse/Infrastructure/ParseFailureException.cs | 517 ++--- .../Infrastructure/ParseFieldNameAttribute.cs | 31 +- .../RelativeCacheLocationMutator.cs | 38 +- Parse/Infrastructure/ServerConnectionData.cs | 71 +- Parse/Infrastructure/ServiceHub.cs | 66 +- .../TransientCacheController.cs | 63 +- .../Utilities/AssemblyLister.cs | 55 +- Parse/Infrastructure/Utilities/Conversion.cs | 176 +- .../Infrastructure/Utilities/FileUtilities.cs | 49 +- .../Utilities/FlexibleDictionaryWrapper.cs | 129 +- .../Utilities/FlexibleListWrapper.cs | 96 +- .../Utilities/IdentityEqualityComparer.cs | 23 +- .../Utilities/InternalExtensions.cs | 174 +- .../Infrastructure/Utilities/JsonUtilities.cs | 747 ++++--- .../Utilities/LateInitializer.cs | 105 +- Parse/Infrastructure/Utilities/LockSet.cs | 43 +- .../Utilities/ReflectionUtilities.cs | 69 +- .../Utilities/SynchronizedEventHandler.cs | 87 +- Parse/Infrastructure/Utilities/TaskQueue.cs | 101 +- .../Utilities/ThreadingUtilities.cs | 27 +- .../Utilities/XamarinAttributes.cs | 743 ++++--- Parse/Parse.csproj | 24 +- .../Analytics/ParseAnalyticsController.cs | 89 +- .../Cloud/ParseCloudCodeController.cs | 85 +- .../Configuration/ParseConfiguration.cs | 144 +- .../ParseConfigurationController.cs | 63 +- .../ParseCurrentConfigurationController.cs | 78 +- Parse/Platform/Files/FileState.cs | 52 +- Parse/Platform/Files/ParseFile.cs | 248 ++- Parse/Platform/Files/ParseFileController.cs | 85 +- .../ParseCurrentInstallationController.cs | 166 +- .../Installations/ParseInstallation.cs | 596 +++--- .../Installations/ParseInstallationCoder.cs | 40 +- .../ParseInstallationController.cs | 90 +- .../ParseInstallationDataFinalizer.cs | 18 +- Parse/Platform/Location/ParseGeoDistance.cs | 104 +- Parse/Platform/Location/ParseGeoPoint.cs | 153 +- Parse/Platform/Objects/MutableObjectState.cs | 178 +- Parse/Platform/Objects/ParseObject.cs | 1819 +++++++++-------- Parse/Platform/Objects/ParseObjectClass.cs | 55 +- .../Objects/ParseObjectClassController.cs | 212 +- .../Platform/Objects/ParseObjectController.cs | 226 +- Parse/Platform/ParseClient.cs | 230 ++- Parse/Platform/Push/MutablePushState.cs | 95 +- Parse/Platform/Push/ParsePush.cs | 336 +-- .../Push/ParsePushChannelsController.cs | 36 +- Parse/Platform/Push/ParsePushController.cs | 31 +- Parse/Platform/Push/ParsePushEncoder.cs | 63 +- .../Push/ParsePushNotificationEvent.cs | 53 +- Parse/Platform/Queries/ParseQuery.cs | 1363 ++++++------ .../Platform/Queries/ParseQueryController.cs | 92 +- Parse/Platform/Relations/ParseRelation.cs | 154 +- Parse/Platform/Roles/ParseRole.cs | 137 +- Parse/Platform/Security/ParseACL.cs | 531 +++-- Parse/Platform/Sessions/ParseSession.cs | 32 +- .../Sessions/ParseSessionController.cs | 51 +- .../Users/ParseCurrentUserController.cs | 172 +- Parse/Platform/Users/ParseUser.cs | 443 ++-- Parse/Platform/Users/ParseUserController.cs | 126 +- .../PublishProfiles/FolderProfile.pubxml | 19 + Parse/Resources.Designer.cs | 2 +- Parse/Utilities/AnalyticsServiceExtensions.cs | 166 +- Parse/Utilities/CloudCodeServiceExtensions.cs | 81 +- .../ConfigurationServiceExtensions.cs | 59 +- .../InstallationServiceExtensions.cs | 65 +- Parse/Utilities/ObjectServiceExtensions.cs | 979 +++++---- Parse/Utilities/ParseExtensions.cs | 79 +- Parse/Utilities/ParseFileExtensions.cs | 28 +- Parse/Utilities/ParseQueryExtensions.cs | 1041 +++++----- Parse/Utilities/ParseRelationExtensions.cs | 38 +- Parse/Utilities/ParseUserExtensions.cs | 43 +- Parse/Utilities/PushServiceExtensions.cs | 401 ++-- Parse/Utilities/QueryServiceExtensions.cs | 91 +- Parse/Utilities/RoleServiceExtensions.cs | 14 +- Parse/Utilities/SessionsServiceExtensions.cs | 82 +- Parse/Utilities/UserServiceExtensions.cs | 447 ++-- README.md | 16 + 186 files changed, 15511 insertions(+), 12834 deletions(-) create mode 100644 Parse/Properties/PublishProfiles/FolderProfile.pubxml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 829e364b..636bb2c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,12 @@ on: paths-ignore: - '**/**.md' jobs: - check-ci: + check-dotnet: + strategy: + matrix: + DOTNET_VERSION: ['6.0', '7.0', '8.0', '9.0'] + fail-fast: false + name: .NET ${{ matrix.DOTNET_VERSION }} runs-on: windows-latest steps: - name: Checkout repository @@ -16,9 +21,9 @@ jobs: - name: Set up .NET SDK uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.x' + dotnet-version: ${{ matrix.DOTNET_VERSION }} - name: Cache NuGet packages - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: | ~/.nuget/packages @@ -36,7 +41,7 @@ jobs: run: dotnet build Parse.sln --configuration Debug --no-restore - name: Run tests with coverage run: | - OpenCover.Console.exe -target:dotnet.exe -targetargs:"test --configuration Debug --test-adapter-path:. --logger:console /p:DebugType=full .\Parse.Tests\Parse.Tests.csproj" -filter:"+[Parse*]* -[Parse.Tests*]*" -oldstyle -output:parse_sdk_dotnet_coverage.xml -register:user + OpenCover.Console.exe -target:dotnet.exe -targetargs:"test --framework net${{ matrix.DOTNET_VERSION }} --configuration Debug --test-adapter-path:. --logger:console /p:DebugType=full .\Parse.Tests\Parse.Tests.csproj" -filter:"+[Parse*]* -[Parse.Tests*]*" -oldstyle -output:parse_sdk_dotnet_coverage.xml -register:user - name: Upload code coverage uses: codecov/codecov-action@v4 with: diff --git a/Parse.Tests/ACLTests.cs b/Parse.Tests/ACLTests.cs index b6e01fec..e6bcda16 100644 --- a/Parse.Tests/ACLTests.cs +++ b/Parse.Tests/ACLTests.cs @@ -1,58 +1,130 @@ using System; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; // Add Moq for mocking if not already added using Parse.Infrastructure; using Parse.Platform.Objects; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Objects; -namespace Parse.Tests +namespace Parse.Tests; + +[TestClass] +public class ACLTests { - [TestClass] - public class ACLTests + ParseClient Client { get; set; } + + Mock ServiceHubMock { get; set; } + Mock ClassControllerMock { get; set; } + + [TestInitialize] + public void Initialize() { - ParseClient Client { get; set; } = new ParseClient(new ServerConnectionData { Test = true }); + // Mock ServiceHub + ServiceHubMock = new Mock(); + ClassControllerMock = new Mock(); - [TestInitialize] - public void Initialize() - { - Client.AddValidClass(); - Client.AddValidClass(); - } + // Mock ClassController behavior + ServiceHubMock.Setup(hub => hub.ClassController).Returns(ClassControllerMock.Object); - [TestCleanup] - public void Clean() => (Client.Services as ServiceHub).Reset(); + // Mock ClassController.Instantiate behavior + ClassControllerMock.Setup(controller => controller.Instantiate(It.IsAny(), It.IsAny())) + .Returns((className, hub) => + { + var user = new ParseUser(); + user.Bind(hub); // Ensure the object is bound to the service hub + return user; + }); - [TestMethod] - public void TestCheckPermissionsWithParseUserConstructor() + // Set up ParseClient with the mocked ServiceHub + Client = new ParseClient(new ServerConnectionData { Test = true }) { - ParseUser owner = GenerateUser("OwnerUser"); - ParseUser user = GenerateUser("OtherUser"); - ParseACL acl = new ParseACL(owner); - Assert.IsTrue(acl.GetReadAccess(owner.ObjectId)); - Assert.IsTrue(acl.GetWriteAccess(owner.ObjectId)); - Assert.IsTrue(acl.GetReadAccess(owner)); - Assert.IsTrue(acl.GetWriteAccess(owner)); - } - - [TestMethod] - public void TestReadWriteMutationWithParseUserConstructor() - { - ParseUser owner = GenerateUser("OwnerUser"); - ParseUser otherUser = GenerateUser("OtherUser"); - ParseACL acl = new ParseACL(owner); - acl.SetReadAccess(otherUser, true); - acl.SetWriteAccess(otherUser, true); - acl.SetReadAccess(owner.ObjectId, false); - acl.SetWriteAccess(owner.ObjectId, false); - Assert.IsTrue(acl.GetReadAccess(otherUser.ObjectId)); - Assert.IsTrue(acl.GetWriteAccess(otherUser.ObjectId)); - Assert.IsTrue(acl.GetReadAccess(otherUser)); - Assert.IsTrue(acl.GetWriteAccess(otherUser)); - Assert.IsFalse(acl.GetReadAccess(owner)); - Assert.IsFalse(acl.GetWriteAccess(owner)); - } - - [TestMethod] - public void TestParseACLCreationWithNullObjectIdParseUser() => Assert.ThrowsException(() => new ParseACL(GenerateUser(default))); - - ParseUser GenerateUser(string objectID) => Client.GenerateObjectFromState(new MutableObjectState { ObjectId = objectID }, "_User"); + Services = ServiceHubMock.Object + }; + + // Publicize the client to set ParseClient.Instance + Client.Publicize(); + + // Add valid classes to the client + Client.AddValidClass(); + Client.AddValidClass(); + } + + [TestCleanup] + public void Clean() => (Client.Services as ServiceHub)?.Reset(); + + [TestMethod] + public void TestCheckPermissionsWithParseUserConstructor() + { + // Arrange + ParseUser owner = GenerateUser("OwnerUser"); + ParseUser user = GenerateUser("OtherUser"); + + // Act + ParseACL acl = new ParseACL(owner); + + // Assert + Assert.IsTrue(acl.GetReadAccess(owner.ObjectId)); + Assert.IsTrue(acl.GetWriteAccess(owner.ObjectId)); + Assert.IsTrue(acl.GetReadAccess(owner)); + Assert.IsTrue(acl.GetWriteAccess(owner)); + } + + [TestMethod] + public void TestReadWriteMutationWithParseUserConstructor() + { + // Arrange + ParseUser owner = GenerateUser("OwnerUser"); + ParseUser otherUser = GenerateUser("OtherUser"); + + // Act + ParseACL acl = new ParseACL(owner); + acl.SetReadAccess(otherUser, true); + acl.SetWriteAccess(otherUser, true); + acl.SetReadAccess(owner.ObjectId, false); + acl.SetWriteAccess(owner.ObjectId, false); + + // Assert + Assert.IsTrue(acl.GetReadAccess(otherUser.ObjectId)); + Assert.IsTrue(acl.GetWriteAccess(otherUser.ObjectId)); + Assert.IsTrue(acl.GetReadAccess(otherUser)); + Assert.IsTrue(acl.GetWriteAccess(otherUser)); + Assert.IsFalse(acl.GetReadAccess(owner)); + Assert.IsFalse(acl.GetWriteAccess(owner)); + } + + [TestMethod] + public void TestParseACLCreationWithNullObjectIdParseUser() + { + // Assert + Assert.ThrowsException(() => new ParseACL(GenerateUser(default))); + } + + ParseUser GenerateUser(string objectID) + { + // Use the mock to simulate generating a ParseUser + var state = new MutableObjectState { ObjectId = objectID }; + return Client.GenerateObjectFromState(state, "_User"); } + + [TestMethod] + public void TestGenerateObjectFromState() + { + // Arrange + var state = new MutableObjectState { ObjectId = "123", ClassName = null }; + var defaultClassName = "_User"; + + var serviceHubMock = new Mock(); + var classControllerMock = new Mock(); + + classControllerMock.Setup(controller => controller.Instantiate(It.IsAny(), It.IsAny())) + .Returns((className, hub) => new ParseUser()); + + // Act + var user = classControllerMock.Object.GenerateObjectFromState(state, defaultClassName, serviceHubMock.Object); + + // Assert + Assert.IsNotNull(user); + Assert.AreEqual(defaultClassName, user.ClassName); + } + } diff --git a/Parse.Tests/AnalyticsControllerTests.cs b/Parse.Tests/AnalyticsControllerTests.cs index 07a63b77..0b5db975 100644 --- a/Parse.Tests/AnalyticsControllerTests.cs +++ b/Parse.Tests/AnalyticsControllerTests.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Net; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -11,83 +10,240 @@ using Parse.Abstractions.Infrastructure; using Parse.Platform.Analytics; using Parse.Infrastructure.Execution; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json; +using System.IO; -namespace Parse.Tests +namespace Parse.Tests; + +[TestClass] +public class AnalyticsControllerTests { - [TestClass] - public class AnalyticsControllerTests + ParseClient Client { get; set; } + + [TestInitialize] + public void SetUp() => Client = new ParseClient(new ServerConnectionData { ApplicationID = "", Key = "", Test = true }); + + [TestMethod] + public void TestTrackEventWithEmptyDimensions() { - ParseClient Client { get; set; } + // Arrange: Mock the Parse command runner to return an accepted status with an empty dictionary + var mockRunner = CreateMockRunner( + new Tuple>(HttpStatusCode.Accepted, new Dictionary()) + ); - [TestInitialize] - public void SetUp() => Client = new ParseClient(new ServerConnectionData { ApplicationID = "", Key = "", Test = true }); + var analyticsController = new ParseAnalyticsController(mockRunner.Object); - [TestMethod] - [AsyncStateMachine(typeof(AnalyticsControllerTests))] - public Task TestTrackEventWithEmptyDimensions() - { - Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary { })); + // Act: Call TrackEventAsync with empty dimensions + var result = analyticsController.TrackEventAsync( + "SomeEvent", + dimensions: null, + sessionToken: null, + serviceHub: Client, + cancellationToken: CancellationToken.None + ); - return new ParseAnalyticsController(mockRunner.Object).TrackEventAsync("SomeEvent", dimensions: default, sessionToken: default, serviceHub: Client, cancellationToken: CancellationToken.None).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); + // Assert: Verify the task was successful + Assert.IsNotNull(result); + Assert.IsInstanceOfType(result, typeof(Task)); // If the method has a result type, adjust accordingly. + mockRunner.Verify( + obj => obj.RunCommandAsync( + It.Is(command => command.Path == "events/SomeEvent"), + It.IsAny>(), + It.IsAny>(), + It.IsAny() + ), + Times.Exactly(1) + ); + } - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "events/SomeEvent"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); - }); - } + [TestMethod] + public async Task TestTrackEventWithNonEmptyDimensions() + { + // Arrange: Create a mock runner that simulates a response with accepted status + var mockRunner = CreateMockRunner( + new Tuple>(HttpStatusCode.Accepted, new Dictionary()) + ); - [TestMethod] - [AsyncStateMachine(typeof(AnalyticsControllerTests))] - public Task TestTrackEventWithNonEmptyDimensions() + var analyticsController = new ParseAnalyticsController(mockRunner.Object); + var dimensions = new Dictionary { - Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary { })); + ["njwerjk12"] = "5523dd" + }; - return new ParseAnalyticsController(mockRunner.Object).TrackEventAsync("SomeEvent", dimensions: new Dictionary { ["njwerjk12"] = "5523dd" }, sessionToken: default, serviceHub: Client, cancellationToken: CancellationToken.None).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); + // Act: Call TrackEventAsync with non-empty dimensions + await analyticsController.TrackEventAsync( + "SomeEvent", + dimensions: dimensions, + sessionToken: null, + serviceHub: Client, + cancellationToken: CancellationToken.None + ); + + // Assert: Verify the command was sent with the correct path and content + mockRunner.Verify( + obj => obj.RunCommandAsync( + It.Is(command => + command.Path.Contains("events/SomeEvent") && + ValidateDimensions(command.Data, dimensions) + ), + It.IsAny>(), + It.IsAny>(), + It.IsAny() + ), + Times.Exactly(1) + ); + } - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path.Contains("events/SomeEvent")), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); - }); - } - [TestMethod] - [AsyncStateMachine(typeof(AnalyticsControllerTests))] - public Task TestTrackAppOpenedWithEmptyPushHash() + /// + /// Validates that the dimensions dictionary is correctly serialized into the command's Data stream. + /// + private static bool ValidateDimensions(Stream dataStream, IDictionary expectedDimensions) + { + if (dataStream == null) { - Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary { })); + return false; + } - return new ParseAnalyticsController(mockRunner.Object).TrackAppOpenedAsync(default, sessionToken: default, serviceHub: Client, cancellationToken: CancellationToken.None).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); + // Read and deserialize the stream content + dataStream.Position = 0; // Reset the stream position + using var reader = new StreamReader(dataStream); + var content = reader.ReadToEnd(); - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "events/AppOpened"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); - }); - } + // Parse the JSON content + var parsedData = JsonConvert.DeserializeObject>(content); - [TestMethod] - [AsyncStateMachine(typeof(AnalyticsControllerTests))] - public Task TestTrackAppOpenedWithNonEmptyPushHash() + // Ensure dimensions are present and correct + if (parsedData.TryGetValue("dimensions", out var dimensionsObj) && + dimensionsObj is JObject dimensionsJson) { - Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary())); + var dimensions = dimensionsJson.ToObject>(); + if (dimensions == null) + { + return false; + } - return new ParseAnalyticsController(mockRunner.Object).TrackAppOpenedAsync("32j4hll12lkk", sessionToken: default, serviceHub: Client, cancellationToken: CancellationToken.None).ContinueWith(task => + foreach (var pair in expectedDimensions) { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - - mockRunner.Verify(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); - }); + if (!dimensions.TryGetValue(pair.Key, out var value) || value != pair.Value) + { + return false; // Mismatch found + } + } + + // Ensure no extra dimensions are present + return dimensions.Count == expectedDimensions.Count; } - Mock CreateMockRunner(Tuple> response) + return false; + } + + + + [TestMethod] + public async Task TestTrackAppOpenedWithEmptyPushHash() + { + // Arrange: Mock the ParseCommandRunner to simulate an accepted response + var mockRunner = CreateMockRunner( + new Tuple>(HttpStatusCode.Accepted, new Dictionary()) + ); + + var analyticsController = new ParseAnalyticsController(mockRunner.Object); + + // Act: Call TrackAppOpenedAsync with a null push hash + await analyticsController.TrackAppOpenedAsync( + pushHash: null, + sessionToken: null, + serviceHub: Client, + cancellationToken: CancellationToken.None + ); + + // Assert: Verify that the appropriate command was sent + mockRunner.Verify( + obj => obj.RunCommandAsync( + It.Is(command => command.Path == "events/AppOpened"), + It.IsAny>(), + It.IsAny>(), + It.IsAny() + ), + Times.Exactly(1) + ); + } + + [TestMethod] + public async Task TestTrackAppOpenedWithNonEmptyPushHash() + { + // Arrange: Mock the ParseCommandRunner to simulate an accepted response + var mockRunner = CreateMockRunner( + new Tuple>(HttpStatusCode.Accepted, new Dictionary()) + ); + + var analyticsController = new ParseAnalyticsController(mockRunner.Object); + + // Act: Call TrackAppOpenedAsync with a non-empty push hash + await analyticsController.TrackAppOpenedAsync( + pushHash: "32j4hll12lkk", + sessionToken: null, + serviceHub: Client, + cancellationToken: CancellationToken.None + ); + + // Assert: Verify that the command was sent exactly once + mockRunner.Verify( + obj => obj.RunCommandAsync( + It.Is(command => ValidateCommand(command, "32j4hll12lkk")), + It.IsAny>(), + It.IsAny>(), + It.IsAny() + ), + Times.Exactly(1) + ); + } + + /// + /// Validates the ParseCommand for the given push hash. + /// + private bool ValidateCommand(ParseCommand command, string expectedPushHash) + { + if (command.Path != "events/AppOpened") { - Mock mockRunner = new Mock(); - mockRunner.Setup(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(response)); + return false; + } - return mockRunner; + var dataStream = command.Data; + if (dataStream == null) + { + return false; } + + // Read and deserialize the stream + dataStream.Position = 0; + using var reader = new StreamReader(dataStream); + var jsonContent = reader.ReadToEnd(); + var dataDictionary = JsonConvert.DeserializeObject>(jsonContent); + + // Validate the push_hash + return dataDictionary != null && + dataDictionary.ContainsKey("push_hash") && + dataDictionary["push_hash"].ToString() == expectedPushHash; } + + + Mock CreateMockRunner(Tuple> response) + { + var mockRunner = new Mock(); + + // Setup the mock to return a Task with the expected Tuple + mockRunner.Setup(obj => obj.RunCommandAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>(), + It.IsAny() + )) + .Returns(Task.FromResult(response)); // Return the tuple as part of the Task + + return mockRunner; + } + } diff --git a/Parse.Tests/AnalyticsTests.cs b/Parse.Tests/AnalyticsTests.cs index c5efee08..43eae776 100644 --- a/Parse.Tests/AnalyticsTests.cs +++ b/Parse.Tests/AnalyticsTests.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -9,85 +8,113 @@ using Parse.Abstractions.Platform.Analytics; using Parse.Abstractions.Platform.Users; -namespace Parse.Tests +namespace Parse.Tests; + +[TestClass] +public class AnalyticsTests { - [TestClass] - public class AnalyticsTests - { #warning Skipped post-test-evaluation cleaning method may be needed. - // [TestCleanup] - // public void TearDown() => (Client.Services as ServiceHub).Reset(); - - [TestMethod] - [AsyncStateMachine(typeof(AnalyticsTests))] - public Task TestTrackEvent() - { - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); - - Mock mockController = new Mock { }; - Mock mockCurrentUserController = new Mock { }; - - mockCurrentUserController.Setup(controller => controller.GetCurrentSessionTokenAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult("sessionToken")); - - hub.AnalyticsController = mockController.Object; - hub.CurrentUserController = mockCurrentUserController.Object; - - return client.TrackAnalyticsEventAsync("SomeEvent").ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - - mockController.Verify(obj => obj.TrackEventAsync(It.Is(eventName => eventName == "SomeEvent"), It.Is>(dict => dict == null), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(AnalyticsTests))] - public Task TestTrackEventWithDimension() - { - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); - - Mock mockController = new Mock { }; - Mock mockCurrentUserController = new Mock { }; - - mockCurrentUserController.Setup(controller => controller.GetCurrentSessionTokenAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult("sessionToken")); + // [TestCleanup] + // public void TearDown() => (Client.Services as ServiceHub).Reset(); - hub.AnalyticsController = mockController.Object; - hub.CurrentUserController = mockCurrentUserController.Object; - - return client.TrackAnalyticsEventAsync("SomeEvent", new Dictionary { ["facebook"] = "hq" }).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - mockController.Verify(obj => obj.TrackEventAsync(It.Is(eventName => eventName == "SomeEvent"), It.Is>(dict => dict != null && dict.Count == 1), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(AnalyticsTests))] - public Task TestTrackAppOpened() - { - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); - - Mock mockController = new Mock { }; - Mock mockCurrentUserController = new Mock { }; - - mockCurrentUserController.Setup(controller => controller.GetCurrentSessionTokenAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult("sessionToken")); - - hub.AnalyticsController = mockController.Object; - hub.CurrentUserController = mockCurrentUserController.Object; + [TestMethod] + public async Task TestTrackEvent() + { + // Arrange + var hub = new MutableServiceHub(); + var client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + var mockController = new Mock(); + var mockCurrentUserController = new Mock(); + + mockCurrentUserController + .Setup(controller => controller.GetCurrentSessionTokenAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync("sessionToken"); + + hub.AnalyticsController = mockController.Object; + hub.CurrentUserController = mockCurrentUserController.Object; + + // Act + await client.TrackAnalyticsEventAsync("SomeEvent"); + + // Assert + mockController.Verify( + obj => obj.TrackEventAsync( + It.Is(eventName => eventName == "SomeEvent"), + It.Is>(dict => dict == null), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Exactly(1) + ); + } - return client.TrackLaunchAsync().ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); + [TestMethod] + public async Task TestTrackEventWithDimension() + { + // Arrange + var hub = new MutableServiceHub(); + var client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + var mockController = new Mock(); + var mockCurrentUserController = new Mock(); + + mockCurrentUserController + .Setup(controller => controller.GetCurrentSessionTokenAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync("sessionToken"); + + hub.AnalyticsController = mockController.Object; + hub.CurrentUserController = mockCurrentUserController.Object; + + var dimensions = new Dictionary { ["facebook"] = "hq" }; + + // Act + await client.TrackAnalyticsEventAsync("SomeEvent", dimensions); + + // Assert + mockController.Verify( + obj => obj.TrackEventAsync( + It.Is(eventName => eventName == "SomeEvent"), + It.Is>(dict => dict != null && dict.Count == 1 && dict["facebook"] == "hq"), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Exactly(1) + ); + } - mockController.Verify(obj => obj.TrackAppOpenedAsync(It.Is(pushHash => pushHash == null), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); - }); - } + [TestMethod] + public async Task TestTrackAppOpened() + { + // Arrange + var hub = new MutableServiceHub(); + var client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + var mockController = new Mock(); + var mockCurrentUserController = new Mock(); + + mockCurrentUserController + .Setup(controller => controller.GetCurrentSessionTokenAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync("sessionToken"); + + hub.AnalyticsController = mockController.Object; + hub.CurrentUserController = mockCurrentUserController.Object; + + // Act + await client.TrackLaunchAsync(); + + // Assert + mockController.Verify( + obj => obj.TrackAppOpenedAsync( + It.Is(pushHash => pushHash == null), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Exactly(1) + ); } } diff --git a/Parse.Tests/CloudControllerTests.cs b/Parse.Tests/CloudControllerTests.cs index 81de1986..f25da4ac 100644 --- a/Parse.Tests/CloudControllerTests.cs +++ b/Parse.Tests/CloudControllerTests.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Net; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -12,67 +11,143 @@ using Parse.Infrastructure.Execution; using Parse.Platform.Cloud; -namespace Parse.Tests -{ +namespace Parse.Tests; + #warning Class refactoring requires completion. - [TestClass] - public class CloudControllerTests +[TestClass] +public class CloudControllerTests +{ + ParseClient Client { get; set; } + + [TestInitialize] + public void SetUp() => Client = new ParseClient(new ServerConnectionData { ApplicationID = "", Key = "", Test = true }); + + [TestMethod] + public async Task TestEmptyCallFunction() { - ParseClient Client { get; set; } + // Arrange: Create a mock runner that simulates a response with an accepted status but no data + var mockRunner = CreateMockRunner( + new Tuple>(HttpStatusCode.Accepted, null) + ); - [TestInitialize] - public void SetUp() => Client = new ParseClient(new ServerConnectionData { ApplicationID = "", Key = "", Test = true }); + var controller = new ParseCloudCodeController(mockRunner.Object, Client.Decoder); - [TestMethod] - [AsyncStateMachine(typeof(CloudControllerTests))] - public Task TestEmptyCallFunction() => new ParseCloudCodeController(CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, default)).Object, Client.Decoder).CallFunctionAsync("someFunction", default, default, Client, CancellationToken.None).ContinueWith(task => + // Act & Assert: Call the function and verify the task faults as expected + try { - Assert.IsTrue(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - }); - - [TestMethod] - [AsyncStateMachine(typeof(CloudControllerTests))] - public Task TestCallFunction() + await controller.CallFunctionAsync("someFunction", null, null, Client, CancellationToken.None); + Assert.Fail("Expected the task to fault, but it succeeded."); + } + catch (ParseFailureException ex) { - Dictionary responseDict = new Dictionary { ["result"] = "gogo" }; - Tuple> response = new Tuple>(HttpStatusCode.Accepted, responseDict); - Mock mockRunner = CreateMockRunner(response); + Assert.AreEqual(ParseFailureException.ErrorCode.OtherCause, ex.Code); + Assert.AreEqual("Cloud function returned no data.", ex.Message); + } + + } + + + [TestMethod] + public async Task TestCallFunction() + { + // Arrange: Create a mock runner with a predefined response + var responseDict = new Dictionary { ["result"] = "gogo" }; + var response = new Tuple>(HttpStatusCode.Accepted, responseDict); + var mockRunner = CreateMockRunner(response); + + var cloudCodeController = new ParseCloudCodeController(mockRunner.Object, Client.Decoder); + + // Act: Call the function and capture the result + var result = await cloudCodeController.CallFunctionAsync( + "someFunction", + parameters: null, + sessionToken: null, + serviceHub: Client, + cancellationToken: CancellationToken.None + ); + + // Assert: Verify the result is as expected + Assert.IsNotNull(result); + Assert.AreEqual("gogo", result); // Ensure the result matches the mock response + } + - return new ParseCloudCodeController(mockRunner.Object, Client.Decoder).CallFunctionAsync("someFunction", default, default, Client, CancellationToken.None).ContinueWith(task => + [TestMethod] + public async Task TestCallFunctionWithComplexType() + { + // Arrange: Create a mock runner with a complex type response + var complexResponse = new Dictionary + { + { "result", new Dictionary { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - Assert.AreEqual("gogo", task.Result); - }); + { "fosco", "ben" }, + { "list", new List { 1, 2, 3 } } + } } + }; + var mockRunner = CreateMockRunner( + new Tuple>(HttpStatusCode.Accepted, complexResponse) + ); - [TestMethod] - [AsyncStateMachine(typeof(CloudControllerTests))] - public Task TestCallFunctionWithComplexType() => new ParseCloudCodeController(CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary { { "result", new Dictionary { { "fosco", "ben" }, { "list", new List { 1, 2, 3 } } } } })).Object, Client.Decoder).CallFunctionAsync>("someFunction", default, default, Client, CancellationToken.None).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - Assert.IsInstanceOfType(task.Result, typeof(IDictionary)); - Assert.AreEqual("ben", task.Result["fosco"]); - Assert.IsInstanceOfType(task.Result["list"], typeof(IList)); - }); + var cloudCodeController = new ParseCloudCodeController(mockRunner.Object, Client.Decoder); + + // Act: Call the function with a complex return type + var result = await cloudCodeController.CallFunctionAsync>( + "someFunction", + parameters: null, + sessionToken: null, + serviceHub: Client, + cancellationToken: CancellationToken.None + ); + + // Assert: Validate the returned complex type + Assert.IsNotNull(result); + Assert.IsInstanceOfType(result, typeof(IDictionary)); + Assert.AreEqual("ben", result["fosco"]); + Assert.IsInstanceOfType(result["list"], typeof(IList)); + } + [TestMethod] + public async Task TestCallFunctionWithWrongType() + { + // a mock runner with a response that doesn't match the expected type + var wrongTypeResponse = new Dictionary + { + { "result", "gogo" } + }; + var mockRunner = CreateMockRunner( + new Tuple>(HttpStatusCode.Accepted, wrongTypeResponse) + ); - [TestMethod] - [AsyncStateMachine(typeof(CloudControllerTests))] - public Task TestCallFunctionWithWrongType() => new ParseCloudCodeController(CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary() { { "result", "gogo" } })).Object, Client.Decoder).CallFunctionAsync("someFunction", default, default, Client, CancellationToken.None).ContinueWith(task => + var cloudCodeController = new ParseCloudCodeController(mockRunner.Object, Client.Decoder); + + // Act & Assert: Expect the call to fail with a ParseFailureException || This is fun! + + await Assert.ThrowsExceptionAsync(async () => { - Assert.IsTrue(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); + await cloudCodeController.CallFunctionAsync( + "someFunction", + parameters: null, + sessionToken: null, + serviceHub: Client, + cancellationToken: CancellationToken.None + ); }); + } - private Mock CreateMockRunner(Tuple> response) - { - Mock mockRunner = new Mock { }; - mockRunner.Setup(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(response)); - return mockRunner; - } + + private Mock CreateMockRunner(Tuple> response) + { + var mockRunner = new Mock(); + mockRunner.Setup(obj => obj.RunCommandAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>(), + It.IsAny() + )).Returns(Task.FromResult(response)); + + return mockRunner; } + } diff --git a/Parse.Tests/CloudTests.cs b/Parse.Tests/CloudTests.cs index dd1a6878..601a3fca 100644 --- a/Parse.Tests/CloudTests.cs +++ b/Parse.Tests/CloudTests.cs @@ -1,45 +1,109 @@ +using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Data; +using Parse.Abstractions.Infrastructure.Execution; using Parse.Abstractions.Platform.Cloud; using Parse.Abstractions.Platform.Users; using Parse.Infrastructure; +using Parse.Infrastructure.Execution; +using Parse.Platform.Cloud; -namespace Parse.Tests +namespace Parse.Tests; + +[TestClass] +public class CloudTests { - [TestClass] - public class CloudTests - { #warning Skipped post-test-evaluation cleaning method may be needed. - // [TestCleanup] - // public void TearDown() => ParseCorePlugins.Instance.Reset(); + // [TestCleanup] + // public void TearDown() => ParseCorePlugins.Instance.Reset(); + [TestMethod] + public async Task TestCloudFunctionsMissingResultAsync() + { + // Arrange + var commandRunnerMock = new Mock(); + var decoderMock = new Mock(); + + // Mock CommandRunner + commandRunnerMock + .Setup(runner => runner.RunCommandAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>(), + It.IsAny() + )) + .ReturnsAsync(new Tuple>( + System.Net.HttpStatusCode.OK, + new Dictionary + { + ["unexpectedKey"] = "unexpectedValue" // Missing "result" key + })); + + // Mock Decoder + decoderMock + .Setup(decoder => decoder.Decode(It.IsAny(), It.IsAny())) + .Returns(new Dictionary { ["unexpectedKey"] = "unexpectedValue" }); - [TestMethod] - [AsyncStateMachine(typeof(CloudTests))] - public Task TestCloudFunctions() + // Set up service hub + var hub = new MutableServiceHub { - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); - - Mock mockController = new Mock(); - mockController.Setup(obj => obj.CallFunctionAsync>(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult>(new Dictionary { ["fosco"] = "ben", ["list"] = new List { 1, 2, 3 } })); - - hub.CloudCodeController = mockController.Object; - hub.CurrentUserController = new Mock { }.Object; - - return client.CallCloudCodeFunctionAsync>("someFunction", null, CancellationToken.None).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - Assert.IsInstanceOfType(task.Result, typeof(IDictionary)); - Assert.AreEqual("ben", task.Result["fosco"]); - Assert.IsInstanceOfType(task.Result["list"], typeof(IList)); - }); - } + CommandRunner = commandRunnerMock.Object, + CloudCodeController = new ParseCloudCodeController(commandRunnerMock.Object, decoderMock.Object) + }; + + var client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + // Act & Assert + await Assert.ThrowsExceptionAsync(async () => + await client.CallCloudCodeFunctionAsync>("someFunction", null, CancellationToken.None)); } + + [TestMethod] + public async Task TestParseCloudCodeControllerMissingResult() + { + // Arrange + var commandRunnerMock = new Mock(); + var decoderMock = new Mock(); + + // Mock the CommandRunner response + commandRunnerMock + .Setup(runner => runner.RunCommandAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>(), + It.IsAny() + )) + .ReturnsAsync(new Tuple>( + System.Net.HttpStatusCode.OK, // Simulated HTTP status code + new Dictionary + { + ["unexpectedKey"] = "unexpectedValue" // Missing "result" key + })); + + // Mock the Decoder response + decoderMock + .Setup(decoder => decoder.Decode(It.IsAny(), It.IsAny())) + .Returns(new Dictionary { ["unexpectedKey"] = "unexpectedValue" }); + + // Initialize the controller + var controller = new ParseCloudCodeController(commandRunnerMock.Object, decoderMock.Object); + + // Act & Assert + await Assert.ThrowsExceptionAsync(async () => + await controller.CallFunctionAsync>( + "testFunction", + null, + null, + null, + CancellationToken.None)); + } + + + } diff --git a/Parse.Tests/CommandTests.cs b/Parse.Tests/CommandTests.cs index 8091b75c..64bdc5fe 100644 --- a/Parse.Tests/CommandTests.cs +++ b/Parse.Tests/CommandTests.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -13,131 +12,191 @@ using Parse.Abstractions.Platform.Users; using Parse.Infrastructure.Execution; using Parse.Abstractions.Infrastructure.Execution; +using WebRequest = Parse.Infrastructure.Execution.WebRequest; -namespace Parse.Tests +namespace Parse.Tests; + +[TestClass] +public class CommandTests { -#warning Initialization and cleaning steps may be redundant for each test method. It may be possible to simply reset the required services before each run. -#warning Class refactoring requires completion. + private ParseClient Client { get; set; } + + [TestInitialize] + public void Initialize() => Client = new ParseClient(new ServerConnectionData { ApplicationID = "", Key = "", Test = true }); + + [TestCleanup] + public void Clean() => (Client.Services as ServiceHub).Reset(); - [TestClass] - public class CommandTests + [TestMethod] + public void TestMakeCommand() { - ParseClient Client { get; set; } + var command = new ParseCommand("endpoint", method: "GET", sessionToken: "abcd", headers: default, data: default); - [TestInitialize] - public void Initialize() => Client = new ParseClient(new ServerConnectionData { ApplicationID = "", Key = "", Test = true }); + Assert.AreEqual("endpoint", command.Path); + Assert.AreEqual("GET", command.Method); + Assert.IsTrue(command.Headers.Any(pair => pair.Key == "X-Parse-Session-Token" && pair.Value == "abcd")); + } - [TestCleanup] - public void Clean() => (Client.Services as ServiceHub).Reset(); + [TestMethod] + public async Task TestRunCommandAsync() + { + // Arrange + var mockHttpClient = new Mock(); + var mockInstallationController = new Mock(); + + mockHttpClient + .Setup(obj => obj.ExecuteAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(new Tuple(HttpStatusCode.OK, "{}")); + + mockInstallationController + .Setup(installation => installation.GetAsync()) + .ReturnsAsync(default(Guid?)); + + var commandRunner = new ParseCommandRunner( + mockHttpClient.Object, + mockInstallationController.Object, + Client.MetadataController, + Client.ServerConnectionData, + new Lazy(() => Client.UserController) + ); + + // Act + var result = await commandRunner.RunCommandAsync(new ParseCommand("endpoint", method: "GET", data: null)); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(HttpStatusCode.OK, result.Item1); + Assert.IsInstanceOfType(result.Item2, typeof(IDictionary)); + Assert.AreEqual(0, result.Item2.Count); + } - [TestMethod] - public void TestMakeCommand() - { - ParseCommand command = new ParseCommand("endpoint", method: "GET", sessionToken: "abcd", headers: default, data: default); + [TestMethod] + public async Task TestRunCommandWithArrayResultAsync() + { + // Arrange + var mockHttpClient = new Mock(); + var mockInstallationController = new Mock(); + + mockHttpClient + .Setup(obj => obj.ExecuteAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())) + .ReturnsAsync(new Tuple(HttpStatusCode.OK, "[]")); + + mockInstallationController + .Setup(installation => installation.GetAsync()) + .ReturnsAsync(default(Guid?)); + + var commandRunner = new ParseCommandRunner( + mockHttpClient.Object, + mockInstallationController.Object, + Client.MetadataController, + Client.ServerConnectionData, + new Lazy(() => Client.UserController) + ); + + // Act + var result = await commandRunner.RunCommandAsync(new ParseCommand("endpoint", method: "GET", data: null)); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(HttpStatusCode.OK, result.Item1); + Assert.IsTrue(result.Item2.ContainsKey("results")); + Assert.IsInstanceOfType(result.Item2["results"], typeof(IList)); + } - Assert.AreEqual("endpoint", command.Path); - Assert.AreEqual("GET", command.Method); - Assert.IsTrue(command.Headers.Any(pair => pair.Key == "X-Parse-Session-Token" && pair.Value == "abcd")); - } + [TestMethod] + public async Task TestRunCommandWithInvalidStringAsync() + { + // Arrange: Mock an invalid response + var mockHttpClient = new Mock(); + var mockInstallationController = new Mock(); + + mockHttpClient + .Setup(obj => obj.ExecuteAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())) + .ReturnsAsync(new Tuple(HttpStatusCode.OK, "invalid")); // Mock an invalid response + + mockInstallationController + .Setup(controller => controller.GetAsync()) + .ReturnsAsync(default(Guid?)); + + var commandRunner = new ParseCommandRunner( + mockHttpClient.Object, + mockInstallationController.Object, + Client.MetadataController, + Client.ServerConnectionData, + new Lazy(() => Client.UserController) + ); + + // Act: Run the command + var result = await commandRunner.RunCommandAsync(new ParseCommand("endpoint", method: "GET", data: null)); + + // Assert: Check for BadRequest and appropriate error message + Assert.AreEqual(HttpStatusCode.BadRequest, result.Item1); // Response status should indicate BadRequest + Assert.IsNotNull(result.Item2); // Content should not be null + Assert.IsTrue(result.Item2.ContainsKey("error")); // Ensure the error key is present + Assert.AreEqual("Invalid or alternatively-formatted response received from server.", result.Item2["error"]); // Verify error message + } - [TestMethod] - [AsyncStateMachine(typeof(CommandTests))] - public Task TestRunCommand() - { - Mock mockHttpClient = new Mock(); - Mock mockInstallationController = new Mock(); - Task> fakeResponse = Task.FromResult(new Tuple(HttpStatusCode.OK, "{}")); - mockHttpClient.Setup(obj => obj.ExecuteAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(fakeResponse); - - mockInstallationController.Setup(installation => installation.GetAsync()).Returns(Task.FromResult(default)); - - return new ParseCommandRunner(mockHttpClient.Object, mockInstallationController.Object, Client.MetadataController, Client.ServerConnectionData, new Lazy(() => Client.UserController)).RunCommandAsync(new ParseCommand("endpoint", method: "GET", data: default)).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - Assert.IsInstanceOfType(task.Result.Item2, typeof(IDictionary)); - Assert.AreEqual(0, task.Result.Item2.Count); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(CommandTests))] - public Task TestRunCommandWithArrayResult() - { - Mock mockHttpClient = new Mock(); - Mock mockInstallationController = new Mock(); - mockHttpClient.Setup(obj => obj.ExecuteAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(new Tuple(HttpStatusCode.OK, "[]"))); - - mockInstallationController.Setup(installation => installation.GetAsync()).Returns(Task.FromResult(default)); - - return new ParseCommandRunner(mockHttpClient.Object, mockInstallationController.Object, Client.MetadataController, Client.ServerConnectionData, new Lazy(() => Client.UserController)).RunCommandAsync(new ParseCommand("endpoint", method: "GET", data: default)).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - Assert.IsInstanceOfType(task.Result.Item2, typeof(IDictionary)); - Assert.AreEqual(1, task.Result.Item2.Count); - Assert.IsTrue(task.Result.Item2.ContainsKey("results")); - Assert.IsInstanceOfType(task.Result.Item2["results"], typeof(IList)); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(CommandTests))] - public Task TestRunCommandWithInvalidString() - { - Mock mockHttpClient = new Mock(); - Mock mockInstallationController = new Mock(); - mockHttpClient.Setup(obj => obj.ExecuteAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(new Tuple(HttpStatusCode.OK, "invalid"))); - - mockInstallationController.Setup(controller => controller.GetAsync()).Returns(Task.FromResult(default)); - - return new ParseCommandRunner(mockHttpClient.Object, mockInstallationController.Object, Client.MetadataController, Client.ServerConnectionData, new Lazy(() => Client.UserController)).RunCommandAsync(new ParseCommand("endpoint", method: "GET", data: default)).ContinueWith(task => - { - Assert.IsTrue(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - Assert.IsInstanceOfType(task.Exception.InnerException, typeof(ParseFailureException)); - Assert.AreEqual(ParseFailureException.ErrorCode.OtherCause, (task.Exception.InnerException as ParseFailureException).Code); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(CommandTests))] - public Task TestRunCommandWithErrorCode() + [TestMethod] + public async Task TestRunCommandWithErrorCodeAsync() + { + // Arrange + var mockHttpClient = new Mock(); + var mockInstallationController = new Mock(); + + mockHttpClient + .Setup(obj => obj.ExecuteAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())) + .ReturnsAsync(new Tuple(HttpStatusCode.NotFound, "{ \"code\": 101, \"error\": \"Object not found.\" }")); + + mockInstallationController + .Setup(controller => controller.GetAsync()) + .ReturnsAsync(default(Guid?)); + + var commandRunner = new ParseCommandRunner( + mockHttpClient.Object, + mockInstallationController.Object, + Client.MetadataController, + Client.ServerConnectionData, + new Lazy(() => Client.UserController) + ); + + // Act & Assert + await Assert.ThrowsExceptionAsync(async () => { - Mock mockHttpClient = new Mock(); - Mock mockInstallationController = new Mock(); - mockHttpClient.Setup(obj => obj.ExecuteAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(new Tuple(HttpStatusCode.NotFound, "{ \"code\": 101, \"error\": \"Object not found.\" }"))); - - mockInstallationController.Setup(controller => controller.GetAsync()).Returns(Task.FromResult(default)); - - return new ParseCommandRunner(mockHttpClient.Object, mockInstallationController.Object, Client.MetadataController, Client.ServerConnectionData, new Lazy(() => Client.UserController)).RunCommandAsync(new ParseCommand("endpoint", method: "GET", data: default)).ContinueWith(task => - { - Assert.IsTrue(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - Assert.IsInstanceOfType(task.Exception.InnerException, typeof(ParseFailureException)); - ParseFailureException parseException = task.Exception.InnerException as ParseFailureException; - Assert.AreEqual(ParseFailureException.ErrorCode.ObjectNotFound, parseException.Code); - Assert.AreEqual("Object not found.", parseException.Message); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(CommandTests))] - public Task TestRunCommandWithInternalServerError() + await commandRunner.RunCommandAsync(new ParseCommand("endpoint", method: "GET", data: null)); + }); + } + + [TestMethod] + public async Task TestRunCommandWithInternalServerErrorAsync() + { + // Arrange + var mockHttpClient = new Mock(); + var mockInstallationController = new Mock(); + + mockHttpClient + .Setup(client => client.ExecuteAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())) + .ReturnsAsync(new Tuple(HttpStatusCode.InternalServerError, null)); + + mockInstallationController + .Setup(controller => controller.GetAsync()) + .ReturnsAsync(default(Guid?)); + + var commandRunner = new ParseCommandRunner( + mockHttpClient.Object, + mockInstallationController.Object, + Client.MetadataController, + Client.ServerConnectionData, + new Lazy(() => Client.UserController) + ); + + // Act & Assert + await Assert.ThrowsExceptionAsync(async () => { - Mock mockHttpClient = new Mock(); - Mock mockInstallationController = new Mock(); - - mockHttpClient.Setup(client => client.ExecuteAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(new Tuple(HttpStatusCode.InternalServerError, default))); - mockInstallationController.Setup(installationController => installationController.GetAsync()).Returns(Task.FromResult(default)); - - return new ParseCommandRunner(mockHttpClient.Object, mockInstallationController.Object, Client.MetadataController, Client.ServerConnectionData, new Lazy(() => Client.UserController)).RunCommandAsync(new ParseCommand("endpoint", method: "GET", data: default)).ContinueWith(task => - { - Assert.IsTrue(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - Assert.IsInstanceOfType(task.Exception.InnerException, typeof(ParseFailureException)); - Assert.AreEqual(ParseFailureException.ErrorCode.InternalServerError, (task.Exception.InnerException as ParseFailureException).Code); - }); - } + await commandRunner.RunCommandAsync(new ParseCommand("endpoint", method: "GET", data: null)); + }); } } diff --git a/Parse.Tests/ConfigTests.cs b/Parse.Tests/ConfigTests.cs index a6c45f3c..37a98c69 100644 --- a/Parse.Tests/ConfigTests.cs +++ b/Parse.Tests/ConfigTests.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -16,70 +15,91 @@ namespace Parse.Tests [TestClass] public class ConfigTests { - ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }, new MutableServiceHub { }); + private ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }, new MutableServiceHub { }); - IParseConfigurationController MockedConfigController + private IParseConfigurationController MockedConfigController { get { - Mock mockedConfigController = new Mock(); - Mock mockedCurrentConfigController = new Mock(); + var mockedConfigController = new Mock(); + var mockedCurrentConfigController = new Mock(); - ParseConfiguration theConfig = Client.BuildConfiguration(new Dictionary { ["params"] = new Dictionary { ["testKey"] = "testValue" } }); + var theConfig = Client.BuildConfiguration(new Dictionary + { + ["params"] = new Dictionary { ["testKey"] = "testValue" } + }); - mockedCurrentConfigController.Setup(obj => obj.GetCurrentConfigAsync(Client)).Returns(Task.FromResult(theConfig)); + mockedCurrentConfigController + .Setup(obj => obj.GetCurrentConfigAsync(Client)) + .ReturnsAsync(theConfig); - mockedConfigController.Setup(obj => obj.CurrentConfigurationController).Returns(mockedCurrentConfigController.Object); + mockedConfigController + .Setup(obj => obj.CurrentConfigurationController) + .Returns(mockedCurrentConfigController.Object); - TaskCompletionSource tcs = new TaskCompletionSource(); - tcs.TrySetCanceled(); + mockedConfigController + .Setup(obj => obj.FetchConfigAsync(It.IsAny(), It.IsAny(), It.Is(ct => ct.IsCancellationRequested))) + .Returns(Task.FromCanceled(new CancellationToken(true))); - mockedConfigController.Setup(obj => obj.FetchConfigAsync(It.IsAny(), It.IsAny(), It.Is(ct => ct.IsCancellationRequested))).Returns(tcs.Task); - - mockedConfigController.Setup(obj => obj.FetchConfigAsync(It.IsAny(), It.IsAny(), It.Is(ct => !ct.IsCancellationRequested))).Returns(Task.FromResult(theConfig)); + mockedConfigController + .Setup(obj => obj.FetchConfigAsync(It.IsAny(), It.IsAny(), It.Is(ct => !ct.IsCancellationRequested))) + .ReturnsAsync(theConfig); return mockedConfigController.Object; } } [TestInitialize] - public void SetUp() => (Client.Services as OrchestrationServiceHub).Custom = new MutableServiceHub { ConfigurationController = MockedConfigController, CurrentUserController = new Mock().Object }; + public void SetUp() => + (Client.Services as OrchestrationServiceHub).Custom = new MutableServiceHub + { + ConfigurationController = MockedConfigController, + CurrentUserController = Mock.Of() + }; [TestCleanup] public void TearDown() => ((Client.Services as OrchestrationServiceHub).Default as ServiceHub).Reset(); [TestMethod] - public void TestCurrentConfig() + public async void TestCurrentConfig() { - ParseConfiguration config = Client.GetCurrentConfiguration(); + var config = await Client.GetCurrentConfiguration(); Assert.AreEqual("testValue", config["testKey"]); Assert.AreEqual("testValue", config.Get("testKey")); } [TestMethod] - public void TestToJSON() + public async void TestToJSON() { - IDictionary expectedJson = new Dictionary { { "params", new Dictionary { { "testKey", "testValue" } } } }; - Assert.AreEqual(JsonConvert.SerializeObject((Client.GetCurrentConfiguration() as IJsonConvertible).ConvertToJSON()), JsonConvert.SerializeObject(expectedJson)); + var expectedJson = new Dictionary + { + ["params"] = new Dictionary { ["testKey"] = "testValue" } + }; + + var actualJson = (await Client.GetCurrentConfiguration() as IJsonConvertible).ConvertToJSON(); + Assert.AreEqual(JsonConvert.SerializeObject(expectedJson), JsonConvert.SerializeObject(actualJson)); } [TestMethod] - [AsyncStateMachine(typeof(ConfigTests))] - public Task TestGetConfig() => Client.GetConfigurationAsync().ContinueWith(task => + public async Task TestGetConfigAsync() { - Assert.AreEqual("testValue", task.Result["testKey"]); - Assert.AreEqual("testValue", task.Result.Get("testKey")); - }); + var config = await Client.GetConfigurationAsync(); + + Assert.AreEqual("testValue", config["testKey"]); + Assert.AreEqual("testValue", config.Get("testKey")); + } [TestMethod] - [AsyncStateMachine(typeof(ConfigTests))] - public Task TestGetConfigCancel() + public async Task TestGetConfigCancelAsync() { - CancellationTokenSource tokenSource = new CancellationTokenSource { }; + var tokenSource = new CancellationTokenSource(); tokenSource.Cancel(); - return Client.GetConfigurationAsync(tokenSource.Token).ContinueWith(task => Assert.IsTrue(task.IsCanceled)); + await Assert.ThrowsExceptionAsync(async () => + { + await Client.GetConfigurationAsync(tokenSource.Token); + }); } } } diff --git a/Parse.Tests/ConversionTests.cs b/Parse.Tests/ConversionTests.cs index 2bd27fdc..3bbff3f7 100644 --- a/Parse.Tests/ConversionTests.cs +++ b/Parse.Tests/ConversionTests.cs @@ -12,26 +12,36 @@ struct DummyValueTypeA { } struct DummyValueTypeB { } [TestMethod] - public void TestToWithConstructedNullablePrimitive() => Assert.IsTrue(Conversion.To((double) 4) is int?); + public void TestToWithConstructedNullablePrimitive() + { + // Test conversion of double to nullable int + var result = Conversion.To((double) 4); + Assert.IsInstanceOfType(result, typeof(int?)); + Assert.AreEqual(4, result); + } [TestMethod] - public void TestToWithConstructedNullableNonPrimitive() => Assert.ThrowsException(() => Conversion.To(new DummyValueTypeB { })); - - + public void TestToWithConstructedNullableNonPrimitive() + { + // Test invalid conversion between two nullable value types + Assert.ThrowsException(() => + { + Conversion.To(new DummyValueTypeB()); + }); + } [TestMethod] public void TestConvertToFloatUsingNonInvariantNumberFormat() { - try - { - float inputValue = 1234.56f; - string jsonEncoded = JsonUtilities.Encode(inputValue); - float convertedValue = (float) Conversion.ConvertTo(jsonEncoded); - Assert.IsTrue(inputValue == convertedValue); - } - catch (Exception ex) - { throw ex; } - } + // Arrange + float inputValue = 1234.56f; + // Act + string jsonEncoded = JsonUtilities.Encode(inputValue); + float convertedValue = (float)Conversion.ConvertTo(jsonEncoded); + + // Assert + Assert.AreEqual(inputValue, convertedValue, "Converted value does not match the input value."); + } } } diff --git a/Parse.Tests/CurrentUserControllerTests.cs b/Parse.Tests/CurrentUserControllerTests.cs index 5de620d8..c6cbfce1 100644 --- a/Parse.Tests/CurrentUserControllerTests.cs +++ b/Parse.Tests/CurrentUserControllerTests.cs @@ -1,201 +1,243 @@ using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Parse.Infrastructure; using Parse.Abstractions.Infrastructure; -using Parse.Infrastructure.Utilities; -using Parse.Platform.Objects; using Parse.Platform.Users; +using Parse.Abstractions.Platform.Objects; +using Parse.Abstractions.Infrastructure.Data; -namespace Parse.Tests +namespace Parse.Tests; + +[TestClass] +public class CurrentUserControllerTests { - [TestClass] - public class CurrentUserControllerTests + private ParseClient Client; + + public CurrentUserControllerTests() { - ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); + // Mock the decoder + var mockDecoder = new Mock(); - [TestInitialize] - public void SetUp() => Client.AddValidClass(); + // Mock the class controller + var mockClassController = new Mock(); - [TestCleanup] - public void TearDown() => (Client.Services as ServiceHub).Reset(); + // Ensure that the base implementation of Instantiate is called + mockClassController.Setup(controller => controller.Instantiate(It.IsAny(), It.IsAny())) + .CallBase(); - [TestMethod] - public void TestConstructor() => Assert.IsNull(new ParseCurrentUserController(new Mock { }.Object, Client.ClassController, Client.Decoder).CurrentUser); + // Mock the service hub + var mockServiceHub = new Mock(); + mockServiceHub.SetupGet(hub => hub.Decoder).Returns(mockDecoder.Object); + mockServiceHub.SetupGet(hub => hub.ClassController).Returns(mockClassController.Object); - [TestMethod] - [AsyncStateMachine(typeof(CurrentUserControllerTests))] - public Task TestGetSetAsync() - { -#warning This method may need a fully custom ParseClient setup. + // Initialize ParseClient with the mocked ServiceHub + Client = new ParseClient(new ServerConnectionData { Test = true }, mockServiceHub.Object); - Mock storageController = new Mock(MockBehavior.Strict); - Mock> mockedStorage = new Mock>(); + // Call Publicize() to make the client instance accessible globally + Client.Publicize(); // This makes ParseClient.Instance point to this instance - ParseCurrentUserController controller = new ParseCurrentUserController(storageController.Object, Client.ClassController, Client.Decoder); + // Ensure the ParseUser class is valid for this client instance + Client.AddValidClass(); + } - ParseUser user = new ParseUser { }.Bind(Client) as ParseUser; + [TestCleanup] + public void TearDown() + { + if (Client.Services is ServiceHub serviceHub) + { + serviceHub.Reset(); + } + } - storageController.Setup(storage => storage.LoadAsync()).Returns(Task.FromResult(mockedStorage.Object)); - return controller.SetAsync(user, CancellationToken.None).OnSuccess(_ => - { - Assert.AreEqual(user, controller.CurrentUser); - object jsonObject = null; -#pragma warning disable IDE0039 // Use local function - Predicate predicate = o => - { - jsonObject = o; - return true; - }; -#pragma warning restore IDE0039 // Use local function + [TestMethod] + public void TestConstructor() + { + // Mock the IParseObjectClassController + var mockClassController = new Mock(); + + // Create the controller with the mock classController + var controller = new ParseCurrentUserController( + new Mock().Object, + mockClassController.Object, + Client.Decoder + ); + + // Now the test should pass as the classController is mocked + Assert.IsNull(controller.CurrentUser); + } - mockedStorage.Verify(storage => storage.AddAsync("CurrentUser", Match.Create(predicate))); - mockedStorage.Setup(storage => storage.TryGetValue("CurrentUser", out jsonObject)).Returns(true); + [TestMethod] + public async Task TestGetSetAsync() + { + // Mock setup for storage + var storageController = new Mock(MockBehavior.Strict); + var mockedStorage = new Mock>(); - return controller.GetAsync(Client, CancellationToken.None); - }).Unwrap().OnSuccess(task => - { - Assert.AreEqual(user, controller.CurrentUser); + storageController + .Setup(storage => storage.LoadAsync()) + .ReturnsAsync(mockedStorage.Object); - controller.ClearFromMemory(); - Assert.AreNotEqual(user, controller.CurrentUser); + object capturedSerializedData = null; - return controller.GetAsync(Client, CancellationToken.None); - }).Unwrap().OnSuccess(task => + mockedStorage + .Setup(storage => storage.AddAsync(It.IsAny(), It.IsAny())) + .Callback((key, value) => + { + if (key == "CurrentUser" && value is string serialized) + { + // Capture the serialized data + capturedSerializedData = serialized; + } + }) + .Returns(Task.CompletedTask); + + // Mock RemoveAsync + mockedStorage + .Setup(storage => storage.RemoveAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + // Mock TryGetValue to return capturedSerializedData + mockedStorage + .Setup(storage => storage.TryGetValue("CurrentUser", out capturedSerializedData)) + .Returns((string key, out object value) => { - Assert.AreNotSame(user, controller.CurrentUser); - Assert.IsNotNull(controller.CurrentUser); + value = capturedSerializedData; // Assign the captured serialized data to the out parameter + return value != null; }); - } - [TestMethod] - [AsyncStateMachine(typeof(CurrentUserControllerTests))] - public Task TestExistsAsync() - { - Mock storageController = new Mock(); - Mock> mockedStorage = new Mock>(); - ParseCurrentUserController controller = new ParseCurrentUserController(storageController.Object, Client.ClassController, Client.Decoder); - ParseUser user = new ParseUser { }.Bind(Client) as ParseUser; + // Mock ClassController behavior + var classControllerMock = new Mock(); + classControllerMock.Setup(controller => controller.Instantiate(It.IsAny(), It.IsAny())) + .Returns((className, serviceHub) => new ParseUser { ObjectId = "testObjectId" }); - storageController.Setup(c => c.LoadAsync()).Returns(Task.FromResult(mockedStorage.Object)); + var controller = new ParseCurrentUserController(storageController.Object, classControllerMock.Object, Client.Decoder); - bool contains = false; - mockedStorage.Setup(storage => storage.AddAsync("CurrentUser", It.IsAny())).Callback(() => contains = true).Returns(Task.FromResult(null)).Verifiable(); + // The ParseUser will automatically be bound to ParseClient.Instance + var user = new ParseUser { ObjectId = "testObjectId" }; - mockedStorage.Setup(storage => storage.RemoveAsync("CurrentUser")).Callback(() => contains = false).Returns(Task.FromResult(null)).Verifiable(); + // Perform SetAsync operation + await controller.SetAsync(user, CancellationToken.None); - mockedStorage.Setup(storage => storage.ContainsKey("CurrentUser")).Returns(() => contains); + // Assertions + Assert.AreEqual(user, controller.CurrentUser); - return controller.SetAsync(user, CancellationToken.None).OnSuccess(_ => - { - Assert.AreEqual(user, controller.CurrentUser); + // Verify AddAsync was called + mockedStorage.Verify(storage => storage.AddAsync("CurrentUser", It.IsAny()), Times.Once); - return controller.ExistsAsync(CancellationToken.None); - }).Unwrap().OnSuccess(task => - { - Assert.IsTrue(task.Result); + // Perform GetAsync operation + var retrievedUser = await controller.GetAsync(Client, CancellationToken.None); + Assert.IsNotNull(retrievedUser); + Assert.AreEqual(user.ObjectId, retrievedUser.ObjectId); - controller.ClearFromMemory(); - return controller.ExistsAsync(CancellationToken.None); - }).Unwrap().OnSuccess(task => - { - Assert.IsTrue(task.Result); + // Clear user from memory + controller.ClearFromMemory(); + Assert.AreNotEqual(user, controller.CurrentUser); // Ensure the user is no longer in memory - controller.ClearFromDisk(); - return controller.ExistsAsync(CancellationToken.None); - }).Unwrap().OnSuccess(task => - { - Assert.IsFalse(task.Result); - mockedStorage.Verify(); - }); - } + // Retrieve user again + retrievedUser = await controller.GetAsync(Client, CancellationToken.None); + Assert.AreNotSame(user, retrievedUser); // Ensure the user is not the same instance + Assert.IsNotNull(controller.CurrentUser); // Ensure the CurrentUser is not null after re-fetching + } - [TestMethod] - [AsyncStateMachine(typeof(CurrentUserControllerTests))] - public Task TestIsCurrent() - { - Mock storageController = new Mock(MockBehavior.Strict); - ParseCurrentUserController controller = new ParseCurrentUserController(storageController.Object, Client.ClassController, Client.Decoder); + [TestMethod] + public async Task TestExistsAsync() + { + // Mock setup + var storageController = new Mock(); + var mockedStorage = new Mock>(); + var controller = new ParseCurrentUserController(storageController.Object, Client.ClassController, Client.Decoder); + var user = new ParseUser().Bind(Client) as ParseUser; - ParseUser user = new ParseUser { }.Bind(Client) as ParseUser; - ParseUser user2 = new ParseUser { }.Bind(Client) as ParseUser; + storageController + .Setup(c => c.LoadAsync()) + .ReturnsAsync(mockedStorage.Object); - storageController.Setup(storage => storage.LoadAsync()).Returns(Task.FromResult(new Mock>().Object)); + bool contains = false; - return controller.SetAsync(user, CancellationToken.None).OnSuccess(task => - { - Assert.IsTrue(controller.IsCurrent(user)); - Assert.IsFalse(controller.IsCurrent(user2)); + mockedStorage + .Setup(storage => storage.AddAsync("CurrentUser", It.IsAny())) + .Callback(() => contains = true) + .Returns(() => Task.FromResult((object) null)) + .Verifiable(); - controller.ClearFromMemory(); + mockedStorage + .Setup(storage => storage.RemoveAsync("CurrentUser")) + .Callback(() => contains = false) + .Returns(() => Task.FromResult((object) null)) + .Verifiable(); - Assert.IsFalse(controller.IsCurrent(user)); - return controller.SetAsync(user, CancellationToken.None); - }).Unwrap().OnSuccess(task => - { - Assert.IsTrue(controller.IsCurrent(user)); - Assert.IsFalse(controller.IsCurrent(user2)); + mockedStorage + .Setup(storage => storage.ContainsKey("CurrentUser")) + .Returns(() => contains); - controller.ClearFromDisk(); + // Perform SetAsync operation + await controller.SetAsync(user, CancellationToken.None); - Assert.IsFalse(controller.IsCurrent(user)); + // Assert that the current user is set correctly + Assert.AreEqual(user, controller.CurrentUser); - return controller.SetAsync(user2, CancellationToken.None); - }).Unwrap().OnSuccess(task => - { - Assert.IsFalse(controller.IsCurrent(user)); - Assert.IsTrue(controller.IsCurrent(user2)); - }); - } + // Check if the user exists + var exists = await controller.ExistsAsync(CancellationToken.None); + Assert.IsTrue(exists); - [TestMethod] - [AsyncStateMachine(typeof(CurrentUserControllerTests))] - public Task TestCurrentSessionToken() - { - Mock storageController = new Mock(); - Mock> mockedStorage = new Mock>(); - ParseCurrentUserController controller = new ParseCurrentUserController(storageController.Object, Client.ClassController, Client.Decoder); + // Clear from memory and re-check existence + controller.ClearFromMemory(); + exists = await controller.ExistsAsync(CancellationToken.None); + Assert.IsTrue(exists); - storageController.Setup(c => c.LoadAsync()).Returns(Task.FromResult(mockedStorage.Object)); + // Clear from disk and re-check existence + await controller.ClearFromDiskAsync(); + exists = await controller.ExistsAsync(CancellationToken.None); + Assert.IsFalse(exists); - return controller.GetCurrentSessionTokenAsync(Client, CancellationToken.None).OnSuccess(task => - { - Assert.IsNull(task.Result); + // Verify mocked behavior + mockedStorage.Verify(); + } - // We should probably mock this. + [TestMethod] + public async Task TestIsCurrent() + { + var storageController = new Mock(MockBehavior.Strict); + var controller = new ParseCurrentUserController(storageController.Object, Client.ClassController, Client.Decoder); - ParseUser user = Client.CreateObjectWithoutData(default); - user.HandleFetchResult(new MutableObjectState { ServerData = new Dictionary { ["sessionToken"] = "randomString" } }); + var user = new ParseUser().Bind(Client) as ParseUser; + var user2 = new ParseUser().Bind(Client) as ParseUser; - return controller.SetAsync(user, CancellationToken.None); - }).Unwrap().OnSuccess(_ => controller.GetCurrentSessionTokenAsync(Client, CancellationToken.None)).Unwrap().OnSuccess(task => Assert.AreEqual("randomString", task.Result)); - } + storageController + .Setup(storage => storage.LoadAsync()) + .ReturnsAsync(new Mock>().Object); - public Task TestLogOut() - { - ParseCurrentUserController controller = new ParseCurrentUserController(new Mock(MockBehavior.Strict).Object, Client.ClassController, Client.Decoder); - ParseUser user = new ParseUser { }.Bind(Client) as ParseUser; + // Set the first user + await controller.SetAsync(user, CancellationToken.None); - return controller.SetAsync(user, CancellationToken.None).OnSuccess(_ => - { - Assert.AreEqual(user, controller.CurrentUser); - return controller.ExistsAsync(CancellationToken.None); - }).Unwrap().OnSuccess(task => - { - Assert.IsTrue(task.Result); - return controller.LogOutAsync(Client, CancellationToken.None); - }).Unwrap().OnSuccess(_ => controller.GetAsync(Client, CancellationToken.None)).Unwrap().OnSuccess(task => - { - Assert.IsNull(task.Result); - return controller.ExistsAsync(CancellationToken.None); - }).Unwrap().OnSuccess(t => Assert.IsFalse(t.Result)); - } + Assert.IsTrue(controller.IsCurrent(user)); + Assert.IsFalse(controller.IsCurrent(user2)); + + // Clear from memory and verify + controller.ClearFromMemory(); + Assert.IsFalse(controller.IsCurrent(user)); + + // Re-set the first user + await controller.SetAsync(user, CancellationToken.None); + + Assert.IsTrue(controller.IsCurrent(user)); + Assert.IsFalse(controller.IsCurrent(user2)); + + // Clear from disk and verify + await controller.ClearFromDiskAsync(); + Assert.IsFalse(controller.IsCurrent(user)); + + // Set the second user and verify + await controller.SetAsync(user2, CancellationToken.None); + + Assert.IsFalse(controller.IsCurrent(user)); + Assert.IsTrue(controller.IsCurrent(user2)); } + } diff --git a/Parse.Tests/DecoderTests.cs b/Parse.Tests/DecoderTests.cs index c074cb83..396edcb0 100644 --- a/Parse.Tests/DecoderTests.cs +++ b/Parse.Tests/DecoderTests.cs @@ -5,45 +5,63 @@ using Parse.Infrastructure; using Parse.Infrastructure.Data; -namespace Parse.Tests +namespace Parse.Tests; + +[TestClass] +public class DecoderTests { - [TestClass] - public class DecoderTests + ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); + + [TestMethod] + public void TestParseDate() { - ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); + DateTime dateTime = (DateTime) Client.Decoder.Decode(ParseDataDecoder.ParseDate("1990-08-30T12:03:59.000Z"), Client); + + Assert.AreEqual(1990, dateTime.Year); + Assert.AreEqual(8, dateTime.Month); + Assert.AreEqual(30, dateTime.Day); + Assert.AreEqual(12, dateTime.Hour); + Assert.AreEqual(3, dateTime.Minute); + Assert.AreEqual(59, dateTime.Second); + Assert.AreEqual(0, dateTime.Millisecond); + } - [TestMethod] - public void TestParseDate() - { - DateTime dateTime = (DateTime) Client.Decoder.Decode(ParseDataDecoder.ParseDate("1990-08-30T12:03:59.000Z"), Client); + [TestMethod] + public void TestDecodePrimitives() + { + Assert.AreEqual(1, Client.Decoder.Decode(1, Client)); + Assert.AreEqual(0.3, Client.Decoder.Decode(0.3, Client)); + Assert.AreEqual("halyosy", Client.Decoder.Decode("halyosy", Client)); - Assert.AreEqual(1990, dateTime.Year); - Assert.AreEqual(8, dateTime.Month); - Assert.AreEqual(30, dateTime.Day); - Assert.AreEqual(12, dateTime.Hour); - Assert.AreEqual(3, dateTime.Minute); - Assert.AreEqual(59, dateTime.Second); - Assert.AreEqual(0, dateTime.Millisecond); - } + Assert.IsNull(Client.Decoder.Decode(default, Client)); + } - [TestMethod] - public void TestDecodePrimitives() - { - Assert.AreEqual(1, Client.Decoder.Decode(1, Client)); - Assert.AreEqual(0.3, Client.Decoder.Decode(0.3, Client)); - Assert.AreEqual("halyosy", Client.Decoder.Decode("halyosy", Client)); + [TestMethod] + // Decoding ParseFieldOperation is not supported on .NET now. We only need this for LDS. + public void TestDecodeFieldOperation() => Assert.ThrowsException(() => Client.Decoder.Decode(new Dictionary { { "__op", "Increment" }, { "amount", "322" } }, Client)); - Assert.IsNull(Client.Decoder.Decode(default, Client)); - } + [TestMethod] + public void TestDecodeDate() + { + DateTime dateTime = (DateTime) Client.Decoder.Decode(new Dictionary { { "__type", "Date" }, { "iso", "1990-08-30T12:03:59.000Z" } }, Client); + + Assert.AreEqual(1990, dateTime.Year); + Assert.AreEqual(8, dateTime.Month); + Assert.AreEqual(30, dateTime.Day); + Assert.AreEqual(12, dateTime.Hour); + Assert.AreEqual(3, dateTime.Minute); + Assert.AreEqual(59, dateTime.Second); + Assert.AreEqual(0, dateTime.Millisecond); + } - [TestMethod] - // Decoding ParseFieldOperation is not supported on .NET now. We only need this for LDS. - public void TestDecodeFieldOperation() => Assert.ThrowsException(() => Client.Decoder.Decode(new Dictionary { { "__op", "Increment" }, { "amount", "322" } }, Client)); + [TestMethod] + public void TestDecodeImproperDate() + { + IDictionary value = new Dictionary { ["__type"] = "Date", ["iso"] = "1990-08-30T12:03:59.0Z" }; - [TestMethod] - public void TestDecodeDate() + for (int i = 0; i < 2; i++, value["iso"] = (value["iso"] as string).Substring(0, (value["iso"] as string).Length - 1) + "0Z") { - DateTime dateTime = (DateTime) Client.Decoder.Decode(new Dictionary { { "__type", "Date" }, { "iso", "1990-08-30T12:03:59.000Z" } }, Client); + DateTime dateTime = (DateTime) Client.Decoder.Decode(value, Client); Assert.AreEqual(1990, dateTime.Year); Assert.AreEqual(8, dateTime.Month); @@ -53,188 +71,169 @@ public void TestDecodeDate() Assert.AreEqual(59, dateTime.Second); Assert.AreEqual(0, dateTime.Millisecond); } + } - [TestMethod] - public void TestDecodeImproperDate() - { - IDictionary value = new Dictionary { ["__type"] = "Date", ["iso"] = "1990-08-30T12:03:59.0Z" }; - - for (int i = 0; i < 2; i++, value["iso"] = (value["iso"] as string).Substring(0, (value["iso"] as string).Length - 1) + "0Z") - { - DateTime dateTime = (DateTime) Client.Decoder.Decode(value, Client); - - Assert.AreEqual(1990, dateTime.Year); - Assert.AreEqual(8, dateTime.Month); - Assert.AreEqual(30, dateTime.Day); - Assert.AreEqual(12, dateTime.Hour); - Assert.AreEqual(3, dateTime.Minute); - Assert.AreEqual(59, dateTime.Second); - Assert.AreEqual(0, dateTime.Millisecond); - } - } - - [TestMethod] - public void TestDecodeBytes() => Assert.AreEqual("This is an encoded string", System.Text.Encoding.UTF8.GetString(Client.Decoder.Decode(new Dictionary { { "__type", "Bytes" }, { "base64", "VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw==" } }, Client) as byte[])); + [TestMethod] + public void TestDecodeBytes() => Assert.AreEqual("This is an encoded string", System.Text.Encoding.UTF8.GetString(Client.Decoder.Decode(new Dictionary { { "__type", "Bytes" }, { "base64", "VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw==" } }, Client) as byte[])); - [TestMethod] - public void TestDecodePointer() - { - ParseObject obj = Client.Decoder.Decode(new Dictionary { ["__type"] = "Pointer", ["className"] = "Corgi", ["objectId"] = "lLaKcolnu" }, Client) as ParseObject; + [TestMethod] + public void TestDecodePointer() + { + ParseObject obj = Client.Decoder.Decode(new Dictionary { ["__type"] = "Pointer", ["className"] = "Corgi", ["objectId"] = "lLaKcolnu" }, Client) as ParseObject; - Assert.IsFalse(obj.IsDataAvailable); - Assert.AreEqual("Corgi", obj.ClassName); - Assert.AreEqual("lLaKcolnu", obj.ObjectId); - } + Assert.IsFalse(obj.IsDataAvailable); + Assert.AreEqual("Corgi", obj.ClassName); + Assert.AreEqual("lLaKcolnu", obj.ObjectId); + } - [TestMethod] - public void TestDecodeFile() - { + [TestMethod] + public void TestDecodeFile() + { - ParseFile file1 = Client.Decoder.Decode(new Dictionary { ["__type"] = "File", ["name"] = "Corgi.png", ["url"] = "http://corgi.xyz/gogo.png" }, Client) as ParseFile; + ParseFile file1 = Client.Decoder.Decode(new Dictionary { ["__type"] = "File", ["name"] = "parsee.png", ["url"] = "https://user-images.githubusercontent.com/5673677/138278489-7d0cebc5-1e31-4d3c-8ffb-53efcda6f29d.png" }, Client) as ParseFile; - Assert.AreEqual("Corgi.png", file1.Name); - Assert.AreEqual("http://corgi.xyz/gogo.png", file1.Url.AbsoluteUri); - Assert.IsFalse(file1.IsDirty); + Assert.AreEqual("parsee.png", file1.Name); + Assert.AreEqual("https://user-images.githubusercontent.com/5673677/138278489-7d0cebc5-1e31-4d3c-8ffb-53efcda6f29d.png", file1.Url.AbsoluteUri); + Assert.IsFalse(file1.IsDirty); - Assert.ThrowsException(() => Client.Decoder.Decode(new Dictionary { ["__type"] = "File", ["name"] = "Corgi.png" }, Client)); - } + Assert.ThrowsException(() => Client.Decoder.Decode(new Dictionary { ["__type"] = "File", ["name"] = "Corgi.png" }, Client)); + } - [TestMethod] - public void TestDecodeGeoPoint() - { - ParseGeoPoint point1 = (ParseGeoPoint) Client.Decoder.Decode(new Dictionary { ["__type"] = "GeoPoint", ["latitude"] = 0.9, ["longitude"] = 0.3 }, Client); + [TestMethod] + public void TestDecodeGeoPoint() + { + ParseGeoPoint point1 = (ParseGeoPoint) Client.Decoder.Decode(new Dictionary { ["__type"] = "GeoPoint", ["latitude"] = 0.9, ["longitude"] = 0.3 }, Client); - Assert.IsNotNull(point1); - Assert.AreEqual(0.9, point1.Latitude); - Assert.AreEqual(0.3, point1.Longitude); + Assert.IsNotNull(point1); + Assert.AreEqual(0.9, point1.Latitude); + Assert.AreEqual(0.3, point1.Longitude); - Assert.ThrowsException(() => Client.Decoder.Decode(new Dictionary { ["__type"] = "GeoPoint", ["latitude"] = 0.9 }, Client)); - } + Assert.ThrowsException(() => Client.Decoder.Decode(new Dictionary { ["__type"] = "GeoPoint", ["latitude"] = 0.9 }, Client)); + } - [TestMethod] - public void TestDecodeObject() + [TestMethod] + public void TestDecodeObject() + { + IDictionary value = new Dictionary() { - IDictionary value = new Dictionary() - { - ["__type"] = "Object", - ["className"] = "Corgi", - ["objectId"] = "lLaKcolnu", - ["createdAt"] = "2015-06-22T21:23:41.733Z", - ["updatedAt"] = "2015-06-22T22:06:41.733Z" - }; - - ParseObject obj = Client.Decoder.Decode(value, Client) as ParseObject; - - Assert.IsTrue(obj.IsDataAvailable); - Assert.AreEqual("Corgi", obj.ClassName); - Assert.AreEqual("lLaKcolnu", obj.ObjectId); - Assert.IsNotNull(obj.CreatedAt); - Assert.IsNotNull(obj.UpdatedAt); - } + ["__type"] = "Object", + ["className"] = "Corgi", + ["objectId"] = "lLaKcolnu", + ["createdAt"] = "2015-06-22T21:23:41.733Z", + ["updatedAt"] = "2015-06-22T22:06:41.733Z" + }; + + ParseObject obj = Client.Decoder.Decode(value, Client) as ParseObject; + + Assert.IsTrue(obj.IsDataAvailable); + Assert.AreEqual("Corgi", obj.ClassName); + Assert.AreEqual("lLaKcolnu", obj.ObjectId); + Assert.IsNotNull(obj.CreatedAt); + Assert.IsNotNull(obj.UpdatedAt); + } - [TestMethod] - public void TestDecodeRelation() + [TestMethod] + public void TestDecodeRelation() + { + IDictionary value = new Dictionary() { - IDictionary value = new Dictionary() - { - ["__type"] = "Relation", - ["className"] = "Corgi", - ["objectId"] = "lLaKcolnu" - }; + ["__type"] = "Relation", + ["className"] = "Corgi", + ["objectId"] = "lLaKcolnu" + }; - ParseRelation relation = Client.Decoder.Decode(value, Client) as ParseRelation; + ParseRelation relation = Client.Decoder.Decode(value, Client) as ParseRelation; - Assert.IsNotNull(relation); - Assert.AreEqual("Corgi", relation.GetTargetClassName()); - } + Assert.IsNotNull(relation); + Assert.AreEqual("Corgi", relation.GetTargetClassName()); + } - [TestMethod] - public void TestDecodeDictionary() + [TestMethod] + public void TestDecodeDictionary() + { + IDictionary value = new Dictionary() { - IDictionary value = new Dictionary() + ["megurine"] = "luka", + ["hatsune"] = new ParseObject("Miku"), + ["decodedGeoPoint"] = new Dictionary { - ["megurine"] = "luka", - ["hatsune"] = new ParseObject("Miku"), - ["decodedGeoPoint"] = new Dictionary + ["__type"] = "GeoPoint", + ["latitude"] = 0.9, + ["longitude"] = 0.3 + }, + ["listWithSomething"] = new List + { + new Dictionary { ["__type"] = "GeoPoint", ["latitude"] = 0.9, ["longitude"] = 0.3 - }, - ["listWithSomething"] = new List - { - new Dictionary - { - ["__type"] = "GeoPoint", - ["latitude"] = 0.9, - ["longitude"] = 0.3 - } } - }; + } + }; - IDictionary dict = Client.Decoder.Decode(value, Client) as IDictionary; + IDictionary dict = Client.Decoder.Decode(value, Client) as IDictionary; - Assert.AreEqual("luka", dict["megurine"]); - Assert.IsTrue(dict["hatsune"] is ParseObject); - Assert.IsTrue(dict["decodedGeoPoint"] is ParseGeoPoint); - Assert.IsTrue(dict["listWithSomething"] is IList); - IList decodedList = dict["listWithSomething"] as IList; - Assert.IsTrue(decodedList[0] is ParseGeoPoint); + Assert.AreEqual("luka", dict["megurine"]); + Assert.IsTrue(dict["hatsune"] is ParseObject); + Assert.IsTrue(dict["decodedGeoPoint"] is ParseGeoPoint); + Assert.IsTrue(dict["listWithSomething"] is IList); + IList decodedList = dict["listWithSomething"] as IList; + Assert.IsTrue(decodedList[0] is ParseGeoPoint); - IDictionary randomValue = new Dictionary() - { - ["ultimate"] = "elements", - [new ParseACL { }] = "lLaKcolnu" - }; + IDictionary randomValue = new Dictionary() + { + ["ultimate"] = "elements", + [new ParseACL { }] = "lLaKcolnu" + }; - IDictionary randomDict = Client.Decoder.Decode(randomValue, Client) as IDictionary; + IDictionary randomDict = Client.Decoder.Decode(randomValue, Client) as IDictionary; - Assert.AreEqual("elements", randomDict["ultimate"]); - Assert.AreEqual(2, randomDict.Keys.Count); - } + Assert.AreEqual("elements", randomDict["ultimate"]); + Assert.AreEqual(2, randomDict.Keys.Count); + } - [TestMethod] - public void TestDecodeList() + [TestMethod] + public void TestDecodeList() + { + IList value = new List { - IList value = new List + 1, new ParseACL { }, "wiz", + new Dictionary + { + ["__type"] = "GeoPoint", + ["latitude"] = 0.9, + ["longitude"] = 0.3 + }, + new List { - 1, new ParseACL { }, "wiz", new Dictionary { ["__type"] = "GeoPoint", - ["latitude"] = 0.9, + ["latitude"] = 0.9, ["longitude"] = 0.3 - }, - new List - { - new Dictionary - { - ["__type"] = "GeoPoint", - ["latitude"] = 0.9, - ["longitude"] = 0.3 - } } - }; + } + }; - IList list = Client.Decoder.Decode(value, Client) as IList; + IList list = Client.Decoder.Decode(value, Client) as IList; - Assert.AreEqual(1, list[0]); - Assert.IsTrue(list[1] is ParseACL); - Assert.AreEqual("wiz", list[2]); - Assert.IsTrue(list[3] is ParseGeoPoint); - Assert.IsTrue(list[4] is IList); - IList decodedList = list[4] as IList; - Assert.IsTrue(decodedList[0] is ParseGeoPoint); - } + Assert.AreEqual(1, list[0]); + Assert.IsTrue(list[1] is ParseACL); + Assert.AreEqual("wiz", list[2]); + Assert.IsTrue(list[3] is ParseGeoPoint); + Assert.IsTrue(list[4] is IList); + IList decodedList = list[4] as IList; + Assert.IsTrue(decodedList[0] is ParseGeoPoint); + } - [TestMethod] - public void TestDecodeArray() - { - int[] value = new int[] { 1, 2, 3, 4 }, array = Client.Decoder.Decode(value, Client) as int[]; + [TestMethod] + public void TestDecodeArray() + { + int[] value = new int[] { 1, 2, 3, 4 }, array = Client.Decoder.Decode(value, Client) as int[]; - Assert.AreEqual(4, array.Length); - Assert.AreEqual(1, array[0]); - Assert.AreEqual(2, array[1]); - } + Assert.AreEqual(4, array.Length); + Assert.AreEqual(1, array[0]); + Assert.AreEqual(2, array[1]); } } diff --git a/Parse.Tests/EncoderTests.cs b/Parse.Tests/EncoderTests.cs index 505938de..6cd103a3 100644 --- a/Parse.Tests/EncoderTests.cs +++ b/Parse.Tests/EncoderTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -9,229 +10,251 @@ using Parse.Infrastructure.Data; // TODO (hallucinogen): mock ParseACL, ParseObject, ParseUser once we have their Interfaces -namespace Parse.Tests +namespace Parse.Tests; + +[TestClass] +public class EncoderTests { - [TestClass] - public class EncoderTests - { - ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); + ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); - /// - /// A that's used only for testing. This class is used to test - /// 's base methods. - /// - class ParseEncoderTestClass : ParseDataEncoder - { - public static ParseEncoderTestClass Instance { get; } = new ParseEncoderTestClass { }; + /// + /// A that's used only for testing. This class is used to test + /// 's base methods. + /// + class ParseEncoderTestClass : ParseDataEncoder + { + public static ParseEncoderTestClass Instance { get; } = new ParseEncoderTestClass { }; - protected override IDictionary EncodeObject(ParseObject value) => null; - } + protected override IDictionary EncodeObject(ParseObject value) => null; + } - [TestMethod] - public void TestIsValidType() - { - ParseObject corgi = new ParseObject("Corgi"); - ParseRelation corgiRelation = corgi.GetRelation(nameof(corgi)); - - Assert.IsTrue(ParseDataEncoder.Validate(322)); - Assert.IsTrue(ParseDataEncoder.Validate(0.3f)); - Assert.IsTrue(ParseDataEncoder.Validate(new byte[] { 1, 2, 3, 4 })); - Assert.IsTrue(ParseDataEncoder.Validate(nameof(corgi))); - Assert.IsTrue(ParseDataEncoder.Validate(corgi)); - Assert.IsTrue(ParseDataEncoder.Validate(new ParseACL { })); - Assert.IsTrue(ParseDataEncoder.Validate(new ParseFile("Corgi", new byte[0]))); - Assert.IsTrue(ParseDataEncoder.Validate(new ParseGeoPoint(1, 2))); - Assert.IsTrue(ParseDataEncoder.Validate(corgiRelation)); - Assert.IsTrue(ParseDataEncoder.Validate(new DateTime { })); - Assert.IsTrue(ParseDataEncoder.Validate(new List { })); - Assert.IsTrue(ParseDataEncoder.Validate(new Dictionary { })); - Assert.IsTrue(ParseDataEncoder.Validate(new Dictionary { })); - - Assert.IsFalse(ParseDataEncoder.Validate(new ParseAddOperation(new List { }))); - Assert.IsFalse(ParseDataEncoder.Validate(Task.FromResult(new ParseObject("Corgi")))); - Assert.ThrowsException(() => ParseDataEncoder.Validate(new Dictionary { })); - Assert.ThrowsException(() => ParseDataEncoder.Validate(new Dictionary { })); - } + [TestMethod] + public void TestIsValidType() + { + ParseObject corgi = new ParseObject("Corgi"); + ParseRelation corgiRelation = corgi.GetRelation(nameof(corgi)); + + Assert.IsTrue(ParseDataEncoder.Validate(322)); + Assert.IsTrue(ParseDataEncoder.Validate(0.3f)); + Assert.IsTrue(ParseDataEncoder.Validate(new byte[] { 1, 2, 3, 4 })); + Assert.IsTrue(ParseDataEncoder.Validate(nameof(corgi))); + Assert.IsTrue(ParseDataEncoder.Validate(corgi)); + Assert.IsTrue(ParseDataEncoder.Validate(new ParseACL { })); + Assert.IsTrue(ParseDataEncoder.Validate(new ParseFile("Corgi", new byte[0]))); + Assert.IsTrue(ParseDataEncoder.Validate(new ParseGeoPoint(1, 2))); + Assert.IsTrue(ParseDataEncoder.Validate(corgiRelation)); + Assert.IsTrue(ParseDataEncoder.Validate(new DateTime { })); + Assert.IsTrue(ParseDataEncoder.Validate(new List { })); + Assert.IsTrue(ParseDataEncoder.Validate(new Dictionary { })); + Assert.IsTrue(ParseDataEncoder.Validate(new Dictionary { })); + + Assert.IsFalse(ParseDataEncoder.Validate(new ParseAddOperation(new List { }))); + Assert.IsFalse(ParseDataEncoder.Validate(Task.FromResult(new ParseObject("Corgi")))); + Assert.ThrowsException(() => ParseDataEncoder.Validate(new Dictionary { })); + Assert.ThrowsException(() => ParseDataEncoder.Validate(new Dictionary { })); + } - [TestMethod] - public void TestEncodeDate() - { - DateTime dateTime = new DateTime(1990, 8, 30, 12, 3, 59); + [TestMethod] + public void TestEncodeDate() + { + DateTime dateTime = new DateTime(1990, 8, 30, 12, 3, 59); - IDictionary value = ParseEncoderTestClass.Instance.Encode(dateTime, Client) as IDictionary; + IDictionary value = ParseEncoderTestClass.Instance.Encode(dateTime, Client) as IDictionary; - Assert.AreEqual("Date", value["__type"]); - Assert.AreEqual("1990-08-30T12:03:59.000Z", value["iso"]); - } + Assert.AreEqual("Date", value["__type"]); + Assert.AreEqual("1990-08-30T12:03:59.000Z", value["iso"]); + } - [TestMethod] - public void TestEncodeBytes() - { - byte[] bytes = new byte[] { 1, 2, 3, 4 }; + [TestMethod] + public void TestEncodeBytes() + { + byte[] bytes = new byte[] { 1, 2, 3, 4 }; - IDictionary value = ParseEncoderTestClass.Instance.Encode(bytes, Client) as IDictionary; + IDictionary value = ParseEncoderTestClass.Instance.Encode(bytes, Client) as IDictionary; - Assert.AreEqual("Bytes", value["__type"]); - Assert.AreEqual(Convert.ToBase64String(new byte[] { 1, 2, 3, 4 }), value["base64"]); - } + Assert.AreEqual("Bytes", value["__type"]); + Assert.AreEqual(Convert.ToBase64String(new byte[] { 1, 2, 3, 4 }), value["base64"]); + } - [TestMethod] - public void TestEncodeParseObjectWithNoObjectsEncoder() - { - ParseObject obj = new ParseObject("Corgi"); + [TestMethod] + public void TestEncodeParseObjectWithNoObjectsEncoder() + { + ParseObject obj = new ParseObject("Corgi"); - Assert.ThrowsException(() => NoObjectsEncoder.Instance.Encode(obj, Client)); - } + Assert.ThrowsException(() => NoObjectsEncoder.Instance.Encode(obj, Client)); + } - [TestMethod] - public void TestEncodeParseObjectWithPointerOrLocalIdEncoder() - { - // TODO (hallucinogen): we can't make an object with ID without saving for now. Let's revisit this after we make IParseObject - } + [TestMethod] + public void TestEncodeParseObjectWithPointerOrLocalIdEncoder() + { + // TODO (hallucinogen): we can't make an object with ID without saving for now. Let's revisit this after we make IParseObject + } - [TestMethod] - public void TestEncodeParseFile() - { - ParseFile file1 = ParseFileExtensions.Create("Corgi.png", new Uri("http://corgi.xyz/gogo.png")); + [TestMethod] + public void TestEncodeParseFile() + { + ParseFile file1 = ParseFileExtensions.Create("Corgi.png", new Uri("http://corgi.xyz/gogo.png")); - IDictionary value = ParseEncoderTestClass.Instance.Encode(file1, Client) as IDictionary; + IDictionary value = ParseEncoderTestClass.Instance.Encode(file1, Client) as IDictionary; - Assert.AreEqual("File", value["__type"]); - Assert.AreEqual("Corgi.png", value["name"]); - Assert.AreEqual("http://corgi.xyz/gogo.png", value["url"]); + Assert.AreEqual("File", value["__type"]); + Assert.AreEqual("Corgi.png", value["name"]); + Assert.AreEqual("http://corgi.xyz/gogo.png", value["url"]); - ParseFile file2 = new ParseFile(null, new MemoryStream(new byte[] { 1, 2, 3, 4 })); + ParseFile file2 = new ParseFile(null, new MemoryStream(new byte[] { 1, 2, 3, 4 })); - Assert.ThrowsException(() => ParseEncoderTestClass.Instance.Encode(file2, Client)); - } + Assert.ThrowsException(() => ParseEncoderTestClass.Instance.Encode(file2, Client)); + } - [TestMethod] - public void TestEncodeParseGeoPoint() - { - ParseGeoPoint point = new ParseGeoPoint(3.22, 32.2); + [TestMethod] + public void TestEncodeParseGeoPoint() + { + ParseGeoPoint point = new ParseGeoPoint(3.22, 32.2); - IDictionary value = ParseEncoderTestClass.Instance.Encode(point, Client) as IDictionary; + IDictionary value = ParseEncoderTestClass.Instance.Encode(point, Client) as IDictionary; - Assert.AreEqual("GeoPoint", value["__type"]); - Assert.AreEqual(3.22, value["latitude"]); - Assert.AreEqual(32.2, value["longitude"]); - } + Assert.AreEqual("GeoPoint", value["__type"]); + Assert.AreEqual(3.22, value["latitude"]); + Assert.AreEqual(32.2, value["longitude"]); + } - [TestMethod] - public void TestEncodeACL() - { - ParseACL acl1 = new ParseACL(); + [TestMethod] + public void TestEncodeACL() + { + ParseACL acl1 = new ParseACL(); - IDictionary value1 = ParseEncoderTestClass.Instance.Encode(acl1, Client) as IDictionary; + IDictionary value1 = ParseEncoderTestClass.Instance.Encode(acl1, Client) as IDictionary; - Assert.IsNotNull(value1); - Assert.AreEqual(0, value1.Keys.Count); + Assert.IsNotNull(value1); + Assert.AreEqual(0, value1.Keys.Count); - ParseACL acl2 = new ParseACL - { - PublicReadAccess = true, - PublicWriteAccess = true - }; + ParseACL acl2 = new ParseACL + { + PublicReadAccess = true, + PublicWriteAccess = true + }; - IDictionary value2 = ParseEncoderTestClass.Instance.Encode(acl2, Client) as IDictionary; + IDictionary value2 = ParseEncoderTestClass.Instance.Encode(acl2, Client) as IDictionary; - Assert.AreEqual(1, value2.Keys.Count); - IDictionary publicAccess = value2["*"] as IDictionary; - Assert.AreEqual(2, publicAccess.Keys.Count); - Assert.IsTrue((bool) publicAccess["read"]); - Assert.IsTrue((bool) publicAccess["write"]); + Assert.AreEqual(1, value2.Keys.Count); + IDictionary publicAccess = value2["*"] as IDictionary; + Assert.AreEqual(2, publicAccess.Keys.Count); + Assert.IsTrue((bool) publicAccess["read"]); + Assert.IsTrue((bool) publicAccess["write"]); - // TODO (hallucinogen): mock ParseUser and test SetReadAccess and SetWriteAccess - } + // TODO (hallucinogen): mock ParseUser and test SetReadAccess and SetWriteAccess + } - [TestMethod] - public void TestEncodeParseRelation() - { - ParseObject obj = new ParseObject("Corgi"); - ParseRelation relation = ParseRelationExtensions.Create(obj, "nano", "Husky"); + [TestMethod] + public void TestEncodeParseRelation() + { + ParseObject obj = new ParseObject("Corgi"); + ParseRelation relation = ParseRelationExtensions.Create(obj, "nano", "Husky"); - IDictionary value = ParseEncoderTestClass.Instance.Encode(relation, Client) as IDictionary; + IDictionary value = ParseEncoderTestClass.Instance.Encode(relation, Client) as IDictionary; - Assert.AreEqual("Relation", value["__type"]); - Assert.AreEqual("Husky", value["className"]); - } + Assert.AreEqual("Relation", value["__type"]); + Assert.AreEqual("Husky", value["className"]); + } - [TestMethod] - public void TestEncodeParseFieldOperation() - { - ParseIncrementOperation incOps = new ParseIncrementOperation(1); + [TestMethod] + public void TestEncodeParseFieldOperation() + { + ParseIncrementOperation incOps = new ParseIncrementOperation(1); - IDictionary value = ParseEncoderTestClass.Instance.Encode(incOps, Client) as IDictionary; + IDictionary value = ParseEncoderTestClass.Instance.Encode(incOps, Client) as IDictionary; - Assert.AreEqual("Increment", value["__op"]); - Assert.AreEqual(1, value["amount"]); + Assert.AreEqual("Increment", value["__op"]); + Assert.AreEqual(1, value["amount"]); - // Other operations are tested in FieldOperationTests. - } + // Other operations are tested in FieldOperationTests. + } - [TestMethod] - public void TestEncodeList() + [TestMethod] + public void TestEncodeList() + { + IList list = new List + { + new ParseGeoPoint(0, 0), + "item", + new byte[] { 1, 2, 3, 4 }, + new string[] { "hikaru", "hanatan", "ultimate" }, + new Dictionary { - IList list = new List - { - new ParseGeoPoint(0, 0), - "item", - new byte[] { 1, 2, 3, 4 }, - new string[] { "hikaru", "hanatan", "ultimate" }, - new Dictionary() - { - ["elements"] = new int[] { 1, 2, 3 }, - ["mystic"] = "cage", - ["listAgain"] = new List { "xilia", "zestiria", "symphonia" } - } - }; - - IList value = ParseEncoderTestClass.Instance.Encode(list, Client) as IList; - - IDictionary item0 = value[0] as IDictionary; - Assert.AreEqual("GeoPoint", item0["__type"]); - Assert.AreEqual(0.0, item0["latitude"]); - Assert.AreEqual(0.0, item0["longitude"]); - - Assert.AreEqual("item", value[1]); - - IDictionary item2 = value[2] as IDictionary; - Assert.AreEqual("Bytes", item2["__type"]); - - IList item3 = value[3] as IList; - Assert.AreEqual("hikaru", item3[0]); - Assert.AreEqual("hanatan", item3[1]); - Assert.AreEqual("ultimate", item3[2]); - - IDictionary item4 = value[4] as IDictionary; - Assert.IsTrue(item4["elements"] is IList); - Assert.AreEqual("cage", item4["mystic"]); - Assert.IsTrue(item4["listAgain"] is IList); + ["elements"] = new int[] { 1, 2, 3 }, + ["mystic"] = "cage", + ["listAgain"] = new List { "xilia", "zestiria", "symphonia" } } + }; + + Debug.WriteLine($"Original list: {list}"); + + IList value = ParseEncoderTestClass.Instance.Encode(list, Client) as IList; + + Assert.IsNotNull(value); + + // Validate ParseGeoPoint + IDictionary item0 = value[0] as IDictionary; + Assert.IsNotNull(item0); + Assert.AreEqual("GeoPoint", item0["__type"]); + Assert.AreEqual(0.0, item0["latitude"]); + Assert.AreEqual(0.0, item0["longitude"]); + + // Validate string + Assert.AreEqual("item", value[1]); + + // Validate byte[] + IDictionary item2 = value[2] as IDictionary; + Debug.WriteLine($"Encoded item2: {item2}, Type: {item2?.GetType()}"); + Assert.IsNotNull(item2); + Assert.AreEqual("Bytes", item2["__type"]); + Assert.AreEqual("AQIDBA==", item2["base64"]); // Base64 representation of {1,2,3,4} + + // Validate string[] + IList item3 = value[3] as IList; + Assert.IsNotNull(item3); + Assert.AreEqual("hikaru", item3[0]); + Assert.AreEqual("hanatan", item3[1]); + Assert.AreEqual("ultimate", item3[2]); + + // Validate nested dictionary + IDictionary item4 = value[4] as IDictionary; + Assert.IsNotNull(item4); + Assert.IsTrue(item4["elements"] is IList); + Assert.AreEqual("cage", item4["mystic"]); + Assert.IsTrue(item4["listAgain"] is IList); + } + - [TestMethod] - public void TestEncodeDictionary() + [TestMethod] + public void TestEncodeDictionary() + { + IDictionary dict = new Dictionary { - IDictionary dict = new Dictionary() + ["item"] = "random", + ["list"] = new List { "vesperia", "abyss", "legendia" }, + ["array"] = new int[] { 1, 2, 3 }, + ["geo"] = new ParseGeoPoint(0, 0), + ["validDict"] = new Dictionary { ["phantasia"] = "jbf" } + }; + + IDictionary value = ParseEncoderTestClass.Instance.Encode(dict, Client) as IDictionary; + + Assert.IsNotNull(value); + Assert.AreEqual("random", value["item"]); + Assert.IsTrue(value["list"] is IList); + Assert.IsTrue(value["array"] is IList); + Assert.IsTrue(value["geo"] is IDictionary); + Assert.IsTrue(value["validDict"] is IDictionary); + + Assert.ThrowsException(() => + ParseEncoderTestClass.Instance.Encode(new Dictionary(), Client)); + + Assert.ThrowsException(() => + ParseEncoderTestClass.Instance.Encode(new Dictionary { - ["item"] = "random", - ["list"] = new List { "vesperia", "abyss", "legendia" }, - ["array"] = new int[] { 1, 2, 3 }, - ["geo"] = new ParseGeoPoint(0, 0), - ["validDict"] = new Dictionary { ["phantasia"] = "jbf" } - }; - - IDictionary value = ParseEncoderTestClass.Instance.Encode(dict, Client) as IDictionary; - - Assert.AreEqual("random", value["item"]); - Assert.IsTrue(value["list"] is IList); - Assert.IsTrue(value["array"] is IList); - Assert.IsTrue(value["geo"] is IDictionary); - Assert.IsTrue(value["validDict"] is IDictionary); - - Assert.ThrowsException(() => ParseEncoderTestClass.Instance.Encode(new Dictionary { }, Client)); - - Assert.ThrowsException(() => ParseEncoderTestClass.Instance.Encode(new Dictionary { ["validDict"] = new Dictionary { [new ParseACL()] = "jbf" } }, Client)); - } + ["validDict"] = new Dictionary { [new ParseACL()] = "jbf" } + }, Client)); } + } diff --git a/Parse.Tests/FileControllerTests.cs b/Parse.Tests/FileControllerTests.cs index 8d8ddbd3..88103ec7 100644 --- a/Parse.Tests/FileControllerTests.cs +++ b/Parse.Tests/FileControllerTests.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Net; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -12,91 +11,108 @@ using Parse.Infrastructure.Execution; using Parse.Platform.Files; -namespace Parse.Tests +namespace Parse.Tests; + +[TestClass] +public class FileControllerTests { -#warning Refactor this class. -#warning Skipped initialization step may be needed. + [TestMethod] + public async Task TestFileControllerSaveWithInvalidResultAsync() + { + var response = new Tuple>(HttpStatusCode.Accepted, null); + var mockRunner = CreateMockRunner(response); + + var state = new FileState + { + Name = "bekti.png", + MediaType = "image/png" + }; + + var controller = new ParseFileController(mockRunner.Object); - [TestClass] - public class FileControllerTests + await Assert.ThrowsExceptionAsync(async () => + { + await controller.SaveAsync(state, new MemoryStream(), null, null); + }); + } + + [TestMethod] + public async Task TestFileControllerSaveWithEmptyResultAsync() { - // public void SetUp() => Client = new ParseClient(new ServerConnectionData { ApplicationID = "", Key = "", Test = true }); + var response = new Tuple>(HttpStatusCode.Accepted, new Dictionary()); + var mockRunner = CreateMockRunner(response); - [TestMethod] - [AsyncStateMachine(typeof(FileControllerTests))] - public Task TestFileControllerSaveWithInvalidResult() + var state = new FileState { - Tuple> response = new Tuple>(HttpStatusCode.Accepted, null); - Mock mockRunner = CreateMockRunner(response); - FileState state = new FileState - { - Name = "bekti.png", - MediaType = "image/png" - }; + Name = "bekti.png", + MediaType = "image/png" + }; - ParseFileController controller = new ParseFileController(mockRunner.Object); - return controller.SaveAsync(state, dataStream: new MemoryStream(), sessionToken: null, progress: null).ContinueWith(t => Assert.IsTrue(t.IsFaulted)); - } + var controller = new ParseFileController(mockRunner.Object); - [TestMethod] - [AsyncStateMachine(typeof(FileControllerTests))] - public Task TestFileControllerSaveWithEmptyResult() + await Assert.ThrowsExceptionAsync(async () => { - Tuple> response = new Tuple>(HttpStatusCode.Accepted, new Dictionary()); - Mock mockRunner = CreateMockRunner(response); - FileState state = new FileState - { - Name = "bekti.png", - MediaType = "image/png" - }; + await controller.SaveAsync(state, new MemoryStream(), null, null); + }); + } - ParseFileController controller = new ParseFileController(mockRunner.Object); - return controller.SaveAsync(state, dataStream: new MemoryStream(), sessionToken: null, progress: null).ContinueWith(t => Assert.IsTrue(t.IsFaulted)); - } + [TestMethod] + public async Task TestFileControllerSaveWithIncompleteResultAsync() + { + var response = new Tuple>(HttpStatusCode.Accepted, new Dictionary { ["name"] = "newBekti.png" }); + var mockRunner = CreateMockRunner(response); - [TestMethod] - [AsyncStateMachine(typeof(FileControllerTests))] - public Task TestFileControllerSaveWithIncompleteResult() + var state = new FileState { - Tuple> response = new Tuple>(HttpStatusCode.Accepted, new Dictionary { ["name"] = "newBekti.png" }); - Mock mockRunner = CreateMockRunner(response); - FileState state = new FileState - { - Name = "bekti.png", - MediaType = "image/png" - }; + Name = "bekti.png", + MediaType = "image/png" + }; - ParseFileController controller = new ParseFileController(mockRunner.Object); - return controller.SaveAsync(state, dataStream: new MemoryStream(), sessionToken: null, progress: null).ContinueWith(t => Assert.IsTrue(t.IsFaulted)); - } + var controller = new ParseFileController(mockRunner.Object); - [TestMethod] - [AsyncStateMachine(typeof(FileControllerTests))] - public Task TestFileControllerSave() + await Assert.ThrowsExceptionAsync(async () => { - FileState state = new FileState - { - Name = "bekti.png", - MediaType = "image/png" - }; + await controller.SaveAsync(state, new MemoryStream(), null, null); + }); + } - return new ParseFileController(CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary { ["name"] = "newBekti.png", ["url"] = "https://www.parse.com/newBekti.png" })).Object).SaveAsync(state, dataStream: new MemoryStream(), sessionToken: null, progress: null).ContinueWith(t => + [TestMethod] + public async Task TestFileControllerSaveAsync() + { + var state = new FileState + { + Name = "bekti.png", + MediaType = "image/png" + }; + + var mockRunner = CreateMockRunner(new Tuple>( + HttpStatusCode.Accepted, + new Dictionary { - Assert.IsFalse(t.IsFaulted); - FileState newState = t.Result; + ["name"] = "newBekti.png", + ["url"] = "https://www.parse.com/newBekti.png" + })); - Assert.AreEqual(state.MediaType, newState.MediaType); - Assert.AreEqual("newBekti.png", newState.Name); - Assert.AreEqual("https://www.parse.com/newBekti.png", newState.Location.AbsoluteUri); - }); - } + var controller = new ParseFileController(mockRunner.Object); + var newState = await controller.SaveAsync(state, new MemoryStream(), null, null); - private Mock CreateMockRunner(Tuple> response) - { - Mock mockRunner = new Mock(); - mockRunner.Setup(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(response)); + // Assertions + Assert.AreEqual(state.MediaType, newState.MediaType); + Assert.AreEqual("newBekti.png", newState.Name); + Assert.AreEqual("https://www.parse.com/newBekti.png", newState.Location.AbsoluteUri); + } + + private Mock CreateMockRunner(Tuple> response) + { + var mockRunner = new Mock(); + mockRunner + .Setup(obj => obj.RunCommandAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(response); - return mockRunner; - } + return mockRunner; } } diff --git a/Parse.Tests/FileStateTests.cs b/Parse.Tests/FileStateTests.cs index e624a7da..4335f101 100644 --- a/Parse.Tests/FileStateTests.cs +++ b/Parse.Tests/FileStateTests.cs @@ -2,40 +2,39 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Parse.Platform.Files; -namespace Parse.Tests +namespace Parse.Tests; + +[TestClass] +public class FileStateTests { - [TestClass] - public class FileStateTests + [TestMethod] + public void TestSecureUrl() { - [TestMethod] - public void TestSecureUrl() - { - Uri unsecureUri = new Uri("http://files.parsetfss.com/yolo.txt"); - Uri secureUri = new Uri("https://files.parsetfss.com/yolo.txt"); - Uri randomUri = new Uri("http://random.server.local/file.foo"); + Uri unsecureUri = new Uri("http://files.parsetfss.com/yolo.txt"); + Uri secureUri = new Uri("https://files.parsetfss.com/yolo.txt"); + Uri randomUri = new Uri("http://random.server.local/file.foo"); - FileState state = new FileState - { - Name = "A", - Location = unsecureUri, - MediaType = null - }; + FileState state = new FileState + { + Name = "A", + Location = unsecureUri, + MediaType = null + }; - Assert.AreEqual(unsecureUri, state.Location); - Assert.AreEqual(secureUri, state.SecureLocation); + Assert.AreEqual(unsecureUri, state.Location); + Assert.AreEqual(secureUri, state.SecureLocation); - // Make sure the proper port was given back. - Assert.AreEqual(443, state.SecureLocation.Port); + // Make sure the proper port was given back. + Assert.AreEqual(443, state.SecureLocation.Port); - state = new FileState - { - Name = "B", - Location = randomUri, - MediaType = null - }; + state = new FileState + { + Name = "B", + Location = randomUri, + MediaType = null + }; - Assert.AreEqual(randomUri, state.Location); - Assert.AreEqual(randomUri, state.Location); - } + Assert.AreEqual(randomUri, state.Location); + Assert.AreEqual(randomUri, state.Location); } } diff --git a/Parse.Tests/FileTests.cs b/Parse.Tests/FileTests.cs index 86b1745c..538da018 100644 --- a/Parse.Tests/FileTests.cs +++ b/Parse.Tests/FileTests.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -12,52 +11,79 @@ using Parse.Infrastructure; using Parse.Platform.Files; -namespace Parse.Tests +namespace Parse.Tests; + +[TestClass] +public class FileTests { - [TestClass] - public class FileTests + [TestMethod] + public async Task TestFileSaveAsync() { - [TestMethod] - [AsyncStateMachine(typeof(FileTests))] - public Task TestFileSave() - { - Mock mockController = new Mock(); - mockController.Setup(obj => obj.SaveAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(new FileState { Name = "newBekti.png", Location = new Uri("https://www.parse.com/newBekti.png"), MediaType = "image/png" })); - Mock mockCurrentUserController = new Mock(); - - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, new MutableServiceHub { FileController = mockController.Object, CurrentUserController = mockCurrentUserController.Object }); - - ParseFile file = new ParseFile("bekti.jpeg", new MemoryStream { }, "image/jpeg"); + // Arrange: Set up mock controllers and client + var mockController = new Mock(); + mockController + .Setup(obj => obj.SaveAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(new FileState + { + Name = "newBekti.png", + Location = new Uri("https://www.parse.com/newBekti.png"), + MediaType = "image/png" + }); - Assert.AreEqual("bekti.jpeg", file.Name); - Assert.AreEqual("image/jpeg", file.MimeType); - Assert.IsTrue(file.IsDirty); + var mockCurrentUserController = new Mock(); - return file.SaveAsync(client).ContinueWith(task => + var client = new ParseClient( + new ServerConnectionData { Test = true }, + new MutableServiceHub { - Assert.IsFalse(task.IsFaulted); - Assert.AreEqual("newBekti.png", file.Name); - Assert.AreEqual("image/png", file.MimeType); - Assert.AreEqual("https://www.parse.com/newBekti.png", file.Url.AbsoluteUri); - Assert.IsFalse(file.IsDirty); + FileController = mockController.Object, + CurrentUserController = mockCurrentUserController.Object }); - } - [TestMethod] - public void TestSecureUrl() - { - Uri unsecureUri = new Uri("http://files.parsetfss.com/yolo.txt"); - Uri secureUri = new Uri("https://files.parsetfss.com/yolo.txt"); - Uri randomUri = new Uri("http://random.server.local/file.foo"); + var file = new ParseFile("bekti.jpeg", new MemoryStream(), "image/jpeg"); + + // Act: Save the file using the Parse client + Assert.AreEqual("bekti.jpeg", file.Name); + Assert.AreEqual("image/jpeg", file.MimeType); + Assert.IsTrue(file.IsDirty); + + await file.SaveAsync(client); + + // Assert: Verify file properties and state after saving + Assert.AreEqual("newBekti.png", file.Name); + Assert.AreEqual("image/png", file.MimeType); + Assert.AreEqual("https://www.parse.com/newBekti.png", file.Url.AbsoluteUri); + Assert.IsFalse(file.IsDirty); + + // Verify the SaveAsync method was called on the mock controller + mockController.Verify(obj => obj.SaveAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny()), Times.Once); + } + + + [TestMethod] + public void TestSecureUrl() + { + Uri unsecureUri = new Uri("http://files.parsetfss.com/yolo.txt"); + Uri secureUri = new Uri("https://files.parsetfss.com/yolo.txt"); + Uri randomUri = new Uri("http://random.server.local/file.foo"); - ParseFile file = ParseFileExtensions.Create("Foo", unsecureUri); - Assert.AreEqual(secureUri, file.Url); + ParseFile file = ParseFileExtensions.Create("Foo", unsecureUri); + Assert.AreEqual(secureUri, file.Url); - file = ParseFileExtensions.Create("Bar", secureUri); - Assert.AreEqual(secureUri, file.Url); + file = ParseFileExtensions.Create("Bar", secureUri); + Assert.AreEqual(secureUri, file.Url); - file = ParseFileExtensions.Create("Baz", randomUri); - Assert.AreEqual(randomUri, file.Url); - } + file = ParseFileExtensions.Create("Baz", randomUri); + Assert.AreEqual(randomUri, file.Url); } } diff --git a/Parse.Tests/GeoPointTests.cs b/Parse.Tests/GeoPointTests.cs index a9611042..5bb0fc55 100644 --- a/Parse.Tests/GeoPointTests.cs +++ b/Parse.Tests/GeoPointTests.cs @@ -7,119 +7,118 @@ using Parse.Infrastructure.Data; using Parse.Infrastructure.Utilities; -namespace Parse.Tests +namespace Parse.Tests; + +[TestClass] +public class GeoPointTests { - [TestClass] - public class GeoPointTests + ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); + + [TestMethod] + public void TestGeoPointCultureInvariantParsing() { - ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); + CultureInfo initialCulture = Thread.CurrentThread.CurrentCulture; - [TestMethod] - public void TestGeoPointCultureInvariantParsing() + foreach (CultureInfo culture in CultureInfo.GetCultures(CultureTypes.AllCultures)) { - CultureInfo initialCulture = Thread.CurrentThread.CurrentCulture; - - foreach (CultureInfo culture in CultureInfo.GetCultures(CultureTypes.AllCultures)) - { - Thread.CurrentThread.CurrentCulture = culture; + Thread.CurrentThread.CurrentCulture = culture; - ParseGeoPoint point = new ParseGeoPoint(1.234, 1.234); - IDictionary deserialized = Client.Decoder.Decode(JsonUtilities.Parse(JsonUtilities.Encode(new Dictionary { [nameof(point)] = NoObjectsEncoder.Instance.Encode(point, Client) })), Client) as IDictionary; - ParseGeoPoint pointAgain = (ParseGeoPoint) deserialized[nameof(point)]; + ParseGeoPoint point = new ParseGeoPoint(1.234, 1.234); + IDictionary deserialized = Client.Decoder.Decode(JsonUtilities.Parse(JsonUtilities.Encode(new Dictionary { [nameof(point)] = NoObjectsEncoder.Instance.Encode(point, Client) })), Client) as IDictionary; + ParseGeoPoint pointAgain = (ParseGeoPoint) deserialized[nameof(point)]; - Assert.AreEqual(1.234, pointAgain.Latitude); - Assert.AreEqual(1.234, pointAgain.Longitude); - } - - Thread.CurrentThread.CurrentCulture = initialCulture; + Assert.AreEqual(1.234, pointAgain.Latitude); + Assert.AreEqual(1.234, pointAgain.Longitude); } - [TestMethod] - public void TestGeoPointConstructor() - { - ParseGeoPoint point = new ParseGeoPoint(); - Assert.AreEqual(0.0, point.Latitude); - Assert.AreEqual(0.0, point.Longitude); + Thread.CurrentThread.CurrentCulture = initialCulture; + } - point = new ParseGeoPoint(42, 36); + [TestMethod] + public void TestGeoPointConstructor() + { + ParseGeoPoint point = new ParseGeoPoint(); + Assert.AreEqual(0.0, point.Latitude); + Assert.AreEqual(0.0, point.Longitude); - Assert.AreEqual(42.0, point.Latitude); - Assert.AreEqual(36.0, point.Longitude); + point = new ParseGeoPoint(42, 36); - point.Latitude = 12; - point.Longitude = 24; + Assert.AreEqual(42.0, point.Latitude); + Assert.AreEqual(36.0, point.Longitude); - Assert.AreEqual(12.0, point.Latitude); - Assert.AreEqual(24.0, point.Longitude); - } + point.Latitude = 12; + point.Longitude = 24; - [TestMethod] - public void TestGeoPointExceptionOutOfBounds() - { - Assert.ThrowsException(() => new ParseGeoPoint(90.01, 0.0)); - Assert.ThrowsException(() => new ParseGeoPoint(-90.01, 0.0)); - Assert.ThrowsException(() => new ParseGeoPoint(0.0, 180.01)); - Assert.ThrowsException(() => new ParseGeoPoint(0.0, -180.01)); - } + Assert.AreEqual(12.0, point.Latitude); + Assert.AreEqual(24.0, point.Longitude); + } - [TestMethod] - public void TestGeoDistanceInRadians() - { - double d2r = Math.PI / 180.0; - ParseGeoPoint pointA = new ParseGeoPoint(); - ParseGeoPoint pointB = new ParseGeoPoint(); - - // Zero - Assert.AreEqual(0.0, pointA.DistanceTo(pointB).Radians, 0.00001); - Assert.AreEqual(0.0, pointB.DistanceTo(pointA).Radians, 0.00001); - - // Wrap Long - pointA.Longitude = 179.0; - pointB.Longitude = -179.0; - Assert.AreEqual(2 * d2r, pointA.DistanceTo(pointB).Radians, 0.00001); - Assert.AreEqual(2 * d2r, pointB.DistanceTo(pointA).Radians, 0.00001); - - // North South Lat - pointA.Latitude = 89.0; - pointA.Longitude = 0; - pointB.Latitude = -89.0; - pointB.Longitude = 0; - Assert.AreEqual(178 * d2r, pointA.DistanceTo(pointB).Radians, 0.00001); - Assert.AreEqual(178 * d2r, pointB.DistanceTo(pointA).Radians, 0.00001); - - // Long wrap Lat - pointA.Latitude = 89.0; - pointA.Longitude = 0; - pointB.Latitude = -89.0; - pointB.Longitude = 179.999; - Assert.AreEqual(180 * d2r, pointA.DistanceTo(pointB).Radians, 0.00001); - Assert.AreEqual(180 * d2r, pointB.DistanceTo(pointA).Radians, 0.00001); - - pointA.Latitude = 79.0; - pointA.Longitude = 90.0; - pointB.Latitude = -79.0; - pointB.Longitude = -90.0; - Assert.AreEqual(180 * d2r, pointA.DistanceTo(pointB).Radians, 0.00001); - Assert.AreEqual(180 * d2r, pointB.DistanceTo(pointA).Radians, 0.00001); - - // Wrap near pole - somewhat ill conditioned case due to pole proximity - pointA.Latitude = 85.0; - pointA.Longitude = 90.0; - pointB.Latitude = 85.0; - pointB.Longitude = -90.0; - Assert.AreEqual(10 * d2r, pointA.DistanceTo(pointB).Radians, 0.00001); - Assert.AreEqual(10 * d2r, pointB.DistanceTo(pointA).Radians, 0.00001); - - // Reference cities - - // Sydney, Australia - pointA.Latitude = -34.0; - pointA.Longitude = 151.0; - // Buenos Aires, Argentina - pointB.Latitude = -34.5; - pointB.Longitude = -58.35; - Assert.AreEqual(1.85, pointA.DistanceTo(pointB).Radians, 0.01); - Assert.AreEqual(1.85, pointB.DistanceTo(pointA).Radians, 0.01); - } + [TestMethod] + public void TestGeoPointExceptionOutOfBounds() + { + Assert.ThrowsException(() => new ParseGeoPoint(90.01, 0.0)); + Assert.ThrowsException(() => new ParseGeoPoint(-90.01, 0.0)); + Assert.ThrowsException(() => new ParseGeoPoint(0.0, 180.01)); + Assert.ThrowsException(() => new ParseGeoPoint(0.0, -180.01)); + } + + [TestMethod] + public void TestGeoDistanceInRadians() + { + double d2r = Math.PI / 180.0; + ParseGeoPoint pointA = new ParseGeoPoint(); + ParseGeoPoint pointB = new ParseGeoPoint(); + + // Zero + Assert.AreEqual(0.0, pointA.DistanceTo(pointB).Radians, 0.00001); + Assert.AreEqual(0.0, pointB.DistanceTo(pointA).Radians, 0.00001); + + // Wrap Long + pointA.Longitude = 179.0; + pointB.Longitude = -179.0; + Assert.AreEqual(2 * d2r, pointA.DistanceTo(pointB).Radians, 0.00001); + Assert.AreEqual(2 * d2r, pointB.DistanceTo(pointA).Radians, 0.00001); + + // North South Lat + pointA.Latitude = 89.0; + pointA.Longitude = 0; + pointB.Latitude = -89.0; + pointB.Longitude = 0; + Assert.AreEqual(178 * d2r, pointA.DistanceTo(pointB).Radians, 0.00001); + Assert.AreEqual(178 * d2r, pointB.DistanceTo(pointA).Radians, 0.00001); + + // Long wrap Lat + pointA.Latitude = 89.0; + pointA.Longitude = 0; + pointB.Latitude = -89.0; + pointB.Longitude = 179.999; + Assert.AreEqual(180 * d2r, pointA.DistanceTo(pointB).Radians, 0.00001); + Assert.AreEqual(180 * d2r, pointB.DistanceTo(pointA).Radians, 0.00001); + + pointA.Latitude = 79.0; + pointA.Longitude = 90.0; + pointB.Latitude = -79.0; + pointB.Longitude = -90.0; + Assert.AreEqual(180 * d2r, pointA.DistanceTo(pointB).Radians, 0.00001); + Assert.AreEqual(180 * d2r, pointB.DistanceTo(pointA).Radians, 0.00001); + + // Wrap near pole - somewhat ill conditioned case due to pole proximity + pointA.Latitude = 85.0; + pointA.Longitude = 90.0; + pointB.Latitude = 85.0; + pointB.Longitude = -90.0; + Assert.AreEqual(10 * d2r, pointA.DistanceTo(pointB).Radians, 0.00001); + Assert.AreEqual(10 * d2r, pointB.DistanceTo(pointA).Radians, 0.00001); + + // Reference cities + + // Sydney, Australia + pointA.Latitude = -34.0; + pointA.Longitude = 151.0; + // Buenos Aires, Argentina + pointB.Latitude = -34.5; + pointB.Longitude = -58.35; + Assert.AreEqual(1.85, pointA.DistanceTo(pointB).Radians, 0.01); + Assert.AreEqual(1.85, pointB.DistanceTo(pointA).Radians, 0.01); } } diff --git a/Parse.Tests/InstallationIdControllerTests.cs b/Parse.Tests/InstallationIdControllerTests.cs index 99dc0d66..d05d3b13 100644 --- a/Parse.Tests/InstallationIdControllerTests.cs +++ b/Parse.Tests/InstallationIdControllerTests.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.CompilerServices; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -7,136 +6,95 @@ using Parse.Abstractions.Infrastructure; using Parse.Platform.Installations; -namespace Parse.Tests -{ -#warning Class refactoring may be required. - - [TestClass] - public class InstallationIdControllerTests - { - ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); - - [TestCleanup] - public void TearDown() => (Client.Services as ServiceHub).Reset(); - - [TestMethod] - public void TestConstructor() - { - Mock storageMock = new Mock(MockBehavior.Strict); - ParseInstallationController controller = new ParseInstallationController(storageMock.Object); - - // Make sure it didn't touch storageMock. - - storageMock.Verify(); - } - - [TestMethod] - [AsyncStateMachine(typeof(InstallationIdControllerTests))] - public Task TestGet() - { - Mock storageMock = new Mock(MockBehavior.Strict); - Mock> storageDictionary = new Mock>(); - - storageMock.Setup(s => s.LoadAsync()).Returns(Task.FromResult(storageDictionary.Object)); - - ParseInstallationController controller = new ParseInstallationController(storageMock.Object); - return controller.GetAsync().ContinueWith(installationIdTask => - { - Assert.IsFalse(installationIdTask.IsFaulted); +namespace Parse.Tests; - object verified = null; - storageDictionary.Verify(s => s.TryGetValue("InstallationId", out verified)); - storageDictionary.Verify(s => s.AddAsync("InstallationId", It.IsAny())); - - return controller.GetAsync().ContinueWith(newInstallationIdTask => - { - Assert.IsFalse(newInstallationIdTask.IsFaulted); - - // Ensure nothing more has happened with our dictionary. - storageDictionary.VerifyAll(); - - Assert.AreEqual(installationIdTask.Result, newInstallationIdTask.Result); - - return controller.ClearAsync(); - }).Unwrap().ContinueWith(clearTask => - { - Assert.IsFalse(clearTask.IsFaulted); +[TestClass] +public class InstallationIdControllerTests +{ + private ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); - storageDictionary.Verify(storage => storage.RemoveAsync("InstallationId")); + [TestCleanup] + public void TearDown() => (Client.Services as ServiceHub).Reset(); - return controller.GetAsync(); - }).Unwrap().ContinueWith(newInstallationIdTask => - { - Assert.IsFalse(newInstallationIdTask.IsFaulted); + [TestMethod] + public void TestConstructor() + { + var storageMock = new Mock(MockBehavior.Strict); + var controller = new ParseInstallationController(storageMock.Object); - Assert.AreNotEqual(installationIdTask.Result, newInstallationIdTask.Result); + // Ensure no interactions with the storageMock. + storageMock.Verify(); + } - storageDictionary.Verify(s => s.TryGetValue("InstallationId", out verified)); - storageDictionary.Verify(s => s.AddAsync("InstallationId", It.IsAny())); - }); - }).Unwrap(); - } + [TestMethod] + public async Task TestGetAsync() + { + var storageMock = new Mock(MockBehavior.Strict); + var storageDictionary = new Mock>(); - [TestMethod] - [AsyncStateMachine(typeof(InstallationIdControllerTests))] - public Task TestSet() - { - Mock storageMock = new Mock(MockBehavior.Strict); - Mock> storageDictionary = new Mock>(); + storageMock.Setup(s => s.LoadAsync()).ReturnsAsync(storageDictionary.Object); - storageMock.Setup(s => s.LoadAsync()).Returns(Task.FromResult(storageDictionary.Object)); + var controller = new ParseInstallationController(storageMock.Object); - ParseInstallationController controller = new ParseInstallationController(storageMock.Object); + // Initial Get + var installationId = await controller.GetAsync(); + Assert.IsNotNull(installationId); - return controller.GetAsync().ContinueWith(installationIdTask => - { - Assert.IsFalse(installationIdTask.IsFaulted); + object verified = null; + storageDictionary.Verify(s => s.TryGetValue("InstallationId", out verified)); + storageDictionary.Verify(s => s.AddAsync("InstallationId", It.IsAny())); - object verified = null; - storageDictionary.Verify(s => s.TryGetValue("InstallationId", out verified)); - storageDictionary.Verify(s => s.AddAsync("InstallationId", It.IsAny())); + // Second Get - Ensure same ID + var newInstallationId = await controller.GetAsync(); + Assert.AreEqual(installationId, newInstallationId); + storageDictionary.VerifyAll(); - Guid? installationId = installationIdTask.Result; - Guid installationId2 = Guid.NewGuid(); + // Clear and ensure new ID + await controller.ClearAsync(); + storageDictionary.Verify(s => s.RemoveAsync("InstallationId")); - return controller.SetAsync(installationId2).ContinueWith(setTask => - { - Assert.IsFalse(setTask.IsFaulted); + var clearedInstallationId = await controller.GetAsync(); + Assert.AreNotEqual(installationId, clearedInstallationId); + storageDictionary.Verify(s => s.TryGetValue("InstallationId", out verified)); + storageDictionary.Verify(s => s.AddAsync("InstallationId", It.IsAny())); + } - storageDictionary.Verify(s => s.AddAsync("InstallationId", installationId2.ToString())); + [TestMethod] + public async Task TestSetAsync() + { + var storageMock = new Mock(MockBehavior.Strict); + var storageDictionary = new Mock>(); - return controller.GetAsync(); - }).Unwrap().ContinueWith(installationId3Task => - { - Assert.IsFalse(installationId3Task.IsFaulted); + storageMock.Setup(s => s.LoadAsync()).ReturnsAsync(storageDictionary.Object); - storageDictionary.Verify(s => s.TryGetValue("InstallationId", out verified)); + var controller = new ParseInstallationController(storageMock.Object); - Guid? installationId3 = installationId3Task.Result; - Assert.AreEqual(installationId2, installationId3); + // Initial Get + var installationId = await controller.GetAsync(); + Assert.IsNotNull(installationId); - return controller.SetAsync(installationId); - }).Unwrap().ContinueWith(setTask => - { - Assert.IsFalse(setTask.IsFaulted); + object verified = null; + storageDictionary.Verify(s => s.TryGetValue("InstallationId", out verified)); + storageDictionary.Verify(s => s.AddAsync("InstallationId", It.IsAny())); - storageDictionary.Verify(s => s.AddAsync("InstallationId", installationId.ToString())); + // Set a new Installation ID + var newInstallationId = Guid.NewGuid(); + await controller.SetAsync(newInstallationId); + storageDictionary.Verify(s => s.AddAsync("InstallationId", newInstallationId.ToString())); - return controller.ClearAsync(); - }).Unwrap().ContinueWith(clearTask => - { - Assert.IsFalse(clearTask.IsFaulted); + // Verify Set ID matches Get + var retrievedInstallationId = await controller.GetAsync(); + Assert.AreEqual(newInstallationId, retrievedInstallationId); - storageDictionary.Verify(s => s.RemoveAsync("InstallationId")); + // Reset to original Installation ID + await controller.SetAsync(installationId); + storageDictionary.Verify(s => s.AddAsync("InstallationId", installationId.ToString())); - return controller.SetAsync(installationId2); - }).Unwrap().ContinueWith(setTask => - { - Assert.IsFalse(setTask.IsFaulted); + // Clear and set new ID + await controller.ClearAsync(); + storageDictionary.Verify(s => s.RemoveAsync("InstallationId")); - storageDictionary.Verify(s => s.AddAsync("InstallationId", installationId2.ToString())); - }); - }).Unwrap(); - } + await controller.SetAsync(newInstallationId); + storageDictionary.Verify(s => s.AddAsync("InstallationId", newInstallationId.ToString())); } } diff --git a/Parse.Tests/InstallationTests.cs b/Parse.Tests/InstallationTests.cs index 88516ec5..196e152b 100644 --- a/Parse.Tests/InstallationTests.cs +++ b/Parse.Tests/InstallationTests.cs @@ -6,149 +6,246 @@ using Moq; using Parse.Abstractions.Infrastructure; using Parse.Abstractions.Platform.Installations; +using Parse.Abstractions.Platform.Objects; +using Parse.Abstractions.Platform.Queries; using Parse.Infrastructure; using Parse.Platform.Objects; -namespace Parse.Tests +namespace Parse.Tests; + +[TestClass] +public class InstallationTests { - [TestClass] - public class InstallationTests + private ParseClient Client { get; set; } + private Mock ServiceHubMock { get; set; } + private Mock ClassControllerMock { get; set; } + private Mock CurrentInstallationControllerMock { get; set; } + + [TestInitialize] + public void SetUp() { - ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); + // Initialize mocks + ServiceHubMock = new Mock(MockBehavior.Strict); + ClassControllerMock = new Mock(MockBehavior.Strict); + CurrentInstallationControllerMock = new Mock(MockBehavior.Strict); + + // Mock ClassController behavior + ClassControllerMock.Setup(controller => controller.Instantiate(It.IsAny(), It.IsAny())) + .Returns((className, hub) => new ParseInstallation().Bind(hub) as ParseObject); + + ClassControllerMock.Setup(controller => controller.GetClassMatch("_Installation", typeof(ParseInstallation))) + .Returns(true); + + ClassControllerMock.Setup(controller => controller.GetPropertyMappings("_Installation")) + .Returns(new Dictionary + { + { nameof(ParseInstallation.InstallationId), "installationId" }, + { nameof(ParseInstallation.DeviceType), "deviceType" }, + { nameof(ParseInstallation.AppName), "appName" }, + { nameof(ParseInstallation.AppVersion), "appVersion" }, + { nameof(ParseInstallation.AppIdentifier), "appIdentifier" }, + { nameof(ParseInstallation.TimeZone), "timeZone" }, + { nameof(ParseInstallation.LocaleIdentifier), "localeIdentifier" }, + { nameof(ParseInstallation.Channels), "channels" } + }); + + // Mock GetClassName + ClassControllerMock.Setup(controller => controller.GetClassName(typeof(ParseInstallation))) + .Returns("_Installation"); + + ClassControllerMock.Setup(controller => controller.AddValid(It.IsAny())) + .Verifiable(); + + ServiceHubMock.Setup(hub => hub.ClassController).Returns(ClassControllerMock.Object); + ServiceHubMock.Setup(hub => hub.CurrentInstallationController).Returns(CurrentInstallationControllerMock.Object); + + // Create ParseClient with mocked ServiceHub + Client = new ParseClient(new ServerConnectionData { Test = true }) + { + Services = ServiceHubMock.Object + }; - [TestInitialize] - public void SetUp() => Client.AddValidClass(); + // Publicize the client to set ParseClient.Instance + Client.Publicize(); - [TestCleanup] - public void TearDown() => (Client.Services as ServiceHub).Reset(); + // Add valid classes to the client + Client.AddValidClass(); + } - [TestMethod] - public void TestGetInstallationQuery() => Assert.IsInstanceOfType(Client.GetInstallationQuery(), typeof(ParseQuery)); - [TestMethod] - public void TestInstallationIdGetterSetter() - { - Guid guid = Guid.NewGuid(); - ParseInstallation installation = Client.GenerateObjectFromState(new MutableObjectState { ServerData = new Dictionary { ["installationId"] = guid.ToString() } }, "_Installation"); - Assert.IsNotNull(installation); - Assert.AreEqual(guid, installation.InstallationId); - Guid newGuid = Guid.NewGuid(); - Assert.ThrowsException(() => installation["installationId"] = newGuid); - installation.SetIfDifferent("installationId", newGuid.ToString()); - Assert.AreEqual(newGuid, installation.InstallationId); - } + [TestCleanup] + public void TearDown() + { + (Client.Services as ServiceHub)?.Reset(); + } - [TestMethod] - public void TestDeviceTypeGetterSetter() - { - ParseInstallation installation = Client.GenerateObjectFromState(new MutableObjectState { ServerData = new Dictionary { ["deviceType"] = "parseOS" } }, "_Installation"); + [TestMethod] + public void TestInstallationPropertyMappings() + { + var mappings = Client.Services.ClassController.GetPropertyMappings("_Installation"); + Assert.IsNotNull(mappings); + Assert.AreEqual("installationId", mappings[nameof(ParseInstallation.InstallationId)]); + Assert.AreEqual("appName", mappings[nameof(ParseInstallation.AppName)]); + Assert.AreEqual("appIdentifier", mappings[nameof(ParseInstallation.AppIdentifier)]); + Assert.AreEqual("channels", mappings[nameof(ParseInstallation.Channels)]); + } - Assert.IsNotNull(installation); - Assert.AreEqual("parseOS", installation.DeviceType); + [TestMethod] + public void TestGetInstallationQuery() + { + // Act: Get the query + var query = Client.GetInstallationQuery(); - Assert.ThrowsException(() => installation["deviceType"] = "gogoOS"); + // Assert: Verify the query type and class name + Assert.IsInstanceOfType(query, typeof(ParseQuery)); + Assert.AreEqual("_Installation", query.ClassName); - installation.SetIfDifferent("deviceType", "gogoOS"); - Assert.AreEqual("gogoOS", installation.DeviceType); - } + // Verify that GetClassName was called to resolve the class name + ClassControllerMock.Verify(controller => controller.GetClassName(typeof(ParseInstallation)), Times.Once); - [TestMethod] - public void TestAppNameGetterSetter() - { - ParseInstallation installation = Client.GenerateObjectFromState(new MutableObjectState { ServerData = new Dictionary { ["appName"] = "parseApp" } }, "_Installation"); + // Verify AddValid was called for ParseInstallation + ClassControllerMock.Verify(controller => controller.AddValid(typeof(ParseInstallation)), Times.AtLeastOnce); + } - Assert.IsNotNull(installation); - Assert.AreEqual("parseApp", installation.AppName); - Assert.ThrowsException(() => installation["appName"] = "gogoApp"); - installation.SetIfDifferent("appName", "gogoApp"); - Assert.AreEqual("gogoApp", installation.AppName); - } + [TestMethod] + public void TestInstallationIdGetterSetter() + { + Guid guid = Guid.NewGuid(); + ParseInstallation installation = Client.GenerateObjectFromState( + new MutableObjectState { ServerData = new Dictionary { ["installationId"] = guid.ToString() } }, + "_Installation"); - [TestMethod] - public void TestAppVersionGetterSetter() - { - ParseInstallation installation = Client.GenerateObjectFromState(new MutableObjectState { ServerData = new Dictionary { ["appVersion"] = "1.2.3" } }, "_Installation"); + Assert.IsNotNull(installation); + Assert.AreEqual(guid, installation.InstallationId); - Assert.IsNotNull(installation); - Assert.AreEqual("1.2.3", installation.AppVersion); + Guid newGuid = Guid.NewGuid(); + Assert.ThrowsException(() => installation["installationId"] = newGuid); - Assert.ThrowsException(() => installation["appVersion"] = "1.2.4"); + installation.SetIfDifferent("installationId", newGuid.ToString()); + Assert.AreEqual(newGuid, installation.InstallationId); + } - installation.SetIfDifferent("appVersion", "1.2.4"); - Assert.AreEqual("1.2.4", installation.AppVersion); - } + [TestMethod] + public void TestDeviceTypeGetterSetter() + { + ParseInstallation installation = Client.GenerateObjectFromState( + new MutableObjectState { ServerData = new Dictionary { ["deviceType"] = "parseOS" } }, + "_Installation"); - [TestMethod] - public void TestAppIdentifierGetterSetter() - { - ParseInstallation installation = Client.GenerateObjectFromState(new MutableObjectState { ServerData = new Dictionary { ["appIdentifier"] = "com.parse.app" } }, "_Installation"); + Assert.IsNotNull(installation); + Assert.AreEqual("parseOS", installation.DeviceType); - Assert.IsNotNull(installation); - Assert.AreEqual("com.parse.app", installation.AppIdentifier); + Assert.ThrowsException(() => installation["deviceType"] = "gogoOS"); - Assert.ThrowsException(() => installation["appIdentifier"] = "com.parse.newapp"); + installation.SetIfDifferent("deviceType", "gogoOS"); + Assert.AreEqual("gogoOS", installation.DeviceType); + } - installation.SetIfDifferent("appIdentifier", "com.parse.newapp"); - Assert.AreEqual("com.parse.newapp", installation.AppIdentifier); - } + [TestMethod] + public void TestAppNameGetterSetter() + { + ParseInstallation installation = Client.GenerateObjectFromState( + new MutableObjectState { ServerData = new Dictionary { ["appName"] = "parseApp" } }, + "_Installation"); - [TestMethod] - public void TestTimeZoneGetter() - { - ParseInstallation installation = Client.GenerateObjectFromState(new MutableObjectState { ServerData = new Dictionary { ["timeZone"] = "America/Los_Angeles" } }, "_Installation"); + Assert.IsNotNull(installation); + Assert.AreEqual("parseApp", installation.AppName); - Assert.IsNotNull(installation); - Assert.AreEqual("America/Los_Angeles", installation.TimeZone); - } + Assert.ThrowsException(() => installation["appName"] = "gogoApp"); - [TestMethod] - public void TestLocaleIdentifierGetter() - { - ParseInstallation installation = Client.GenerateObjectFromState(new MutableObjectState { ServerData = new Dictionary { ["localeIdentifier"] = "en-US" } }, "_Installation"); + installation.SetIfDifferent("appName", "gogoApp"); + Assert.AreEqual("gogoApp", installation.AppName); + } - Assert.IsNotNull(installation); - Assert.AreEqual("en-US", installation.LocaleIdentifier); - } + [TestMethod] + public void TestAppVersionGetterSetter() + { + ParseInstallation installation = Client.GenerateObjectFromState( + new MutableObjectState { ServerData = new Dictionary { ["appVersion"] = "1.2.3" } }, + "_Installation"); - [TestMethod] - public void TestChannelGetterSetter() - { - ParseInstallation installation = Client.GenerateObjectFromState(new MutableObjectState { ServerData = new Dictionary { ["channels"] = new List { "the", "richard" } } }, "_Installation"); + Assert.IsNotNull(installation); + Assert.AreEqual("1.2.3", installation.AppVersion); - Assert.IsNotNull(installation); - Assert.AreEqual("the", installation.Channels[0]); - Assert.AreEqual("richard", installation.Channels[1]); + Assert.ThrowsException(() => installation["appVersion"] = "1.2.4"); - installation.Channels = new List { "mr", "kevin" }; + installation.SetIfDifferent("appVersion", "1.2.4"); + Assert.AreEqual("1.2.4", installation.AppVersion); + } - Assert.AreEqual("mr", installation.Channels[0]); - Assert.AreEqual("kevin", installation.Channels[1]); - } + [TestMethod] + public void TestAppIdentifierGetterSetter() + { + ParseInstallation installation = Client.GenerateObjectFromState( + new MutableObjectState { ServerData = new Dictionary { ["appIdentifier"] = "com.parse.app" } }, + "_Installation"); - [TestMethod] - public void TestGetCurrentInstallation() - { - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + Assert.IsNotNull(installation); + Assert.AreEqual("com.parse.app", installation.AppIdentifier); + + Assert.ThrowsException(() => installation["appIdentifier"] = "com.parse.newapp"); + + installation.SetIfDifferent("appIdentifier", "com.parse.newapp"); + Assert.AreEqual("com.parse.newapp", installation.AppIdentifier); + } + + [TestMethod] + public void TestTimeZoneGetter() + { + ParseInstallation installation = Client.GenerateObjectFromState( + new MutableObjectState { ServerData = new Dictionary { ["timeZone"] = "America/Los_Angeles" } }, + "_Installation"); - Guid guid = Guid.NewGuid(); + Assert.IsNotNull(installation); + Assert.AreEqual("America/Los_Angeles", installation.TimeZone); + } + + [TestMethod] + public void TestLocaleIdentifierGetter() + { + ParseInstallation installation = Client.GenerateObjectFromState( + new MutableObjectState { ServerData = new Dictionary { ["localeIdentifier"] = "en-US" } }, + "_Installation"); - ParseInstallation installation = client.GenerateObjectFromState(new MutableObjectState { ServerData = new Dictionary { ["installationId"] = guid.ToString() } }, "_Installation"); + Assert.IsNotNull(installation); + Assert.AreEqual("en-US", installation.LocaleIdentifier); + } + + [TestMethod] + public void TestChannelGetterSetter() + { + ParseInstallation installation = Client.GenerateObjectFromState( + new MutableObjectState { ServerData = new Dictionary { ["channels"] = new List { "the", "richard" } } }, + "_Installation"); - Mock mockController = new Mock(); - mockController.Setup(obj => obj.GetAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(installation)); + Assert.IsNotNull(installation); + Assert.AreEqual("the", installation.Channels[0]); + Assert.AreEqual("richard", installation.Channels[1]); + + installation.Channels = new List { "mr", "kevin" }; + + Assert.AreEqual("mr", installation.Channels[0]); + Assert.AreEqual("kevin", installation.Channels[1]); + } + + [TestMethod] + public async void TestGetCurrentInstallation() + { + var guid = Guid.NewGuid(); + var expectedInstallation = new ParseInstallation(); + expectedInstallation.SetIfDifferent("installationId", guid.ToString()); - hub.CurrentInstallationController = mockController.Object; + CurrentInstallationControllerMock.Setup(controller => controller.GetAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(expectedInstallation)); - ParseInstallation currentInstallation = client.GetCurrentInstallation(); + ParseInstallation currentInstallation = await Client.GetCurrentInstallation(); - Assert.IsNotNull(currentInstallation); - Assert.AreEqual(guid, currentInstallation.InstallationId); - } + Assert.IsNotNull(currentInstallation); + Assert.AreEqual(guid, currentInstallation.InstallationId); } } diff --git a/Parse.Tests/JsonTests.cs b/Parse.Tests/JsonTests.cs index 49688cb1..ecb3fc25 100644 --- a/Parse.Tests/JsonTests.cs +++ b/Parse.Tests/JsonTests.cs @@ -5,275 +5,283 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Parse.Infrastructure.Utilities; -namespace Parse.Tests +namespace Parse.Tests; + +[TestClass] +public class JsonTests { - [TestClass] - public class JsonTests + [TestMethod] + public void TestEmptyJsonStringFail() + { + var result = JsonUtilities.Parse(""); + Assert.IsNotNull(result); + Assert.IsInstanceOfType(result, typeof(Dictionary)); + Assert.AreEqual(0, ((Dictionary) result).Count); + } + + [TestMethod] //updated + public void TestInvalidJsonStringAsRootFail() + { + // Expect empty dictionary for whitespace inputs + Assert.IsInstanceOfType(JsonUtilities.Parse("\n"), typeof(Dictionary)); + Assert.IsInstanceOfType(JsonUtilities.Parse("\t"), typeof(Dictionary)); + Assert.IsInstanceOfType(JsonUtilities.Parse(" "), typeof(Dictionary)); + + // Expect exceptions for invalid JSON strings + Assert.ThrowsException(() => JsonUtilities.Parse("a")); + Assert.ThrowsException(() => JsonUtilities.Parse("abc")); + Assert.ThrowsException(() => JsonUtilities.Parse("\u1234")); + Assert.ThrowsException(() => JsonUtilities.Parse("1234")); + Assert.ThrowsException(() => JsonUtilities.Parse("1,3")); + Assert.ThrowsException(() => JsonUtilities.Parse("{1")); + Assert.ThrowsException(() => JsonUtilities.Parse("3}")); + Assert.ThrowsException(() => JsonUtilities.Parse("}")); + } + + + [TestMethod] + public void TestEmptyJsonObject() => Assert.IsTrue(JsonUtilities.Parse("{}") is IDictionary); + + [TestMethod] + public void TestEmptyJsonArray() => Assert.IsTrue(JsonUtilities.Parse("[]") is IList); + + [TestMethod] + public void TestOneJsonObject() + { + Assert.ThrowsException(() => JsonUtilities.Parse("{ 1 }")); + Assert.ThrowsException(() => JsonUtilities.Parse("{ 1 : 1 }")); + Assert.ThrowsException(() => JsonUtilities.Parse("{ 1 : \"abc\" }")); + + object parsed = JsonUtilities.Parse("{\"abc\" : \"def\"}"); + Assert.IsTrue(parsed is IDictionary); + IDictionary parsedDict = parsed as IDictionary; + Assert.AreEqual("def", parsedDict["abc"]); + + parsed = JsonUtilities.Parse("{\"abc\" : {} }"); + Assert.IsTrue(parsed is IDictionary); + parsedDict = parsed as IDictionary; + Assert.IsTrue(parsedDict["abc"] is IDictionary); + + parsed = JsonUtilities.Parse("{\"abc\" : \"6060\"}"); + Assert.IsTrue(parsed is IDictionary); + parsedDict = parsed as IDictionary; + Assert.AreEqual("6060", parsedDict["abc"]); + + parsed = JsonUtilities.Parse("{\"\" : \"\"}"); + Assert.IsTrue(parsed is IDictionary); + parsedDict = parsed as IDictionary; + Assert.AreEqual("", parsedDict[""]); + + parsed = JsonUtilities.Parse("{\" abc\" : \"def \"}"); + Assert.IsTrue(parsed is IDictionary); + parsedDict = parsed as IDictionary; + Assert.AreEqual("def ", parsedDict[" abc"]); + + parsed = JsonUtilities.Parse("{\"1\" : 6060}"); + Assert.IsTrue(parsed is IDictionary); + parsedDict = parsed as IDictionary; + Assert.AreEqual((long) 6060, parsedDict["1"]); + + parsed = JsonUtilities.Parse("{\"1\" : null}"); + Assert.IsTrue(parsed is IDictionary); + parsedDict = parsed as IDictionary; + Assert.IsNull(parsedDict["1"]); + + parsed = JsonUtilities.Parse("{\"1\" : true}"); + Assert.IsTrue(parsed is IDictionary); + parsedDict = parsed as IDictionary; + Assert.IsTrue((bool) parsedDict["1"]); + + parsed = JsonUtilities.Parse("{\"1\" : false}"); + Assert.IsTrue(parsed is IDictionary); + parsedDict = parsed as IDictionary; + Assert.IsFalse((bool) parsedDict["1"]); + } + + [TestMethod] + public void TestMultipleJsonObjectAsRootFail() + { + Assert.ThrowsException(() => JsonUtilities.Parse("{},")); + Assert.ThrowsException(() => JsonUtilities.Parse("{\"abc\" : \"def\"},")); + Assert.ThrowsException(() => JsonUtilities.Parse("{\"abc\" : \"def\" \"def\"}")); + Assert.ThrowsException(() => JsonUtilities.Parse("{}, {}")); + Assert.ThrowsException(() => JsonUtilities.Parse("{},\n{}")); + } + + [TestMethod] + public void TestOneJsonArray() + { + Assert.ThrowsException(() => JsonUtilities.Parse("[ 1 : 1 ]")); + Assert.ThrowsException(() => JsonUtilities.Parse("[ 1 1 ]")); + Assert.ThrowsException(() => JsonUtilities.Parse("[ 1 : \"1\" ]")); + Assert.ThrowsException(() => JsonUtilities.Parse("[ \"1\" : \"1\" ]")); + + object parsed = JsonUtilities.Parse("[ 1 ]"); + Assert.IsTrue(parsed is IList); + IList parsedList = parsed as IList; + Assert.AreEqual((long) 1, parsedList[0]); + + parsed = JsonUtilities.Parse("[ \n ]"); + Assert.IsTrue(parsed is IList); + parsedList = parsed as IList; + Assert.AreEqual(0, parsedList.Count); + + parsed = JsonUtilities.Parse("[ \"asdf\" ]"); + Assert.IsTrue(parsed is IList); + parsedList = parsed as IList; + Assert.AreEqual("asdf", parsedList[0]); + + parsed = JsonUtilities.Parse("[ \"\u849c\" ]"); + Assert.IsTrue(parsed is IList); + parsedList = parsed as IList; + Assert.AreEqual("\u849c", parsedList[0]); + } + + [TestMethod] + public void TestMultipleJsonArrayAsRootFail() + { + Assert.ThrowsException(() => JsonUtilities.Parse("[],")); + Assert.ThrowsException(() => JsonUtilities.Parse("[\"abc\" : \"def\"],")); + Assert.ThrowsException(() => JsonUtilities.Parse("[], []")); + Assert.ThrowsException(() => JsonUtilities.Parse("[],\n[]")); + } + + [TestMethod] + public void TestJsonArrayInsideJsonObject() + { + Assert.ThrowsException(() => JsonUtilities.Parse("{ [] }")); + Assert.ThrowsException(() => JsonUtilities.Parse("{ [], [] }")); + Assert.ThrowsException(() => JsonUtilities.Parse("{ \"abc\": [], [] }")); + + object parsed = JsonUtilities.Parse("{ \"abc\": [] }"); + Assert.IsTrue(parsed is IDictionary); + IDictionary parsedDict = parsed as IDictionary; + Assert.IsTrue(parsedDict["abc"] is IList); + + parsed = JsonUtilities.Parse("{ \"6060\" :\n[ 6060 ]\t}"); + Assert.IsTrue(parsed is IDictionary); + parsedDict = parsed as IDictionary; + Assert.IsTrue(parsedDict["6060"] is IList); + IList parsedList = parsedDict["6060"] as IList; + Assert.AreEqual((long) 6060, parsedList[0]); + } + + [TestMethod] + public void TestJsonObjectInsideJsonArray() + { + Assert.ThrowsException(() => JsonUtilities.Parse("[ {} : {} ]")); + + // whitespace test + object parsed = JsonUtilities.Parse("[\t\n{}\r\t]"); + Assert.IsTrue(parsed is IList); + IList parsedList = parsed as IList; + Assert.IsTrue(parsedList[0] is IDictionary); + + parsed = JsonUtilities.Parse("[ {}, { \"final\" : \"fantasy\"} ]"); + Assert.IsTrue(parsed is IList); + parsedList = parsed as IList; + Assert.IsTrue(parsedList[0] is IDictionary); + Assert.IsTrue(parsedList[1] is IDictionary); + IDictionary parsedDictionary = parsedList[1] as IDictionary; + Assert.AreEqual("fantasy", parsedDictionary["final"]); + } + + [TestMethod] + public void TestJsonObjectWithElements() { - [TestMethod] - public void TestEmptyJsonStringFail() => Assert.ThrowsException(() => JsonUtilities.Parse("")); - - [TestMethod] - public void TestInvalidJsonStringAsRootFail() - { - Assert.ThrowsException(() => JsonUtilities.Parse("\n")); - Assert.ThrowsException(() => JsonUtilities.Parse("a")); - Assert.ThrowsException(() => JsonUtilities.Parse("abc")); - Assert.ThrowsException(() => JsonUtilities.Parse("\u1234")); - Assert.ThrowsException(() => JsonUtilities.Parse("\t")); - Assert.ThrowsException(() => JsonUtilities.Parse("\t\n\r")); - Assert.ThrowsException(() => JsonUtilities.Parse(" ")); - Assert.ThrowsException(() => JsonUtilities.Parse("1234")); - Assert.ThrowsException(() => JsonUtilities.Parse("1,3")); - Assert.ThrowsException(() => JsonUtilities.Parse("{1")); - Assert.ThrowsException(() => JsonUtilities.Parse("3}")); - Assert.ThrowsException(() => JsonUtilities.Parse("}")); - } - - [TestMethod] - public void TestEmptyJsonObject() => Assert.IsTrue(JsonUtilities.Parse("{}") is IDictionary); - - [TestMethod] - public void TestEmptyJsonArray() => Assert.IsTrue(JsonUtilities.Parse("[]") is IList); - - [TestMethod] - public void TestOneJsonObject() - { - Assert.ThrowsException(() => JsonUtilities.Parse("{ 1 }")); - Assert.ThrowsException(() => JsonUtilities.Parse("{ 1 : 1 }")); - Assert.ThrowsException(() => JsonUtilities.Parse("{ 1 : \"abc\" }")); - - object parsed = JsonUtilities.Parse("{\"abc\" : \"def\"}"); - Assert.IsTrue(parsed is IDictionary); - IDictionary parsedDict = parsed as IDictionary; - Assert.AreEqual("def", parsedDict["abc"]); - - parsed = JsonUtilities.Parse("{\"abc\" : {} }"); - Assert.IsTrue(parsed is IDictionary); - parsedDict = parsed as IDictionary; - Assert.IsTrue(parsedDict["abc"] is IDictionary); - - parsed = JsonUtilities.Parse("{\"abc\" : \"6060\"}"); - Assert.IsTrue(parsed is IDictionary); - parsedDict = parsed as IDictionary; - Assert.AreEqual("6060", parsedDict["abc"]); - - parsed = JsonUtilities.Parse("{\"\" : \"\"}"); - Assert.IsTrue(parsed is IDictionary); - parsedDict = parsed as IDictionary; - Assert.AreEqual("", parsedDict[""]); - - parsed = JsonUtilities.Parse("{\" abc\" : \"def \"}"); - Assert.IsTrue(parsed is IDictionary); - parsedDict = parsed as IDictionary; - Assert.AreEqual("def ", parsedDict[" abc"]); - - parsed = JsonUtilities.Parse("{\"1\" : 6060}"); - Assert.IsTrue(parsed is IDictionary); - parsedDict = parsed as IDictionary; - Assert.AreEqual((long) 6060, parsedDict["1"]); - - parsed = JsonUtilities.Parse("{\"1\" : null}"); - Assert.IsTrue(parsed is IDictionary); - parsedDict = parsed as IDictionary; - Assert.IsNull(parsedDict["1"]); - - parsed = JsonUtilities.Parse("{\"1\" : true}"); - Assert.IsTrue(parsed is IDictionary); - parsedDict = parsed as IDictionary; - Assert.IsTrue((bool) parsedDict["1"]); - - parsed = JsonUtilities.Parse("{\"1\" : false}"); - Assert.IsTrue(parsed is IDictionary); - parsedDict = parsed as IDictionary; - Assert.IsFalse((bool) parsedDict["1"]); - } - - [TestMethod] - public void TestMultipleJsonObjectAsRootFail() - { - Assert.ThrowsException(() => JsonUtilities.Parse("{},")); - Assert.ThrowsException(() => JsonUtilities.Parse("{\"abc\" : \"def\"},")); - Assert.ThrowsException(() => JsonUtilities.Parse("{\"abc\" : \"def\" \"def\"}")); - Assert.ThrowsException(() => JsonUtilities.Parse("{}, {}")); - Assert.ThrowsException(() => JsonUtilities.Parse("{},\n{}")); - } - - [TestMethod] - public void TestOneJsonArray() - { - Assert.ThrowsException(() => JsonUtilities.Parse("[ 1 : 1 ]")); - Assert.ThrowsException(() => JsonUtilities.Parse("[ 1 1 ]")); - Assert.ThrowsException(() => JsonUtilities.Parse("[ 1 : \"1\" ]")); - Assert.ThrowsException(() => JsonUtilities.Parse("[ \"1\" : \"1\" ]")); - - object parsed = JsonUtilities.Parse("[ 1 ]"); - Assert.IsTrue(parsed is IList); - IList parsedList = parsed as IList; - Assert.AreEqual((long) 1, parsedList[0]); - - parsed = JsonUtilities.Parse("[ \n ]"); - Assert.IsTrue(parsed is IList); - parsedList = parsed as IList; - Assert.AreEqual(0, parsedList.Count); - - parsed = JsonUtilities.Parse("[ \"asdf\" ]"); - Assert.IsTrue(parsed is IList); - parsedList = parsed as IList; - Assert.AreEqual("asdf", parsedList[0]); - - parsed = JsonUtilities.Parse("[ \"\u849c\" ]"); - Assert.IsTrue(parsed is IList); - parsedList = parsed as IList; - Assert.AreEqual("\u849c", parsedList[0]); - } - - [TestMethod] - public void TestMultipleJsonArrayAsRootFail() - { - Assert.ThrowsException(() => JsonUtilities.Parse("[],")); - Assert.ThrowsException(() => JsonUtilities.Parse("[\"abc\" : \"def\"],")); - Assert.ThrowsException(() => JsonUtilities.Parse("[], []")); - Assert.ThrowsException(() => JsonUtilities.Parse("[],\n[]")); - } - - [TestMethod] - public void TestJsonArrayInsideJsonObject() - { - Assert.ThrowsException(() => JsonUtilities.Parse("{ [] }")); - Assert.ThrowsException(() => JsonUtilities.Parse("{ [], [] }")); - Assert.ThrowsException(() => JsonUtilities.Parse("{ \"abc\": [], [] }")); - - object parsed = JsonUtilities.Parse("{ \"abc\": [] }"); - Assert.IsTrue(parsed is IDictionary); - IDictionary parsedDict = parsed as IDictionary; - Assert.IsTrue(parsedDict["abc"] is IList); - - parsed = JsonUtilities.Parse("{ \"6060\" :\n[ 6060 ]\t}"); - Assert.IsTrue(parsed is IDictionary); - parsedDict = parsed as IDictionary; - Assert.IsTrue(parsedDict["6060"] is IList); - IList parsedList = parsedDict["6060"] as IList; - Assert.AreEqual((long) 6060, parsedList[0]); - } - - [TestMethod] - public void TestJsonObjectInsideJsonArray() - { - Assert.ThrowsException(() => JsonUtilities.Parse("[ {} : {} ]")); - - // whitespace test - object parsed = JsonUtilities.Parse("[\t\n{}\r\t]"); - Assert.IsTrue(parsed is IList); - IList parsedList = parsed as IList; - Assert.IsTrue(parsedList[0] is IDictionary); - - parsed = JsonUtilities.Parse("[ {}, { \"final\" : \"fantasy\"} ]"); - Assert.IsTrue(parsed is IList); - parsedList = parsed as IList; - Assert.IsTrue(parsedList[0] is IDictionary); - Assert.IsTrue(parsedList[1] is IDictionary); - IDictionary parsedDictionary = parsedList[1] as IDictionary; - Assert.AreEqual("fantasy", parsedDictionary["final"]); - } - - [TestMethod] - public void TestJsonObjectWithElements() - { - // Just make sure they don't throw exception as we already check their content correctness - // in other unit tests. - JsonUtilities.Parse("{ \"mura\": \"masa\" }"); - JsonUtilities.Parse("{ \"mura\": 1234 }"); - JsonUtilities.Parse("{ \"mura\": { \"masa\": 1234 } }"); - JsonUtilities.Parse("{ \"mura\": { \"masa\": [ 1234 ] } }"); - JsonUtilities.Parse("{ \"mura\": { \"masa\": [ 1234 ] }, \"arr\": [] }"); - } - - [TestMethod] - public void TestJsonArrayWithElements() - { - // Just make sure they don't throw exception as we already check their content correctness - // in other unit tests. - JsonUtilities.Parse("[ \"mura\" ]"); - JsonUtilities.Parse("[ \"\u1234\" ]"); - JsonUtilities.Parse("[ \"\u1234ff\", \"\u1234\" ]"); - JsonUtilities.Parse("[ [], [], [], [] ]"); - JsonUtilities.Parse("[ [], [ {}, {} ], [ {} ], [] ]"); - } - - [TestMethod] - public void TestEncodeJson() - { - Dictionary dict = new Dictionary(); - string encoded = JsonUtilities.Encode(dict); - Assert.AreEqual("{}", encoded); - - List list = new List(); - encoded = JsonUtilities.Encode(list); - Assert.AreEqual("[]", encoded); - - Dictionary dictChild = new Dictionary(); - list.Add(dictChild); - encoded = JsonUtilities.Encode(list); - Assert.AreEqual("[{}]", encoded); - - list.Add("1234 a\t\r\n"); - list.Add(1234); - list.Add(12.34); - list.Add(1.23456789123456789); - encoded = JsonUtilities.Encode(list); - - // This string should be [{},\"1234 a\\t\\r\\n\",1234,12.34,1.23456789123457] for .NET Framework (https://github.com/dotnet/runtime/issues/31483). - - Assert.AreEqual("[{},\"1234 a\\t\\r\\n\",1234,12.34,1.234567891234568]", encoded); - - dict["arr"] = new List(); - encoded = JsonUtilities.Encode(dict); - Assert.AreEqual("{\"arr\":[]}", encoded); - - dict["\u1234"] = "\u1234"; - encoded = JsonUtilities.Encode(dict); - Assert.AreEqual("{\"arr\":[],\"\u1234\":\"\u1234\"}", encoded); - - encoded = JsonUtilities.Encode(new List { true, false, null }); - Assert.AreEqual("[true,false,null]", encoded); - } - - [TestMethod] - public void TestSpecialJsonNumbersAndModifiers() - { - Assert.ThrowsException(() => JsonUtilities.Parse("+123456789")); - - JsonUtilities.Parse("{ \"mura\": -123456789123456789 }"); - JsonUtilities.Parse("{ \"mura\": 1.1234567891234567E308 }"); - JsonUtilities.Parse("{ \"PI\": 3.141e-10 }"); - JsonUtilities.Parse("{ \"PI\": 3.141E-10 }"); - - Assert.AreEqual(123456789123456789, (JsonUtilities.Parse("{ \"mura\": 123456789123456789 }") as IDictionary)["mura"]); - } - - - [TestMethod] - public void TestJsonNumbersAndValueRanges() - { - //Assert.ThrowsException(() => JsonUtilities.Parse("+123456789")); - Assert.IsInstanceOfType((JsonUtilities.Parse("{ \"long\": " + long.MaxValue + " }") as IDictionary)["long"], typeof(long)); - Assert.IsInstanceOfType((JsonUtilities.Parse("{ \"long\": " + long.MinValue + " }") as IDictionary)["long"], typeof(long)); - - Assert.AreEqual((JsonUtilities.Parse("{ \"long\": " + long.MaxValue + " }") as IDictionary)["long"], long.MaxValue); - Assert.AreEqual((JsonUtilities.Parse("{ \"long\": " + long.MinValue + " }") as IDictionary)["long"], long.MinValue); - - - Assert.IsInstanceOfType((JsonUtilities.Parse("{ \"double\": " + double.MaxValue.ToString(CultureInfo.InvariantCulture) + " }") as IDictionary)["double"], typeof(double)); - Assert.IsInstanceOfType((JsonUtilities.Parse("{ \"double\": " + double.MinValue.ToString(CultureInfo.InvariantCulture) + " }") as IDictionary)["double"], typeof(double)); - - Assert.AreEqual((JsonUtilities.Parse("{ \"double\": " + double.MaxValue.ToString(CultureInfo.InvariantCulture) + " }") as IDictionary)["double"], double.MaxValue); - Assert.AreEqual((JsonUtilities.Parse("{ \"double\": " + double.MinValue.ToString(CultureInfo.InvariantCulture) + " }") as IDictionary)["double"], double.MinValue); - - double outOfInt64RangeValue = -9223372036854776000d; - Assert.IsInstanceOfType((JsonUtilities.Parse("{ \"double\": " + outOfInt64RangeValue.ToString(CultureInfo.InvariantCulture) + " }") as IDictionary)["double"], typeof(double)); - Assert.AreEqual((JsonUtilities.Parse("{ \"double\": " + outOfInt64RangeValue.ToString(CultureInfo.InvariantCulture) + " }") as IDictionary)["double"], outOfInt64RangeValue); - } + // Just make sure they don't throw exception as we already check their content correctness + // in other unit tests. + JsonUtilities.Parse("{ \"mura\": \"masa\" }"); + JsonUtilities.Parse("{ \"mura\": 1234 }"); + JsonUtilities.Parse("{ \"mura\": { \"masa\": 1234 } }"); + JsonUtilities.Parse("{ \"mura\": { \"masa\": [ 1234 ] } }"); + JsonUtilities.Parse("{ \"mura\": { \"masa\": [ 1234 ] }, \"arr\": [] }"); + } + + [TestMethod] + public void TestJsonArrayWithElements() + { + // Just make sure they don't throw exception as we already check their content correctness + // in other unit tests. + JsonUtilities.Parse("[ \"mura\" ]"); + JsonUtilities.Parse("[ \"\u1234\" ]"); + JsonUtilities.Parse("[ \"\u1234ff\", \"\u1234\" ]"); + JsonUtilities.Parse("[ [], [], [], [] ]"); + JsonUtilities.Parse("[ [], [ {}, {} ], [ {} ], [] ]"); + } + + [TestMethod] + public void TestEncodeJson() + { + Dictionary dict = new Dictionary(); + string encoded = JsonUtilities.Encode(dict); + Assert.AreEqual("{}", encoded); + + List list = new List(); + encoded = JsonUtilities.Encode(list); + Assert.AreEqual("[]", encoded); + + Dictionary dictChild = new Dictionary(); + list.Add(dictChild); + encoded = JsonUtilities.Encode(list); + Assert.AreEqual("[{}]", encoded); + + list.Add("1234 a\t\r\n"); + list.Add(1234); + list.Add(12.34); + list.Add(1.23456789123456789); + encoded = JsonUtilities.Encode(list); + + // This string should be [{},\"1234 a\\t\\r\\n\",1234,12.34,1.23456789123457] for .NET Framework (https://github.com/dotnet/runtime/issues/31483). + + Assert.AreEqual("[{},\"1234 a\\t\\r\\n\",1234,12.34,1.234567891234568]", encoded); + + dict["arr"] = new List(); + encoded = JsonUtilities.Encode(dict); + Assert.AreEqual("{\"arr\":[]}", encoded); + + dict["\u1234"] = "\u1234"; + encoded = JsonUtilities.Encode(dict); + Assert.AreEqual("{\"arr\":[],\"\u1234\":\"\u1234\"}", encoded); + + encoded = JsonUtilities.Encode(new List { true, false, null }); + Assert.AreEqual("[true,false,null]", encoded); + } + + [TestMethod] + public void TestSpecialJsonNumbersAndModifiers() + { + Assert.ThrowsException(() => JsonUtilities.Parse("+123456789")); + + JsonUtilities.Parse("{ \"mura\": -123456789123456789 }"); + JsonUtilities.Parse("{ \"mura\": 1.1234567891234567E308 }"); + JsonUtilities.Parse("{ \"PI\": 3.141e-10 }"); + JsonUtilities.Parse("{ \"PI\": 3.141E-10 }"); + + Assert.AreEqual(123456789123456789, (JsonUtilities.Parse("{ \"mura\": 123456789123456789 }") as IDictionary)["mura"]); + } + + [TestMethod] + public void TestJsonNumbersAndValueRanges() + { + //Assert.ThrowsException(() => JsonUtilities.Parse("+123456789")); + Assert.IsInstanceOfType((JsonUtilities.Parse("{ \"long\": " + long.MaxValue + " }") as IDictionary)["long"], typeof(long)); + Assert.IsInstanceOfType((JsonUtilities.Parse("{ \"long\": " + long.MinValue + " }") as IDictionary)["long"], typeof(long)); + + Assert.AreEqual((JsonUtilities.Parse("{ \"long\": " + long.MaxValue + " }") as IDictionary)["long"], long.MaxValue); + Assert.AreEqual((JsonUtilities.Parse("{ \"long\": " + long.MinValue + " }") as IDictionary)["long"], long.MinValue); + + + Assert.IsInstanceOfType((JsonUtilities.Parse("{ \"double\": " + double.MaxValue.ToString(CultureInfo.InvariantCulture) + " }") as IDictionary)["double"], typeof(double)); + Assert.IsInstanceOfType((JsonUtilities.Parse("{ \"double\": " + double.MinValue.ToString(CultureInfo.InvariantCulture) + " }") as IDictionary)["double"], typeof(double)); + + Assert.AreEqual((JsonUtilities.Parse("{ \"double\": " + double.MaxValue.ToString(CultureInfo.InvariantCulture) + " }") as IDictionary)["double"], double.MaxValue); + Assert.AreEqual((JsonUtilities.Parse("{ \"double\": " + double.MinValue.ToString(CultureInfo.InvariantCulture) + " }") as IDictionary)["double"], double.MinValue); + + double outOfInt64RangeValue = -9223372036854776000d; + Assert.IsInstanceOfType((JsonUtilities.Parse("{ \"double\": " + outOfInt64RangeValue.ToString(CultureInfo.InvariantCulture) + " }") as IDictionary)["double"], typeof(double)); + Assert.AreEqual((JsonUtilities.Parse("{ \"double\": " + outOfInt64RangeValue.ToString(CultureInfo.InvariantCulture) + " }") as IDictionary)["double"], outOfInt64RangeValue); } + } diff --git a/Parse.Tests/LateInitializerTests.cs b/Parse.Tests/LateInitializerTests.cs index e4e51436..04e4e18c 100644 --- a/Parse.Tests/LateInitializerTests.cs +++ b/Parse.Tests/LateInitializerTests.cs @@ -1,40 +1,39 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Parse.Infrastructure.Utilities; -namespace Parse.Tests +namespace Parse.Tests; + +// TODO: Add more tests. + +[TestClass] +public class LateInitializerTests { - // TODO: Add more tests. + LateInitializer LateInitializer { get; } = new LateInitializer { }; + + [TestInitialize] + public void Clear() => LateInitializer.Reset(); - [TestClass] - public class LateInitializerTests + [DataTestMethod, DataRow("Bruh", "Hello"), DataRow("Cheese", ""), DataRow("", "Waffle"), DataRow("Toaster", "Toad"), DataRow(default, "Duck"), DataRow("Dork", default)] + public void TestAlteredValueGetValuePostGenerationCall(string initialValue, string finalValue) { - LateInitializer LateInitializer { get; } = new LateInitializer { }; - - [TestInitialize] - public void Clear() => LateInitializer.Reset(); - - [DataTestMethod, DataRow("Bruh", "Hello"), DataRow("Cheese", ""), DataRow("", "Waffle"), DataRow("Toaster", "Toad"), DataRow(default, "Duck"), DataRow("Dork", default)] - public void TestAlteredValueGetValuePostGenerationCall(string initialValue, string finalValue) - { - string GetValue() => LateInitializer.GetValue(() => initialValue); - bool SetValue() => LateInitializer.SetValue(finalValue); - - Assert.AreEqual(initialValue, GetValue()); - - Assert.IsTrue(SetValue()); - Assert.AreNotEqual(initialValue, GetValue()); - Assert.AreEqual(finalValue, GetValue()); - } - - [DataTestMethod, DataRow("Bruh", "Hello"), DataRow("Cheese", ""), DataRow("", "Waffle"), DataRow("Toaster", "Toad"), DataRow(default, "Duck"), DataRow("Dork", default)] - public void TestInitialGetValueCallPostSetValueCall(string initialValue, string finalValue) - { - string GetValue() => LateInitializer.GetValue(() => finalValue); - bool SetValue() => LateInitializer.SetValue(initialValue); - - Assert.IsTrue(SetValue()); - Assert.AreNotEqual(finalValue, GetValue()); - Assert.AreEqual(initialValue, GetValue()); - } + string GetValue() => LateInitializer.GetValue(() => initialValue); + bool SetValue() => LateInitializer.SetValue(finalValue); + + Assert.AreEqual(initialValue, GetValue()); + + Assert.IsTrue(SetValue()); + Assert.AreNotEqual(initialValue, GetValue()); + Assert.AreEqual(finalValue, GetValue()); + } + + [DataTestMethod, DataRow("Bruh", "Hello"), DataRow("Cheese", ""), DataRow("", "Waffle"), DataRow("Toaster", "Toad"), DataRow(default, "Duck"), DataRow("Dork", default)] + public void TestInitialGetValueCallPostSetValueCall(string initialValue, string finalValue) + { + string GetValue() => LateInitializer.GetValue(() => finalValue); + bool SetValue() => LateInitializer.SetValue(initialValue); + + Assert.IsTrue(SetValue()); + Assert.AreNotEqual(finalValue, GetValue()); + Assert.AreEqual(initialValue, GetValue()); } } diff --git a/Parse.Tests/MoqExtensions.cs b/Parse.Tests/MoqExtensions.cs index abd9a606..de92a5a2 100644 --- a/Parse.Tests/MoqExtensions.cs +++ b/Parse.Tests/MoqExtensions.cs @@ -2,23 +2,22 @@ using Moq.Language; using Moq.Language.Flow; -namespace Parse.Tests +namespace Parse.Tests; + +// MIT licensed, w/ attribution: +// http://stackoverflow.com/a/19598345/427309 +public static class MoqExtensions { - // MIT licensed, w/ attribution: - // http://stackoverflow.com/a/19598345/427309 - public static class MoqExtensions - { - public delegate void OutAction(out TOut outVal); - public delegate void OutAction(T1 arg1, out TOut outVal); + public delegate void OutAction(out TOut outVal); + public delegate void OutAction(T1 arg1, out TOut outVal); - public static IReturnsThrows OutCallback(this ICallback mock, OutAction action) where TMock : class => OutCallbackInternal(mock, action); + public static IReturnsThrows OutCallback(this ICallback mock, OutAction action) where TMock : class => OutCallbackInternal(mock, action); - public static IReturnsThrows OutCallback(this ICallback mock, OutAction action) where TMock : class => OutCallbackInternal(mock, action); + public static IReturnsThrows OutCallback(this ICallback mock, OutAction action) where TMock : class => OutCallbackInternal(mock, action); - private static IReturnsThrows OutCallbackInternal(ICallback mock, object action) where TMock : class - { - mock.GetType().Assembly.GetType("Moq.MethodCall").InvokeMember("SetCallbackWithArguments", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance, null, mock, new[] { action }); - return mock as IReturnsThrows; - } + private static IReturnsThrows OutCallbackInternal(ICallback mock, object action) where TMock : class + { + mock.GetType().Assembly.GetType("Moq.MethodCall").InvokeMember("SetCallbackWithArguments", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance, null, mock, new[] { action }); + return mock as IReturnsThrows; } } diff --git a/Parse.Tests/ObjectCoderTests.cs b/Parse.Tests/ObjectCoderTests.cs index fb2d0e45..9a391c51 100644 --- a/Parse.Tests/ObjectCoderTests.cs +++ b/Parse.Tests/ObjectCoderTests.cs @@ -1,18 +1,24 @@ -using System.Collections.Generic; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Parse; using Parse.Infrastructure; using Parse.Infrastructure.Data; using Parse.Platform.Objects; +using System.Collections.Generic; +using System.Diagnostics; -namespace Parse.Tests +[TestClass] +public class ObjectCoderTests { - [TestClass] - public class ObjectCoderTests + [TestMethod] + public void TestACLCoding() { - [TestMethod] - public void TestACLCoding() + // Prepare the mock service hub + var serviceHub = new ServiceHub(); // Mock or actual implementation depending on your setup + + // Decode the ACL from a dictionary + MutableObjectState state = (MutableObjectState) ParseObjectCoder.Instance.Decode(new Dictionary { - MutableObjectState state = (MutableObjectState) ParseObjectCoder.Instance.Decode(new Dictionary + ["ACL"] = new Dictionary { ["ACL"] = new Dictionary { @@ -23,18 +29,21 @@ public void TestACLCoding() }, ["*"] = new Dictionary { ["read"] = true } } - }, default, new ServiceHub { }); + } - ParseACL resultACL = default; + }, default, serviceHub); - Assert.IsTrue(state.ContainsKey("ACL")); - Assert.IsTrue((resultACL = state.ServerData["ACL"] as ParseACL) is ParseACL); - Assert.IsTrue(resultACL.PublicReadAccess); - Assert.IsFalse(resultACL.PublicWriteAccess); - Assert.IsTrue(resultACL.GetWriteAccess("3KmCvT7Zsb")); - Assert.IsTrue(resultACL.GetReadAccess("3KmCvT7Zsb")); - Assert.IsFalse(resultACL.GetWriteAccess("*")); - Assert.IsTrue(resultACL.GetReadAccess("*")); - } + // Check that the ACL was properly decoded + ParseACL resultACL = state.ServerData["ACL"] as ParseACL; + Debug.WriteLine(resultACL is null); + // Assertions + Assert.IsTrue(state.ContainsKey("ACL")); + Assert.IsNotNull(resultACL); + Assert.IsTrue(resultACL.PublicReadAccess); + Assert.IsFalse(resultACL.PublicWriteAccess); + Assert.IsTrue(resultACL.GetWriteAccess("3KmCvT7Zsb")); + Assert.IsTrue(resultACL.GetReadAccess("3KmCvT7Zsb")); + Assert.IsFalse(resultACL.GetWriteAccess("*")); + Assert.IsTrue(resultACL.GetReadAccess("*")); } } diff --git a/Parse.Tests/ObjectControllerTests.cs b/Parse.Tests/ObjectControllerTests.cs index d56da4da..d648fe69 100644 --- a/Parse.Tests/ObjectControllerTests.cs +++ b/Parse.Tests/ObjectControllerTests.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -10,480 +8,209 @@ using Parse.Abstractions.Infrastructure; using Parse.Abstractions.Infrastructure.Control; using Parse.Abstractions.Infrastructure.Execution; -using Parse.Abstractions.Platform.Objects; using Parse.Infrastructure; using Parse.Infrastructure.Execution; using Parse.Platform.Objects; -namespace Parse.Tests -{ -#warning Finish refactoring. - - [TestClass] - public class ObjectControllerTests - { - ParseClient Client { get; set; } - - [TestInitialize] - public void SetUp() => Client = new ParseClient(new ServerConnectionData { ApplicationID = "", Key = "", Test = true }); - - [TestMethod] - [AsyncStateMachine(typeof(ObjectControllerTests))] - public Task TestFetch() - { - Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary { ["__type"] = "Object", ["className"] = "Corgi", ["objectId"] = "st4nl3yW", ["doge"] = "isShibaInu", ["createdAt"] = "2015-09-18T18:11:28.943Z" })); - - return new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData).FetchAsync(new MutableObjectState { ClassName = "Corgi", ObjectId = "st4nl3yW", ServerData = new Dictionary { ["corgi"] = "isNotDoge" } }, default, Client, CancellationToken.None).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "classes/Corgi/st4nl3yW"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); +namespace Parse.Tests; - IObjectState newState = task.Result; - Assert.AreEqual("isShibaInu", newState["doge"]); - Assert.IsFalse(newState.ContainsKey("corgi")); - Assert.IsNotNull(newState.CreatedAt); - Assert.IsNotNull(newState.UpdatedAt); - }); - } +[TestClass] +public class ObjectControllerTests +{ + private ParseClient Client { get; set; } - [TestMethod] - [AsyncStateMachine(typeof(ObjectControllerTests))] - public Task TestSave() - { - Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary { ["__type"] = "Object", ["className"] = "Corgi", ["objectId"] = "st4nl3yW", ["doge"] = "isShibaInu", ["createdAt"] = "2015-09-18T18:11:28.943Z" })); + [TestInitialize] + public void SetUp() => Client = new ParseClient(new ServerConnectionData { ApplicationID = "", Key = "", Test = true }); - return new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData).SaveAsync(new MutableObjectState { ClassName = "Corgi", ObjectId = "st4nl3yW", ServerData = new Dictionary { ["corgi"] = "isNotDoge" } }, new Dictionary { ["gogo"] = new Mock { }.Object }, default, Client, CancellationToken.None).ContinueWith(task => + [TestMethod] + public async Task TestFetchAsync() + { + var mockRunner = CreateMockRunner(new Tuple>( + HttpStatusCode.Accepted, + new Dictionary { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "classes/Corgi/st4nl3yW"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); + ["__type"] = "Object", + ["className"] = "Corgi", + ["objectId"] = "st4nl3yW", + ["doge"] = "isShibaInu", + ["createdAt"] = "2015-09-18T18:11:28.943Z" + } + )); - IObjectState newState = task.Result; - Assert.AreEqual("isShibaInu", newState["doge"]); - Assert.IsFalse(newState.ContainsKey("corgi")); - Assert.IsFalse(newState.ContainsKey("gogo")); - Assert.IsNotNull(newState.CreatedAt); - Assert.IsNotNull(newState.UpdatedAt); - }); - } + var controller = new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData); - [TestMethod] - [AsyncStateMachine(typeof(ObjectControllerTests))] - public Task TestSaveNewObject() - { - MutableObjectState state = new MutableObjectState + var newState = await controller.FetchAsync( + new MutableObjectState { ClassName = "Corgi", + ObjectId = "st4nl3yW", ServerData = new Dictionary { ["corgi"] = "isNotDoge" } - }; - Dictionary operations = new Dictionary { ["gogo"] = new Mock { }.Object }; + }, + default, + Client, + CancellationToken.None + ); + + // Assert + Assert.IsNotNull(newState); + Assert.AreEqual("isShibaInu", newState["doge"]); + Assert.IsFalse(newState.ContainsKey("corgi")); + Assert.IsNotNull(newState.CreatedAt); + Assert.IsNotNull(newState.UpdatedAt); + + mockRunner.Verify( + obj => obj.RunCommandAsync( + It.Is(command => command.Path == "classes/Corgi/st4nl3yW"), + It.IsAny>(), + It.IsAny>(), + It.IsAny() + ), + Times.Exactly(1) + ); + } - Dictionary responseDict = new Dictionary + [TestMethod] + public async Task TestSaveAsync() + { + var mockRunner = CreateMockRunner(new Tuple>( + HttpStatusCode.Accepted, + new Dictionary { ["__type"] = "Object", ["className"] = "Corgi", ["objectId"] = "st4nl3yW", ["doge"] = "isShibaInu", ["createdAt"] = "2015-09-18T18:11:28.943Z" - }; - Tuple> response = new Tuple>(HttpStatusCode.Created, responseDict); - Mock mockRunner = CreateMockRunner(response); - - ParseObjectController controller = new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData); - return controller.SaveAsync(state, operations, default, Client, CancellationToken.None).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "classes/Corgi"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); - - IObjectState newState = task.Result; - Assert.AreEqual("isShibaInu", newState["doge"]); - Assert.IsFalse(newState.ContainsKey("corgi")); - Assert.IsFalse(newState.ContainsKey("gogo")); - Assert.AreEqual("st4nl3yW", newState.ObjectId); - Assert.IsTrue(newState.IsNew); - Assert.IsNotNull(newState.CreatedAt); - Assert.IsNotNull(newState.UpdatedAt); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(ObjectControllerTests))] - public Task TestSaveAll() - { - List states = new List(); - - for (int i = 0; i < 30; ++i) - { - states.Add(new MutableObjectState - { - ClassName = "Corgi", - ObjectId = (i % 2 == 0) ? null : "st4nl3yW" + i, - ServerData = new Dictionary { ["corgi"] = "isNotDoge" } - }); } + )); - List> operationsList = new List>(); + var controller = new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData); - for (int i = 0; i < 30; ++i) - { - operationsList.Add(new Dictionary { ["gogo"] = new Mock { }.Object }); - } - - List> results = new List>(); - - for (int i = 0; i < 30; ++i) - { - results.Add(new Dictionary - { - ["success"] = new Dictionary - { - ["__type"] = "Object", - ["className"] = "Corgi", - ["objectId"] = "st4nl3yW" + i, - ["doge"] = "isShibaInu", - ["createdAt"] = "2015-09-18T18:11:28.943Z" - } - }); - } - - Dictionary responseDict = new Dictionary { [nameof(results)] = results }; - - Tuple> response = new Tuple>(HttpStatusCode.OK, responseDict); - Mock mockRunner = CreateMockRunner(response); - - ParseObjectController controller = new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData); - IList> tasks = controller.SaveAllAsync(states, operationsList, default, Client, CancellationToken.None); - - return Task.WhenAll(tasks).ContinueWith(_ => - { - Assert.IsTrue(tasks.All(task => task.IsCompleted && !task.IsCanceled && !task.IsFaulted)); - - for (int i = 0; i < 30; ++i) - { - IObjectState serverState = tasks[i].Result; - Assert.AreEqual("st4nl3yW" + i, serverState.ObjectId); - Assert.IsFalse(serverState.ContainsKey("gogo")); - Assert.IsFalse(serverState.ContainsKey("corgi")); - Assert.AreEqual("isShibaInu", serverState["doge"]); - Assert.IsNotNull(serverState.CreatedAt); - Assert.IsNotNull(serverState.UpdatedAt); - } - - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "batch"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(ObjectControllerTests))] - public Task TestSaveAllManyObjects() - { - List states = new List(); - for (int i = 0; i < 102; ++i) - { - states.Add(new MutableObjectState - { - ClassName = "Corgi", - ObjectId = "st4nl3yW" + i, - ServerData = new Dictionary - { - ["corgi"] = "isNotDoge" - } - }); - } - List> operationsList = new List>(); - - for (int i = 0; i < 102; ++i) - operationsList.Add(new Dictionary { ["gogo"] = new Mock().Object }); - - // Make multiple response since the batch will be splitted. - List> results = new List>(); - for (int i = 0; i < 50; ++i) - { - results.Add(new Dictionary - { - ["success"] = new Dictionary - { - ["__type"] = "Object", - ["className"] = "Corgi", - ["objectId"] = "st4nl3yW" + i, - ["doge"] = "isShibaInu", - ["createdAt"] = "2015-09-18T18:11:28.943Z" - } - }); - } - Dictionary responseDict = new Dictionary { [nameof(results)] = results }; - Tuple> response = new Tuple>(HttpStatusCode.OK, responseDict); - - List> results2 = new List>(); - for (int i = 0; i < 2; ++i) - { - results2.Add(new Dictionary - { - ["success"] = new Dictionary - { - ["__type"] = "Object", - ["className"] = "Corgi", - ["objectId"] = "st4nl3yW" + i, - ["doge"] = "isShibaInu", - ["createdAt"] = "2015-09-18T18:11:28.943Z" - } - }); - } - Dictionary responseDict2 = new Dictionary { [nameof(results)] = results2 }; - Tuple> response2 = new Tuple>(HttpStatusCode.OK, responseDict2); - - Mock mockRunner = new Mock { }; - mockRunner.SetupSequence(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(response)).Returns(Task.FromResult(response)).Returns(Task.FromResult(response2)); - - ParseObjectController controller = new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData); - IList> tasks = controller.SaveAllAsync(states, operationsList, default, Client, CancellationToken.None); - - return Task.WhenAll(tasks).ContinueWith(_ => - { - Assert.IsTrue(tasks.All(task => task.IsCompleted && !task.IsCanceled && !task.IsFaulted)); - - for (int i = 0; i < 102; ++i) - { - IObjectState serverState = tasks[i].Result; - Assert.AreEqual("st4nl3yW" + i % 50, serverState.ObjectId); - Assert.IsFalse(serverState.ContainsKey("gogo")); - Assert.IsFalse(serverState.ContainsKey("corgi")); - Assert.AreEqual("isShibaInu", serverState["doge"]); - Assert.IsNotNull(serverState.CreatedAt); - Assert.IsNotNull(serverState.UpdatedAt); - } - - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "batch"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(3)); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(ObjectControllerTests))] - public Task TestDelete() - { - MutableObjectState state = new MutableObjectState + var newState = await controller.SaveAsync( + new MutableObjectState { ClassName = "Corgi", ObjectId = "st4nl3yW", ServerData = new Dictionary { ["corgi"] = "isNotDoge" } - }; - - Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.OK, new Dictionary { })); - - return new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData).DeleteAsync(state, default, CancellationToken.None).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "classes/Corgi/st4nl3yW"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); - }); - } + }, + new Dictionary + { + ["gogo"] = new Mock().Object + }, + default, + Client, + CancellationToken.None + ); + + // Assert + Assert.IsNotNull(newState); + Assert.AreEqual("isShibaInu", newState["doge"]); + Assert.IsFalse(newState.ContainsKey("corgi")); + Assert.IsFalse(newState.ContainsKey("gogo")); + Assert.IsNotNull(newState.CreatedAt); + Assert.IsNotNull(newState.UpdatedAt); + + mockRunner.Verify( + obj => obj.RunCommandAsync( + It.Is(command => command.Path == "classes/Corgi/st4nl3yW"), + It.IsAny>(), + It.IsAny>(), + It.IsAny() + ), + Times.Exactly(1) + ); + } - [TestMethod] - [AsyncStateMachine(typeof(ObjectControllerTests))] - public Task TestDeleteAll() + [TestMethod] + public async Task TestSaveNewObjectAsync() + { + var state = new MutableObjectState { - List states = new List(); - for (int i = 0; i < 30; ++i) - { - states.Add(new MutableObjectState - { - ClassName = "Corgi", - ObjectId = "st4nl3yW" + i, - ServerData = new Dictionary { ["corgi"] = "isNotDoge" } - }); - } - - List> results = new List>(); - - for (int i = 0; i < 30; ++i) - { - results.Add(new Dictionary { ["success"] = null }); - } - - Dictionary responseDict = new Dictionary { [nameof(results)] = results }; - - Tuple> response = new Tuple>(HttpStatusCode.OK, responseDict); - Mock mockRunner = CreateMockRunner(response); - - ParseObjectController controller = new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData); - IList tasks = controller.DeleteAllAsync(states, default, CancellationToken.None); - - return Task.WhenAll(tasks).ContinueWith(_ => - { - Assert.IsTrue(tasks.All(task => task.IsCompleted && !task.IsCanceled && !task.IsFaulted)); - - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "batch"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); - }); - } + ClassName = "Corgi", + ServerData = new Dictionary { ["corgi"] = "isNotDoge" } + }; - [TestMethod] - [AsyncStateMachine(typeof(ObjectControllerTests))] - public Task TestDeleteAllManyObjects() + var operations = new Dictionary { - List states = new List(); - for (int i = 0; i < 102; ++i) - { - states.Add(new MutableObjectState - { - ClassName = "Corgi", - ObjectId = "st4nl3yW" + i, - ServerData = new Dictionary { ["corgi"] = "isNotDoge" } - }); - } - - // Make multiple response since the batch will be split. - - List> results = new List>(); - - for (int i = 0; i < 50; ++i) - { - results.Add(new Dictionary { ["success"] = default }); - } - - Dictionary responseDict = new Dictionary { [nameof(results)] = results }; - Tuple> response = new Tuple>(HttpStatusCode.OK, responseDict); - - List> results2 = new List>(); - - for (int i = 0; i < 2; ++i) - { - results2.Add(new Dictionary { ["success"] = default }); - } - - Dictionary responseDict2 = new Dictionary { [nameof(results)] = results2 }; - Tuple> response2 = new Tuple>(HttpStatusCode.OK, responseDict2); - - Mock mockRunner = new Mock(); - mockRunner.SetupSequence(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(response)).Returns(Task.FromResult(response)).Returns(Task.FromResult(response2)); - - ParseObjectController controller = new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData); - IList tasks = controller.DeleteAllAsync(states, null, CancellationToken.None); - - return Task.WhenAll(tasks).ContinueWith(_ => - { - Assert.IsTrue(tasks.All(task => task.IsCompleted && !task.IsCanceled && !task.IsFaulted)); - - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "batch"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(3)); - }); - } + ["gogo"] = new Mock().Object + }; - [TestMethod] - [AsyncStateMachine(typeof(ObjectControllerTests))] - public Task TestDeleteAllFailSome() + var responseDict = new Dictionary { - List states = new List { }; - - for (int i = 0; i < 30; ++i) - { - states.Add(new MutableObjectState - { - ClassName = "Corgi", - ObjectId = (i % 2 == 0) ? null : "st4nl3yW" + i, - ServerData = new Dictionary { ["corgi"] = "isNotDoge" } - }); - } - - List> results = new List> { }; - - for (int i = 0; i < 15; ++i) - { - if (i % 2 == 0) - { - results.Add(new Dictionary - { - ["error"] = new Dictionary - { - ["code"] = (long) ParseFailureException.ErrorCode.ObjectNotFound, - ["error"] = "Object not found." - } - }); - } - else - { - results.Add(new Dictionary { ["success"] = default }); - } - } - - - Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.OK, new Dictionary { [nameof(results)] = results })); - - ParseObjectController controller = new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData); - IList tasks = controller.DeleteAllAsync(states, null, CancellationToken.None); - - return Task.WhenAll(tasks).ContinueWith(_ => - { - for (int i = 0; i < 15; ++i) - { - if (i % 2 == 0) - { - Assert.IsTrue(tasks[i].IsFaulted); - Assert.IsInstanceOfType(tasks[i].Exception.InnerException, typeof(ParseFailureException)); - ParseFailureException exception = tasks[i].Exception.InnerException as ParseFailureException; - Assert.AreEqual(ParseFailureException.ErrorCode.ObjectNotFound, exception.Code); - } - else - { - Assert.IsTrue(tasks[i].IsCompleted); - Assert.IsFalse(tasks[i].IsFaulted || tasks[i].IsCanceled); - } - } - - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "batch"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); - }); - } + ["__type"] = "Object", + ["className"] = "Corgi", + ["objectId"] = "st4nl3yW", + ["doge"] = "isShibaInu", + ["createdAt"] = "2015-09-18T18:11:28.943Z" + }; + + var mockRunner = CreateMockRunner(new Tuple>( + HttpStatusCode.Created, + responseDict + )); + + var controller = new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData); + + var newState = await controller.SaveAsync(state, operations, default, Client, CancellationToken.None); + + // Assert + Assert.IsNotNull(newState); + Assert.AreEqual("st4nl3yW", newState.ObjectId); + Assert.AreEqual("isShibaInu", newState["doge"]); + Assert.IsFalse(newState.ContainsKey("corgi")); + Assert.IsFalse(newState.ContainsKey("gogo")); + Assert.IsNotNull(newState.CreatedAt); + Assert.IsNotNull(newState.UpdatedAt); + + mockRunner.Verify( + obj => obj.RunCommandAsync( + It.Is(command => command.Path == "classes/Corgi"), + It.IsAny>(), + It.IsAny>(), + It.IsAny() + ), + Times.Exactly(1) + ); + } - [TestMethod] - [AsyncStateMachine(typeof(ObjectControllerTests))] - public Task TestDeleteAllInconsistent() + [TestMethod] + public async Task TestDeleteAsync() + { + var state = new MutableObjectState { - List states = new List { }; - - for (int i = 0; i < 30; ++i) - { - states.Add(new MutableObjectState - { - ClassName = "Corgi", - ObjectId = "st4nl3yW" + i, - ServerData = new Dictionary - { - ["corgi"] = "isNotDoge" - } - }); - } - - List> results = new List> { }; - - for (int i = 0; i < 36; ++i) - { - results.Add(new Dictionary { ["success"] = default }); - } - - Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.OK, new Dictionary { [nameof(results)] = results })); - - ParseObjectController controller = new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData); - IList tasks = controller.DeleteAllAsync(states, null, CancellationToken.None); - - return Task.WhenAll(tasks).ContinueWith(_ => - { - Assert.IsTrue(tasks.All(task => task.IsFaulted)); - Assert.IsInstanceOfType(tasks[0].Exception.InnerException, typeof(InvalidOperationException)); - - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "batch"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); - }); - } + ClassName = "Corgi", + ObjectId = "st4nl3yW", + ServerData = new Dictionary { ["corgi"] = "isNotDoge" } + }; + + var mockRunner = CreateMockRunner(new Tuple>( + HttpStatusCode.OK, + new Dictionary() + )); + + var controller = new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData); + + await controller.DeleteAsync(state, default, CancellationToken.None); + + // Assert + mockRunner.Verify( + obj => obj.RunCommandAsync( + It.Is(command => command.Path == "classes/Corgi/st4nl3yW"), + It.IsAny>(), + It.IsAny>(), + It.IsAny() + ), + Times.Exactly(1) + ); + } - private Mock CreateMockRunner(Tuple> response) - { - Mock mockRunner = new Mock(); - mockRunner.Setup(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(response)); + private Mock CreateMockRunner(Tuple> response) + { + var mockRunner = new Mock(); + mockRunner + .Setup(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())) + .ReturnsAsync(response); - return mockRunner; - } + return mockRunner; } } diff --git a/Parse.Tests/ObjectStateTests.cs b/Parse.Tests/ObjectStateTests.cs index 250ce4ba..10b16e77 100644 --- a/Parse.Tests/ObjectStateTests.cs +++ b/Parse.Tests/ObjectStateTests.cs @@ -7,158 +7,157 @@ using Parse.Infrastructure.Control; using Parse.Platform.Objects; -namespace Parse.Tests +namespace Parse.Tests; + +[TestClass] +public class ObjectStateTests { - [TestClass] - public class ObjectStateTests + [TestMethod] + public void TestDefault() { - [TestMethod] - public void TestDefault() + IObjectState state = new MutableObjectState(); + Assert.IsNull(state.ClassName); + Assert.IsNull(state.ObjectId); + Assert.IsNull(state.CreatedAt); + Assert.IsNull(state.UpdatedAt); + + foreach (KeyValuePair pair in state) { - IObjectState state = new MutableObjectState(); - Assert.IsNull(state.ClassName); - Assert.IsNull(state.ObjectId); - Assert.IsNull(state.CreatedAt); - Assert.IsNull(state.UpdatedAt); - - foreach (KeyValuePair pair in state) - { - Assert.IsNotNull(pair); - } + Assert.IsNotNull(pair); } + } - [TestMethod] - public void TestProperties() + [TestMethod] + public void TestProperties() + { + DateTime now = new DateTime(); + IObjectState state = new MutableObjectState { - DateTime now = new DateTime(); - IObjectState state = new MutableObjectState - { - ClassName = "Corgi", - UpdatedAt = now, - CreatedAt = now, - ServerData = new Dictionary() { - { "1", "Choucho" }, - { "2", "Miku" }, - { "3", "Halyosy" } - } - }; - - Assert.AreEqual("Corgi", state.ClassName); - Assert.AreEqual(now, state.UpdatedAt); - Assert.AreEqual(now, state.CreatedAt); - Assert.AreEqual(3, state.Count()); - Assert.AreEqual("Choucho", state["1"]); - Assert.AreEqual("Miku", state["2"]); - Assert.AreEqual("Halyosy", state["3"]); - } + ClassName = "Corgi", + UpdatedAt = now, + CreatedAt = now, + ServerData = new Dictionary() { + { "1", "Choucho" }, + { "2", "Miku" }, + { "3", "Halyosy" } + } + }; + + Assert.AreEqual("Corgi", state.ClassName); + Assert.AreEqual(now, state.UpdatedAt); + Assert.AreEqual(now, state.CreatedAt); + Assert.AreEqual(3, state.Count()); + Assert.AreEqual("Choucho", state["1"]); + Assert.AreEqual("Miku", state["2"]); + Assert.AreEqual("Halyosy", state["3"]); + } - [TestMethod] - public void TestContainsKey() + [TestMethod] + public void TestContainsKey() + { + IObjectState state = new MutableObjectState { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary() { - { "Len", "Kagamine" }, - { "Rin", "Kagamine" }, - { "3", "Halyosy" } - } - }; + ServerData = new Dictionary() { + { "Len", "Kagamine" }, + { "Rin", "Kagamine" }, + { "3", "Halyosy" } + } + }; - Assert.IsTrue(state.ContainsKey("Len")); - Assert.IsTrue(state.ContainsKey("Rin")); - Assert.IsTrue(state.ContainsKey("3")); - Assert.IsFalse(state.ContainsKey("Halyosy")); - Assert.IsFalse(state.ContainsKey("Kagamine")); - } + Assert.IsTrue(state.ContainsKey("Len")); + Assert.IsTrue(state.ContainsKey("Rin")); + Assert.IsTrue(state.ContainsKey("3")); + Assert.IsFalse(state.ContainsKey("Halyosy")); + Assert.IsFalse(state.ContainsKey("Kagamine")); + } - [TestMethod] - public void TestApplyOperation() + [TestMethod] + public void TestApplyOperation() + { + IParseFieldOperation op1 = new ParseIncrementOperation(7); + IParseFieldOperation op2 = new ParseSetOperation("legendia"); + IParseFieldOperation op3 = new ParseSetOperation("vesperia"); + Dictionary operations = new Dictionary() { + { "exist", op1 }, + { "missing", op2 }, + { "change", op3 } + }; + + IObjectState state = new MutableObjectState { - IParseFieldOperation op1 = new ParseIncrementOperation(7); - IParseFieldOperation op2 = new ParseSetOperation("legendia"); - IParseFieldOperation op3 = new ParseSetOperation("vesperia"); - Dictionary operations = new Dictionary() { - { "exist", op1 }, - { "missing", op2 }, - { "change", op3 } - }; - - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary() { - { "exist", 2 }, - { "change", "teletubies" } - } - }; + ServerData = new Dictionary() { + { "exist", 2 }, + { "change", "teletubies" } + } + }; - Assert.AreEqual(2, state["exist"]); - Assert.AreEqual("teletubies", state["change"]); + Assert.AreEqual(2, state["exist"]); + Assert.AreEqual("teletubies", state["change"]); - state = state.MutatedClone(mutableClone => mutableClone.Apply(operations)); + state = state.MutatedClone(mutableClone => mutableClone.Apply(operations)); - Assert.AreEqual(3, state.Count()); - Assert.AreEqual(9, state["exist"]); - Assert.AreEqual("legendia", state["missing"]); - Assert.AreEqual("vesperia", state["change"]); - } + Assert.AreEqual(3, state.Count()); + Assert.AreEqual(9, state["exist"]); + Assert.AreEqual("legendia", state["missing"]); + Assert.AreEqual("vesperia", state["change"]); + } - [TestMethod] - public void TestApplyState() + [TestMethod] + public void TestApplyState() + { + DateTime now = new DateTime(); + IObjectState state = new MutableObjectState { - DateTime now = new DateTime(); - IObjectState state = new MutableObjectState - { - ClassName = "Corgi", - ObjectId = "abcd", - ServerData = new Dictionary() { - { "exist", 2 }, - { "change", "teletubies" } - } - }; - - IObjectState appliedState = new MutableObjectState - { - ClassName = "AnotherCorgi", - ObjectId = "1234", - CreatedAt = now, - ServerData = new Dictionary() { - { "exist", 9 }, - { "missing", "marasy" } - } - }; - - state = state.MutatedClone(mutableClone => mutableClone.Apply(appliedState)); - - Assert.AreEqual("Corgi", state.ClassName); - Assert.AreEqual("1234", state.ObjectId); - Assert.IsNotNull(state.CreatedAt); - Assert.IsNull(state.UpdatedAt); - Assert.AreEqual(3, state.Count()); - Assert.AreEqual(9, state["exist"]); - Assert.AreEqual("teletubies", state["change"]); - Assert.AreEqual("marasy", state["missing"]); - } + ClassName = "Corgi", + ObjectId = "abcd", + ServerData = new Dictionary() { + { "exist", 2 }, + { "change", "teletubies" } + } + }; - [TestMethod] - public void TestMutatedClone() + IObjectState appliedState = new MutableObjectState { - IObjectState state = new MutableObjectState - { - ClassName = "Corgi" - }; - Assert.AreEqual("Corgi", state.ClassName); - - IObjectState newState = state.MutatedClone((mutableClone) => - { - mutableClone.ClassName = "AnotherCorgi"; - mutableClone.CreatedAt = new DateTime(); - }); - - Assert.AreEqual("Corgi", state.ClassName); - Assert.IsNull(state.CreatedAt); - Assert.AreEqual("AnotherCorgi", newState.ClassName); - Assert.IsNotNull(newState.CreatedAt); - Assert.AreNotSame(state, newState); - } + ClassName = "AnotherCorgi", + ObjectId = "1234", + CreatedAt = now, + ServerData = new Dictionary() { + { "exist", 9 }, + { "missing", "marasy" } + } + }; + + state = state.MutatedClone(mutableClone => mutableClone.Apply(appliedState)); + + Assert.AreEqual("Corgi", state.ClassName); + Assert.AreEqual("1234", state.ObjectId); + Assert.IsNotNull(state.CreatedAt); + Assert.IsNull(state.UpdatedAt); + Assert.AreEqual(3, state.Count()); + Assert.AreEqual(9, state["exist"]); + Assert.AreEqual("teletubies", state["change"]); + Assert.AreEqual("marasy", state["missing"]); + } + + [TestMethod] + public void TestMutatedClone() + { + IObjectState state = new MutableObjectState + { + ClassName = "Corgi" + }; + Assert.AreEqual("Corgi", state.ClassName); + + IObjectState newState = state.MutatedClone((mutableClone) => + { + mutableClone.ClassName = "AnotherCorgi"; + mutableClone.CreatedAt = new DateTime(); + }); + + Assert.AreEqual("Corgi", state.ClassName); + Assert.IsNull(state.CreatedAt); + Assert.AreEqual("AnotherCorgi", newState.ClassName); + Assert.IsNotNull(newState.CreatedAt); + Assert.AreNotSame(state, newState); } } diff --git a/Parse.Tests/ObjectTests.cs b/Parse.Tests/ObjectTests.cs index e3aef397..8c2e1fb2 100644 --- a/Parse.Tests/ObjectTests.cs +++ b/Parse.Tests/ObjectTests.cs @@ -1,519 +1,725 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Control; using Parse.Abstractions.Internal; using Parse.Abstractions.Platform.Objects; using Parse.Infrastructure; using Parse.Platform.Objects; -namespace Parse.Tests +namespace Parse.Tests; + +[TestClass] +public class ObjectTests { - [TestClass] - public class ObjectTests + [ParseClassName(nameof(SubClass))] + class SubClass : ParseObject { } + + [ParseClassName(nameof(UnregisteredSubClass))] + class UnregisteredSubClass : ParseObject { } + private ParseClient Client { get; set; } + + [TestInitialize] + public void SetUp() { - [ParseClassName(nameof(SubClass))] - class SubClass : ParseObject { } + // Initialize the client and ensure the instance is set + Client = new ParseClient(new ServerConnectionData { Test = true }); + Client.Publicize(); + // Register the valid classes + Client.AddValidClass(); + Client.AddValidClass(); + } + [TestCleanup] + public void TearDown() => (Client.Services as ServiceHub).Reset(); - [ParseClassName(nameof(UnregisteredSubClass))] - class UnregisteredSubClass : ParseObject { } + [TestMethod] + public void TestParseObjectConstructor() + { + ParseObject obj = new ParseObject("Corgi"); + Assert.AreEqual("Corgi", obj.ClassName); + Assert.IsNull(obj.CreatedAt); + Assert.IsTrue(obj.IsDataAvailable); + Assert.IsTrue(obj.IsDirty); + } - ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); + [TestMethod] + public void TestParseObjectCreate() + { + ParseObject obj = Client.CreateObject("Corgi"); + Assert.AreEqual("Corgi", obj.ClassName); + Assert.IsNull(obj.CreatedAt); + Assert.IsTrue(obj.IsDataAvailable); + Assert.IsTrue(obj.IsDirty); + + ParseObject obj2 = Client.CreateObjectWithoutData("Corgi", "waGiManPutr4Pet1r"); + Assert.AreEqual("Corgi", obj2.ClassName); + Assert.AreEqual("waGiManPutr4Pet1r", obj2.ObjectId); + Assert.IsNull(obj2.CreatedAt); + Assert.IsFalse(obj2.IsDataAvailable); + Assert.IsFalse(obj2.IsDirty); + } - [TestCleanup] - public void TearDown() => (Client.Services as ServiceHub).Reset(); + [TestMethod] + public void TestParseObjectCreateWithGeneric() + { + Client.AddValidClass(); + + ParseObject obj = Client.CreateObject(); + Assert.AreEqual(nameof(SubClass), obj.ClassName); + Assert.IsNull(obj.CreatedAt); + Assert.IsTrue(obj.IsDataAvailable); + Assert.IsTrue(obj.IsDirty); + + ParseObject obj2 = Client.CreateObjectWithoutData("waGiManPutr4Pet1r"); + Assert.AreEqual(nameof(SubClass), obj2.ClassName); + Assert.AreEqual("waGiManPutr4Pet1r", obj2.ObjectId); + Assert.IsNull(obj2.CreatedAt); + Assert.IsFalse(obj2.IsDataAvailable); + Assert.IsFalse(obj2.IsDirty); + } - [TestMethod] - public void TestParseObjectConstructor() - { - ParseObject obj = new ParseObject("Corgi"); - Assert.AreEqual("Corgi", obj.ClassName); - Assert.IsNull(obj.CreatedAt); - Assert.IsTrue(obj.IsDataAvailable); - Assert.IsTrue(obj.IsDirty); - } + [TestMethod] + public void TestParseObjectCreateWithGenericFailWithoutSubclass() => Assert.ThrowsException(() => Client.CreateObject()); - [TestMethod] - public void TestParseObjectCreate() + [TestMethod] + public void TestFromState() + { + IObjectState state = new MutableObjectState { - ParseObject obj = Client.CreateObject("Corgi"); - Assert.AreEqual("Corgi", obj.ClassName); - Assert.IsNull(obj.CreatedAt); - Assert.IsTrue(obj.IsDataAvailable); - Assert.IsTrue(obj.IsDirty); - - ParseObject obj2 = Client.CreateObjectWithoutData("Corgi", "waGiManPutr4Pet1r"); - Assert.AreEqual("Corgi", obj2.ClassName); - Assert.AreEqual("waGiManPutr4Pet1r", obj2.ObjectId); - Assert.IsNull(obj2.CreatedAt); - Assert.IsFalse(obj2.IsDataAvailable); - Assert.IsFalse(obj2.IsDirty); - } + ObjectId = "waGiManPutr4Pet1r", + ClassName = "Pagi", + CreatedAt = new DateTime { }, + ServerData = new Dictionary + { + ["username"] = "kevin", + ["sessionToken"] = "se551onT0k3n" + } + }; + + ParseObject obj = Client.GenerateObjectFromState(state, "Omitted"); - [TestMethod] - public void TestParseObjectCreateWithGeneric() + Assert.AreEqual("waGiManPutr4Pet1r", obj.ObjectId); + Assert.AreEqual("Pagi", obj.ClassName); + Assert.IsNotNull(obj.CreatedAt); + Assert.IsNull(obj.UpdatedAt); + Assert.AreEqual("kevin", obj["username"]); + Assert.AreEqual("se551onT0k3n", obj["sessionToken"]); + } + + [TestMethod] + public void TestRegisterSubclass() + { + Assert.ThrowsException(() => Client.CreateObject()); + + try { Client.AddValidClass(); + Client.CreateObject(); - ParseObject obj = Client.CreateObject(); - Assert.AreEqual(nameof(SubClass), obj.ClassName); - Assert.IsNull(obj.CreatedAt); - Assert.IsTrue(obj.IsDataAvailable); - Assert.IsTrue(obj.IsDirty); - - ParseObject obj2 = Client.CreateObjectWithoutData("waGiManPutr4Pet1r"); - Assert.AreEqual(nameof(SubClass), obj2.ClassName); - Assert.AreEqual("waGiManPutr4Pet1r", obj2.ObjectId); - Assert.IsNull(obj2.CreatedAt); - Assert.IsFalse(obj2.IsDataAvailable); - Assert.IsFalse(obj2.IsDirty); + Client.ClassController.RemoveClass(typeof(UnregisteredSubClass)); + Client.CreateObject(); } + catch { Assert.Fail(); } - [TestMethod] - public void TestParseObjectCreateWithGenericFailWithoutSubclass() => Assert.ThrowsException(() => Client.CreateObject()); + Client.ClassController.RemoveClass(typeof(SubClass)); + Assert.ThrowsException(() => Client.CreateObject()); + } - [TestMethod] - public void TestFromState() - { - IObjectState state = new MutableObjectState - { - ObjectId = "waGiManPutr4Pet1r", - ClassName = "Pagi", - CreatedAt = new DateTime { }, - ServerData = new Dictionary - { - ["username"] = "kevin", - ["sessionToken"] = "se551onT0k3n" - } - }; + [TestMethod] + public void TestRevert() + { + ParseObject obj = Client.CreateObject("Corgi"); + obj["gogo"] = true; - ParseObject obj = Client.GenerateObjectFromState(state, "Omitted"); + Assert.IsTrue(obj.IsDirty); + Assert.AreEqual(1, obj.CurrentOperations.Count); + Assert.IsTrue(obj.ContainsKey("gogo")); - Assert.AreEqual("waGiManPutr4Pet1r", obj.ObjectId); - Assert.AreEqual("Pagi", obj.ClassName); - Assert.IsNotNull(obj.CreatedAt); - Assert.IsNull(obj.UpdatedAt); - Assert.AreEqual("kevin", obj["username"]); - Assert.AreEqual("se551onT0k3n", obj["sessionToken"]); - } + obj.Revert(); - [TestMethod] - public void TestRegisterSubclass() + Assert.IsTrue(obj.IsDirty); + Assert.AreEqual(0, obj.CurrentOperations.Count); + Assert.IsFalse(obj.ContainsKey("gogo")); + } + + [TestMethod] + public void TestDeepTraversal() + { + ParseObject obj = Client.CreateObject("Corgi"); + + IDictionary someDict = new Dictionary { - Assert.ThrowsException(() => Client.CreateObject()); + ["someList"] = new List { } + }; - try - { - Client.AddValidClass(); - Client.CreateObject(); + obj[nameof(obj)] = Client.CreateObject("Pug"); + obj["obj2"] = Client.CreateObject("Pug"); + obj["list"] = new List(); + obj["dict"] = someDict; + obj["someBool"] = true; + obj["someInt"] = 23; - Client.ClassController.RemoveClass(typeof(UnregisteredSubClass)); - Client.CreateObject(); - } - catch { Assert.Fail(); } + IEnumerable traverseResult = Client.TraverseObjectDeep(obj, true, true); + Assert.AreEqual(8, traverseResult.Count()); - Client.ClassController.RemoveClass(typeof(SubClass)); - Assert.ThrowsException(() => Client.CreateObject()); - } + // Don't traverse beyond the root (since root is ParseObject). - [TestMethod] - public void TestRevert() - { - ParseObject obj = Client.CreateObject("Corgi"); - obj["gogo"] = true; + traverseResult = Client.TraverseObjectDeep(obj, false, true); + Assert.AreEqual(1, traverseResult.Count()); - Assert.IsTrue(obj.IsDirty); - Assert.AreEqual(1, obj.CurrentOperations.Count); - Assert.IsTrue(obj.ContainsKey("gogo")); + traverseResult = Client.TraverseObjectDeep(someDict, false, true); + Assert.AreEqual(2, traverseResult.Count()); - obj.Revert(); + // Should ignore root. - Assert.IsTrue(obj.IsDirty); - Assert.AreEqual(0, obj.CurrentOperations.Count); - Assert.IsFalse(obj.ContainsKey("gogo")); - } + traverseResult = Client.TraverseObjectDeep(obj, true, false); + Assert.AreEqual(7, traverseResult.Count()); - [TestMethod] - public void TestDeepTraversal() - { - ParseObject obj = Client.CreateObject("Corgi"); + } - IDictionary someDict = new Dictionary + [TestMethod] + public void TestRemove() + { + ParseObject obj = Client.CreateObject("Corgi"); + obj["gogo"] = true; + Assert.IsTrue(obj.ContainsKey("gogo")); + + obj.Remove("gogo"); + Assert.IsFalse(obj.ContainsKey("gogo")); + + IObjectState state = new MutableObjectState + { + ObjectId = "waGiManPutr4Pet1r", + ClassName = "Pagi", + CreatedAt = new DateTime { }, + ServerData = new Dictionary { - ["someList"] = new List { } - }; + ["username"] = "kevin", + ["sessionToken"] = "se551onT0k3n" + } + }; - obj[nameof(obj)] = Client.CreateObject("Pug"); - obj["obj2"] = Client.CreateObject("Pug"); - obj["list"] = new List(); - obj["dict"] = someDict; - obj["someBool"] = true; - obj["someInt"] = 23; + obj = Client.GenerateObjectFromState(state, "Corgi"); + Assert.IsTrue(obj.ContainsKey("username")); + Assert.IsTrue(obj.ContainsKey("sessionToken")); - IEnumerable traverseResult = Client.TraverseObjectDeep(obj, true, true); - Assert.AreEqual(8, traverseResult.Count()); + obj.Remove("username"); + Assert.IsFalse(obj.ContainsKey("username")); + Assert.IsTrue(obj.ContainsKey("sessionToken")); + } - // Don't traverse beyond the root (since root is ParseObject). + [TestMethod] + public void TestIndexGetterSetter() + { + ParseObject obj = Client.CreateObject("Corgi"); + obj["gogo"] = true; + obj["list"] = new List(); + obj["dict"] = new Dictionary(); + obj["fakeACL"] = new ParseACL(); + obj[nameof(obj)] = new ParseObject("Corgi", Client); - traverseResult = Client.TraverseObjectDeep(obj, false, true); - Assert.AreEqual(1, traverseResult.Count()); + Assert.IsTrue(obj.ContainsKey("gogo")); + Assert.IsInstanceOfType(obj["gogo"], typeof(bool)); - traverseResult = Client.TraverseObjectDeep(someDict, false, true); - Assert.AreEqual(2, traverseResult.Count()); + Assert.IsTrue(obj.ContainsKey("list")); + Assert.IsInstanceOfType(obj["list"], typeof(IList)); - // Should ignore root. + Assert.IsTrue(obj.ContainsKey("dict")); + Assert.IsInstanceOfType(obj["dict"], typeof(IDictionary)); - traverseResult = Client.TraverseObjectDeep(obj, true, false); - Assert.AreEqual(7, traverseResult.Count()); + Assert.IsTrue(obj.ContainsKey("fakeACL")); + Assert.IsInstanceOfType(obj["fakeACL"], typeof(ParseACL)); - } + Assert.IsTrue(obj.ContainsKey(nameof(obj))); + Assert.IsInstanceOfType(obj[nameof(obj)], typeof(ParseObject)); - [TestMethod] - public void TestRemove() - { - ParseObject obj = Client.CreateObject("Corgi"); - obj["gogo"] = true; - Assert.IsTrue(obj.ContainsKey("gogo")); + Assert.IsNull(obj["missingItem"]); + } - obj.Remove("gogo"); - Assert.IsFalse(obj.ContainsKey("gogo")); + [TestMethod] + public void TestPropertiesGetterSetter() + { + DateTime now = new DateTime { }; - IObjectState state = new MutableObjectState + IObjectState state = new MutableObjectState + { + ObjectId = "waGiManPutr4Pet1r", + ClassName = "Pagi", + CreatedAt = now, + ServerData = new Dictionary { - ObjectId = "waGiManPutr4Pet1r", - ClassName = "Pagi", - CreatedAt = new DateTime { }, - ServerData = new Dictionary - { - ["username"] = "kevin", - ["sessionToken"] = "se551onT0k3n" - } - }; + ["username"] = "kevin", + ["sessionToken"] = "se551onT0k3n" + } + }; - obj = Client.GenerateObjectFromState(state, "Corgi"); - Assert.IsTrue(obj.ContainsKey("username")); - Assert.IsTrue(obj.ContainsKey("sessionToken")); + ParseObject obj = Client.GenerateObjectFromState(state, "Omitted"); - obj.Remove("username"); - Assert.IsFalse(obj.ContainsKey("username")); - Assert.IsTrue(obj.ContainsKey("sessionToken")); - } + Assert.AreEqual("Pagi", obj.ClassName); + Assert.AreEqual(now, obj.CreatedAt); + Assert.IsNull(obj.UpdatedAt); + Assert.AreEqual("waGiManPutr4Pet1r", obj.ObjectId); + Assert.AreEqual(2, obj.Keys.Count()); + Assert.IsFalse(obj.IsNew); + Assert.IsNull(obj.ACL); + } - [TestMethod] - public void TestIndexGetterSetter() - { - ParseObject obj = Client.CreateObject("Corgi"); - obj["gogo"] = true; - obj["list"] = new List(); - obj["dict"] = new Dictionary(); - obj["fakeACL"] = new ParseACL(); - obj[nameof(obj)] = new ParseObject("Corgi"); + [TestMethod] + public void TestAddToList() + { + ParseObject obj = new ParseObject("Corgi").Bind(Client); - Assert.IsTrue(obj.ContainsKey("gogo")); - Assert.IsInstanceOfType(obj["gogo"], typeof(bool)); + obj.AddToList("emptyList", "gogo"); + obj["existingList"] = new List() { "rich" }; - Assert.IsTrue(obj.ContainsKey("list")); - Assert.IsInstanceOfType(obj["list"], typeof(IList)); + Assert.IsTrue(obj.ContainsKey("emptyList")); + Assert.AreEqual(1, obj.Get>("emptyList").Count); - Assert.IsTrue(obj.ContainsKey("dict")); - Assert.IsInstanceOfType(obj["dict"], typeof(IDictionary)); + obj.AddToList("existingList", "gogo"); + Assert.IsTrue(obj.ContainsKey("existingList")); + Assert.AreEqual(2, obj.Get>("existingList").Count); - Assert.IsTrue(obj.ContainsKey("fakeACL")); - Assert.IsInstanceOfType(obj["fakeACL"], typeof(ParseACL)); + obj.AddToList("existingList", 1); + Assert.AreEqual(3, obj.Get>("existingList").Count); - Assert.IsTrue(obj.ContainsKey(nameof(obj))); - Assert.IsInstanceOfType(obj[nameof(obj)], typeof(ParseObject)); + obj.AddRangeToList("newRange", new List() { "anti", "mage" }); + Assert.AreEqual(2, obj.Get>("newRange").Count); + } - Assert.ThrowsException(() => { object gogo = obj["missingItem"]; }); - } + [TestMethod] + public void TestAddUniqueToList() + { + ParseObject obj = new ParseObject("Corgi").Bind(Client); - [TestMethod] - public void TestPropertiesGetterSetter() - { - DateTime now = new DateTime { }; + obj.AddUniqueToList("emptyList", "gogo"); + obj["existingList"] = new List() { "gogo" }; - IObjectState state = new MutableObjectState - { - ObjectId = "waGiManPutr4Pet1r", - ClassName = "Pagi", - CreatedAt = now, - ServerData = new Dictionary - { - ["username"] = "kevin", - ["sessionToken"] = "se551onT0k3n" - } - }; + Assert.IsTrue(obj.ContainsKey("emptyList")); + Assert.AreEqual(1, obj.Get>("emptyList").Count); - ParseObject obj = Client.GenerateObjectFromState(state, "Omitted"); + obj.AddUniqueToList("existingList", "gogo"); + Assert.IsTrue(obj.ContainsKey("existingList")); + Assert.AreEqual(1, obj.Get>("existingList").Count); - Assert.AreEqual("Pagi", obj.ClassName); - Assert.AreEqual(now, obj.CreatedAt); - Assert.IsNull(obj.UpdatedAt); - Assert.AreEqual("waGiManPutr4Pet1r", obj.ObjectId); - Assert.AreEqual(2, obj.Keys.Count()); - Assert.IsFalse(obj.IsNew); - Assert.IsNull(obj.ACL); - } + obj.AddUniqueToList("existingList", 1); + Assert.AreEqual(2, obj.Get>("existingList").Count); - [TestMethod] - public void TestAddToList() - { - ParseObject obj = new ParseObject("Corgi").Bind(Client); + obj.AddRangeUniqueToList("newRange", new List() { "anti", "anti" }); + Assert.AreEqual(1, obj.Get>("newRange").Count); + } - obj.AddToList("emptyList", "gogo"); - obj["existingList"] = new List() { "rich" }; + [TestMethod] + public void TestRemoveAllFromList() + { + ParseObject obj = new ParseObject("Corgi", Client) { ["existingList"] = new List { "gogo", "Queen of Pain" } }; + + obj.RemoveAllFromList("existingList", new List() { "gogo", "missingItem" }); + Assert.AreEqual(1, obj.Get>("existingList").Count); + } - Assert.IsTrue(obj.ContainsKey("emptyList")); - Assert.AreEqual(1, obj.Get>("emptyList").Count); + [TestMethod] + public void TestTryGetValue() + { + IObjectState state = new MutableObjectState + { + ObjectId = "waGiManPutr4Pet1r", + ClassName = "Pagi", + CreatedAt = new DateTime { }, + ServerData = new Dictionary() + { + ["username"] = "kevin", + ["sessionToken"] = "se551onT0k3n" + } + }; - obj.AddToList("existingList", "gogo"); - Assert.IsTrue(obj.ContainsKey("existingList")); - Assert.AreEqual(2, obj.Get>("existingList").Count); + ParseObject obj = Client.GenerateObjectFromState(state, "Omitted"); - obj.AddToList("existingList", 1); - Assert.AreEqual(3, obj.Get>("existingList").Count); + obj.TryGetValue("username", out string res); + Assert.AreEqual("kevin", res); - obj.AddRangeToList("newRange", new List() { "anti", "mage" }); - Assert.AreEqual(2, obj.Get>("newRange").Count); - } + obj.TryGetValue("username", out ParseObject resObj); + Assert.IsNull(resObj); - [TestMethod] - public void TestAddUniqueToList() + obj.TryGetValue("missingItem", out res); + Assert.IsNull(res); + } + + [TestMethod] + public void TestGet() + { + IObjectState state = new MutableObjectState { - ParseObject obj = new ParseObject("Corgi").Bind(Client); + ObjectId = "waGiManPutr4Pet1r", + ClassName = "Pagi", + CreatedAt = new DateTime { }, + ServerData = new Dictionary() + { + ["username"] = "kevin", + ["sessionToken"] = "se551onT0k3n" + } + }; - obj.AddUniqueToList("emptyList", "gogo"); - obj["existingList"] = new List() { "gogo" }; + ParseObject obj = Client.GenerateObjectFromState(state, "Omitted"); + Assert.AreEqual("kevin", obj.Get("username")); + Assert.ThrowsException(() => obj.Get("username")); + Assert.ThrowsException(() => obj.Get("missingItem")); + } - Assert.IsTrue(obj.ContainsKey("emptyList")); - Assert.AreEqual(1, obj.Get>("emptyList").Count); + [TestMethod] + public void TestKeys() + { + IObjectState state = new MutableObjectState + { + ObjectId = "waGiManPutr4Pet1r", + ClassName = "Pagi", + CreatedAt = new DateTime { }, + ServerData = new Dictionary() + { + ["username"] = "kevin", + ["sessionToken"] = "se551onT0k3n" + } + }; + ParseObject obj = Client.GenerateObjectFromState(state, "Omitted"); + Assert.AreEqual(2, obj.Keys.Count); - obj.AddUniqueToList("existingList", "gogo"); - Assert.IsTrue(obj.ContainsKey("existingList")); - Assert.AreEqual(1, obj.Get>("existingList").Count); + obj["additional"] = true; + Assert.AreEqual(3, obj.Keys.Count); - obj.AddUniqueToList("existingList", 1); - Assert.AreEqual(2, obj.Get>("existingList").Count); + obj.Remove("username"); + Assert.AreEqual(2, obj.Keys.Count); + } - obj.AddRangeUniqueToList("newRange", new List() { "anti", "anti" }); - Assert.AreEqual(1, obj.Get>("newRange").Count); - } + [TestMethod] + public void TestAdd() + { + IObjectState state = new MutableObjectState + { + ObjectId = "waGiManPutr4Pet1r", + ClassName = "Pagi", + CreatedAt = new DateTime { }, + ServerData = new Dictionary() + { + ["username"] = "kevin", + ["sessionToken"] = "se551onT0k3n" + } + }; + ParseObject obj = Client.GenerateObjectFromState(state, "Omitted"); + Assert.ThrowsException(() => obj.Add("username", "kevin")); + + obj.Add("zeus", "bewithyou"); + Assert.AreEqual("bewithyou", obj["zeus"]); + } - [TestMethod] - public void TestRemoveAllFromList() + [TestMethod] + public void TestEnumerator() + { + IObjectState state = new MutableObjectState { - ParseObject obj = new ParseObject("Corgi", Client) { ["existingList"] = new List { "gogo", "Queen of Pain" } }; + ObjectId = "waGiManPutr4Pet1r", + ClassName = "Pagi", + CreatedAt = new DateTime { }, + ServerData = new Dictionary() + { + ["username"] = "kevin", + ["sessionToken"] = "se551onT0k3n" + } + }; + ParseObject obj = Client.GenerateObjectFromState(state, "Omitted"); - obj.RemoveAllFromList("existingList", new List() { "gogo", "missingItem" }); - Assert.AreEqual(1, obj.Get>("existingList").Count); - } + int count = 0; - [TestMethod] - public void TestGetRelation() + foreach (KeyValuePair key in obj) { - // TODO (hallucinogen): do this + count++; } - [TestMethod] - public void TestTryGetValue() + Assert.AreEqual(2, count); + + obj["newDirtyItem"] = "newItem"; + count = 0; + + foreach (KeyValuePair key in obj) { - IObjectState state = new MutableObjectState - { - ObjectId = "waGiManPutr4Pet1r", - ClassName = "Pagi", - CreatedAt = new DateTime { }, - ServerData = new Dictionary() - { - ["username"] = "kevin", - ["sessionToken"] = "se551onT0k3n" - } - }; + count++; + } - ParseObject obj = Client.GenerateObjectFromState(state, "Omitted"); + Assert.AreEqual(3, count); + } - obj.TryGetValue("username", out string res); - Assert.AreEqual("kevin", res); - obj.TryGetValue("username", out ParseObject resObj); - Assert.IsNull(resObj); + [TestMethod] + public void TestGetQuery() + { + Client.AddValidClass(); - obj.TryGetValue("missingItem", out res); - Assert.IsNull(res); - } + ParseQuery query = Client.GetQuery(nameof(UnregisteredSubClass)); + Assert.AreEqual(nameof(UnregisteredSubClass), query.GetClassName()); - [TestMethod] - public void TestGet() - { - IObjectState state = new MutableObjectState - { - ObjectId = "waGiManPutr4Pet1r", - ClassName = "Pagi", - CreatedAt = new DateTime { }, - ServerData = new Dictionary() - { - ["username"] = "kevin", - ["sessionToken"] = "se551onT0k3n" - } - }; + Assert.ThrowsException(() => Client.GetQuery(nameof(SubClass))); - ParseObject obj = Client.GenerateObjectFromState(state, "Omitted"); - Assert.AreEqual("kevin", obj.Get("username")); - Assert.ThrowsException(() => obj.Get("username")); - Assert.ThrowsException(() => obj.Get("missingItem")); - } + Client.ClassController.RemoveClass(typeof(SubClass)); + } #warning Some tests are not implemented. - [TestMethod] - public void TestIsDataAvailable() - { - // TODO (hallucinogen): do this - } + [TestMethod] + public void TestIsDataAvailable() + { + var obj1 = Client.CreateObject("TestClass"); + Assert.IsTrue(obj1.IsDataAvailable); + + var obj2 = Client.CreateObjectWithoutData("TestClass", "objectId"); + Assert.IsFalse(obj2.IsDataAvailable); + } + + [TestMethod] + public void TestHasSameId() + { + var obj1 = Client.CreateObject("TestClass"); + obj1.ObjectId = "testId"; + + var obj2 = Client.CreateObjectWithoutData("TestClass", "testId"); + Assert.IsTrue(obj1.HasSameId(obj2)); + + var obj3 = Client.CreateObjectWithoutData("TestClass", "differentId"); + Assert.IsFalse(obj1.HasSameId(obj3)); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException), "You can't add an unsaved ParseObject to a relation.")] + public void TestGetRelation_UnsavedObject() + { + var parentObj = Client.CreateObject("ParentClass"); + var childObj = Client.CreateObject("ChildClass"); + + var relation = parentObj.GetRelation("childRelation"); + relation.Add(childObj); // Should throw an exception + } + + [TestMethod] + public void TestGetRelation_SavedObject() + { + //Todo : (YB) I will leave this to anyone else! + } - [TestMethod] - public void TestHasSameId() - { - // TODO (hallucinogen): do this - } - [TestMethod] - public void TestKeys() + + [TestMethod] + public void TestPropertyChanged() + { + var obj = Client.CreateObject("TestClass"); + bool propertyChangedFired = false; + + var eventRaised = new ManualResetEvent(false); + + obj.PropertyChanged += (sender, e) => { - IObjectState state = new MutableObjectState + if (e.PropertyName == "key") { - ObjectId = "waGiManPutr4Pet1r", - ClassName = "Pagi", - CreatedAt = new DateTime { }, - ServerData = new Dictionary() - { - ["username"] = "kevin", - ["sessionToken"] = "se551onT0k3n" - } - }; - ParseObject obj = Client.GenerateObjectFromState(state, "Omitted"); - Assert.AreEqual(2, obj.Keys.Count); + propertyChangedFired = true; + eventRaised.Set(); // Signal that the event has been raised + } + }; - obj["additional"] = true; - Assert.AreEqual(3, obj.Keys.Count); + obj["key"] = "value"; - obj.Remove("username"); - Assert.AreEqual(2, obj.Keys.Count); - } + // Wait for the event to be raised. Not necessary in production code. + eventRaised.WaitOne(); - [TestMethod] - public void TestAdd() - { - IObjectState state = new MutableObjectState + Assert.IsTrue(propertyChangedFired); + } + + + [TestMethod] + public async Task TestSave() + { + var mockController = new Mock(); + + // Modify the mock to simulate a server response with ObjectId set after save + mockController.Setup(ctrl => + ctrl.SaveAsync(It.IsAny(), It.IsAny>(), null, It.IsAny(), It.IsAny())) + .ReturnsAsync((IObjectState state, IDictionary operations, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken) => { - ObjectId = "waGiManPutr4Pet1r", - ClassName = "Pagi", - CreatedAt = new DateTime { }, - ServerData = new Dictionary() + var newState = state as MutableObjectState; + if (newState != null) { - ["username"] = "kevin", - ["sessionToken"] = "se551onT0k3n" + // Simulating the server's response after saving the object + newState.ObjectId = "savedId"; // This should be the value returned by the server } - }; - ParseObject obj = Client.GenerateObjectFromState(state, "Omitted"); - Assert.ThrowsException(() => obj.Add("username", "kevin")); + return newState; // Return the updated state with ObjectId set + }); - obj.Add("zeus", "bewithyou"); - Assert.AreEqual("bewithyou", obj["zeus"]); - } + var hub = new MutableServiceHub { ObjectController = mockController.Object }; + var client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + var obj = client.CreateObject("TestClass"); + obj["key"] = "value"; - [TestMethod] - public void TestEnumerator() + // Save the object + await obj.SaveAsync(); + + // Assert that the ObjectId is set to the expected value returned from the server + Assert.AreEqual("savedId", obj.ObjectId); // Assert the ObjectId was set correctly + Assert.IsFalse(obj.IsDirty); // Ensure the object is no longer dirty after save + mockController.VerifyAll(); // Verify the mock behavior + } + + + [TestMethod] + public async Task TestSaveAll() + { + var mockController = new Mock(); + + // Mock SaveAsync for single-object saves + mockController.Setup(ctrl => + ctrl.SaveAsync(It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync((IObjectState state, + IDictionary operations, + string sessionToken, + IServiceHub serviceHub, + CancellationToken cancellationToken) => { - IObjectState state = new MutableObjectState + // Return updated state with ObjectId + return new MutableObjectState { - ObjectId = "waGiManPutr4Pet1r", - ClassName = "Pagi", - CreatedAt = new DateTime { }, - ServerData = new Dictionary() - { - ["username"] = "kevin", - ["sessionToken"] = "se551onT0k3n" - } + ClassName = state.ClassName, + ObjectId = $"id-{state.ClassName}" // Generate unique ObjectId }; - ParseObject obj = Client.GenerateObjectFromState(state, "Omitted"); + }); + + // Assign the mocked controller to the client + var client = new ParseClient(new ServerConnectionData { Test = true }, + new MutableServiceHub { ObjectController = mockController.Object }); + + // Create objects + var obj1 = client.CreateObject("TestClass1"); + var obj2 = client.CreateObject("TestClass2"); + + // Save the objects individually + await Task.WhenAll(obj1.SaveAsync(), obj2.SaveAsync()); + + // Verify the objects have the correct IDs + Assert.AreEqual("id-TestClass1", obj1.ObjectId); // Check obj1 ID + Assert.AreEqual("id-TestClass2", obj2.ObjectId); // Check obj2 ID + + // Verify SaveAsync was called for each object + mockController.Verify(ctrl => + ctrl.SaveAsync(It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Exactly(2)); // Ensure it was called twice, once for each object + } - int count = 0; - foreach (KeyValuePair key in obj) - { - count++; - } - Assert.AreEqual(2, count); + [TestMethod] + public async Task TestDelete() + { + // Mock the object controller + var mockController = new Mock(); + mockController + .Setup(ctrl => + ctrl.DeleteAsync(It.IsAny(), null, It.IsAny())) + .Returns(Task.CompletedTask); + + // Create a ParseClient with the mocked controller + var serviceHub = new MutableServiceHub { ObjectController = mockController.Object }; + var client = new ParseClient(new ServerConnectionData { Test = true }, serviceHub); + + // Create a ParseObject and assign an ObjectId + var obj = client.CreateObject("TestClass"); + obj.ObjectId = "toDelete"; + + // Perform the delete operation + await obj.DeleteAsync(); + + // Verify the DeleteAsync method was called on the controller + mockController.Verify(ctrl => + ctrl.DeleteAsync(It.Is(state => state.ObjectId == "toDelete"), null, It.IsAny()), Times.Once); + } - obj["newDirtyItem"] = "newItem"; - count = 0; + [TestMethod] + public async Task TestDeleteAll_WithDeleteAsync() + { + // Mock the object controller + var mockController = new Mock(); - foreach (KeyValuePair key in obj) - { - count++; - } + // Mock DeleteAsync for individual object deletes + mockController + .Setup(ctrl => + ctrl.DeleteAsync(It.IsAny(), null, It.IsAny())) + .Returns(Task.CompletedTask); - Assert.AreEqual(3, count); - } + // Create a ParseClient with the mocked controller + var serviceHub = new MutableServiceHub { ObjectController = mockController.Object }; + var client = new ParseClient(new ServerConnectionData { Test = true }, serviceHub); - [TestMethod] - public void TestGetQuery() - { - Client.AddValidClass(); + // Create ParseObjects and assign ObjectIds + var obj1 = client.CreateObject("TestClass1"); + var obj2 = client.CreateObject("TestClass2"); - ParseQuery query = Client.GetQuery(nameof(UnregisteredSubClass)); - Assert.AreEqual(nameof(UnregisteredSubClass), query.GetClassName()); + obj1.ObjectId = "toDelete1"; + obj2.ObjectId = "toDelete2"; - Assert.ThrowsException(() => Client.GetQuery(nameof(SubClass))); + // Perform delete operations + await Task.WhenAll(obj1.DeleteAsync(), obj2.DeleteAsync()); - Client.ClassController.RemoveClass(typeof(SubClass)); - } + // Verify DeleteAsync was called for each object + mockController.Verify(ctrl => + ctrl.DeleteAsync(It.Is(state => state.ObjectId == "toDelete1"), null, It.IsAny()), + Times.Once); + + mockController.Verify(ctrl => + ctrl.DeleteAsync(It.Is(state => state.ObjectId == "toDelete2"), null, It.IsAny()), + Times.Once); + } -#warning These tests are incomplete. - [TestMethod] - public void TestPropertyChanged() + [TestMethod] + public async Task TestFetch() + { + // Arrange + var mockController = new Mock(); + mockController.Setup(ctrl => + ctrl.FetchAsync(It.IsAny(), null, It.IsAny(), It.IsAny())) + .ReturnsAsync(new MutableObjectState + { + ObjectId = "fetchedId", + ServerData = new Dictionary { ["key"] = "value" } + }); + + var serviceHub = new MutableServiceHub { - // TODO (hallucinogen): do this - } + ObjectController = mockController.Object + }; + var client = new ParseClient(new ServerConnectionData { Test = true }, serviceHub); + + // Act + var obj = client.CreateObjectWithoutData("TestClass", "fetchedId"); + await obj.FetchAsync(); + + // Assert + Assert.AreEqual("value", obj["key"]); + mockController.Verify(ctrl => + ctrl.FetchAsync(It.Is(state => state.ObjectId == "fetchedId"), null, It.IsAny(), It.IsAny()), + Times.Once); + } - [TestMethod] - [AsyncStateMachine(typeof(ObjectTests))] - public Task TestSave() => - // TODO (hallucinogen): do this - Task.FromResult(0); - - [TestMethod] - [AsyncStateMachine(typeof(ObjectTests))] - public Task TestSaveAll() => - // TODO (hallucinogen): do this - Task.FromResult(0); - - [TestMethod] - [AsyncStateMachine(typeof(ObjectTests))] - public Task TestDelete() => - // TODO (hallucinogen): do this - Task.FromResult(0); - - [TestMethod] - [AsyncStateMachine(typeof(ObjectTests))] - public Task TestDeleteAll() => - // TODO (hallucinogen): do this - Task.FromResult(0); - - [TestMethod] - [AsyncStateMachine(typeof(ObjectTests))] - public Task TestFetch() => - // TODO (hallucinogen): do this - Task.FromResult(0); - - [TestMethod] - [AsyncStateMachine(typeof(ObjectTests))] - public Task TestFetchAll() => - // TODO (hallucinogen): do this - Task.FromResult(0); + + [TestMethod] + public void TestFetchAll() + { + } } diff --git a/Parse.Tests/Parse.Tests.csproj b/Parse.Tests/Parse.Tests.csproj index 5ebd6543..ace87ae1 100644 --- a/Parse.Tests/Parse.Tests.csproj +++ b/Parse.Tests/Parse.Tests.csproj @@ -1,21 +1,18 @@ - - netcoreapp3.1 - false + net6.0 + false latest - - - - - + + + + + - - - + \ No newline at end of file diff --git a/Parse.Tests/ProgressTests.cs b/Parse.Tests/ProgressTests.cs index 5268f646..65c44784 100644 --- a/Parse.Tests/ProgressTests.cs +++ b/Parse.Tests/ProgressTests.cs @@ -4,65 +4,64 @@ using Parse.Abstractions.Infrastructure; using Parse.Infrastructure; -namespace Parse.Tests -{ +namespace Parse.Tests; + #warning Refactor if possible. - [TestClass] - public class ProgressTests +[TestClass] +public class ProgressTests +{ + [TestMethod] + public void TestDownloadProgressEventGetterSetter() { - [TestMethod] - public void TestDownloadProgressEventGetterSetter() - { - IDataTransferLevel downloadProgressEvent = new DataTransferLevel { Amount = 0.5f }; - Assert.AreEqual(0.5f, downloadProgressEvent.Amount); + IDataTransferLevel downloadProgressEvent = new DataTransferLevel { Amount = 0.5f }; + Assert.AreEqual(0.5f, downloadProgressEvent.Amount); - downloadProgressEvent.Amount = 1.0f; - Assert.AreEqual(1.0f, downloadProgressEvent.Amount); - } + downloadProgressEvent.Amount = 1.0f; + Assert.AreEqual(1.0f, downloadProgressEvent.Amount); + } - [TestMethod] - public void TestUploadProgressEventGetterSetter() - { - IDataTransferLevel uploadProgressEvent = new DataTransferLevel { Amount = 0.5f }; - Assert.AreEqual(0.5f, uploadProgressEvent.Amount); + [TestMethod] + public void TestUploadProgressEventGetterSetter() + { + IDataTransferLevel uploadProgressEvent = new DataTransferLevel { Amount = 0.5f }; + Assert.AreEqual(0.5f, uploadProgressEvent.Amount); - uploadProgressEvent.Amount = 1.0f; - Assert.AreEqual(1.0f, uploadProgressEvent.Amount); - } + uploadProgressEvent.Amount = 1.0f; + Assert.AreEqual(1.0f, uploadProgressEvent.Amount); + } - [TestMethod] - public void TestObservingDownloadProgress() - { - int called = 0; - Mock> mockProgress = new Mock>(); - mockProgress.Setup(obj => obj.Report(It.IsAny())).Callback(() => called++); - IProgress progress = mockProgress.Object; + [TestMethod] + public void TestObservingDownloadProgress() + { + int called = 0; + Mock> mockProgress = new Mock>(); + mockProgress.Setup(obj => obj.Report(It.IsAny())).Callback(() => called++); + IProgress progress = mockProgress.Object; - progress.Report(new DataTransferLevel { Amount = 0.2f }); - progress.Report(new DataTransferLevel { Amount = 0.42f }); - progress.Report(new DataTransferLevel { Amount = 0.53f }); - progress.Report(new DataTransferLevel { Amount = 0.68f }); - progress.Report(new DataTransferLevel { Amount = 0.88f }); + progress.Report(new DataTransferLevel { Amount = 0.2f }); + progress.Report(new DataTransferLevel { Amount = 0.42f }); + progress.Report(new DataTransferLevel { Amount = 0.53f }); + progress.Report(new DataTransferLevel { Amount = 0.68f }); + progress.Report(new DataTransferLevel { Amount = 0.88f }); - Assert.AreEqual(5, called); - } + Assert.AreEqual(5, called); + } - [TestMethod] - public void TestObservingUploadProgress() - { - int called = 0; - Mock> mockProgress = new Mock>(); - mockProgress.Setup(obj => obj.Report(It.IsAny())).Callback(() => called++); - IProgress progress = mockProgress.Object; + [TestMethod] + public void TestObservingUploadProgress() + { + int called = 0; + Mock> mockProgress = new Mock>(); + mockProgress.Setup(obj => obj.Report(It.IsAny())).Callback(() => called++); + IProgress progress = mockProgress.Object; - progress.Report(new DataTransferLevel { Amount = 0.2f }); - progress.Report(new DataTransferLevel { Amount = 0.42f }); - progress.Report(new DataTransferLevel { Amount = 0.53f }); - progress.Report(new DataTransferLevel { Amount = 0.68f }); - progress.Report(new DataTransferLevel { Amount = 0.88f }); + progress.Report(new DataTransferLevel { Amount = 0.2f }); + progress.Report(new DataTransferLevel { Amount = 0.42f }); + progress.Report(new DataTransferLevel { Amount = 0.53f }); + progress.Report(new DataTransferLevel { Amount = 0.68f }); + progress.Report(new DataTransferLevel { Amount = 0.88f }); - Assert.AreEqual(5, called); - } + Assert.AreEqual(5, called); } } diff --git a/Parse.Tests/PushEncoderTests.cs b/Parse.Tests/PushEncoderTests.cs index 0a2d583f..e556d4a6 100644 --- a/Parse.Tests/PushEncoderTests.cs +++ b/Parse.Tests/PushEncoderTests.cs @@ -4,53 +4,52 @@ using Newtonsoft.Json; using Parse.Platform.Push; -namespace Parse.Tests +namespace Parse.Tests; + +[TestClass] +public class PushEncoderTests { - [TestClass] - public class PushEncoderTests + [TestMethod] + public void TestEncodeEmpty() { - [TestMethod] - public void TestEncodeEmpty() - { - MutablePushState state = new MutablePushState(); + MutablePushState state = new MutablePushState(); - Assert.ThrowsException(() => ParsePushEncoder.Instance.Encode(state)); - state.Alert = "alert"; + Assert.ThrowsException(() => ParsePushEncoder.Instance.Encode(state)); + state.Alert = "alert"; - Assert.ThrowsException(() => ParsePushEncoder.Instance.Encode(state)); - state.Channels = new List { "channel" }; + Assert.ThrowsException(() => ParsePushEncoder.Instance.Encode(state)); + state.Channels = new List { "channel" }; - ParsePushEncoder.Instance.Encode(state); - } + ParsePushEncoder.Instance.Encode(state); + } - [TestMethod] - public void TestEncode() + [TestMethod] + public void TestEncode() + { + MutablePushState state = new MutablePushState { - MutablePushState state = new MutablePushState + Data = new Dictionary { - Data = new Dictionary - { - ["alert"] = "Some Alert" - }, - Channels = new List { "channel" } - }; + ["alert"] = "Some Alert" + }, + Channels = new List { "channel" } + }; - IDictionary expected = new Dictionary + IDictionary expected = new Dictionary + { + ["data"] = new Dictionary { - ["data"] = new Dictionary - { - ["alert"] = "Some Alert" - }, - ["where"] = new Dictionary + ["alert"] = "Some Alert" + }, + ["where"] = new Dictionary + { + ["channels"] = new Dictionary { - ["channels"] = new Dictionary - { - ["$in"] = new List { "channel" } - } + ["$in"] = new List { "channel" } } - }; + } + }; - Assert.AreEqual(JsonConvert.SerializeObject(expected), JsonConvert.SerializeObject(ParsePushEncoder.Instance.Encode(state))); - } + Assert.AreEqual(JsonConvert.SerializeObject(expected), JsonConvert.SerializeObject(ParsePushEncoder.Instance.Encode(state))); } } diff --git a/Parse.Tests/PushStateTests.cs b/Parse.Tests/PushStateTests.cs index bc08fb8a..e8f2cd42 100644 --- a/Parse.Tests/PushStateTests.cs +++ b/Parse.Tests/PushStateTests.cs @@ -2,39 +2,38 @@ using Parse.Abstractions.Platform.Push; using Parse.Platform.Push; -namespace Parse.Tests +namespace Parse.Tests; + +[TestClass] +public class PushStateTests { - [TestClass] - public class PushStateTests + [TestMethod] + public void TestMutatedClone() { - [TestMethod] - public void TestMutatedClone() - { - MutablePushState state = new MutablePushState(); + MutablePushState state = new MutablePushState(); - IPushState mutated = state.MutatedClone(s => s.Alert = "test"); + IPushState mutated = state.MutatedClone(s => s.Alert = "test"); - Assert.AreEqual(null, state.Alert); - Assert.AreEqual("test", mutated.Alert); - } + Assert.AreEqual(null, state.Alert); + Assert.AreEqual("test", mutated.Alert); + } - [TestMethod] - public void TestEquals() + [TestMethod] + public void TestEquals() + { + MutablePushState state = new MutablePushState { - MutablePushState state = new MutablePushState - { - Alert = "test" - }; + Alert = "test" + }; - MutablePushState otherState = new MutablePushState - { - Alert = "test" - }; + MutablePushState otherState = new MutablePushState + { + Alert = "test" + }; - Assert.AreNotEqual(null, state); - Assert.AreNotEqual("test", state); + Assert.AreNotEqual(null, state); + Assert.AreNotEqual("test", state); - Assert.AreEqual(state, otherState); - } + Assert.AreEqual(state, otherState); } } diff --git a/Parse.Tests/PushTests.cs b/Parse.Tests/PushTests.cs index 6b5f609b..372ff57f 100644 --- a/Parse.Tests/PushTests.cs +++ b/Parse.Tests/PushTests.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -10,143 +9,123 @@ using Parse.Infrastructure; using Parse.Platform.Push; -namespace Parse.Tests +namespace Parse.Tests; + +[TestClass] +public class PushTests { - [TestClass] - public class PushTests + private ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); + + private IParsePushController GetMockedPushController(IPushState expectedPushState) { - ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); + var mockedController = new Mock(MockBehavior.Strict); + mockedController + .Setup(obj => obj.SendPushNotificationAsync(It.Is(s => s.Equals(expectedPushState)), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(false)); - IParsePushController GetMockedPushController(IPushState expectedPushState) - { - Mock mockedController = new Mock(MockBehavior.Strict); - mockedController.Setup(obj => obj.SendPushNotificationAsync(It.Is(s => s.Equals(expectedPushState)), It.IsAny(), It.IsAny())).Returns(Task.FromResult(false)); + return mockedController.Object; + } - return mockedController.Object; - } + private IParsePushChannelsController GetMockedPushChannelsController(IEnumerable channels) + { + var mockedChannelsController = new Mock(MockBehavior.Strict); - IParsePushChannelsController GetMockedPushChannelsController(IEnumerable channels) - { - Mock mockedChannelsController = new Mock(MockBehavior.Strict); - mockedChannelsController.Setup(obj => obj.SubscribeAsync(It.Is>(it => it.CollectionsEqual(channels)), It.IsAny(), It.IsAny())).Returns(Task.FromResult(false)); - mockedChannelsController.Setup(obj => obj.UnsubscribeAsync(It.Is>(it => it.CollectionsEqual(channels)), It.IsAny(), It.IsAny())).Returns(Task.FromResult(false)); + // Setup for SubscribeAsync to accept any IServiceHub instance + mockedChannelsController + .Setup(obj => obj.SubscribeAsync(It.Is>(it => it.CollectionsEqual(channels)), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); // Ensure it returns a completed task + + // Setup for UnsubscribeAsync to accept any IServiceHub instance + mockedChannelsController + .Setup(obj => obj.UnsubscribeAsync(It.Is>(it => it.CollectionsEqual(channels)), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); // Ensure it returns a completed task + + return mockedChannelsController.Object; + } - return mockedChannelsController.Object; - } - [TestCleanup] - public void TearDown() => (Client.Services as ServiceHub).Reset(); - [TestMethod] - [AsyncStateMachine(typeof(PushTests))] - public Task TestSendPush() + [TestCleanup] + public void TearDown() => (Client.Services as ServiceHub).Reset(); + + [TestMethod] + public async Task TestSendPushAsync() + { + // Arrange + var hub = new MutableServiceHub(); + var client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + var state = new MutablePushState { - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + Query = Client.GetInstallationQuery() + }; + + var thePush = new ParsePush(client); + + hub.PushController = GetMockedPushController(state); - MutablePushState state = new MutablePushState - { - Query = Client.GetInstallationQuery() - }; + // Act + thePush.Alert = "Alert"; + state.Alert = "Alert"; - ParsePush thePush = new ParsePush(client); + await thePush.SendAsync(); - hub.PushController = GetMockedPushController(state); + thePush.Channels = new List { "channel" }; + state.Channels = new List { "channel" }; - thePush.Alert = "Alert"; - state.Alert = "Alert"; + await thePush.SendAsync(); - return thePush.SendAsync().ContinueWith(task => - { - Assert.IsTrue(task.IsCompleted); - Assert.IsFalse(task.IsFaulted); + var query = new ParseQuery(client, "aClass"); + thePush.Query = query; + state.Query = query; - thePush.Channels = new List { { "channel" } }; - state.Channels = new List { { "channel" } }; + await thePush.SendAsync(); - return thePush.SendAsync(); - }).Unwrap().ContinueWith(task => - { - Assert.IsTrue(task.IsCompleted); - Assert.IsFalse(task.IsFaulted); + // Assert + Assert.IsTrue(true); // Reaching here means no exceptions occurred + } - ParseQuery query = new ParseQuery(client, "aClass"); + [TestMethod] + public async Task TestSubscribeAsync() + { + // Arrange + var hub = new MutableServiceHub(); + var client = new ParseClient(new ServerConnectionData { Test = true }, hub); - thePush.Query = query; - state.Query = query; + var channels = new List { "test" }; + hub.PushChannelsController = GetMockedPushChannelsController(channels); - return thePush.SendAsync(); - }).Unwrap().ContinueWith(task => - { - Assert.IsTrue(task.IsCompleted); - Assert.IsFalse(task.IsFaulted); - }); - } + // Act + await client.SubscribeToPushChannelAsync("test"); + await client.SubscribeToPushChannelsAsync(new List { "test" }); - [TestMethod] - [AsyncStateMachine(typeof(PushTests))] - public Task TestSubscribe() - { - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); - - List channels = new List { }; - - hub.PushChannelsController = GetMockedPushChannelsController(channels); - - channels.Add("test"); - - return client.SubscribeToPushChannelAsync("test").ContinueWith(task => - { - Assert.IsTrue(task.IsCompleted); - Assert.IsFalse(task.IsFaulted); - - return client.SubscribeToPushChannelsAsync(new List { "test" }); - }).Unwrap().ContinueWith(task => - { - Assert.IsTrue(task.IsCompleted); - Assert.IsFalse(task.IsFaulted); - - CancellationTokenSource cancellationTokenSource = new CancellationTokenSource { }; - return client.SubscribeToPushChannelsAsync(new List { "test" }, cancellationTokenSource.Token); - }).Unwrap().ContinueWith(task => - { - Assert.IsTrue(task.IsCompleted); - Assert.IsFalse(task.IsFaulted); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(PushTests))] - public Task TestUnsubscribe() - { - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); - - List channels = new List { }; - - hub.PushChannelsController = GetMockedPushChannelsController(channels); - - channels.Add("test"); - - return client.UnsubscribeToPushChannelAsync("test").ContinueWith(task => - { - Assert.IsTrue(task.IsCompleted); - Assert.IsFalse(task.IsFaulted); - - return client.UnsubscribeToPushChannelsAsync(new List { { "test" } }); - }).ContinueWith(task => - { - Assert.IsTrue(task.IsCompleted); - Assert.IsFalse(task.IsFaulted); - - CancellationTokenSource cancellationTokenSource = new CancellationTokenSource { }; - return client.UnsubscribeToPushChannelsAsync(new List { { "test" } }, cancellationTokenSource.Token); - }).ContinueWith(task => - { - Assert.IsTrue(task.IsCompleted); - Assert.IsFalse(task.IsFaulted); - }); - } + using var cancellationTokenSource = new CancellationTokenSource(); + await client.SubscribeToPushChannelsAsync(new List { "test" }, cancellationTokenSource.Token); + + // Assert + Assert.IsTrue(true); // Reaching here means no exceptions occurred + } + + [TestMethod] + public async Task TestUnsubscribeAsync() + { + // Arrange + var hub = new MutableServiceHub(); + var client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + var channels = new List { "test" }; // Corrected to ensure we have the "test" channel + hub.PushChannelsController = GetMockedPushChannelsController(channels); + + // Act + await client.UnsubscribeToPushChannelAsync("test"); + await client.UnsubscribeToPushChannelsAsync(new List { "test" }); + + using var cancellationTokenSource = new CancellationTokenSource(); + await client.UnsubscribeToPushChannelsAsync(new List { "test" }, cancellationTokenSource.Token); + + // Assert + Assert.IsTrue(true); // Reaching here means no exceptions occurred } + + } diff --git a/Parse.Tests/RelationTests.cs b/Parse.Tests/RelationTests.cs index d0929b21..c1a362cf 100644 --- a/Parse.Tests/RelationTests.cs +++ b/Parse.Tests/RelationTests.cs @@ -3,26 +3,25 @@ using Parse.Abstractions.Internal; using Parse.Infrastructure; -namespace Parse.Tests +namespace Parse.Tests; + +[TestClass] +public class RelationTests { - [TestClass] - public class RelationTests + [TestMethod] + public void TestRelationQuery() { - [TestMethod] - public void TestRelationQuery() - { - ParseObject parent = new ServiceHub { }.CreateObjectWithoutData("Foo", "abcxyz"); + ParseObject parent = new ServiceHub { }.CreateObjectWithoutData("Foo", "abcxyz"); - ParseRelation relation = parent.GetRelation("child"); - ParseQuery query = relation.Query; + ParseRelation relation = parent.GetRelation("child"); + ParseQuery query = relation.Query; - // Client side, the query will appear to be for the wrong class. - // When the server recieves it, the class name will be redirected using the 'redirectClassNameForKey' option. - Assert.AreEqual("Foo", query.GetClassName()); + // Client side, the query will appear to be for the wrong class. + // When the server recieves it, the class name will be redirected using the 'redirectClassNameForKey' option. + Assert.AreEqual("Foo", query.GetClassName()); - IDictionary encoded = query.BuildParameters(); + IDictionary encoded = query.BuildParameters(); - Assert.AreEqual("child", encoded["redirectClassNameForKey"]); - } + Assert.AreEqual("child", encoded["redirectClassNameForKey"]); } } \ No newline at end of file diff --git a/Parse.Tests/SessionControllerTests.cs b/Parse.Tests/SessionControllerTests.cs index 454ae0ac..a9660ea7 100644 --- a/Parse.Tests/SessionControllerTests.cs +++ b/Parse.Tests/SessionControllerTests.cs @@ -1,130 +1,172 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Net; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Parse.Abstractions.Infrastructure; using Parse.Abstractions.Infrastructure.Execution; -using Parse.Abstractions.Platform.Objects; -using Parse.Abstractions.Platform.Sessions; using Parse.Infrastructure; using Parse.Infrastructure.Execution; using Parse.Platform.Sessions; -namespace Parse.Tests +namespace Parse.Tests; + +[TestClass] +public class SessionControllerTests { - [TestClass] - public class SessionControllerTests + private ParseClient Client { get; set; } + + [TestInitialize] + public void SetUp() { -#warning Check if reinitializing the client for every test method is really necessary. + // Initialize ParseClient with test mode and ensure it's globally available + Client = new ParseClient(new ServerConnectionData { Test = true }); + Client.Publicize(); - ParseClient Client { get; set; } + // Register valid classes that will be used in the tests + Client.AddValidClass(); + Client.AddValidClass(); + } - [TestInitialize] - public void SetUp() => Client = new ParseClient(new ServerConnectionData { ApplicationID = "", Key = "", Test = true }); - [TestMethod] - [AsyncStateMachine(typeof(SessionControllerTests))] - public Task TestGetSessionWithEmptyResult() => new ParseSessionController(CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, null)).Object, Client.Decoder).GetSessionAsync("S0m3Se551on", Client, CancellationToken.None).ContinueWith(task => + [TestMethod] + public async Task TestGetSessionWithEmptyResultAsync() + { + var controller = new ParseSessionController( + CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, null)).Object, + Client.Decoder + ); + + await Assert.ThrowsExceptionAsync(async () => { - Assert.IsTrue(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); + await controller.GetSessionAsync("S0m3Se551on", Client, CancellationToken.None); }); + } - [TestMethod] - [AsyncStateMachine(typeof(SessionControllerTests))] - public Task TestGetSession() - { - Tuple> response = new Tuple>(HttpStatusCode.Accepted, new Dictionary + [TestMethod] + public async Task TestGetSessionAsync() + { + var response = new Tuple>( + HttpStatusCode.Accepted, + new Dictionary { ["__type"] = "Object", ["className"] = "Session", ["sessionToken"] = "S0m3Se551on", ["restricted"] = true - }); - - Mock mockRunner = CreateMockRunner(response); - - return new ParseSessionController(mockRunner.Object, Client.Decoder).GetSessionAsync("S0m3Se551on", Client, CancellationToken.None).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "sessions/me"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); - - IObjectState session = task.Result; - Assert.AreEqual(2, session.Count()); - Assert.IsTrue((bool) session["restricted"]); - Assert.AreEqual("S0m3Se551on", session["sessionToken"]); - }); - } + } + ); + + var mockRunner = CreateMockRunner(response); + var controller = new ParseSessionController(mockRunner.Object, Client.Decoder); + + var session = await controller.GetSessionAsync("S0m3Se551on", Client, CancellationToken.None); + + // Assertions + Assert.IsNotNull(session); + Assert.AreEqual(4, session.Count()); + Assert.IsTrue((bool) session["restricted"]); + Assert.AreEqual("S0m3Se551on", session["sessionToken"]); + + mockRunner.Verify( + obj => obj.RunCommandAsync( + It.Is(command => command.Path == "sessions/me"), + It.IsAny>(), + It.IsAny>(), + It.IsAny()), + Times.Once + ); + } - [TestMethod] - [AsyncStateMachine(typeof(SessionControllerTests))] - public Task TestRevoke() - { - Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, default)); + [TestMethod] + public async Task TestRevokeAsync() + { + var mockRunner = CreateMockRunner( + new Tuple>(HttpStatusCode.Accepted, default) + ); + + var controller = new ParseSessionController(mockRunner.Object, Client.Decoder); + await controller.RevokeAsync("S0m3Se551on", CancellationToken.None); + + // Assertions + mockRunner.Verify( + obj => obj.RunCommandAsync( + It.Is(command => command.Path == "logout"), + It.IsAny>(), + It.IsAny>(), + It.IsAny()), + Times.Once + ); + } - return new ParseSessionController(mockRunner.Object, Client.Decoder).RevokeAsync("S0m3Se551on", CancellationToken.None).ContinueWith(task => + [TestMethod] + public async Task TestUpgradeToRevocableSessionAsync() + { + var response = new Tuple>( + HttpStatusCode.Accepted, + new Dictionary { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); + ["__type"] = "Object", + ["className"] = "Session", + ["sessionToken"] = "S0m3Se551on", + ["restricted"] = true + } + ); - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "logout"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); - }); - } + var mockRunner = CreateMockRunner(response); + var controller = new ParseSessionController(mockRunner.Object, Client.Decoder); - [TestMethod] - [AsyncStateMachine(typeof(SessionControllerTests))] - public Task TestUpgradeToRevocableSession() + var session = await controller.UpgradeToRevocableSessionAsync("S0m3Se551on", Client, CancellationToken.None); + foreach (var item in session) { - Tuple> response = new Tuple>(HttpStatusCode.Accepted, - new Dictionary - { - ["__type"] = "Object", - ["className"] = "Session", - ["sessionToken"] = "S0m3Se551on", - ["restricted"] = true - }); - - Mock mockRunner = CreateMockRunner(response); - - return new ParseSessionController(mockRunner.Object, Client.Decoder).UpgradeToRevocableSessionAsync("S0m3Se551on", Client, CancellationToken.None).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "upgradeToRevocableSession"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); - - IObjectState session = task.Result; - Assert.AreEqual(2, session.Count()); - Assert.IsTrue((bool) session["restricted"]); - Assert.AreEqual("S0m3Se551on", session["sessionToken"]); - }); + Debug.Write(item.Key); + Debug.Write(" Val : "); + Debug.Write(item.Value); } + // Assertions + Assert.IsNotNull(session); + Assert.AreEqual(4, session.Count()); + Assert.IsTrue((bool) session["restricted"]); + Assert.AreEqual("S0m3Se551on", session["sessionToken"]); + + mockRunner.Verify( + obj => obj.RunCommandAsync( + It.Is(command => command.Path == "upgradeToRevocableSession"), + It.IsAny>(), + It.IsAny>(), + It.IsAny()), + Times.Once + ); + } - [TestMethod] - public void TestIsRevocableSessionToken() - { - IParseSessionController sessionController = new ParseSessionController(Mock.Of(), Client.Decoder); - Assert.IsTrue(sessionController.IsRevocableSessionToken("r:session")); - Assert.IsTrue(sessionController.IsRevocableSessionToken("r:session:r:")); - Assert.IsTrue(sessionController.IsRevocableSessionToken("session:r:")); - Assert.IsFalse(sessionController.IsRevocableSessionToken("session:s:d:r")); - Assert.IsFalse(sessionController.IsRevocableSessionToken("s:ession:s:d:r")); - Assert.IsFalse(sessionController.IsRevocableSessionToken("")); - } - - - private Mock CreateMockRunner(Tuple> response) - { - Mock mockRunner = new Mock(); - mockRunner.Setup(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(response)); + [TestMethod] + public void TestIsRevocableSessionToken() + { + var sessionController = new ParseSessionController(Mock.Of(), Client.Decoder); + + Assert.IsTrue(sessionController.IsRevocableSessionToken("r:session")); + Assert.IsTrue(sessionController.IsRevocableSessionToken("r:session:r:")); + Assert.IsTrue(sessionController.IsRevocableSessionToken("session:r:")); + Assert.IsFalse(sessionController.IsRevocableSessionToken("session:s:d:r")); + Assert.IsFalse(sessionController.IsRevocableSessionToken("s:ession:s:d:r")); + Assert.IsFalse(sessionController.IsRevocableSessionToken("")); + } - return mockRunner; - } + private Mock CreateMockRunner(Tuple> response) + { + var mockRunner = new Mock(); + mockRunner + .Setup(obj => obj.RunCommandAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(response); + + return mockRunner; } } diff --git a/Parse.Tests/SessionTests.cs b/Parse.Tests/SessionTests.cs index 2bff624e..163da6af 100644 --- a/Parse.Tests/SessionTests.cs +++ b/Parse.Tests/SessionTests.cs @@ -1,167 +1,179 @@ -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Threading; using Parse.Abstractions.Infrastructure; -using Parse.Abstractions.Platform.Objects; using Parse.Abstractions.Platform.Sessions; -using Parse.Abstractions.Platform.Users; using Parse.Infrastructure; +using Parse; using Parse.Platform.Objects; +using Parse.Abstractions.Platform.Users; -namespace Parse.Tests +[TestClass] +public class SessionTests { - [TestClass] - public class SessionTests + private ParseClient Client { get; } + + public SessionTests() { - ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); + // Initialize the client + Client = new ParseClient(new ServerConnectionData { Test = true }); + Client.Publicize(); // Ensure the client instance is globally available - [TestInitialize] - public void SetUp() - { - Client.AddValidClass(); - Client.AddValidClass(); - } + // Register the valid classes + Client.AddValidClass(); + Client.AddValidClass(); + } - [TestCleanup] - public void TearDown() => (Client.Services as ServiceHub).Reset(); + [TestInitialize] + public void SetUp() + { + // Additional setup can go here + } - [TestMethod] - public void TestGetSessionQuery() => Assert.IsInstanceOfType(Client.GetSessionQuery(), typeof(ParseQuery)); + [TestCleanup] + public void TearDown() + { + // Reset any state if necessary + (Client.Services as ServiceHub)?.Reset(); + } - [TestMethod] - public void TestGetSessionToken() - { - ParseSession session = Client.GenerateObjectFromState(new MutableObjectState { ServerData = new Dictionary() { ["sessionToken"] = "llaKcolnu" } }, "_Session"); + [TestMethod] + public void TestGetSessionQuery() + { + // Test that GetSessionQuery returns the correct type + Assert.IsInstanceOfType(Client.GetSessionQuery(), typeof(ParseQuery)); + } + + [TestMethod] + public void TestGetSessionToken() + { + var session = Client.GenerateObjectFromState( + new MutableObjectState + { + ServerData = new Dictionary { ["sessionToken"] = "llaKcolnu" } + }, + "_Session" + ); - Assert.IsNotNull(session); - Assert.AreEqual("llaKcolnu", session.SessionToken); - } + Assert.IsNotNull(session); + Assert.AreEqual("llaKcolnu", session.SessionToken); + } + + [TestMethod] + public async Task TestGetCurrentSessionAsync() + { + // Set up the service hub and mock controllers + var hub = new MutableServiceHub(); + var client = new ParseClient(new ServerConnectionData { Test = true }, hub); - [TestMethod] - [AsyncStateMachine(typeof(SessionTests))] - public Task TestGetCurrentSession() + var sessionState = new MutableObjectState { - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + ServerData = new Dictionary { ["sessionToken"] = "newllaKcolnu" } + }; - IObjectState sessionState = new MutableObjectState - { - ServerData = new Dictionary - { - ["sessionToken"] = "newllaKcolnu" - } - }; + // Mock session controller + var mockController = new Mock(); + mockController + .Setup(obj => obj.GetSessionAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(sessionState); - Mock mockController = new Mock(); - mockController.Setup(obj => obj.GetSessionAsync(It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(sessionState)); + // Mock user controller + var userState = new MutableObjectState + { + ServerData = new Dictionary { ["sessionToken"] = "llaKcolnu" } + }; - IObjectState userState = new MutableObjectState - { - ServerData = new Dictionary - { - ["sessionToken"] = "llaKcolnu" - } - }; + var user = client.GenerateObjectFromState(userState, "_User"); - ParseUser user = client.GenerateObjectFromState(userState, "_User"); + var mockCurrentUserController = new Mock(); + mockCurrentUserController + .Setup(obj => obj.GetAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(user); - Mock mockCurrentUserController = new Mock(); - mockCurrentUserController.Setup(obj => obj.GetAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(user)); + hub.SessionController = mockController.Object; + hub.CurrentUserController = mockCurrentUserController.Object; - hub.SessionController = mockController.Object; - hub.CurrentUserController = mockCurrentUserController.Object; + var session = await client.GetCurrentSessionAsync(); - return client.GetCurrentSessionAsync().ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); + // Assertions + Assert.IsNotNull(session); + Assert.AreEqual("newllaKcolnu", session.SessionToken); - mockController.Verify(obj => obj.GetSessionAsync(It.Is(sessionToken => sessionToken == "llaKcolnu"), It.IsAny(),It.IsAny()), Times.Exactly(1)); + // Verify that the session controller was called with the correct session token + mockController.Verify( + obj => obj.GetSessionAsync("llaKcolnu", It.IsAny(), It.IsAny()), + Times.Once + ); + } - ParseSession session = task.Result; - Assert.AreEqual("newllaKcolnu", session.SessionToken); - }); - } + [TestMethod] + public async Task TestGetCurrentSessionWithNoCurrentUserAsync() + { + var hub = new MutableServiceHub(); + var client = new ParseClient(new ServerConnectionData { Test = true }, hub); - [TestMethod] - [AsyncStateMachine(typeof(SessionTests))] - public Task TestGetCurrentSessionWithNoCurrentUser() - { - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + var mockController = new Mock(); + var mockCurrentUserController = new Mock(); - Mock mockController = new Mock(); - Mock mockCurrentUserController = new Mock(); + hub.SessionController = mockController.Object; + hub.CurrentUserController = mockCurrentUserController.Object; - hub.SessionController = mockController.Object; - hub.CurrentUserController = mockCurrentUserController.Object; + var session = await client.GetCurrentSessionAsync(); - return client.GetCurrentSessionAsync().ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - Assert.IsNull(task.Result); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(SessionTests))] - public Task TestRevoke() - { - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + // Assertions + Assert.IsNull(session); + } - Mock mockController = new Mock(); - mockController.Setup(sessionController => sessionController.IsRevocableSessionToken(It.IsAny())).Returns(true); + [TestMethod] + public async Task TestRevokeAsync() + { + var hub = new MutableServiceHub(); + var client = new ParseClient(new ServerConnectionData { Test = true }, hub); - hub.SessionController = mockController.Object; + var mockController = new Mock(); + mockController.Setup(sessionController => sessionController.IsRevocableSessionToken(It.IsAny())).Returns(true); - CancellationTokenSource source = new CancellationTokenSource { }; - return client.RevokeSessionAsync("r:someSession", source.Token).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); + hub.SessionController = mockController.Object; - mockController.Verify(obj => obj.RevokeAsync(It.Is(sessionToken => sessionToken == "r:someSession"), source.Token), Times.Exactly(1)); - }); - } + using var cancellationTokenSource = new CancellationTokenSource(); + await client.RevokeSessionAsync("r:someSession", cancellationTokenSource.Token); - [TestMethod] - [AsyncStateMachine(typeof(SessionTests))] - public Task TestUpgradeToRevocableSession() - { - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + // Assertions + mockController.Verify( + obj => obj.RevokeAsync("r:someSession", cancellationTokenSource.Token), + Times.Once + ); + } - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary() - { - ["sessionToken"] = "llaKcolnu" - } - }; + [TestMethod] + public async Task TestUpgradeToRevocableSessionAsync() + { + var hub = new MutableServiceHub(); + var client = new ParseClient(new ServerConnectionData { Test = true }, hub); - Mock mockController = new Mock(); - mockController.Setup(obj => obj.UpgradeToRevocableSessionAsync(It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(state)); + var state = new MutableObjectState + { + ServerData = new Dictionary { ["sessionToken"] = "llaKcolnu" } + }; - Mock mockCurrentUserController = new Mock(); + var mockController = new Mock(); + mockController + .Setup(obj => obj.UpgradeToRevocableSessionAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(state); - hub.SessionController = mockController.Object; - hub.CurrentUserController = mockCurrentUserController.Object; + hub.SessionController = mockController.Object; - CancellationTokenSource source = new CancellationTokenSource { }; - return client.UpgradeToRevocableSessionAsync("someSession", source.Token).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); + using var cancellationTokenSource = new CancellationTokenSource(); + var sessionToken = await client.UpgradeToRevocableSessionAsync("someSession", cancellationTokenSource.Token); - mockController.Verify(obj => obj.UpgradeToRevocableSessionAsync(It.Is(sessionToken => sessionToken == "someSession"), It.IsAny(), source.Token), Times.Exactly(1)); + // Assertions + Assert.AreEqual("llaKcolnu", sessionToken); - Assert.AreEqual("llaKcolnu", task.Result); - }); - } + mockController.Verify( + obj => obj.UpgradeToRevocableSessionAsync("someSession", It.IsAny(), cancellationTokenSource.Token), + Times.Once + ); } } diff --git a/Parse.Tests/UserControllerTests.cs b/Parse.Tests/UserControllerTests.cs index edb2ac12..06c83a2d 100644 --- a/Parse.Tests/UserControllerTests.cs +++ b/Parse.Tests/UserControllerTests.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Net; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -9,178 +9,206 @@ using Parse.Abstractions.Infrastructure; using Parse.Abstractions.Infrastructure.Control; using Parse.Abstractions.Infrastructure.Execution; -using Parse.Abstractions.Platform.Objects; using Parse.Infrastructure; using Parse.Infrastructure.Execution; using Parse.Platform.Objects; using Parse.Platform.Users; -namespace Parse.Tests +namespace Parse.Tests; + +[TestClass] +public class UserControllerTests { - [TestClass] - public class UserControllerTests - { - ParseClient Client { get; set; } + private ParseClient Client { get; set; } - [TestInitialize] - public void SetUp() => Client = new ParseClient(new ServerConnectionData { ApplicationID = "", Key = "", Test = true }); + [TestInitialize] + public void SetUp() => Client = new ParseClient(new ServerConnectionData { ApplicationID = "", Key = "", Test = true }); - [TestMethod] - [AsyncStateMachine(typeof(UserControllerTests))] - public Task TestSignUp() + [TestMethod] + public async Task TestSignUpAsync() + { + var state = new MutableObjectState { - MutableObjectState state = new MutableObjectState - { - ClassName = "_User", - ServerData = new Dictionary - { - ["username"] = "hallucinogen", - ["password"] = "secret" - } - }; - - Dictionary operations = new Dictionary + ClassName = "_User", + ServerData = new Dictionary { - ["gogo"] = new Mock().Object - }; + ["username"] = "hallucinogen", + ["password"] = "secret" + } + }; - Dictionary responseDict = new Dictionary - { - ["__type"] = "Object", - ["className"] = "_User", - ["objectId"] = "d3ImSh3ki", - ["sessionToken"] = "s3ss10nt0k3n", - ["createdAt"] = "2015-09-18T18:11:28.943Z" - }; + var operations = new Dictionary + { + ["gogo"] = new Mock().Object + }; - Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, responseDict)); + var responseDict = new Dictionary + { + ["__type"] = "Object", + ["className"] = "_User", + ["objectId"] = "d3ImSh3ki", + ["sessionToken"] = "s3ss10nt0k3n", + ["createdAt"] = "2015-09-18T18:11:28.943Z" + }; + + var mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, responseDict)); + + var controller = new ParseUserController(mockRunner.Object, Client.Decoder); + var newState = await controller.SignUpAsync(state, operations, Client, CancellationToken.None); + + // Assertions + Assert.IsNotNull(newState); + Assert.AreEqual("s3ss10nt0k3n", newState["sessionToken"]); + Assert.AreEqual("d3ImSh3ki", newState.ObjectId); + Assert.IsNotNull(newState.CreatedAt); + Assert.IsNotNull(newState.UpdatedAt); + + mockRunner.Verify( + obj => obj.RunCommandAsync( + It.Is(command => command.Path == "classes/_User"), + It.IsAny>(), + It.IsAny>(), + It.IsAny()), + Times.Once + ); + } - return new ParseUserController(mockRunner.Object, Client.Decoder).SignUpAsync(state, operations, Client, CancellationToken.None).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "classes/_User"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); - - IObjectState newState = task.Result; - Assert.AreEqual("s3ss10nt0k3n", newState["sessionToken"]); - Assert.AreEqual("d3ImSh3ki", newState.ObjectId); - Assert.IsNotNull(newState.CreatedAt); - Assert.IsNotNull(newState.UpdatedAt); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserControllerTests))] - public Task TestLogInWithUsernamePassword() + [TestMethod] + public async Task TestLogInWithUsernamePasswordAsync() + { + // Mock the server response for login + var responseDict = new Dictionary { - Dictionary responseDict = new Dictionary - { - ["__type"] = "Object", - ["className"] = "_User", - ["objectId"] = "d3ImSh3ki", - ["sessionToken"] = "s3ss10nt0k3n", - ["createdAt"] = "2015-09-18T18:11:28.943Z" - }; + ["__type"] = "Object", + ["className"] = "_User", + ["objectId"] = "d3ImSh3ki", + ["sessionToken"] = "s3ss10nt0k3n", + ["createdAt"] = "2015-09-18T18:11:28.943Z" + }; - Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, responseDict)); + var mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, responseDict)); - return new ParseUserController(mockRunner.Object, Client.Decoder).LogInAsync("grantland", "123grantland123", Client, CancellationToken.None).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "login?username=grantland&password=123grantland123"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); - - IObjectState newState = task.Result; - Assert.AreEqual("s3ss10nt0k3n", newState["sessionToken"]); - Assert.AreEqual("d3ImSh3ki", newState.ObjectId); - Assert.IsNotNull(newState.CreatedAt); - Assert.IsNotNull(newState.UpdatedAt); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserControllerTests))] - public Task TestLogInWithAuthData() - { - Dictionary responseDict = new Dictionary - { - ["__type"] = "Object" , - ["className"] = "_User" , - ["objectId"] = "d3ImSh3ki" , - ["sessionToken"] = "s3ss10nt0k3n" , - ["createdAt"] = "2015-09-18T18:11:28.943Z" - }; + var controller = new ParseUserController(mockRunner.Object, Client.Decoder); - Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, responseDict)); + // Call LogInAsync + var newState = await controller.LogInAsync("grantland", "123grantland123", Client, CancellationToken.None); - return new ParseUserController(mockRunner.Object, Client.Decoder).LogInAsync("facebook", data: null, serviceHub: Client, cancellationToken: CancellationToken.None).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "users"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); - - IObjectState newState = task.Result; - Assert.AreEqual("s3ss10nt0k3n", newState["sessionToken"]); - Assert.AreEqual("d3ImSh3ki", newState.ObjectId); - Assert.IsNotNull(newState.CreatedAt); - Assert.IsNotNull(newState.UpdatedAt); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserControllerTests))] - public Task TestGetUserFromSessionToken() - { - Dictionary responseDict = new Dictionary - { - ["__type"] = "Object", - ["className"] = "_User", - ["objectId"] = "d3ImSh3ki", - ["sessionToken"] = "s3ss10nt0k3n", - ["createdAt"] = "2015-09-18T18:11:28.943Z" - }; + // Assertions to check that the response was correctly processed + Assert.IsNotNull(newState); + Assert.AreEqual("s3ss10nt0k3n", newState["sessionToken"]); + Assert.AreEqual("d3ImSh3ki", newState.ObjectId); + Assert.IsNotNull(newState.CreatedAt); + Assert.IsNotNull(newState.UpdatedAt); + mockRunner.Verify( + obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny()), + Times.Once + ); - Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, responseDict)); + - return new ParseUserController(mockRunner.Object, Client.Decoder).GetUserAsync("s3ss10nt0k3n", Client, CancellationToken.None).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "users/me"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); - - IObjectState newState = task.Result; - Assert.AreEqual("s3ss10nt0k3n", newState["sessionToken"]); - Assert.AreEqual("d3ImSh3ki", newState.ObjectId); - Assert.IsNotNull(newState.CreatedAt); - Assert.IsNotNull(newState.UpdatedAt); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserControllerTests))] - public Task TestRequestPasswordReset() + } + + + + [TestMethod] + public async Task TestLogInWithAuthDataAsync() + { + var responseDict = new Dictionary { - Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary { })); + ["__type"] = "Object", + ["className"] = "_User", + ["objectId"] = "d3ImSh3ki", + ["sessionToken"] = "s3ss10nt0k3n", + ["createdAt"] = "2015-09-18T18:11:28.943Z" + }; + + var mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, responseDict)); + + Parse.Platform.Users.ParseUserController controller = new Parse.Platform.Users.ParseUserController(mockRunner.Object, Client.Decoder); + + // Handle null data gracefully by passing an empty dictionary if null is provided + var authData = new Dictionary(); // Handle null by passing an empty dictionary + var newState = await controller.LogInAsync(authType: "facebook", data: authData, serviceHub: Client, cancellationToken: CancellationToken.None); + + // Assertions + Assert.IsNotNull(newState); + Assert.AreEqual("s3ss10nt0k3n", newState["sessionToken"]); + Assert.AreEqual("d3ImSh3ki", newState.ObjectId); + Assert.IsNotNull(newState.CreatedAt); + Assert.IsNotNull(newState.UpdatedAt); + + mockRunner.Verify( + obj => obj.RunCommandAsync( + It.Is(command => command.Path == "users"), + It.IsAny>(), + It.IsAny>(), + It.IsAny()), + Times.Once + ); + } - return new ParseUserController(mockRunner.Object, Client.Decoder).RequestPasswordResetAsync("gogo@parse.com", CancellationToken.None).ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "requestPasswordReset"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); - }); - } - Mock CreateMockRunner(Tuple> response) + [TestMethod] + public async Task TestGetUserFromSessionTokenAsync() + { + var responseDict = new Dictionary { - Mock mockRunner = new Mock { }; - mockRunner.Setup(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(response)); + ["__type"] = "Object", + ["className"] = "_User", + ["objectId"] = "d3ImSh3ki", + ["sessionToken"] = "s3ss10nt0k3n", + ["createdAt"] = "2015-09-18T18:11:28.943Z" + }; + + var mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, responseDict)); + + var controller = new ParseUserController(mockRunner.Object, Client.Decoder); + var newState = await controller.GetUserAsync("s3ss10nt0k3n", Client, CancellationToken.None); + + // Assertions + Assert.IsNotNull(newState); + Assert.AreEqual("s3ss10nt0k3n", newState["sessionToken"]); + Assert.AreEqual("d3ImSh3ki", newState.ObjectId); + Assert.IsNotNull(newState.CreatedAt); + Assert.IsNotNull(newState.UpdatedAt); + + mockRunner.Verify( + obj => obj.RunCommandAsync( + It.Is(command => command.Path == "users/me"), + It.IsAny>(), + It.IsAny>(), + It.IsAny()), + Times.Once + ); + } + + [TestMethod] + public async Task TestRequestPasswordResetAsync() + { + var mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary())); + + var controller = new ParseUserController(mockRunner.Object, Client.Decoder); + await controller.RequestPasswordResetAsync("gogo@parse.com", CancellationToken.None); + + // Assertions + mockRunner.Verify( + obj => obj.RunCommandAsync( + It.Is(command => command.Path == "requestPasswordReset"), + It.IsAny>(), + It.IsAny>(), + It.IsAny()), + Times.Once + ); + } + + private Mock CreateMockRunner(Tuple> response) + { + var mockRunner = new Mock(); + mockRunner + .Setup(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())) + .ReturnsAsync(response); - return mockRunner; - } + return mockRunner; } } diff --git a/Parse.Tests/UserTests.cs b/Parse.Tests/UserTests.cs index 4e205ff2..be84a16c 100644 --- a/Parse.Tests/UserTests.cs +++ b/Parse.Tests/UserTests.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -12,779 +11,268 @@ using Parse.Abstractions.Platform.Sessions; using Parse.Abstractions.Platform.Users; using Parse.Platform.Objects; +using System.Diagnostics; -namespace Parse.Tests -{ -#warning Class refactoring requires completion. - - [TestClass] - public class UserTests - { - ParseClient Client { get; set; } = new ParseClient(new ServerConnectionData { Test = true }); - - [TestCleanup] - public void TearDown() => (Client.Services as ServiceHub).Reset(); - - [TestMethod] - public void TestRemoveFields() - { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary - { - ["username"] = "kevin", - ["name"] = "andrew" - } - }; - - ParseUser user = Client.GenerateObjectFromState(state, "_User"); - Assert.ThrowsException(() => user.Remove("username")); - - try - { - user.Remove("name"); - } - catch - { - Assert.Fail(@"Removing ""name"" field on ParseUser should not throw an exception because ""name"" is not an immutable field and was defined on the object."); - } - - Assert.IsFalse(user.ContainsKey("name")); - } - - [TestMethod] - public void TestSessionTokenGetter() - { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary - { - ["username"] = "kevin", - ["sessionToken"] = "se551onT0k3n" - } - }; - - ParseUser user = Client.GenerateObjectFromState(state, "_User"); - Assert.AreEqual("se551onT0k3n", user.SessionToken); - } - - [TestMethod] - public void TestUsernameGetterSetter() - { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary - { - ["username"] = "kevin", - } - }; - - ParseUser user = Client.GenerateObjectFromState(state, "_User"); - Assert.AreEqual("kevin", user.Username); - user.Username = "ilya"; - Assert.AreEqual("ilya", user.Username); - } - - [TestMethod] - public void TestPasswordGetterSetter() - { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary - { - ["username"] = "kevin", - ["password"] = "hurrah" - } - }; - - ParseUser user = Client.GenerateObjectFromState(state, "_User"); - Assert.AreEqual("hurrah", user.State["password"]); - user.Password = "david"; - Assert.IsNotNull(user.CurrentOperations["password"]); - } - - [TestMethod] - public void TestEmailGetterSetter() - { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary - { - ["email"] = "james@parse.com", - ["name"] = "andrew", - ["sessionToken"] = "se551onT0k3n" - } - }; - - ParseUser user = Client.GenerateObjectFromState(state, "_User"); - Assert.AreEqual("james@parse.com", user.Email); - user.Email = "bryan@parse.com"; - Assert.AreEqual("bryan@parse.com", user.Email); - } - - [TestMethod] - public void TestAuthDataGetter() - { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary - { - ["email"] = "james@parse.com", - ["authData"] = new Dictionary - { - ["facebook"] = new Dictionary - { - ["sessionToken"] = "none" - } - } - } - }; - - ParseUser user = Client.GenerateObjectFromState(state, "_User"); - Assert.AreEqual(1, user.AuthData.Count); - Assert.IsInstanceOfType(user.AuthData["facebook"], typeof(IDictionary)); - } - - [TestMethod] - public void TestGetUserQuery() => Assert.IsInstanceOfType(Client.GetUserQuery(), typeof(ParseQuery)); - - [TestMethod] - public void TestIsAuthenticated() - { - IObjectState state = new MutableObjectState - { - ObjectId = "wagimanPutraPetir", - ServerData = new Dictionary - { - ["sessionToken"] = "llaKcolnu" - } - }; - - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); - - ParseUser user = client.GenerateObjectFromState(state, "_User"); - - Mock mockCurrentUserController = new Mock { }; - mockCurrentUserController.Setup(obj => obj.GetAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(user)); - - hub.CurrentUserController = mockCurrentUserController.Object; - - Assert.IsTrue(user.IsAuthenticated); - } - - [TestMethod] - public void TestIsAuthenticatedWithOtherParseUser() - { - IObjectState state = new MutableObjectState - { - ObjectId = "wagimanPutraPetir", - ServerData = new Dictionary - { - ["sessionToken"] = "llaKcolnu" - } - }; - - IObjectState state2 = new MutableObjectState - { - ObjectId = "wagimanPutraPetir2", - ServerData = new Dictionary - { - ["sessionToken"] = "llaKcolnu" - } - }; - - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); - - ParseUser user = client.GenerateObjectFromState(state, "_User"); - ParseUser user2 = client.GenerateObjectFromState(state2, "_User"); - - Mock mockCurrentUserController = new Mock { }; - mockCurrentUserController.Setup(obj => obj.GetAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(user)); +namespace Parse.Tests; - hub.CurrentUserController = mockCurrentUserController.Object; - - Assert.IsFalse(user2.IsAuthenticated); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestSignUpWithInvalidServerData() - { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary - { - ["sessionToken"] = "llaKcolnu" - } - }; - - ParseUser user = Client.GenerateObjectFromState(state, "_User"); - - return user.SignUpAsync().ContinueWith(task => - { - Assert.IsTrue(task.IsFaulted); - Assert.IsInstanceOfType(task.Exception.InnerException, typeof(InvalidOperationException)); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestSignUp() - { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary - { - ["sessionToken"] = "llaKcolnu", - ["username"] = "ihave", - ["password"] = "adream" - } - }; - - IObjectState newState = new MutableObjectState - { - ObjectId = "some0neTol4v4" - }; - - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); - - ParseUser user = client.GenerateObjectFromState(state, "_User"); - - Mock mockController = new Mock { }; - mockController.Setup(obj => obj.SignUpAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(newState)); - - hub.UserController = mockController.Object; - - return user.SignUpAsync().ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - - mockController.Verify(obj => obj.SignUpAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny()), Times.Exactly(1)); - - Assert.IsFalse(user.IsDirty); - Assert.AreEqual("ihave", user.Username); - Assert.IsFalse(user.State.ContainsKey("password")); - Assert.AreEqual("some0neTol4v4", user.ObjectId); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestLogIn() - { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary - { - ["sessionToken"] = "llaKcolnu", - ["username"] = "ihave", - ["password"] = "adream" - } - }; - - IObjectState newState = new MutableObjectState - { - ObjectId = "some0neTol4v4" - }; - - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); - - Mock mockController = new Mock { }; - mockController.Setup(obj => obj.LogInAsync("ihave", "adream", It.IsAny(), It.IsAny())).Returns(Task.FromResult(newState)); - - hub.UserController = mockController.Object; - - return client.LogInAsync("ihave", "adream").ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - - mockController.Verify(obj => obj.LogInAsync("ihave", "adream", It.IsAny(), It.IsAny()), Times.Exactly(1)); - - ParseUser user = task.Result; - Assert.IsFalse(user.IsDirty); - Assert.IsNull(user.Username); - Assert.AreEqual("some0neTol4v4", user.ObjectId); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestBecome() - { - IObjectState state = new MutableObjectState - { - ObjectId = "some0neTol4v4", - ServerData = new Dictionary { ["sessionToken"] = "llaKcolnu" } - }; - - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); +[TestClass] +public class UserTests +{ + private const string TestSessionToken = "llaKcolnu"; + private const string TestRevocableSessionToken = "r:llaKcolnu"; + private const string TestObjectId = "some0neTol4v4"; + private const string TestUsername = "ihave"; + private const string TestPassword = "adream"; + private const string TestEmail = "gogo@parse.com"; - Mock mockController = new Mock { }; - mockController.Setup(obj => obj.GetUserAsync("llaKcolnu", It.IsAny(), It.IsAny())).Returns(Task.FromResult(state)); + private ParseClient Client { get; set; } - hub.UserController = mockController.Object; + [TestInitialize] + public void SetUp() + { + + Client = new ParseClient(new ServerConnectionData { Test = true }); + Client.Publicize(); // Ensure the Clientinstance is globally available - return client.BecomeAsync("llaKcolnu").ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); + + Client.AddValidClass(); + Client.AddValidClass(); + } + [TestCleanup] + public void CleanUp() + { + (Client.Services as ServiceHub)?.Reset(); + + } - mockController.Verify(obj => obj.GetUserAsync("llaKcolnu", It.IsAny(), It.IsAny()), Times.Exactly(1)); + /// + /// Factory method for creating ParseUser objects with the ServiceHub bound. + /// + private ParseUser CreateParseUser(MutableObjectState state) + { + var user = ParseObject.Create(); + user.HandleFetchResult(state); + user.Bind(Client); + - ParseUser user = task.Result; - Assert.AreEqual("some0neTol4v4", user.ObjectId); - Assert.AreEqual("llaKcolnu", user.SessionToken); - }); - } + return user; + } - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestLogOut() + [TestMethod] + public async Task TestSignUpWithInvalidServerDataAsync() + { + var state = new MutableObjectState { - IObjectState state = new MutableObjectState + ServerData = new Dictionary { - ServerData = new Dictionary - { - ["sessionToken"] = "r:llaKcolnu" - } - }; - - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); - - ParseUser user = client.GenerateObjectFromState(state, "_User"); - - Mock mockCurrentUserController = new Mock { }; - mockCurrentUserController.Setup(obj => obj.GetAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(user)); - - Mock mockSessionController = new Mock(); - mockSessionController.Setup(c => c.IsRevocableSessionToken(It.IsAny())).Returns(true); + ["sessionToken"] = TestSessionToken + } + }; - hub.CurrentUserController = mockCurrentUserController.Object; - hub.SessionController = mockSessionController.Object; + var user = CreateParseUser(state); - return client.LogOutAsync().ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); + // Simulate invalid server data by ensuring username and password are not set + await Assert.ThrowsExceptionAsync( + async () => await user.SignUpAsync(), + "Expected SignUpAsync to throw an exception due to missing username or password." + ); + } - mockCurrentUserController.Verify(obj => obj.LogOutAsync(It.IsAny(), It.IsAny()), Times.Exactly(1)); - mockSessionController.Verify(obj => obj.RevokeAsync("r:llaKcolnu", It.IsAny()), Times.Exactly(1)); - }); - } - [TestMethod] - public void TestCurrentUser() + [TestMethod] + public async Task TestSignUpAsync() + { + var state = new MutableObjectState { - IObjectState state = new MutableObjectState + ServerData = new Dictionary { - ServerData = new Dictionary - { - ["sessionToken"] = "llaKcolnu" - } - }; - - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); - - ParseUser user = client.GenerateObjectFromState(state, "_User"); - - Mock mockCurrentUserController = new Mock { }; - mockCurrentUserController.Setup(obj => obj.GetAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(user)); - - hub.CurrentUserController = mockCurrentUserController.Object; - - Assert.AreEqual(user, client.GetCurrentUser()); - } - - [TestMethod] - public void TestCurrentUserWithEmptyResult() => Assert.IsNull(new ParseClient(new ServerConnectionData { Test = true }, new MutableServiceHub { CurrentUserController = new Mock { }.Object }).GetCurrentUser()); + ["sessionToken"] = TestSessionToken, + ["username"] = TestUsername, + ["password"] = TestPassword + } + }; - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestRevocableSession() + var newState = new MutableObjectState { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary - { - ["sessionToken"] = "llaKcolnu" - } - }; - - IObjectState newState = new MutableObjectState - { - ServerData = new Dictionary - { - ["sessionToken"] = "r:llaKcolnu" - } - }; - - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); - - ParseUser user = client.GenerateObjectFromState(state, "_User"); - - Mock mockSessionController = new Mock(); - mockSessionController.Setup(obj => obj.UpgradeToRevocableSessionAsync("llaKcolnu", It.IsAny(), It.IsAny())).Returns(Task.FromResult(newState)); - - hub.SessionController = mockSessionController.Object; - - return user.UpgradeToRevocableSessionAsync(CancellationToken.None).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - - mockSessionController.Verify(obj => obj.UpgradeToRevocableSessionAsync("llaKcolnu", It.IsAny(), It.IsAny()), Times.Exactly(1)); + ObjectId = TestObjectId + }; + + var hub = new MutableServiceHub(); + var client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + var mockController = new Mock(); + mockController + .Setup(obj => obj.SignUpAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny())) + .ReturnsAsync(newState); + + hub.UserController = mockController.Object; + + var user = CreateParseUser(state); + user.Bind(client); + + + await user.SignUpAsync(); + + + // Verify SignUpAsync is invoked + mockController.Verify( + obj => obj.SignUpAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny()), + Times.Once + ); + + Assert.IsFalse(user.IsDirty); + Assert.AreEqual(TestUsername, user.Username); + Assert.IsFalse(user.State.ContainsKey("password")); + Assert.AreEqual(TestObjectId, user.ObjectId); + } - Assert.AreEqual("r:llaKcolnu", user.SessionToken); - }); - } - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestRequestPasswordReset() + [TestMethod] + public async Task TestLogInAsync() + { + var newState = new MutableObjectState { - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); - - Mock mockController = new Mock { }; - - hub.UserController = mockController.Object; - - return client.RequestPasswordResetAsync("gogo@parse.com").ContinueWith(task => + ObjectId = TestObjectId, + ServerData = new Dictionary { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); + ["username"] = TestUsername + } + }; - mockController.Verify(obj => obj.RequestPasswordResetAsync("gogo@parse.com", It.IsAny()), Times.Exactly(1)); - }); - } + var hub = new MutableServiceHub(); + var client = new ParseClient(new ServerConnectionData { Test = true }, hub); - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestUserSave() - { - IObjectState state = new MutableObjectState - { - ObjectId = "some0neTol4v4", - ServerData = new Dictionary - { - ["sessionToken"] = "llaKcolnu", - ["username"] = "ihave", - ["password"] = "adream" - } - }; - - IObjectState newState = new MutableObjectState - { - ServerData = new Dictionary - { - ["Alliance"] = "rekt" - } - }; + client.Publicize(); - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + var mockController = new Mock(); + mockController + .Setup(obj => obj.LogInAsync(TestUsername, TestPassword, It.IsAny(), It.IsAny())) + .ReturnsAsync(newState); - ParseUser user = client.GenerateObjectFromState(state, "_User"); - Mock mockObjectController = new Mock(); + hub.UserController = mockController.Object; - mockObjectController.Setup(obj => obj.SaveAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(newState)); + var loggedInUser = await client.LogInWithAsync(TestUsername, TestPassword); - hub.ObjectController = mockObjectController.Object; - hub.CurrentUserController = new Mock { }.Object; + // Verify LogInAsync is called + mockController.Verify(obj => obj.LogInAsync(TestUsername, TestPassword, It.IsAny(), It.IsAny()), Times.Once); - user["Alliance"] = "rekt"; + Assert.IsFalse(loggedInUser.IsDirty); + Assert.AreEqual(TestObjectId, loggedInUser.ObjectId); + Assert.AreEqual(TestUsername, loggedInUser.Username); + } - return user.SaveAsync().ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - - mockObjectController.Verify(obj => obj.SaveAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); - - Assert.IsFalse(user.IsDirty); - Assert.AreEqual("ihave", user.Username); - Assert.IsFalse(user.State.ContainsKey("password")); - Assert.AreEqual("some0neTol4v4", user.ObjectId); - Assert.AreEqual("rekt", user["Alliance"]); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestUserFetch() + [TestMethod] + public async Task TestLogOut() + { + // Arrange + var state = new MutableObjectState { - IObjectState state = new MutableObjectState - { - ObjectId = "some0neTol4v4", - ServerData = new Dictionary - { - ["sessionToken"] = "llaKcolnu", - ["username"] = "ihave", - ["password"] = "adream" - } - }; - - IObjectState newState = new MutableObjectState + ServerData = new Dictionary { - ServerData = new Dictionary - { - ["Alliance"] = "rekt" - } - }; + ["sessionToken"] = TestRevocableSessionToken + } + }; - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + var user = CreateParseUser(state); - ParseUser user = client.GenerateObjectFromState(state, "_User"); + var mockCurrentUserController = new Mock(); + mockCurrentUserController + .Setup(obj => obj.GetAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(user); - Mock mockObjectController = new Mock(); - mockObjectController.Setup(obj => obj.FetchAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(newState)); + // Simulate LogOutAsync failure with a controlled exception + mockCurrentUserController + .Setup(obj => obj.LogOutAsync(It.IsAny(), It.IsAny())) + .ThrowsAsync(new Exception("logout failure")); // Force a controlled exception since fb's service - hub.ObjectController = mockObjectController.Object; - hub.CurrentUserController = new Mock { }.Object; + var mockSessionController = new Mock(); - user["Alliance"] = "rekt"; + // Simulate a no-op for RevokeAsync + mockSessionController + .Setup(c => c.RevokeAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); - return user.FetchAsync().ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - - mockObjectController.Verify(obj => obj.FetchAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); - - Assert.IsTrue(user.IsDirty); - Assert.AreEqual("ihave", user.Username); - Assert.IsTrue(user.State.ContainsKey("password")); - Assert.AreEqual("some0neTol4v4", user.ObjectId); - Assert.AreEqual("rekt", user["Alliance"]); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestLink() + // Inject mocks + var hub = new MutableServiceHub { - IObjectState state = new MutableObjectState - { - ObjectId = "some0neTol4v4", - ServerData = new Dictionary - { - ["sessionToken"] = "llaKcolnu" - } - }; - - IObjectState newState = new MutableObjectState - { - ServerData = new Dictionary - { - ["garden"] = "ofWords" - } - }; + CurrentUserController = mockCurrentUserController.Object, + SessionController = mockSessionController.Object + }; - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + var client = new ParseClient(new ServerConnectionData { Test = true }, hub); - ParseUser user = client.GenerateObjectFromState(state, "_User"); + // Act + await client.LogOutAsync(CancellationToken.None); - Mock mockObjectController = new Mock(); - mockObjectController.Setup(obj => obj.SaveAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(newState)); + // Assert: Verify LogOutAsync was invoked once + mockCurrentUserController.Verify( + obj => obj.LogOutAsync(It.IsAny(), It.IsAny()), Times.Once); - hub.ObjectController = mockObjectController.Object; - hub.CurrentUserController = new Mock { }.Object; + // Verify session revocation still occurs + mockSessionController.Verify( + c => c.RevokeAsync(It.IsAny(), It.IsAny()), Times.Once); - return user.LinkWithAsync("parse", new Dictionary { }, CancellationToken.None).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - - mockObjectController.Verify(obj => obj.SaveAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); - - Assert.IsFalse(user.IsDirty); - Assert.IsNotNull(user.AuthData); - Assert.IsNotNull(user.AuthData["parse"]); - Assert.AreEqual("some0neTol4v4", user.ObjectId); - Assert.AreEqual("ofWords", user["garden"]); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestUnlink() - { - IObjectState state = new MutableObjectState - { - ObjectId = "some0neTol4v4", - ServerData = new Dictionary - { - ["sessionToken"] = "llaKcolnu", - ["authData"] = new Dictionary - { - ["parse"] = new Dictionary { } - } - } - }; - - IObjectState newState = new MutableObjectState - { - ServerData = new Dictionary - { - ["garden"] = "ofWords" - } - }; - - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); - - ParseUser user = client.GenerateObjectFromState(state, "_User"); + // Verify session token is cleared + Assert.IsNull(user["sessionToken"], "Session token should be cleared after logout."); + } + [TestMethod] + public async Task TestRequestPasswordResetAsync() + { + var hub = new MutableServiceHub(); + var Client= new ParseClient(new ServerConnectionData { Test = true }, hub); - Mock mockObjectController = new Mock(); - mockObjectController.Setup(obj => obj.SaveAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(newState)); + var mockController = new Mock(); + hub.UserController = mockController.Object; - Mock mockCurrentUserController = new Mock { }; - mockCurrentUserController.Setup(obj => obj.IsCurrent(user)).Returns(true); + await Client.RequestPasswordResetAsync(TestEmail); - hub.ObjectController = mockObjectController.Object; - hub.CurrentUserController = mockCurrentUserController.Object; + mockController.Verify(obj => obj.RequestPasswordResetAsync(TestEmail, It.IsAny()), Times.Once); + } - return user.UnlinkFromAsync("parse", CancellationToken.None).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - - mockObjectController.Verify(obj => obj.SaveAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); - - Assert.IsFalse(user.IsDirty); - Assert.IsNotNull(user.AuthData); - Assert.IsFalse(user.AuthData.ContainsKey("parse")); - Assert.AreEqual("some0neTol4v4", user.ObjectId); - Assert.AreEqual("ofWords", user["garden"]); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestUnlinkNonCurrentUser() + [TestMethod] + public async Task TestLinkAsync() + { + var state = new MutableObjectState { - IObjectState state = new MutableObjectState + ObjectId = TestObjectId, + ServerData = new Dictionary { - ObjectId = "some0neTol4v4", - ServerData = new Dictionary - { - ["sessionToken"] = "llaKcolnu", - ["authData"] = new Dictionary - { - ["parse"] = new Dictionary { } - } - } - }; - - IObjectState newState = new MutableObjectState - { - ServerData = new Dictionary - { - ["garden"] = "ofWords" - } - }; - - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); - - ParseUser user = client.GenerateObjectFromState(state, "_User"); - - Mock mockObjectController = new Mock(); - mockObjectController.Setup(obj => obj.SaveAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(newState)); - - Mock mockCurrentUserController = new Mock { }; - mockCurrentUserController.Setup(obj => obj.IsCurrent(user)).Returns(false); - - hub.ObjectController = mockObjectController.Object; - hub.CurrentUserController = mockCurrentUserController.Object; + ["sessionToken"] = TestSessionToken + } + }; - return user.UnlinkFromAsync("parse", CancellationToken.None).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - - mockObjectController.Verify(obj => obj.SaveAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); - - Assert.IsFalse(user.IsDirty); - Assert.IsNotNull(user.AuthData); - Assert.IsTrue(user.AuthData.ContainsKey("parse")); - Assert.IsNull(user.AuthData["parse"]); - Assert.AreEqual("some0neTol4v4", user.ObjectId); - Assert.AreEqual("ofWords", user["garden"]); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestLogInWith() + var newState = new MutableObjectState { - IObjectState state = new MutableObjectState + ServerData = new Dictionary { - ObjectId = "some0neTol4v4", - ServerData = new Dictionary - { - ["sessionToken"] = "llaKcolnu" - } - }; - - MutableServiceHub hub = new MutableServiceHub { }; - ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); - - Mock mockController = new Mock { }; - mockController.Setup(obj => obj.LogInAsync("parse", It.IsAny>(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(state)); - - hub.UserController = mockController.Object; - - return client.LogInWithAsync("parse", new Dictionary { }, CancellationToken.None).ContinueWith(task => - { - Assert.IsFalse(task.IsFaulted); - Assert.IsFalse(task.IsCanceled); - - mockController.Verify(obj => obj.LogInAsync("parse", It.IsAny>(), It.IsAny(), It.IsAny()), Times.Exactly(1)); - - ParseUser user = task.Result; - - Assert.IsNotNull(user.AuthData); - Assert.IsNotNull(user.AuthData["parse"]); - Assert.AreEqual("some0neTol4v4", user.ObjectId); - }); - } - - [TestMethod] - public void TestImmutableKeys() - { - ParseUser user = new ParseUser { }.Bind(Client) as ParseUser; - string[] immutableKeys = new string[] { "sessionToken", "isNew" }; + ["garden"] = "ofWords" + } + }; - foreach (string key in immutableKeys) - { - Assert.ThrowsException(() => user[key] = "1234567890"); + var hub = new MutableServiceHub(); + var Client= new ParseClient(new ServerConnectionData { Test = true }, hub); - Assert.ThrowsException(() => user.Add(key, "1234567890")); + var user = CreateParseUser(state); - Assert.ThrowsException(() => user.AddRangeUniqueToList(key, new string[] { "1234567890" })); + var mockObjectController = new Mock(); + mockObjectController + .Setup(obj => obj.SaveAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(newState); - Assert.ThrowsException(() => user.Remove(key)); + hub.ObjectController = mockObjectController.Object; - Assert.ThrowsException(() => user.RemoveAllFromList(key, new string[] { "1234567890" })); - } + await user.LinkWithAsync("parse", new Dictionary(), CancellationToken.None); - // Other special keys should be good. + mockObjectController.Verify(obj => obj.SaveAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); - user["username"] = "username"; - user["password"] = "password"; - } + Assert.IsFalse(user.IsDirty); + Assert.IsNotNull(user.AuthData); + Assert.IsNotNull(user.AuthData["parse"]); + Assert.AreEqual(TestObjectId, user.ObjectId); + Assert.AreEqual("ofWords", user["garden"]); } } diff --git a/Parse.sln b/Parse.sln index cb26bb64..82e6de70 100644 --- a/Parse.sln +++ b/Parse.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29905.134 +# Visual Studio Version 17 +VisualStudioVersion = 17.12.35527.113 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Parse", "Parse\Parse.csproj", "{297FE1CA-AEDF-47BB-964D-E82780EA0A1C}" EndProject @@ -15,7 +15,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Parse.Tests", "Parse.Tests\Parse.Tests.csproj", "{E5529694-B75B-4F07-8436-A749B5E801C3}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parse.Tests", "Parse.Tests\Parse.Tests.csproj", "{FEB46D0F-384C-4F27-9E0E-F4A636768C90}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -27,10 +27,10 @@ Global {297FE1CA-AEDF-47BB-964D-E82780EA0A1C}.Debug|Any CPU.Build.0 = Debug|Any CPU {297FE1CA-AEDF-47BB-964D-E82780EA0A1C}.Release|Any CPU.ActiveCfg = Release|Any CPU {297FE1CA-AEDF-47BB-964D-E82780EA0A1C}.Release|Any CPU.Build.0 = Release|Any CPU - {E5529694-B75B-4F07-8436-A749B5E801C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E5529694-B75B-4F07-8436-A749B5E801C3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E5529694-B75B-4F07-8436-A749B5E801C3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E5529694-B75B-4F07-8436-A749B5E801C3}.Release|Any CPU.Build.0 = Release|Any CPU + {FEB46D0F-384C-4F27-9E0E-F4A636768C90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FEB46D0F-384C-4F27-9E0E-F4A636768C90}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FEB46D0F-384C-4F27-9E0E-F4A636768C90}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FEB46D0F-384C-4F27-9E0E-F4A636768C90}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Parse/Abstractions/Infrastructure/Control/IParseFieldOperation.cs b/Parse/Abstractions/Infrastructure/Control/IParseFieldOperation.cs index e372ec24..be6c3c64 100644 --- a/Parse/Abstractions/Infrastructure/Control/IParseFieldOperation.cs +++ b/Parse/Abstractions/Infrastructure/Control/IParseFieldOperation.cs @@ -1,44 +1,47 @@ -namespace Parse.Abstractions.Infrastructure.Control +namespace Parse.Abstractions.Infrastructure.Control; + +/// +/// A ParseFieldOperation represents a modification to a value in a ParseObject. +/// For example, setting, deleting, or incrementing a value are all different kinds of +/// ParseFieldOperations. ParseFieldOperations themselves can be considered to be +/// immutable. +/// +public interface IParseFieldOperation: IJsonConvertible { /// - /// A ParseFieldOperation represents a modification to a value in a ParseObject. - /// For example, setting, deleting, or incrementing a value are all different kinds of - /// ParseFieldOperations. ParseFieldOperations themselves can be considered to be - /// immutable. + /// Converts the ParseFieldOperation to a data structure that can be converted to JSON and sent to + /// Parse as part of a save operation. + /// + /// An object to be JSONified. + /// + //object Encode(IServiceHub serviceHub); + + /// + /// Returns a field operation that is composed of a previous operation followed by + /// this operation. This will not mutate either operation. However, it may return + /// this if the current operation is not affected by previous changes. + /// For example: + /// {increment by 2}.MergeWithPrevious({set to 5}) -> {set to 7} + /// {set to 5}.MergeWithPrevious({increment by 2}) -> {set to 5} + /// {add "foo"}.MergeWithPrevious({delete}) -> {set to ["foo"]} + /// {delete}.MergeWithPrevious({add "foo"}) -> {delete} /// + /// The most recent operation on the field, or null if none. + /// A new ParseFieldOperation or this. + IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous); + + /// + /// Returns a new estimated value based on a previous value and this operation. This + /// value is not intended to be sent to Parse, but it is used locally on the client to + /// inspect the most likely current value for a field. + /// + /// The key and object are used solely for ParseRelation to be able to construct objects + /// that refer back to their parents. /// - public interface IParseFieldOperation - { - /// - /// Converts the ParseFieldOperation to a data structure that can be converted to JSON and sent to - /// Parse as part of a save operation. - /// - /// An object to be JSONified. - object Encode(IServiceHub serviceHub); + /// The previous value for the field. + /// The key that this value is for. + /// The new value for the field. + object Apply(object oldValue, string key); - /// - /// Returns a field operation that is composed of a previous operation followed by - /// this operation. This will not mutate either operation. However, it may return - /// this if the current operation is not affected by previous changes. - /// For example: - /// {increment by 2}.MergeWithPrevious({set to 5}) -> {set to 7} - /// {set to 5}.MergeWithPrevious({increment by 2}) -> {set to 5} - /// {add "foo"}.MergeWithPrevious({delete}) -> {set to ["foo"]} - /// {delete}.MergeWithPrevious({add "foo"}) -> {delete} /// - /// The most recent operation on the field, or null if none. - /// A new ParseFieldOperation or this. - IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous); + object Value { get; } // Added property to expose operation value - /// - /// Returns a new estimated value based on a previous value and this operation. This - /// value is not intended to be sent to Parse, but it is used locally on the client to - /// inspect the most likely current value for a field. - /// - /// The key and object are used solely for ParseRelation to be able to construct objects - /// that refer back to their parents. - /// - /// The previous value for the field. - /// The key that this value is for. - /// The new value for the field. - object Apply(object oldValue, string key); - } } diff --git a/Parse/Abstractions/Infrastructure/CustomServiceHub.cs b/Parse/Abstractions/Infrastructure/CustomServiceHub.cs index 49689c94..554c91bb 100644 --- a/Parse/Abstractions/Infrastructure/CustomServiceHub.cs +++ b/Parse/Abstractions/Infrastructure/CustomServiceHub.cs @@ -11,56 +11,55 @@ using Parse.Abstractions.Platform.Sessions; using Parse.Abstractions.Platform.Users; -namespace Parse.Abstractions.Infrastructure +namespace Parse.Abstractions.Infrastructure; + +public abstract class CustomServiceHub : ICustomServiceHub { - public abstract class CustomServiceHub : ICustomServiceHub - { - public virtual IServiceHub Services { get; internal set; } + public virtual IServiceHub Services { get; internal set; } - public virtual IServiceHubCloner Cloner => Services.Cloner; + public virtual IServiceHubCloner Cloner => Services.Cloner; - public virtual IMetadataController MetadataController => Services.MetadataController; + public virtual IMetadataController MetadataController => Services.MetadataController; - public virtual IWebClient WebClient => Services.WebClient; + public virtual IWebClient WebClient => Services.WebClient; - public virtual ICacheController CacheController => Services.CacheController; + public virtual ICacheController CacheController => Services.CacheController; - public virtual IParseObjectClassController ClassController => Services.ClassController; + public virtual IParseObjectClassController ClassController => Services.ClassController; - public virtual IParseInstallationController InstallationController => Services.InstallationController; + public virtual IParseInstallationController InstallationController => Services.InstallationController; - public virtual IParseCommandRunner CommandRunner => Services.CommandRunner; + public virtual IParseCommandRunner CommandRunner => Services.CommandRunner; - public virtual IParseCloudCodeController CloudCodeController => Services.CloudCodeController; + public virtual IParseCloudCodeController CloudCodeController => Services.CloudCodeController; - public virtual IParseConfigurationController ConfigurationController => Services.ConfigurationController; + public virtual IParseConfigurationController ConfigurationController => Services.ConfigurationController; - public virtual IParseFileController FileController => Services.FileController; + public virtual IParseFileController FileController => Services.FileController; - public virtual IParseObjectController ObjectController => Services.ObjectController; + public virtual IParseObjectController ObjectController => Services.ObjectController; - public virtual IParseQueryController QueryController => Services.QueryController; + public virtual IParseQueryController QueryController => Services.QueryController; - public virtual IParseSessionController SessionController => Services.SessionController; + public virtual IParseSessionController SessionController => Services.SessionController; - public virtual IParseUserController UserController => Services.UserController; + public virtual IParseUserController UserController => Services.UserController; - public virtual IParseCurrentUserController CurrentUserController => Services.CurrentUserController; + public virtual IParseCurrentUserController CurrentUserController => Services.CurrentUserController; - public virtual IParseAnalyticsController AnalyticsController => Services.AnalyticsController; + public virtual IParseAnalyticsController AnalyticsController => Services.AnalyticsController; - public virtual IParseInstallationCoder InstallationCoder => Services.InstallationCoder; + public virtual IParseInstallationCoder InstallationCoder => Services.InstallationCoder; - public virtual IParsePushChannelsController PushChannelsController => Services.PushChannelsController; + public virtual IParsePushChannelsController PushChannelsController => Services.PushChannelsController; - public virtual IParsePushController PushController => Services.PushController; + public virtual IParsePushController PushController => Services.PushController; - public virtual IParseCurrentInstallationController CurrentInstallationController => Services.CurrentInstallationController; + public virtual IParseCurrentInstallationController CurrentInstallationController => Services.CurrentInstallationController; - public virtual IServerConnectionData ServerConnectionData => Services.ServerConnectionData; + public virtual IServerConnectionData ServerConnectionData => Services.ServerConnectionData; - public virtual IParseDataDecoder Decoder => Services.Decoder; + public virtual IParseDataDecoder Decoder => Services.Decoder; - public virtual IParseInstallationDataFinalizer InstallationDataFinalizer => Services.InstallationDataFinalizer; - } + public virtual IParseInstallationDataFinalizer InstallationDataFinalizer => Services.InstallationDataFinalizer; } diff --git a/Parse/Abstractions/Infrastructure/Data/IParseDataDecoder.cs b/Parse/Abstractions/Infrastructure/Data/IParseDataDecoder.cs index 0b4343db..29a0b6ec 100644 --- a/Parse/Abstractions/Infrastructure/Data/IParseDataDecoder.cs +++ b/Parse/Abstractions/Infrastructure/Data/IParseDataDecoder.cs @@ -1,16 +1,15 @@ -namespace Parse.Abstractions.Infrastructure.Data +namespace Parse.Abstractions.Infrastructure.Data; + +/// +/// A generalized input data decoding interface for the Parse SDK. +/// +public interface IParseDataDecoder { /// - /// A generalized input data decoding interface for the Parse SDK. + /// Decodes input data into Parse-SDK-related entities, such as instances, which is why an implementation instance is sometimes required. /// - public interface IParseDataDecoder - { - /// - /// Decodes input data into Parse-SDK-related entities, such as instances, which is why an implementation instance is sometimes required. - /// - /// The target input data to decode. - /// A implementation instance to use when instantiating s. - /// A Parse SDK entity such as a . - object Decode(object data, IServiceHub serviceHub); - } + /// The target input data to decode. + /// A implementation instance to use when instantiating s. + /// A Parse SDK entity such as a . + object Decode(object data, IServiceHub serviceHub); } \ No newline at end of file diff --git a/Parse/Abstractions/Infrastructure/Execution/IParseCommandRunner.cs b/Parse/Abstractions/Infrastructure/Execution/IParseCommandRunner.cs index 28e4b623..7ba9f087 100644 --- a/Parse/Abstractions/Infrastructure/Execution/IParseCommandRunner.cs +++ b/Parse/Abstractions/Infrastructure/Execution/IParseCommandRunner.cs @@ -5,18 +5,17 @@ using System.Threading.Tasks; using Parse.Infrastructure.Execution; -namespace Parse.Abstractions.Infrastructure.Execution +namespace Parse.Abstractions.Infrastructure.Execution; + +public interface IParseCommandRunner { - public interface IParseCommandRunner - { - /// - /// Executes and convert the result into Dictionary. - /// - /// The command to be run. - /// Upload progress callback. - /// Download progress callback. - /// The cancellation token for the request. - /// - Task>> RunCommandAsync(ParseCommand command, IProgress uploadProgress = null, IProgress downloadProgress = null, CancellationToken cancellationToken = default); - } + /// + /// Executes and convert the result into Dictionary. + /// + /// The command to be run. + /// Upload progress callback. + /// Download progress callback. + /// The cancellation token for the request. + /// + Task>> RunCommandAsync(ParseCommand command, IProgress uploadProgress = null, IProgress downloadProgress = null, CancellationToken cancellationToken = default); } diff --git a/Parse/Abstractions/Infrastructure/Execution/IWebClient.cs b/Parse/Abstractions/Infrastructure/Execution/IWebClient.cs index 9da9758a..f32ad121 100644 --- a/Parse/Abstractions/Infrastructure/Execution/IWebClient.cs +++ b/Parse/Abstractions/Infrastructure/Execution/IWebClient.cs @@ -4,19 +4,18 @@ using Parse.Infrastructure.Execution; using Status = System.Net.HttpStatusCode; -namespace Parse.Abstractions.Infrastructure.Execution +namespace Parse.Abstractions.Infrastructure.Execution; + +public interface IWebClient { - public interface IWebClient - { - /// - /// Executes HTTP request to a with HTTP verb - /// and . - /// - /// The HTTP request to be executed. - /// Upload progress callback. - /// Download progress callback. - /// The cancellation token. - /// A task that resolves to Htt - Task> ExecuteAsync(WebRequest httpRequest, IProgress uploadProgress, IProgress downloadProgress, CancellationToken cancellationToken = default); - } + /// + /// Executes HTTP request to a with HTTP verb + /// and . + /// + /// The HTTP request to be executed. + /// Upload progress callback. + /// Download progress callback. + /// The cancellation token. + /// A task that resolves to Htt + Task> ExecuteAsync(WebRequest httpRequest, IProgress uploadProgress, IProgress downloadProgress, CancellationToken cancellationToken = default); } diff --git a/Parse/Abstractions/Infrastructure/ICacheController.cs b/Parse/Abstractions/Infrastructure/ICacheController.cs index e3445deb..7d1319d6 100644 --- a/Parse/Abstractions/Infrastructure/ICacheController.cs +++ b/Parse/Abstractions/Infrastructure/ICacheController.cs @@ -2,46 +2,45 @@ using System.IO; using System.Threading.Tasks; -namespace Parse.Abstractions.Infrastructure -{ - // TODO: Move TransferAsync to IDiskFileCacheController and find viable alternative for use in ICacheController if needed. +namespace Parse.Abstractions.Infrastructure; + +// TODO: Move TransferAsync to IDiskFileCacheController and find viable alternative for use in ICacheController if needed. +/// +/// An abstraction for accessing persistent storage in the Parse SDK. +/// +public interface ICacheController +{ /// - /// An abstraction for accessing persistent storage in the Parse SDK. + /// Cleans up any temporary files and/or directories created during SDK operation. /// - public interface ICacheController - { - /// - /// Cleans up any temporary files and/or directories created during SDK operation. - /// - public void Clear(); + public void Clear(); - /// - /// Gets the file wrapper for the specified . - /// - /// The relative path to the target file - /// An instance of wrapping the the value - FileInfo GetRelativeFile(string path); + /// + /// Gets the file wrapper for the specified . + /// + /// The relative path to the target file + /// An instance of wrapping the the value + FileInfo GetRelativeFile(string path); - /// - /// Transfers a file from to . - /// - /// - /// - /// A task that completes once the file move operation form to completes. - Task TransferAsync(string originFilePath, string targetFilePath); + /// + /// Transfers a file from to . + /// + /// + /// + /// A task that completes once the file move operation form to completes. + Task TransferAsync(string originFilePath, string targetFilePath); - /// - /// Load the contents of this storage controller asynchronously. - /// - /// - Task> LoadAsync(); + /// + /// Load the contents of this storage controller asynchronously. + /// + /// + Task> LoadAsync(); - /// - /// Overwrites the contents of this storage controller asynchronously. - /// - /// - /// - Task> SaveAsync(IDictionary contents); - } + /// + /// Overwrites the contents of this storage controller asynchronously. + /// + /// + /// + Task> SaveAsync(IDictionary contents); } \ No newline at end of file diff --git a/Parse/Abstractions/Infrastructure/ICustomServiceHub.cs b/Parse/Abstractions/Infrastructure/ICustomServiceHub.cs index 967f77bc..491435a7 100644 --- a/Parse/Abstractions/Infrastructure/ICustomServiceHub.cs +++ b/Parse/Abstractions/Infrastructure/ICustomServiceHub.cs @@ -1,7 +1,6 @@ -namespace Parse.Abstractions.Infrastructure +namespace Parse.Abstractions.Infrastructure; + +public interface ICustomServiceHub : IServiceHub { - public interface ICustomServiceHub : IServiceHub - { - IServiceHub Services { get; } - } + IServiceHub Services { get; } } diff --git a/Parse/Abstractions/Infrastructure/IDataCache.cs b/Parse/Abstractions/Infrastructure/IDataCache.cs index 047d489f..d297a4d4 100644 --- a/Parse/Abstractions/Infrastructure/IDataCache.cs +++ b/Parse/Abstractions/Infrastructure/IDataCache.cs @@ -1,30 +1,29 @@ using System.Collections.Generic; using System.Threading.Tasks; -namespace Parse.Abstractions.Infrastructure -{ - // IGeneralizedDataCache +namespace Parse.Abstractions.Infrastructure; + +// IGeneralizedDataCache +/// +/// An interface for a dictionary that is persisted to disk asynchronously. +/// +/// They key type of the dictionary. +/// The value type of the dictionary. +public interface IDataCache : IDictionary +{ /// - /// An interface for a dictionary that is persisted to disk asynchronously. + /// Adds a key to this dictionary, and saves it asynchronously. /// - /// They key type of the dictionary. - /// The value type of the dictionary. - public interface IDataCache : IDictionary - { - /// - /// Adds a key to this dictionary, and saves it asynchronously. - /// - /// The key to insert. - /// The value to insert. - /// - Task AddAsync(TKey key, TValue value); + /// The key to insert. + /// The value to insert. + /// + Task AddAsync(TKey key, TValue value); - /// - /// Removes a key from this dictionary, and saves it asynchronously. - /// - /// - /// - Task RemoveAsync(TKey key); - } + /// + /// Removes a key from this dictionary, and saves it asynchronously. + /// + /// + /// + Task RemoveAsync(TKey key); } \ No newline at end of file diff --git a/Parse/Abstractions/Infrastructure/IDataTransferLevel.cs b/Parse/Abstractions/Infrastructure/IDataTransferLevel.cs index b773dab0..82aa1ef3 100644 --- a/Parse/Abstractions/Infrastructure/IDataTransferLevel.cs +++ b/Parse/Abstractions/Infrastructure/IDataTransferLevel.cs @@ -1,7 +1,6 @@ -namespace Parse.Abstractions.Infrastructure +namespace Parse.Abstractions.Infrastructure; + +public interface IDataTransferLevel { - public interface IDataTransferLevel - { - double Amount { get; set; } - } + double Amount { get; set; } } diff --git a/Parse/Abstractions/Infrastructure/IDiskFileCacheController.cs b/Parse/Abstractions/Infrastructure/IDiskFileCacheController.cs index dff5a045..d51d3aac 100644 --- a/Parse/Abstractions/Infrastructure/IDiskFileCacheController.cs +++ b/Parse/Abstractions/Infrastructure/IDiskFileCacheController.cs @@ -1,26 +1,25 @@ using System; -namespace Parse.Abstractions.Infrastructure +namespace Parse.Abstractions.Infrastructure; + +/// +/// An which stores the cache on disk via a file. +/// +public interface IDiskFileCacheController : ICacheController { /// - /// An which stores the cache on disk via a file. + /// The path to a persistent user-specific storage location specific to the final client assembly of the Parse library. /// - public interface IDiskFileCacheController : ICacheController - { - /// - /// The path to a persistent user-specific storage location specific to the final client assembly of the Parse library. - /// - public string AbsoluteCacheFilePath { get; set; } + public string AbsoluteCacheFilePath { get; set; } - /// - /// The relative path from the on the device an to application-specific persistent storage folder. - /// - public string RelativeCacheFilePath { get; set; } + /// + /// The relative path from the on the device an to application-specific persistent storage folder. + /// + public string RelativeCacheFilePath { get; set; } - /// - /// Refreshes this cache controller's internal tracked cache file to reflect the and/or . - /// - /// This will not delete the active tracked cache file that will be un-tracked after a call to this method. To do so, call . - void RefreshPaths(); - } + /// + /// Refreshes this cache controller's internal tracked cache file to reflect the and/or . + /// + /// This will not delete the active tracked cache file that will be un-tracked after a call to this method. To do so, call . + void RefreshPaths(); } \ No newline at end of file diff --git a/Parse/Abstractions/Infrastructure/IEnvironmentData.cs b/Parse/Abstractions/Infrastructure/IEnvironmentData.cs index 14b454b5..5985c702 100644 --- a/Parse/Abstractions/Infrastructure/IEnvironmentData.cs +++ b/Parse/Abstractions/Infrastructure/IEnvironmentData.cs @@ -1,24 +1,23 @@ -namespace Parse.Abstractions.Infrastructure +namespace Parse.Abstractions.Infrastructure; + +/// +/// Information about the environment in which the library will be operating. +/// +public interface IEnvironmentData { /// - /// Information about the environment in which the library will be operating. + /// The currently active time zone when the library will be used. /// - public interface IEnvironmentData - { - /// - /// The currently active time zone when the library will be used. - /// - string TimeZone { get; } + string TimeZone { get; } - /// - /// The operating system version of the platform the SDK is operating in. - /// - string OSVersion { get; } + /// + /// The operating system version of the platform the SDK is operating in. + /// + string OSVersion { get; } - /// - /// An identifier of the platform. - /// - /// Expected to be one of ios, android, winrt, winphone, or dotnet. - public string Platform { get; set; } - } + /// + /// An identifier of the platform. + /// + /// Expected to be one of ios, android, winrt, winphone, or dotnet. + public string Platform { get; set; } } diff --git a/Parse/Abstractions/Infrastructure/IHostManifestData.cs b/Parse/Abstractions/Infrastructure/IHostManifestData.cs index b8f2c5f7..7262e7b6 100644 --- a/Parse/Abstractions/Infrastructure/IHostManifestData.cs +++ b/Parse/Abstractions/Infrastructure/IHostManifestData.cs @@ -1,28 +1,27 @@ -namespace Parse.Abstractions.Infrastructure +namespace Parse.Abstractions.Infrastructure; + +/// +/// Information about the application using the Parse SDK. +/// +public interface IHostManifestData { /// - /// Information about the application using the Parse SDK. + /// The build number of your app. /// - public interface IHostManifestData - { - /// - /// The build number of your app. - /// - string Version { get; } + string Version { get; } - /// - /// The human friendly version number of your app. - /// - string ShortVersion { get; } + /// + /// The human friendly version number of your app. + /// + string ShortVersion { get; } - /// - /// A unique string representing your app. - /// - string Identifier { get; } + /// + /// A unique string representing your app. + /// + string Identifier { get; } - /// - /// The name of your app. - /// - string Name { get; } - } + /// + /// The name of your app. + /// + string Name { get; } } diff --git a/Parse/Abstractions/Infrastructure/IJsonConvertible.cs b/Parse/Abstractions/Infrastructure/IJsonConvertible.cs index 5139a514..954f626e 100644 --- a/Parse/Abstractions/Infrastructure/IJsonConvertible.cs +++ b/Parse/Abstractions/Infrastructure/IJsonConvertible.cs @@ -1,16 +1,15 @@ using System.Collections.Generic; -namespace Parse.Abstractions.Infrastructure +namespace Parse.Abstractions.Infrastructure; + +/// +/// Represents an object that can be converted into JSON. +/// +public interface IJsonConvertible { /// - /// Represents an object that can be converted into JSON. + /// Converts the object to a data structure that can be converted to JSON. /// - public interface IJsonConvertible - { - /// - /// Converts the object to a data structure that can be converted to JSON. - /// - /// An object to be JSONified. - IDictionary ConvertToJSON(); - } + /// An object to be JSONified. + IDictionary ConvertToJSON(IServiceHub serviceHub=default); } diff --git a/Parse/Abstractions/Infrastructure/IMetadataController.cs b/Parse/Abstractions/Infrastructure/IMetadataController.cs index 90332138..45dd82d6 100644 --- a/Parse/Abstractions/Infrastructure/IMetadataController.cs +++ b/Parse/Abstractions/Infrastructure/IMetadataController.cs @@ -1,19 +1,18 @@ -namespace Parse.Abstractions.Infrastructure +namespace Parse.Abstractions.Infrastructure; + +/// +/// A controller for metadata. This is provided in a dependency injection container because if a beta feature is activated for a client managing a specific aspect of application operation, then this might need to be reflected in the application versioning information as it is used to determine the data cache location. +/// +/// This container could have been implemented as a or , due to it's simplicity but, more information may be added in the future so it is kept general. +public interface IMetadataController { /// - /// A controller for metadata. This is provided in a dependency injection container because if a beta feature is activated for a client managing a specific aspect of application operation, then this might need to be reflected in the application versioning information as it is used to determine the data cache location. + /// Information about the application using the Parse SDK. /// - /// This container could have been implemented as a or , due to it's simplicity but, more information may be added in the future so it is kept general. - public interface IMetadataController - { - /// - /// Information about the application using the Parse SDK. - /// - public IHostManifestData HostManifestData { get; } + public IHostManifestData HostManifestData { get; } - /// - /// Environment data specific to the application hosting the Parse SDK. - /// - public IEnvironmentData EnvironmentData { get; } - } + /// + /// Environment data specific to the application hosting the Parse SDK. + /// + public IEnvironmentData EnvironmentData { get; } } diff --git a/Parse/Abstractions/Infrastructure/IMutableServiceHub.cs b/Parse/Abstractions/Infrastructure/IMutableServiceHub.cs index 7282985f..99bd78b9 100644 --- a/Parse/Abstractions/Infrastructure/IMutableServiceHub.cs +++ b/Parse/Abstractions/Infrastructure/IMutableServiceHub.cs @@ -13,40 +13,39 @@ using Parse.Abstractions.Platform.Sessions; using Parse.Abstractions.Platform.Users; -namespace Parse.Abstractions.Infrastructure +namespace Parse.Abstractions.Infrastructure; + +public interface IMutableServiceHub : IServiceHub { - public interface IMutableServiceHub : IServiceHub - { - IServerConnectionData ServerConnectionData { set; } - IMetadataController MetadataController { set; } + IServerConnectionData ServerConnectionData { set; } + IMetadataController MetadataController { set; } - IServiceHubCloner Cloner { set; } + IServiceHubCloner Cloner { set; } - IWebClient WebClient { set; } - ICacheController CacheController { set; } - IParseObjectClassController ClassController { set; } + IWebClient WebClient { set; } + ICacheController CacheController { set; } + IParseObjectClassController ClassController { set; } - IParseDataDecoder Decoder { set; } + IParseDataDecoder Decoder { set; } - IParseInstallationController InstallationController { set; } - IParseCommandRunner CommandRunner { set; } + IParseInstallationController InstallationController { set; } + IParseCommandRunner CommandRunner { set; } - IParseCloudCodeController CloudCodeController { set; } - IParseConfigurationController ConfigurationController { set; } - IParseFileController FileController { set; } - IParseObjectController ObjectController { set; } - IParseQueryController QueryController { set; } - IParseSessionController SessionController { set; } - IParseUserController UserController { set; } - IParseCurrentUserController CurrentUserController { set; } + IParseCloudCodeController CloudCodeController { set; } + IParseConfigurationController ConfigurationController { set; } + IParseFileController FileController { set; } + IParseObjectController ObjectController { set; } + IParseQueryController QueryController { set; } + IParseSessionController SessionController { set; } + IParseUserController UserController { set; } + IParseCurrentUserController CurrentUserController { set; } - IParseAnalyticsController AnalyticsController { set; } + IParseAnalyticsController AnalyticsController { set; } - IParseInstallationCoder InstallationCoder { set; } + IParseInstallationCoder InstallationCoder { set; } - IParsePushChannelsController PushChannelsController { set; } - IParsePushController PushController { set; } - IParseCurrentInstallationController CurrentInstallationController { set; } - IParseInstallationDataFinalizer InstallationDataFinalizer { set; } - } + IParsePushChannelsController PushChannelsController { set; } + IParsePushController PushController { set; } + IParseCurrentInstallationController CurrentInstallationController { set; } + IParseInstallationDataFinalizer InstallationDataFinalizer { set; } } diff --git a/Parse/Abstractions/Infrastructure/IRelativeCacheLocationGenerator.cs b/Parse/Abstractions/Infrastructure/IRelativeCacheLocationGenerator.cs index 7ceabe26..a5982f5f 100644 --- a/Parse/Abstractions/Infrastructure/IRelativeCacheLocationGenerator.cs +++ b/Parse/Abstractions/Infrastructure/IRelativeCacheLocationGenerator.cs @@ -1,13 +1,12 @@ -namespace Parse.Abstractions.Infrastructure +namespace Parse.Abstractions.Infrastructure; + +/// +/// A unit that can generate a relative path to a persistent storage file. +/// +public interface IRelativeCacheLocationGenerator { /// - /// A unit that can generate a relative path to a persistent storage file. + /// The corresponding relative path generated by this . /// - public interface IRelativeCacheLocationGenerator - { - /// - /// The corresponding relative path generated by this . - /// - string GetRelativeCacheFilePath(IServiceHub serviceHub); - } + string GetRelativeCacheFilePath(IServiceHub serviceHub); } diff --git a/Parse/Abstractions/Infrastructure/IServerConnectionData.cs b/Parse/Abstractions/Infrastructure/IServerConnectionData.cs index bad1c01d..3e4258e9 100644 --- a/Parse/Abstractions/Infrastructure/IServerConnectionData.cs +++ b/Parse/Abstractions/Infrastructure/IServerConnectionData.cs @@ -1,32 +1,31 @@ using System.Collections.Generic; -namespace Parse.Abstractions.Infrastructure +namespace Parse.Abstractions.Infrastructure; + +public interface IServerConnectionData { - public interface IServerConnectionData - { - /// - /// The App ID of your app. - /// - string ApplicationID { get; set; } + /// + /// The App ID of your app. + /// + string ApplicationID { get; set; } - /// - /// A URI pointing to the target Parse Server instance hosting the app targeted by . - /// - string ServerURI { get; set; } + /// + /// A URI pointing to the target Parse Server instance hosting the app targeted by . + /// + string ServerURI { get; set; } - /// - /// The .NET Key for the Parse app targeted by . - /// - string Key { get; set; } + /// + /// The .NET Key for the Parse app targeted by . + /// + string Key { get; set; } - /// - /// The Master Key for the Parse app targeted by . - /// - string MasterKey { get; set; } + /// + /// The Master Key for the Parse app targeted by . + /// + string MasterKey { get; set; } - /// - /// Additional HTTP headers to be sent with network requests from the SDK. - /// - IDictionary Headers { get; set; } - } + /// + /// Additional HTTP headers to be sent with network requests from the SDK. + /// + IDictionary Headers { get; set; } } diff --git a/Parse/Abstractions/Infrastructure/IServiceHub.cs b/Parse/Abstractions/Infrastructure/IServiceHub.cs index c7e65992..9614bab3 100644 --- a/Parse/Abstractions/Infrastructure/IServiceHub.cs +++ b/Parse/Abstractions/Infrastructure/IServiceHub.cs @@ -13,48 +13,47 @@ using Parse.Abstractions.Platform.Sessions; using Parse.Abstractions.Platform.Users; -namespace Parse.Abstractions.Infrastructure -{ - // TODO: Consider splitting up IServiceHub into IResourceHub and IServiceHub, where the former would provide the current functionality of IServiceHub and the latter would be a public-facing sub-section containing formerly-static memebers from classes such as ParseObject which require the use of some broader resource. +namespace Parse.Abstractions.Infrastructure; + +// TODO: Consider splitting up IServiceHub into IResourceHub and IServiceHub, where the former would provide the current functionality of IServiceHub and the latter would be a public-facing sub-section containing formerly-static memebers from classes such as ParseObject which require the use of some broader resource. +/// +/// The dependency injection container for all internal .NET Parse SDK services. +/// +public interface IServiceHub +{ /// - /// The dependency injection container for all internal .NET Parse SDK services. + /// The current server connection data that the the Parse SDK has been initialized with. /// - public interface IServiceHub - { - /// - /// The current server connection data that the the Parse SDK has been initialized with. - /// - IServerConnectionData ServerConnectionData { get; } - IMetadataController MetadataController { get; } - - IServiceHubCloner Cloner { get; } - - IWebClient WebClient { get; } - ICacheController CacheController { get; } - IParseObjectClassController ClassController { get; } - - IParseDataDecoder Decoder { get; } - - IParseInstallationController InstallationController { get; } - IParseCommandRunner CommandRunner { get; } - - IParseCloudCodeController CloudCodeController { get; } - IParseConfigurationController ConfigurationController { get; } - IParseFileController FileController { get; } - IParseObjectController ObjectController { get; } - IParseQueryController QueryController { get; } - IParseSessionController SessionController { get; } - IParseUserController UserController { get; } - IParseCurrentUserController CurrentUserController { get; } - - IParseAnalyticsController AnalyticsController { get; } - - IParseInstallationCoder InstallationCoder { get; } - - IParsePushChannelsController PushChannelsController { get; } - IParsePushController PushController { get; } - IParseCurrentInstallationController CurrentInstallationController { get; } - IParseInstallationDataFinalizer InstallationDataFinalizer { get; } - } + IServerConnectionData ServerConnectionData { get; } + IMetadataController MetadataController { get; } + + IServiceHubCloner Cloner { get; } + + IWebClient WebClient { get; } + ICacheController CacheController { get; } + IParseObjectClassController ClassController { get; } + + IParseDataDecoder Decoder { get; } + + IParseInstallationController InstallationController { get; } + IParseCommandRunner CommandRunner { get; } + + IParseCloudCodeController CloudCodeController { get; } + IParseConfigurationController ConfigurationController { get; } + IParseFileController FileController { get; } + IParseObjectController ObjectController { get; } + IParseQueryController QueryController { get; } + IParseSessionController SessionController { get; } + IParseUserController UserController { get; } + IParseCurrentUserController CurrentUserController { get; } + + IParseAnalyticsController AnalyticsController { get; } + + IParseInstallationCoder InstallationCoder { get; } + + IParsePushChannelsController PushChannelsController { get; } + IParsePushController PushController { get; } + IParseCurrentInstallationController CurrentInstallationController { get; } + IParseInstallationDataFinalizer InstallationDataFinalizer { get; } } diff --git a/Parse/Abstractions/Infrastructure/IServiceHubCloner.cs b/Parse/Abstractions/Infrastructure/IServiceHubCloner.cs index d88dea9e..2c87a844 100644 --- a/Parse/Abstractions/Infrastructure/IServiceHubCloner.cs +++ b/Parse/Abstractions/Infrastructure/IServiceHubCloner.cs @@ -1,7 +1,6 @@ -namespace Parse.Abstractions.Infrastructure +namespace Parse.Abstractions.Infrastructure; + +public interface IServiceHubCloner { - public interface IServiceHubCloner - { - public IServiceHub BuildHub(in IServiceHub reference, IServiceHubComposer composer, params IServiceHubMutator[] requestedMutators); - } + public IServiceHub BuildHub(in IServiceHub reference, IServiceHubComposer composer, params IServiceHubMutator[] requestedMutators); } diff --git a/Parse/Abstractions/Infrastructure/IServiceHubComposer.cs b/Parse/Abstractions/Infrastructure/IServiceHubComposer.cs index 73dc3597..6765a3fe 100644 --- a/Parse/Abstractions/Infrastructure/IServiceHubComposer.cs +++ b/Parse/Abstractions/Infrastructure/IServiceHubComposer.cs @@ -1,9 +1,8 @@ -namespace Parse.Abstractions.Infrastructure -{ - // ALTERNATE NAME: IClient, IDataContainmentHub, IResourceContainmentHub, IDataContainer, IServiceHubComposer +namespace Parse.Abstractions.Infrastructure; + +// ALTERNATE NAME: IClient, IDataContainmentHub, IResourceContainmentHub, IDataContainer, IServiceHubComposer - public interface IServiceHubComposer - { - public IServiceHub BuildHub(IMutableServiceHub serviceHub = default, IServiceHub extension = default, params IServiceHubMutator[] configurators); - } +public interface IServiceHubComposer +{ + public IServiceHub BuildHub(IMutableServiceHub serviceHub = default, IServiceHub extension = default, params IServiceHubMutator[] configurators); } diff --git a/Parse/Abstractions/Infrastructure/IServiceHubMutator.cs b/Parse/Abstractions/Infrastructure/IServiceHubMutator.cs index 6ce77d67..31ced09b 100644 --- a/Parse/Abstractions/Infrastructure/IServiceHubMutator.cs +++ b/Parse/Abstractions/Infrastructure/IServiceHubMutator.cs @@ -1,22 +1,21 @@ -namespace Parse.Abstractions.Infrastructure -{ - // IServiceHubComposer, IServiceHubMutator, IServiceHubConfigurator, IClientConfigurator, IServiceConfigurationLayer +namespace Parse.Abstractions.Infrastructure; + +// IServiceHubComposer, IServiceHubMutator, IServiceHubConfigurator, IClientConfigurator, IServiceConfigurationLayer +/// +/// A class which makes a deliberate mutation to a service. +/// +public interface IServiceHubMutator +{ /// - /// A class which makes a deliberate mutation to a service. + /// A value which dictates whether or not the should be considered in a valid state. /// - public interface IServiceHubMutator - { - /// - /// A value which dictates whether or not the should be considered in a valid state. - /// - bool Valid { get; } + bool Valid { get; } - /// - /// A method which mutates an implementation instance. - /// - /// The target implementation instance - /// A hub which the is composed onto that should be used when needs to access services. - void Mutate(ref IMutableServiceHub target, in IServiceHub composedHub); - } + /// + /// A method which mutates an implementation instance. + /// + /// The target implementation instance + /// A hub which the is composed onto that should be used when needs to access services. + void Mutate(ref IMutableServiceHub target, in IServiceHub composedHub); } diff --git a/Parse/Abstractions/Platform/Analytics/IParseAnalyticsController.cs b/Parse/Abstractions/Platform/Analytics/IParseAnalyticsController.cs index 885174be..d0bfd8f0 100644 --- a/Parse/Abstractions/Platform/Analytics/IParseAnalyticsController.cs +++ b/Parse/Abstractions/Platform/Analytics/IParseAnalyticsController.cs @@ -3,30 +3,29 @@ using System.Threading.Tasks; using Parse.Abstractions.Infrastructure; -namespace Parse.Abstractions.Platform.Analytics +namespace Parse.Abstractions.Platform.Analytics; + +/// +/// The interface for the Parse Analytics API controller. +/// +public interface IParseAnalyticsController { /// - /// The interface for the Parse Analytics API controller. + /// Tracks an event matching the specified details. /// - public interface IParseAnalyticsController - { - /// - /// Tracks an event matching the specified details. - /// - /// The name of the event. - /// The parameters of the event. - /// The session token for the event. - /// The asynchonous cancellation token. - /// A that will complete successfully once the event has been set to be tracked. - Task TrackEventAsync(string name, IDictionary dimensions, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); + /// The name of the event. + /// The parameters of the event. + /// The session token for the event. + /// The asynchonous cancellation token. + /// A that will complete successfully once the event has been set to be tracked. + Task TrackEventAsync(string name, IDictionary dimensions, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); - /// - /// Tracks an app open for the specified event. - /// - /// The hash for the target push notification. - /// The token of the current session. - /// The asynchronous cancellation token. - /// A the will complete successfully once app openings for the target push notification have been set to be tracked. - Task TrackAppOpenedAsync(string pushHash, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); - } + /// + /// Tracks an app open for the specified event. + /// + /// The hash for the target push notification. + /// The token of the current session. + /// The asynchronous cancellation token. + /// A the will complete successfully once app openings for the target push notification have been set to be tracked. + Task TrackAppOpenedAsync(string pushHash, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); } diff --git a/Parse/Abstractions/Platform/Authentication/IParseAuthenticationProvider.cs b/Parse/Abstractions/Platform/Authentication/IParseAuthenticationProvider.cs index c200086f..e97ba356 100644 --- a/Parse/Abstractions/Platform/Authentication/IParseAuthenticationProvider.cs +++ b/Parse/Abstractions/Platform/Authentication/IParseAuthenticationProvider.cs @@ -2,37 +2,36 @@ using System.Threading; using System.Threading.Tasks; -namespace Parse.Abstractions.Platform.Authentication +namespace Parse.Abstractions.Platform.Authentication; + +public interface IParseAuthenticationProvider { - public interface IParseAuthenticationProvider - { - /// - /// Authenticates with the service. - /// - /// The cancellation token. - Task> AuthenticateAsync(CancellationToken cancellationToken); + /// + /// Authenticates with the service. + /// + /// The cancellation token. + Task> AuthenticateAsync(CancellationToken cancellationToken); - /// - /// Deauthenticates (logs out) the user associated with this provider. This - /// call may block. - /// - void Deauthenticate(); + /// + /// Deauthenticates (logs out) the user associated with this provider. This + /// call may block. + /// + void Deauthenticate(); - /// - /// Restores authentication that has been serialized, such as session keys, - /// etc. - /// - /// The auth data for the provider. This value may be null - /// when unlinking an account. - /// true iff the authData was successfully synchronized. A false return - /// value indicates that the user should no longer be associated because of bad auth - /// data. - bool RestoreAuthentication(IDictionary authData); + /// + /// Restores authentication that has been serialized, such as session keys, + /// etc. + /// + /// The auth data for the provider. This value may be null + /// when unlinking an account. + /// true iff the authData was successfully synchronized. A false return + /// value indicates that the user should no longer be associated because of bad auth + /// data. + bool RestoreAuthentication(IDictionary authData); - /// - /// Provides a unique name for the type of authentication the provider does. - /// For example, the FacebookAuthenticationProvider would return "facebook". - /// - string AuthType { get; } - } + /// + /// Provides a unique name for the type of authentication the provider does. + /// For example, the FacebookAuthenticationProvider would return "facebook". + /// + string AuthType { get; } } diff --git a/Parse/Abstractions/Platform/Cloud/IParseCloudCodeController.cs b/Parse/Abstractions/Platform/Cloud/IParseCloudCodeController.cs index d5ddf025..f1de8d00 100644 --- a/Parse/Abstractions/Platform/Cloud/IParseCloudCodeController.cs +++ b/Parse/Abstractions/Platform/Cloud/IParseCloudCodeController.cs @@ -1,12 +1,19 @@ +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Parse.Abstractions.Infrastructure; -namespace Parse.Abstractions.Platform.Cloud +namespace Parse.Abstractions.Platform.Cloud; + +public interface IParseCloudCodeController { - public interface IParseCloudCodeController - { - Task CallFunctionAsync(string name, IDictionary parameters, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); - } + Task CallFunctionAsync( + string name, + IDictionary parameters, + string sessionToken, + IServiceHub serviceHub, + CancellationToken cancellationToken = default, + IProgress uploadProgress = null, + IProgress downloadProgress = null); } diff --git a/Parse/Abstractions/Platform/Configuration/IParseConfigurationController.cs b/Parse/Abstractions/Platform/Configuration/IParseConfigurationController.cs index 240b9068..a689e1fa 100644 --- a/Parse/Abstractions/Platform/Configuration/IParseConfigurationController.cs +++ b/Parse/Abstractions/Platform/Configuration/IParseConfigurationController.cs @@ -3,18 +3,17 @@ using Parse.Abstractions.Infrastructure; using Parse.Platform.Configuration; -namespace Parse.Abstractions.Platform.Configuration +namespace Parse.Abstractions.Platform.Configuration; + +public interface IParseConfigurationController { - public interface IParseConfigurationController - { - public IParseCurrentConfigurationController CurrentConfigurationController { get; } + public IParseCurrentConfigurationController CurrentConfigurationController { get; } - /// - /// Fetches the config from the server asynchronously. - /// - /// The config async. - /// Session token. - /// Cancellation token. - Task FetchConfigAsync(string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); - } + /// + /// Fetches the config from the server asynchronously. + /// + /// The config async. + /// Session token. + /// Cancellation token. + Task FetchConfigAsync(string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); } diff --git a/Parse/Abstractions/Platform/Configuration/IParseCurrentConfigurationController.cs b/Parse/Abstractions/Platform/Configuration/IParseCurrentConfigurationController.cs index 2e4f3f5f..d414e85f 100644 --- a/Parse/Abstractions/Platform/Configuration/IParseCurrentConfigurationController.cs +++ b/Parse/Abstractions/Platform/Configuration/IParseCurrentConfigurationController.cs @@ -2,33 +2,32 @@ using Parse.Abstractions.Infrastructure; using Parse.Platform.Configuration; -namespace Parse.Abstractions.Platform.Configuration +namespace Parse.Abstractions.Platform.Configuration; + +public interface IParseCurrentConfigurationController { - public interface IParseCurrentConfigurationController - { - /// - /// Gets the current config async. - /// - /// The current config async. - Task GetCurrentConfigAsync(IServiceHub serviceHub); + /// + /// Gets the current config async. + /// + /// The current config async. + Task GetCurrentConfigAsync(IServiceHub serviceHub); - /// - /// Sets the current config async. - /// - /// The current config async. - /// Config. - Task SetCurrentConfigAsync(ParseConfiguration config); + /// + /// Sets the current config async. + /// + /// The current config async. + /// Config. + Task SetCurrentConfigAsync(ParseConfiguration config); - /// - /// Clears the current config async. - /// - /// The current config async. - Task ClearCurrentConfigAsync(); + /// + /// Clears the current config async. + /// + /// The current config async. + Task ClearCurrentConfigAsync(); - /// - /// Clears the current config in memory async. - /// - /// The current config in memory async. - Task ClearCurrentConfigInMemoryAsync(); - } + /// + /// Clears the current config in memory async. + /// + /// The current config in memory async. + Task ClearCurrentConfigInMemoryAsync(); } diff --git a/Parse/Abstractions/Platform/Files/IParseFileController.cs b/Parse/Abstractions/Platform/Files/IParseFileController.cs index 38e82bf4..1d062500 100644 --- a/Parse/Abstractions/Platform/Files/IParseFileController.cs +++ b/Parse/Abstractions/Platform/Files/IParseFileController.cs @@ -5,10 +5,9 @@ using Parse.Abstractions.Infrastructure; using Parse.Platform.Files; -namespace Parse.Abstractions.Platform.Files +namespace Parse.Abstractions.Platform.Files; + +public interface IParseFileController { - public interface IParseFileController - { - Task SaveAsync(FileState state, Stream dataStream, string sessionToken, IProgress progress, CancellationToken cancellationToken); - } + Task SaveAsync(FileState state, Stream dataStream, string sessionToken, IProgress progress, CancellationToken cancellationToken); } diff --git a/Parse/Abstractions/Platform/Installations/IParseCurrentInstallationController.cs b/Parse/Abstractions/Platform/Installations/IParseCurrentInstallationController.cs index a5a41a40..9ff9b5c4 100644 --- a/Parse/Abstractions/Platform/Installations/IParseCurrentInstallationController.cs +++ b/Parse/Abstractions/Platform/Installations/IParseCurrentInstallationController.cs @@ -1,8 +1,7 @@ using Parse.Abstractions.Platform.Objects; -namespace Parse.Abstractions.Platform.Installations +namespace Parse.Abstractions.Platform.Installations; + +public interface IParseCurrentInstallationController : IParseObjectCurrentController { - public interface IParseCurrentInstallationController : IParseObjectCurrentController - { - } } diff --git a/Parse/Abstractions/Platform/Installations/IParseInstallationCoder.cs b/Parse/Abstractions/Platform/Installations/IParseInstallationCoder.cs index 0db3b683..7e94ae09 100644 --- a/Parse/Abstractions/Platform/Installations/IParseInstallationCoder.cs +++ b/Parse/Abstractions/Platform/Installations/IParseInstallationCoder.cs @@ -1,14 +1,14 @@ using System.Collections.Generic; using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Objects; -namespace Parse.Abstractions.Platform.Installations -{ - // TODO: (richardross) once coder is refactored, make this extend IParseObjectCoder. +namespace Parse.Abstractions.Platform.Installations; + +// TODO: (richardross) once coder is refactored, make this extend IParseObjectCoder. - public interface IParseInstallationCoder - { - IDictionary Encode(ParseInstallation installation); +public interface IParseInstallationCoder +{ + IDictionary Encode(ParseInstallation installation); - ParseInstallation Decode(IDictionary data, IServiceHub serviceHub); - } + ParseInstallation Decode(IDictionary data, IServiceHub serviceHub); } \ No newline at end of file diff --git a/Parse/Abstractions/Platform/Installations/IParseInstallationController.cs b/Parse/Abstractions/Platform/Installations/IParseInstallationController.cs index 2d857802..d3687c25 100644 --- a/Parse/Abstractions/Platform/Installations/IParseInstallationController.cs +++ b/Parse/Abstractions/Platform/Installations/IParseInstallationController.cs @@ -1,25 +1,24 @@ using System; using System.Threading.Tasks; -namespace Parse.Abstractions.Platform.Installations +namespace Parse.Abstractions.Platform.Installations; + +public interface IParseInstallationController { - public interface IParseInstallationController - { - /// - /// Sets current installationId and saves it to local storage. - /// - /// The installationId to be saved. - Task SetAsync(Guid? installationId); + /// + /// Sets current installationId and saves it to local storage. + /// + /// The installationId to be saved. + Task SetAsync(Guid? installationId); - /// - /// Gets current installationId from local storage. Generates a none exists. - /// - /// Current installationId. - Task GetAsync(); + /// + /// Gets current installationId from local storage. Generates a none exists. + /// + /// Current installationId. + Task GetAsync(); - /// - /// Clears current installationId from memory and local storage. - /// - Task ClearAsync(); - } + /// + /// Clears current installationId from memory and local storage. + /// + Task ClearAsync(); } diff --git a/Parse/Abstractions/Platform/Installations/IParseInstallationDataFinalizer.cs b/Parse/Abstractions/Platform/Installations/IParseInstallationDataFinalizer.cs index 9296ffef..34378d58 100644 --- a/Parse/Abstractions/Platform/Installations/IParseInstallationDataFinalizer.cs +++ b/Parse/Abstractions/Platform/Installations/IParseInstallationDataFinalizer.cs @@ -1,20 +1,19 @@ using System.Threading.Tasks; -namespace Parse.Abstractions.Platform.Installations +namespace Parse.Abstractions.Platform.Installations; + +public interface IParseInstallationDataFinalizer { - public interface IParseInstallationDataFinalizer - { - /// - /// Executes platform specific hook that mutate the installation based on - /// the device platforms. - /// - /// Installation to be mutated. - /// - Task FinalizeAsync(ParseInstallation installation); + /// + /// Executes platform specific hook that mutate the installation based on + /// the device platforms. + /// + /// Installation to be mutated. + /// + Task FinalizeAsync(ParseInstallation installation); - /// - /// Allows an implementation to get static information that needs to be used in . - /// - void Initialize(); - } + /// + /// Allows an implementation to get static information that needs to be used in . + /// + void Initialize(); } diff --git a/Parse/Abstractions/Platform/Objects/IObjectState.cs b/Parse/Abstractions/Platform/Objects/IObjectState.cs index 1546d7e5..d13946d0 100644 --- a/Parse/Abstractions/Platform/Objects/IObjectState.cs +++ b/Parse/Abstractions/Platform/Objects/IObjectState.cs @@ -1,20 +1,20 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using Parse.Platform.Objects; -namespace Parse.Abstractions.Platform.Objects +namespace Parse.Abstractions.Platform.Objects; + +public interface IObjectState : IEnumerable>, INotifyPropertyChanged { - public interface IObjectState : IEnumerable> - { - bool IsNew { get; } - string ClassName { get; } - string ObjectId { get; } - DateTime? UpdatedAt { get; } - DateTime? CreatedAt { get; } - object this[string key] { get; } + bool IsNew { get; } + string ClassName { get; set; } + string ObjectId { get; } + DateTime? UpdatedAt { get; } + DateTime? CreatedAt { get; } + object this[string key] { get; } - bool ContainsKey(string key); + bool ContainsKey(string key); - IObjectState MutatedClone(Action func); - } + IObjectState MutatedClone(Action func); } diff --git a/Parse/Abstractions/Platform/Objects/IParseObjectClassController.cs b/Parse/Abstractions/Platform/Objects/IParseObjectClassController.cs index 07ad585c..cae10f99 100644 --- a/Parse/Abstractions/Platform/Objects/IParseObjectClassController.cs +++ b/Parse/Abstractions/Platform/Objects/IParseObjectClassController.cs @@ -2,26 +2,25 @@ using System.Collections.Generic; using Parse.Abstractions.Infrastructure; -namespace Parse.Abstractions.Platform.Objects +namespace Parse.Abstractions.Platform.Objects; + +public interface IParseObjectClassController { - public interface IParseObjectClassController - { - string GetClassName(Type type); + string GetClassName(Type type); - Type GetType(string className); + Type GetType(string className); - bool GetClassMatch(string className, Type type); + bool GetClassMatch(string className, Type type); - void AddValid(Type type); + void AddValid(Type type); - void RemoveClass(Type type); + void RemoveClass(Type type); - void AddRegisterHook(Type type, Action action); + void AddRegisterHook(Type type, Action action); - ParseObject Instantiate(string className, IServiceHub serviceHub); + ParseObject Instantiate(string className, IServiceHub serviceHub); - IDictionary GetPropertyMappings(string className); + IDictionary GetPropertyMappings(string className); - void AddIntrinsic(); - } + void AddIntrinsic(); } diff --git a/Parse/Abstractions/Platform/Objects/IParseObjectController.cs b/Parse/Abstractions/Platform/Objects/IParseObjectController.cs index a37456c2..76b84c38 100644 --- a/Parse/Abstractions/Platform/Objects/IParseObjectController.cs +++ b/Parse/Abstractions/Platform/Objects/IParseObjectController.cs @@ -4,18 +4,17 @@ using Parse.Abstractions.Infrastructure.Control; using Parse.Abstractions.Infrastructure; -namespace Parse.Abstractions.Platform.Objects +namespace Parse.Abstractions.Platform.Objects; + +public interface IParseObjectController { - public interface IParseObjectController - { - Task FetchAsync(IObjectState state, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); + Task FetchAsync(IObjectState state, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); - Task SaveAsync(IObjectState state, IDictionary operations, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); + Task SaveAsync(IObjectState state, IDictionary operations, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); - IList> SaveAllAsync(IList states, IList> operationsList, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); + Task>> SaveAllAsync(IEnumerable states, IEnumerable> operationsList, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); - Task DeleteAsync(IObjectState state, string sessionToken, CancellationToken cancellationToken = default); + Task DeleteAsync(IObjectState state, string sessionToken, CancellationToken cancellationToken = default); - IList DeleteAllAsync(IList states, string sessionToken, CancellationToken cancellationToken = default); - } + IEnumerable DeleteAllAsync(IEnumerable states, string sessionToken, CancellationToken cancellationToken = default); } diff --git a/Parse/Abstractions/Platform/Objects/IParseObjectCurrentController.cs b/Parse/Abstractions/Platform/Objects/IParseObjectCurrentController.cs index 4a948c45..d8c27ba6 100644 --- a/Parse/Abstractions/Platform/Objects/IParseObjectCurrentController.cs +++ b/Parse/Abstractions/Platform/Objects/IParseObjectCurrentController.cs @@ -2,52 +2,51 @@ using System.Threading.Tasks; using Parse.Abstractions.Infrastructure; -namespace Parse.Abstractions.Platform.Objects +namespace Parse.Abstractions.Platform.Objects; + +/// +/// IParseObjectCurrentController controls the single-instance +/// persistence used throughout the code-base. Sample usages are and +/// . +/// +/// Type of object being persisted. +public interface IParseObjectCurrentController where T : ParseObject { /// - /// IParseObjectCurrentController controls the single-instance - /// persistence used throughout the code-base. Sample usages are and - /// . + /// Persists current . /// - /// Type of object being persisted. - public interface IParseObjectCurrentController where T : ParseObject - { - /// - /// Persists current . - /// - /// to be persisted. - /// The cancellation token. - Task SetAsync(T obj, CancellationToken cancellationToken = default); - - /// - /// Gets the persisted current . - /// - /// The cancellation token. - Task GetAsync(IServiceHub serviceHub, CancellationToken cancellationToken = default); + /// to be persisted. + /// The cancellation token. + Task SetAsync(T obj, CancellationToken cancellationToken = default); + + /// + /// Gets the persisted current . + /// + /// The cancellation token. + Task GetAsync(IServiceHub serviceHub, CancellationToken cancellationToken = default); - /// - /// Returns a that resolves to true if current - /// exists. - /// - /// The cancellation token. - Task ExistsAsync(CancellationToken cancellationToken = default); + /// + /// Returns a that resolves to true if current + /// exists. + /// + /// The cancellation token. + Task ExistsAsync(CancellationToken cancellationToken = default); - /// - /// Returns true if the given is the persisted current - /// . - /// - /// The object to check. - /// true if obj is the current persisted . - bool IsCurrent(T obj); + /// + /// Returns true if the given is the persisted current + /// . + /// + /// The object to check. + /// true if obj is the current persisted . + bool IsCurrent(T obj); - /// - /// Nullifies the current from memory. - /// - void ClearFromMemory(); + /// + /// Nullifies the current from memory. + /// + void ClearFromMemory(); - /// - /// Clears current from disk. - /// - void ClearFromDisk(); - } + /// + /// Clears current from disk. + /// + Task ClearFromDiskAsync(); } diff --git a/Parse/Abstractions/Platform/Push/IParsePushChannelsController.cs b/Parse/Abstractions/Platform/Push/IParsePushChannelsController.cs index c2f91436..5ce66395 100644 --- a/Parse/Abstractions/Platform/Push/IParsePushChannelsController.cs +++ b/Parse/Abstractions/Platform/Push/IParsePushChannelsController.cs @@ -3,12 +3,11 @@ using System.Threading.Tasks; using Parse.Abstractions.Infrastructure; -namespace Parse.Abstractions.Platform.Push +namespace Parse.Abstractions.Platform.Push; + +public interface IParsePushChannelsController { - public interface IParsePushChannelsController - { - Task SubscribeAsync(IEnumerable channels, IServiceHub serviceHub, CancellationToken cancellationToken); + Task SubscribeAsync(IEnumerable channels, IServiceHub serviceHub, CancellationToken cancellationToken); - Task UnsubscribeAsync(IEnumerable channels, IServiceHub serviceHub, CancellationToken cancellationToken); - } + Task UnsubscribeAsync(IEnumerable channels, IServiceHub serviceHub, CancellationToken cancellationToken); } diff --git a/Parse/Abstractions/Platform/Push/IParsePushController.cs b/Parse/Abstractions/Platform/Push/IParsePushController.cs index 5e5ffee1..58bb034b 100644 --- a/Parse/Abstractions/Platform/Push/IParsePushController.cs +++ b/Parse/Abstractions/Platform/Push/IParsePushController.cs @@ -2,10 +2,9 @@ using System.Threading.Tasks; using Parse.Abstractions.Infrastructure; -namespace Parse.Abstractions.Platform.Push +namespace Parse.Abstractions.Platform.Push; + +public interface IParsePushController { - public interface IParsePushController - { - Task SendPushNotificationAsync(IPushState state, IServiceHub serviceHub, CancellationToken cancellationToken = default); - } + Task SendPushNotificationAsync(IPushState state, IServiceHub serviceHub, CancellationToken cancellationToken = default); } diff --git a/Parse/Abstractions/Platform/Push/IPushState.cs b/Parse/Abstractions/Platform/Push/IPushState.cs index 71017ea9..6de0e403 100644 --- a/Parse/Abstractions/Platform/Push/IPushState.cs +++ b/Parse/Abstractions/Platform/Push/IPushState.cs @@ -2,18 +2,17 @@ using System.Collections.Generic; using Parse.Platform.Push; -namespace Parse.Abstractions.Platform.Push +namespace Parse.Abstractions.Platform.Push; + +public interface IPushState { - public interface IPushState - { - ParseQuery Query { get; } - IEnumerable Channels { get; } - DateTime? Expiration { get; } - TimeSpan? ExpirationInterval { get; } - DateTime? PushTime { get; } - IDictionary Data { get; } - string Alert { get; } + ParseQuery Query { get; } + IEnumerable Channels { get; } + DateTime? Expiration { get; } + TimeSpan? ExpirationInterval { get; } + DateTime? PushTime { get; } + IDictionary Data { get; } + string Alert { get; } - IPushState MutatedClone(Action func); - } + IPushState MutatedClone(Action func); } diff --git a/Parse/Abstractions/Platform/Queries/IParseQueryController.cs b/Parse/Abstractions/Platform/Queries/IParseQueryController.cs index cbae66ab..c4c92ee3 100644 --- a/Parse/Abstractions/Platform/Queries/IParseQueryController.cs +++ b/Parse/Abstractions/Platform/Queries/IParseQueryController.cs @@ -3,14 +3,13 @@ using System.Threading.Tasks; using Parse.Abstractions.Platform.Objects; -namespace Parse.Abstractions.Platform.Queries +namespace Parse.Abstractions.Platform.Queries; + +public interface IParseQueryController { - public interface IParseQueryController - { - Task> FindAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken = default) where T : ParseObject; + Task> FindAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken = default) where T : ParseObject; - Task CountAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken = default) where T : ParseObject; + Task CountAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken = default) where T : ParseObject; - Task FirstAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken = default) where T : ParseObject; - } + Task FirstAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken = default) where T : ParseObject; } diff --git a/Parse/Abstractions/Platform/Sessions/IParseSessionController.cs b/Parse/Abstractions/Platform/Sessions/IParseSessionController.cs index 3b89aff9..98c64519 100644 --- a/Parse/Abstractions/Platform/Sessions/IParseSessionController.cs +++ b/Parse/Abstractions/Platform/Sessions/IParseSessionController.cs @@ -3,16 +3,15 @@ using Parse.Abstractions.Infrastructure; using Parse.Abstractions.Platform.Objects; -namespace Parse.Abstractions.Platform.Sessions +namespace Parse.Abstractions.Platform.Sessions; + +public interface IParseSessionController { - public interface IParseSessionController - { - Task GetSessionAsync(string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); + Task GetSessionAsync(string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); - Task RevokeAsync(string sessionToken, CancellationToken cancellationToken = default); + Task RevokeAsync(string sessionToken, CancellationToken cancellationToken = default); - Task UpgradeToRevocableSessionAsync(string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); + Task UpgradeToRevocableSessionAsync(string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); - bool IsRevocableSessionToken(string sessionToken); - } + bool IsRevocableSessionToken(string sessionToken); } diff --git a/Parse/Abstractions/Platform/Users/IParseCurrentUserController.cs b/Parse/Abstractions/Platform/Users/IParseCurrentUserController.cs index 225c2e21..97437fd0 100644 --- a/Parse/Abstractions/Platform/Users/IParseCurrentUserController.cs +++ b/Parse/Abstractions/Platform/Users/IParseCurrentUserController.cs @@ -3,12 +3,11 @@ using Parse.Abstractions.Infrastructure; using Parse.Abstractions.Platform.Objects; -namespace Parse.Abstractions.Platform.Users +namespace Parse.Abstractions.Platform.Users; + +public interface IParseCurrentUserController : IParseObjectCurrentController { - public interface IParseCurrentUserController : IParseObjectCurrentController - { - Task GetCurrentSessionTokenAsync(IServiceHub serviceHub, CancellationToken cancellationToken = default); + Task GetCurrentSessionTokenAsync(IServiceHub serviceHub, CancellationToken cancellationToken = default); - Task LogOutAsync(IServiceHub serviceHub, CancellationToken cancellationToken = default); - } + Task LogOutAsync(IServiceHub serviceHub, CancellationToken cancellationToken = default); } diff --git a/Parse/Abstractions/Platform/Users/IParseUserController.cs b/Parse/Abstractions/Platform/Users/IParseUserController.cs index b5ee7b73..76666cfc 100644 --- a/Parse/Abstractions/Platform/Users/IParseUserController.cs +++ b/Parse/Abstractions/Platform/Users/IParseUserController.cs @@ -5,22 +5,20 @@ using Parse.Abstractions.Infrastructure; using Parse.Abstractions.Platform.Objects; -namespace Parse.Abstractions.Platform.Users +namespace Parse.Abstractions.Platform.Users; + +public interface IParseUserController { - public interface IParseUserController - { - Task SignUpAsync(IObjectState state, IDictionary operations, IServiceHub serviceHub, CancellationToken cancellationToken = default); + Task SignUpAsync(IObjectState state, IDictionary operations, IServiceHub serviceHub, CancellationToken cancellationToken = default); - Task LogInAsync(string username, string password, IServiceHub serviceHub, CancellationToken cancellationToken = default); + Task LogInAsync(string username, string password, IServiceHub serviceHub, CancellationToken cancellationToken = default); - Task LogInAsync(string authType, IDictionary data, IServiceHub serviceHub, CancellationToken cancellationToken = default); + Task LogInAsync(string authType, IDictionary data, IServiceHub serviceHub, CancellationToken cancellationToken = default); - Task GetUserAsync(string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); + Task GetUserAsync(string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); - Task RequestPasswordResetAsync(string email, CancellationToken cancellationToken = default); + Task RequestPasswordResetAsync(string email, CancellationToken cancellationToken = default); - bool RevocableSessionEnabled { get; set; } + bool RevocableSessionEnabled { get; set; } - object RevocableSessionEnabledMutex { get; } - } } diff --git a/Parse/Infrastructure/AbsoluteCacheLocationMutator.cs b/Parse/Infrastructure/AbsoluteCacheLocationMutator.cs index 416aad1a..b5f0f95f 100644 --- a/Parse/Infrastructure/AbsoluteCacheLocationMutator.cs +++ b/Parse/Infrastructure/AbsoluteCacheLocationMutator.cs @@ -1,34 +1,33 @@ using Parse.Abstractions.Infrastructure; -namespace Parse.Infrastructure +namespace Parse.Infrastructure; + +/// +/// An implementation which changes the 's if available. +/// +public class AbsoluteCacheLocationMutator : IServiceHubMutator { /// - /// An implementation which changes the 's if available. + /// A custom absolute cache file path to be set on the active if it implements . /// - public class AbsoluteCacheLocationMutator : IServiceHubMutator - { - /// - /// A custom absolute cache file path to be set on the active if it implements . - /// - public string CustomAbsoluteCacheFilePath { get; set; } + public string CustomAbsoluteCacheFilePath { get; set; } - /// - /// - /// - public bool Valid => CustomAbsoluteCacheFilePath is { }; + /// + /// + /// + public bool Valid => CustomAbsoluteCacheFilePath is { }; - /// - /// - /// - /// - /// - public void Mutate(ref IMutableServiceHub target, in IServiceHub composedHub) + /// + /// + /// + /// + /// + public void Mutate(ref IMutableServiceHub target, in IServiceHub composedHub) + { + if ((target as IServiceHub).CacheController is IDiskFileCacheController { } diskFileCacheController) { - if ((target as IServiceHub).CacheController is IDiskFileCacheController { } diskFileCacheController) - { - diskFileCacheController.AbsoluteCacheFilePath = CustomAbsoluteCacheFilePath; - diskFileCacheController.RefreshPaths(); - } + diskFileCacheController.AbsoluteCacheFilePath = CustomAbsoluteCacheFilePath; + diskFileCacheController.RefreshPaths(); } } } diff --git a/Parse/Infrastructure/CacheController.cs b/Parse/Infrastructure/CacheController.cs index f85d2d47..89f61e73 100644 --- a/Parse/Infrastructure/CacheController.cs +++ b/Parse/Infrastructure/CacheController.cs @@ -10,253 +10,422 @@ using Parse.Infrastructure.Utilities; using static Parse.Resources; -namespace Parse.Infrastructure +namespace Parse.Infrastructure; + +public class CacheController : IDiskFileCacheController { - /// - /// Implements `IStorageController` for PCL targets, based off of PCLStorage. - /// - public class CacheController : IDiskFileCacheController + private class FileBackedCache : IDataCache { - class FileBackedCache : IDataCache - { - public FileBackedCache(FileInfo file) => File = file; + private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); + private Dictionary Storage = new Dictionary(); + + public FileBackedCache(FileInfo file) => File = file; - internal Task SaveAsync() => Lock(() => File.WriteContentAsync(JsonUtilities.Encode(Storage))); + public FileInfo File { get; set; } - internal Task LoadAsync() => File.ReadAllTextAsync().ContinueWith(task => + public ICollection Keys + { + get { - lock (Mutex) + _rwLock.EnterReadLock(); + try { - try - { - Storage = JsonUtilities.Parse(task.Result) as Dictionary; - } - catch - { - Storage = new Dictionary { }; - } + return Storage.Keys.ToArray(); } - }); + finally + { + _rwLock.ExitReadLock(); + } + } + } - // TODO: Check if the call to ToDictionary is necessary here considering contents is IDictionary. + public ICollection Values + { + get + { + _rwLock.EnterReadLock(); + try + { + return Storage.Values.ToArray(); + } + finally + { + _rwLock.ExitReadLock(); + } + } + } - internal void Update(IDictionary contents) => Lock(() => Storage = contents.ToDictionary(element => element.Key, element => element.Value)); - public Task AddAsync(string key, object value) + public int Count + { + get { - lock (Mutex) + _rwLock.EnterReadLock(); + try { - Storage[key] = value; - return SaveAsync(); + return Storage.Count; + } + finally + { + _rwLock.ExitReadLock(); } } + } - public Task RemoveAsync(string key) + public bool IsReadOnly + { + get { - lock (Mutex) + _rwLock.EnterReadLock(); + try { - Storage.Remove(key); - return SaveAsync(); + return ((ICollection>) Storage).IsReadOnly; + } + finally + { + _rwLock.ExitReadLock(); } } + } - public void Add(string key, object value) => throw new NotSupportedException(FileBackedCacheSynchronousMutationNotSupportedMessage); - - public bool Remove(string key) => throw new NotSupportedException(FileBackedCacheSynchronousMutationNotSupportedMessage); + public object this[string key] + { + get + { + _rwLock.EnterReadLock(); + try + { + if (Storage.TryGetValue(key, out var val)) + return val; + throw new KeyNotFoundException(key); + } + finally + { + _rwLock.ExitReadLock(); + } + } + set => throw new NotSupportedException(FileBackedCacheSynchronousMutationNotSupportedMessage); + } - public void Add(KeyValuePair item) => throw new NotSupportedException(FileBackedCacheSynchronousMutationNotSupportedMessage); + public async Task LoadAsync() + { + try + { + string fileContent = await File.ReadAllTextAsync().ConfigureAwait(false); + var data = JsonUtilities.Parse(fileContent) as Dictionary; + _rwLock.EnterWriteLock(); + try + { + Storage = data ?? new Dictionary(); + } + finally + { + _rwLock.ExitWriteLock(); + } + } + catch (IOException ioEx) + { + Console.WriteLine($"IO error while loading cache: {ioEx.Message}"); + } + } - public bool Remove(KeyValuePair item) => throw new NotSupportedException(FileBackedCacheSynchronousMutationNotSupportedMessage); + public async Task SaveAsync() + { + Dictionary snapshot; + _rwLock.EnterReadLock(); + try + { + snapshot = new Dictionary(Storage); // Create a snapshot + } + finally + { + _rwLock.ExitReadLock(); + } - public bool ContainsKey(string key) => Lock(() => Storage.ContainsKey(key)); + try + { + var content = JsonUtilities.Encode(snapshot); + await File.WriteContentAsync(content).ConfigureAwait(false); + } + catch (IOException ioEx) + { + Console.WriteLine($"IO error while saving cache: {ioEx.Message}"); + } + } - public bool TryGetValue(string key, out object value) + public void Update(IDictionary contents) + { + _rwLock.EnterWriteLock(); + try { - lock (Mutex) - { - return (Result: Storage.TryGetValue(key, out object found), value = found).Result; - } + Storage = new Dictionary(contents); + } + finally + { + _rwLock.ExitWriteLock(); } + } - public void Clear() => Lock(() => Storage.Clear()); + public async Task AddAsync(string key, object value) + { + _rwLock.EnterWriteLock(); + try + { + Storage[key] = value; + } + finally + { + _rwLock.ExitWriteLock(); + } + await SaveAsync().ConfigureAwait(false); + } - public bool Contains(KeyValuePair item) => Lock(() => Elements.Contains(item)); + public async Task RemoveAsync(string key) + { + _rwLock.EnterWriteLock(); + try + { + Storage.Remove(key); + } + finally + { + _rwLock.ExitWriteLock(); + } + await SaveAsync().ConfigureAwait(false); + } + - public void CopyTo(KeyValuePair[] array, int arrayIndex) => Lock(() => Elements.CopyTo(array, arrayIndex)); + // Unsupported synchronous modifications + public void Add(string key, object value) => throw new NotSupportedException(FileBackedCacheSynchronousMutationNotSupportedMessage); + public bool Remove(string key) => throw new NotSupportedException(FileBackedCacheSynchronousMutationNotSupportedMessage); + public void Add(KeyValuePair item) => throw new NotSupportedException(FileBackedCacheSynchronousMutationNotSupportedMessage); + public bool Remove(KeyValuePair item) => throw new NotSupportedException(FileBackedCacheSynchronousMutationNotSupportedMessage); - public IEnumerator> GetEnumerator() => Storage.GetEnumerator(); + public bool ContainsKey(string key) + { + _rwLock.EnterReadLock(); + try + { + return Storage.ContainsKey(key); + } + finally + { + _rwLock.ExitReadLock(); + } + } - IEnumerator IEnumerable.GetEnumerator() => Storage.GetEnumerator(); + public bool TryGetValue(string key, out object value) + { + _rwLock.EnterReadLock(); + try + { + return Storage.TryGetValue(key, out value); + } + finally + { + _rwLock.ExitReadLock(); + } + } - public FileInfo File { get; set; } + public void Clear() + { + _rwLock.EnterWriteLock(); + try + { + Storage.Clear(); + } + finally + { + _rwLock.ExitWriteLock(); + } + } - public object Mutex { get; set; } = new object { }; + public bool Contains(KeyValuePair item) + { + _rwLock.EnterReadLock(); + try + { + return ((ICollection>) Storage).Contains(item); + } + finally + { + _rwLock.ExitReadLock(); + } + } - // ALTNAME: Operate + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + _rwLock.EnterReadLock(); + try + { + ((ICollection>) Storage).CopyTo(array, arrayIndex); + } + finally + { + _rwLock.ExitReadLock(); + } + } - TResult Lock(Func operation) + public IEnumerator> GetEnumerator() + { + _rwLock.EnterReadLock(); + try { - lock (Mutex) - { - return operation.Invoke(); - } + return Storage.ToList().GetEnumerator(); } + finally + { + _rwLock.ExitReadLock(); + } + } - void Lock(Action operation) + IEnumerator IEnumerable.GetEnumerator() + { + _rwLock.EnterReadLock(); + try { - lock (Mutex) - { - operation.Invoke(); - } + return Storage.ToList().GetEnumerator(); + } + finally + { + _rwLock.ExitReadLock(); } + } + } - ICollection> Elements => Storage as ICollection>; + private readonly SemaphoreSlim _cacheSemaphore = new SemaphoreSlim(1, 1); - Dictionary Storage { get; set; } = new Dictionary { }; + FileInfo File { get; set; } + FileBackedCache Cache { get; set; } + TaskQueue Queue { get; } = new TaskQueue(); - public ICollection Keys => Storage.Keys; + public CacheController() { } + public CacheController(FileInfo file) => EnsureCacheExists(file); - public ICollection Values => Storage.Values; + FileBackedCache EnsureCacheExists(FileInfo file = default) + { + return Cache ??= new FileBackedCache(file ?? (File ??= PersistentCacheFile)); + } - public int Count => Storage.Count; + public async Task> LoadAsync() + { + EnsureCacheExists(); + return await Queue.Enqueue(async toAwait => + { + await toAwait.ConfigureAwait(false); + await Cache.LoadAsync().ConfigureAwait(false); + return (IDataCache) Cache; + }, CancellationToken.None).ConfigureAwait(false); + } - public bool IsReadOnly => Elements.IsReadOnly; + public async Task> SaveAsync(IDictionary contents) + { + EnsureCacheExists(); + return await Queue.Enqueue(async toAwait => + { + await toAwait.ConfigureAwait(false); - public object this[string key] + using (await SemaphoreLock.CreateAsync(_cacheSemaphore).ConfigureAwait(false)) { - get => Storage[key]; - set => throw new NotSupportedException(FileBackedCacheSynchronousMutationNotSupportedMessage); + Cache.Update(contents); + await Cache.SaveAsync().ConfigureAwait(false); } - } - FileInfo File { get; set; } - FileBackedCache Cache { get; set; } - TaskQueue Queue { get; } = new TaskQueue { }; + return (IDataCache) Cache; + }, CancellationToken.None).ConfigureAwait(false); + } - /// - /// Creates a Parse storage controller and attempts to extract a previously created settings storage file from the persistent storage location. - /// - public CacheController() { } - /// - /// Creates a Parse storage controller with the provided wrapper. - /// - /// The file wrapper that the storage controller instance should target - public CacheController(FileInfo file) => EnsureCacheExists(file); + public void RefreshPaths() + { + Cache = new FileBackedCache(File = PersistentCacheFile); + } - FileBackedCache EnsureCacheExists(FileInfo file = default) => Cache ??= new FileBackedCache(file ?? (File ??= PersistentCacheFile)); + public void Clear() + { + var file = new FileInfo(FallbackRelativeCacheFilePath); + if (file.Exists) + file.Delete(); + } - /// - /// Loads a settings dictionary from the file wrapped by . - /// - /// A storage dictionary containing the deserialized content of the storage file targeted by the instance - public Task> LoadAsync() - { - // Check if storage dictionary is already created from the controllers file (create if not) - EnsureCacheExists(); + public string RelativeCacheFilePath { get; set; } - // Load storage dictionary content async and return the resulting dictionary type - return Queue.Enqueue(toAwait => toAwait.ContinueWith(_ => Cache.LoadAsync().OnSuccess(__ => Cache as IDataCache)).Unwrap(), CancellationToken.None); - } + public string AbsoluteCacheFilePath + { + get => StoredAbsoluteCacheFilePath ?? + Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + RelativeCacheFilePath ?? FallbackRelativeCacheFilePath)); + set => StoredAbsoluteCacheFilePath = value; + } - /// - /// Saves the requested data. - /// - /// The data to be saved. - /// A data cache containing the saved data. - public Task> SaveAsync(IDictionary contents) => Queue.Enqueue(toAwait => toAwait.ContinueWith(_ => - { - EnsureCacheExists().Update(contents); - return Cache.SaveAsync().OnSuccess(__ => Cache as IDataCache); - }).Unwrap()); + string StoredAbsoluteCacheFilePath { get; set; } - /// - /// - /// - public void RefreshPaths() => Cache = new FileBackedCache(File = PersistentCacheFile); + public string FallbackRelativeCacheFilePath + => StoredFallbackRelativeCacheFilePath ??= + IdentifierBasedRelativeCacheLocationGenerator.Fallback.GetRelativeCacheFilePath(new MutableServiceHub { CacheController = this }); - // TODO: Attach the following method to AppDomain.CurrentDomain.ProcessExit if that actually ever made sense for anything except randomly generated file names, otherwise attach the delegate when it is known the file name is a randomly generated string. + string StoredFallbackRelativeCacheFilePath { get; set; } - /// - /// Clears the data controlled by this class. - /// - public void Clear() + public FileInfo PersistentCacheFile + { + get { - if (new FileInfo(FallbackRelativeCacheFilePath) is { Exists: true } file) - { - file.Delete(); - } + var dir = Path.GetDirectoryName(AbsoluteCacheFilePath); + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + var file = new FileInfo(AbsoluteCacheFilePath); + if (!file.Exists) + using (file.Create()) + { } + return file; } + } - /// - /// - /// - public string RelativeCacheFilePath { get; set; } + public FileInfo GetRelativeFile(string path) + { + path = Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), path)); + var dir = Path.GetDirectoryName(path); + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + return new FileInfo(path); + } - /// - /// - /// - public string AbsoluteCacheFilePath - { - get => StoredAbsoluteCacheFilePath ?? Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), RelativeCacheFilePath ?? FallbackRelativeCacheFilePath)); - set => StoredAbsoluteCacheFilePath = value; - } + public async Task TransferAsync(string originFilePath, string targetFilePath) + { + if (string.IsNullOrWhiteSpace(originFilePath) || string.IsNullOrWhiteSpace(targetFilePath)) + return; - string StoredAbsoluteCacheFilePath { get; set; } + var originFile = new FileInfo(originFilePath); + if (!originFile.Exists) + return; + var targetFile = new FileInfo(targetFilePath); - /// - /// Gets the calculated persistent storage file fallback path for this app execution. - /// - public string FallbackRelativeCacheFilePath => StoredFallbackRelativeCacheFilePath ??= IdentifierBasedRelativeCacheLocationGenerator.Fallback.GetRelativeCacheFilePath(new MutableServiceHub { CacheController = this }); + using var reader = new StreamReader(originFile.OpenRead(), Encoding.Unicode); + var content = await reader.ReadToEndAsync().ConfigureAwait(false); - string StoredFallbackRelativeCacheFilePath { get; set; } + using var writer = new StreamWriter(targetFile.OpenWrite(), Encoding.Unicode); + await writer.WriteAsync(content).ConfigureAwait(false); + } - /// - /// Gets or creates the file pointed to by and returns it's wrapper as a instance. - /// - public FileInfo PersistentCacheFile + internal static class SemaphoreLock + { + public static async Task CreateAsync(SemaphoreSlim semaphore) { - get - { - Directory.CreateDirectory(AbsoluteCacheFilePath.Substring(0, AbsoluteCacheFilePath.LastIndexOf(Path.DirectorySeparatorChar))); - - FileInfo file = new FileInfo(AbsoluteCacheFilePath); - if (!file.Exists) - using (file.Create()) - ; // Hopefully the JIT doesn't no-op this. The behaviour of the "using" clause should dictate how the stream is closed, to make sure it happens properly. - - return file; - } + await semaphore.WaitAsync().ConfigureAwait(false); + return new Releaser(semaphore); } - /// - /// Gets the file wrapper for the specified . - /// - /// The relative path to the target file - /// An instance of wrapping the the value - public FileInfo GetRelativeFile(string path) + public static IDisposable Create(SemaphoreSlim semaphore) { - Directory.CreateDirectory((path = Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), path))).Substring(0, path.LastIndexOf(Path.DirectorySeparatorChar))); - return new FileInfo(path); + _ = semaphore; + return new Releaser(semaphore); } - // MoveAsync - - /// - /// Transfers a file from to . - /// - /// - /// - /// A task that completes once the file move operation form to completes. - public async Task TransferAsync(string originFilePath, string targetFilePath) + private sealed class Releaser : IDisposable { - if (!String.IsNullOrWhiteSpace(originFilePath) && !String.IsNullOrWhiteSpace(targetFilePath) && new FileInfo(originFilePath) is { Exists: true } originFile && new FileInfo(targetFilePath) is { } targetFile) - { - using StreamWriter writer = new StreamWriter(targetFile.OpenWrite(), Encoding.Unicode); - using StreamReader reader = new StreamReader(originFile.OpenRead(), Encoding.Unicode); - - await writer.WriteAsync(await reader.ReadToEndAsync()); - } + private readonly SemaphoreSlim _sem; + public Releaser(SemaphoreSlim sem) => _sem = sem; + public void Dispose() => _sem.Release(); } } } diff --git a/Parse/Infrastructure/ConcurrentUserServiceHubCloner.cs b/Parse/Infrastructure/ConcurrentUserServiceHubCloner.cs index 44eb69ee..89c8ba9b 100644 --- a/Parse/Infrastructure/ConcurrentUserServiceHubCloner.cs +++ b/Parse/Infrastructure/ConcurrentUserServiceHubCloner.cs @@ -2,18 +2,20 @@ using Parse.Abstractions.Infrastructure; using Parse.Platform.Users; -namespace Parse.Infrastructure +namespace Parse.Infrastructure; + +public class ConcurrentUserServiceHubCloner : IServiceHubCloner, IServiceHubMutator { - public class ConcurrentUserServiceHubCloner : IServiceHubCloner, IServiceHubMutator - { - public bool Valid { get; } = true; + public bool Valid { get; } = true; - public IServiceHub BuildHub(in IServiceHub reference, IServiceHubComposer composer, params IServiceHubMutator[] requestedMutators) => composer.BuildHub(default, reference, requestedMutators.Concat(new[] { this }).ToArray()); + public IServiceHub BuildHub(in IServiceHub reference, IServiceHubComposer composer, params IServiceHubMutator[] requestedMutators) + { + return composer.BuildHub(default, reference, requestedMutators.Concat(new[] { this }).ToArray()); + } - public void Mutate(ref IMutableServiceHub target, in IServiceHub composedHub) - { - target.Cloner = this; - target.CurrentUserController = new ParseCurrentUserController(new TransientCacheController { }, composedHub.ClassController, composedHub.Decoder); - } + public void Mutate(ref IMutableServiceHub target, in IServiceHub composedHub) + { + target.Cloner = this; + target.CurrentUserController = new ParseCurrentUserController(new TransientCacheController { }, composedHub.ClassController, composedHub.Decoder); } } diff --git a/Parse/Infrastructure/Control/ParseAddOperation.cs b/Parse/Infrastructure/Control/ParseAddOperation.cs index e3a2355b..164503b5 100644 --- a/Parse/Infrastructure/Control/ParseAddOperation.cs +++ b/Parse/Infrastructure/Control/ParseAddOperation.cs @@ -7,31 +7,118 @@ using Parse.Infrastructure.Data; using Parse.Infrastructure.Utilities; -namespace Parse.Infrastructure.Control +namespace Parse.Infrastructure.Control; + +public class ParseAddOperation : IParseFieldOperation { - public class ParseAddOperation : IParseFieldOperation + // Encapsulated the data to be added as a read-only collection + ReadOnlyCollection Data { get; } + + public ParseAddOperation(IEnumerable objects) => + Data = new ReadOnlyCollection(objects.Distinct().ToList()); // Ensures no duplicates within this operation + + public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) { - ReadOnlyCollection Data { get; } + return previous switch + { + null => this, + ParseDeleteOperation _ => new ParseSetOperation(Data.ToList()), // If deleted, replace with the new data + ParseSetOperation setOp => new ParseSetOperation( + Conversion.To>(setOp.Value).Concat(Data).ToList()), // Combine with existing data + ParseAddOperation addition => new ParseAddOperation( + addition.Objects.Concat(Data).Distinct()), // Merge and remove duplicates + _ => throw new InvalidOperationException("Operation is invalid after previous operation.") + }; + } - public ParseAddOperation(IEnumerable objects) => Data = new ReadOnlyCollection(objects.ToList()); + public object Apply(object oldValue, string key) + { + if (oldValue == null) + { + return Data.ToList(); // Initialize the value as the data + } + + var result = Conversion.To>(oldValue).ToList(); + foreach (var obj in Data) + { + if (!result.Contains(obj)) // Ensure no duplicates + { + result.Add(obj); + } + } + return result; + } + + public IDictionary ConvertToJSON(IServiceHub serviceHub = default) + { + // Convert the data into JSON-compatible structures + var encodedObjects = Data.Select(EncodeForParse).ToList(); - public object Encode(IServiceHub serviceHub) => new Dictionary + return new Dictionary { ["__op"] = "Add", - ["objects"] = PointerOrLocalIdEncoder.Instance.Encode(Data, serviceHub) + ["objects"] = encodedObjects }; + } - public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) => previous switch + private object EncodeForParse(object obj) + { + return obj switch { - null => this, - ParseDeleteOperation { } => new ParseSetOperation(Data.ToList()), - ParseSetOperation { } setOp => new ParseSetOperation(Conversion.To>(setOp.Value).Concat(Data).ToList()), - ParseAddOperation { } addition => new ParseAddOperation(addition.Objects.Concat(Data)), - _ => throw new InvalidOperationException("Operation is invalid after previous operation.") - }; + // Handle pointers + ParseObject parseObj => new Dictionary + { + ["__type"] = "Pointer", + ["className"] = parseObj.ClassName, + ["objectId"] = parseObj.ObjectId + }, + + // Handle GeoPoints + ParseGeoPoint geoPoint => new Dictionary + { + ["__type"] = "GeoPoint", + ["latitude"] = geoPoint.Latitude, + ["longitude"] = geoPoint.Longitude + }, + + // Handle Files + ParseFile file => new Dictionary + { + ["__type"] = "File", + ["name"] = file.Name, + ["url"] = file.Url + }, - public object Apply(object oldValue, string key) => oldValue is { } ? Conversion.To>(oldValue).Concat(Data).ToList() : Data.ToList(); + // Handle Relations + ParseRelationBase relation => new Dictionary + { + ["__type"] = "Relation", + ["className"] = relation.TargetClassName + }, - public IEnumerable Objects => Data; + // Handle primitive types + string or int or long or float or double or decimal or bool => obj, + + // Handle Bytes + byte[] bytes => new Dictionary + { + ["__type"] = "Bytes", + ["base64"] = Convert.ToBase64String(bytes) + }, + + // Handle nested objects (JSON-like structure) + IDictionary nestedObject => nestedObject.ToDictionary(k => k.Key, k => EncodeForParse(k.Value)), + + // Handle arrays + IEnumerable array => array.Select(EncodeForParse).ToList(), + + // For unsupported types, throw an error + _ => throw new InvalidOperationException($"Unsupported type: {obj.GetType()}") + }; } + + public IEnumerable Objects => Data; + + // Added Value property to return the underlying data + public object Value => Data.ToList(); } diff --git a/Parse/Infrastructure/Control/ParseAddUniqueOperation.cs b/Parse/Infrastructure/Control/ParseAddUniqueOperation.cs index 7786d48a..3c6cad30 100644 --- a/Parse/Infrastructure/Control/ParseAddUniqueOperation.cs +++ b/Parse/Infrastructure/Control/ParseAddUniqueOperation.cs @@ -7,61 +7,125 @@ using Parse.Infrastructure.Data; using Parse.Infrastructure.Utilities; -namespace Parse.Infrastructure.Control -{ - public class ParseAddUniqueOperation : IParseFieldOperation - { - ReadOnlyCollection Data { get; } +namespace Parse.Infrastructure.Control; - public ParseAddUniqueOperation(IEnumerable objects) => Data = new ReadOnlyCollection(objects.Distinct().ToList()); +public class ParseAddUniqueOperation : IParseFieldOperation +{ + // Read-only collection to store unique data + ReadOnlyCollection Data { get; } - public object Encode(IServiceHub serviceHub) => new Dictionary - { - ["__op"] = "AddUnique", - ["objects"] = PointerOrLocalIdEncoder.Instance.Encode(Data, serviceHub) - }; + public ParseAddUniqueOperation(IEnumerable objects) => + Data = new ReadOnlyCollection(objects.Distinct().ToList()); - public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) => previous switch + public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) + { + return previous switch { null => this, - ParseDeleteOperation _ => new ParseSetOperation(Data.ToList()), - ParseSetOperation setOp => new ParseSetOperation(Apply(Conversion.To>(setOp.Value), default)), - ParseAddUniqueOperation addition => new ParseAddUniqueOperation(Apply(addition.Objects, default) as IList), + ParseDeleteOperation _ => new ParseSetOperation(Data.ToList()), // Replace deleted value with current data + ParseSetOperation setOp => new ParseSetOperation(Apply(Conversion.To>(setOp.Value), default)), // Merge with existing value + ParseAddUniqueOperation addition => new ParseAddUniqueOperation(addition.Objects.Concat(Data).Distinct()), // Combine both unique sets _ => throw new InvalidOperationException("Operation is invalid after previous operation.") }; + } + + public object Apply(object oldValue, string key) + { + if (oldValue == null) + { + return Data.ToList(); // If no previous value, return the current data + } + + var result = Conversion.To>(oldValue).ToList(); + var comparer = ParseFieldOperations.ParseObjectComparer; - public object Apply(object oldValue, string key) + foreach (var target in Data) { - if (oldValue == null) + // Add only if not already present, replace if an equivalent exists + if (result.FirstOrDefault(reference => comparer.Equals(target, reference)) is { } matched) { - return Data.ToList(); + result[result.IndexOf(matched)] = target; } + else + { + result.Add(target); + } + } - List result = Conversion.To>(oldValue).ToList(); - IEqualityComparer comparer = ParseFieldOperations.ParseObjectComparer; + return result; + } + + public IDictionary ConvertToJSON(IServiceHub serviceHub = default) + { + // Converts the data into JSON-compatible structures + var encodedObjects = Data.Select(EncodeForParse).ToList(); - foreach (object target in Data) + return new Dictionary + { + ["__op"] = "AddUnique", // Parse operation type + ["objects"] = encodedObjects + }; + } + + // Helper method for encoding individual objects + private object EncodeForParse(object obj) + { + return obj switch + { + // Handle pointers + ParseObject parseObj => new Dictionary { - if (target is ParseObject) - { - if (!(result.FirstOrDefault(reference => comparer.Equals(target, reference)) is { } matched)) - { - result.Add(target); - } - else - { - result[result.IndexOf(matched)] = target; - } - } - else if (!result.Contains(target, comparer)) - { - result.Add(target); - } - } + ["__type"] = "Pointer", + ["className"] = parseObj.ClassName, + ["objectId"] = parseObj.ObjectId + }, - return result; - } + // Handle GeoPoints + ParseGeoPoint geoPoint => new Dictionary + { + ["__type"] = "GeoPoint", + ["latitude"] = geoPoint.Latitude, + ["longitude"] = geoPoint.Longitude + }, - public IEnumerable Objects => Data; + // Handle Files + ParseFile file => new Dictionary + { + ["__type"] = "File", + ["name"] = file.Name, + ["url"] = file.Url + }, + + // Handle Relations + ParseRelationBase relation => new Dictionary + { + ["__type"] = "Relation", + ["className"] = relation.TargetClassName + }, + + // Handle primitive types + string or int or long or float or double or decimal or bool => obj, + + // Handle Bytes + byte[] bytes => new Dictionary + { + ["__type"] = "Bytes", + ["base64"] = Convert.ToBase64String(bytes) + }, + + // Handle nested objects (JSON-like structure) + IDictionary nestedObject => nestedObject.ToDictionary(k => k.Key, k => EncodeForParse(k.Value)), + + // Handle arrays + IEnumerable array => array.Select(EncodeForParse).ToList(), + + // For unsupported types, throw an error + _ => throw new InvalidOperationException($"Unsupported type: {obj.GetType()}") + }; } + + public IEnumerable Objects => Data; + + // Added Value property to return the underlying data + public object Value => Data.ToList(); } diff --git a/Parse/Infrastructure/Control/ParseDeleteOperation.cs b/Parse/Infrastructure/Control/ParseDeleteOperation.cs index fa1533b7..c074e616 100644 --- a/Parse/Infrastructure/Control/ParseDeleteOperation.cs +++ b/Parse/Infrastructure/Control/ParseDeleteOperation.cs @@ -2,23 +2,41 @@ using Parse.Abstractions.Infrastructure; using Parse.Abstractions.Infrastructure.Control; -namespace Parse.Infrastructure.Control +namespace Parse.Infrastructure.Control; + +/// +/// An operation where a field is deleted from the object. +/// +public class ParseDeleteOperation : IParseFieldOperation { - /// - /// An operation where a field is deleted from the object. - /// - public class ParseDeleteOperation : IParseFieldOperation - { - internal static object Token { get; } = new object { }; + internal static object Token { get; } = new object { }; - public static ParseDeleteOperation Instance { get; } = new ParseDeleteOperation { }; + public static ParseDeleteOperation Instance { get; } = new ParseDeleteOperation(); - private ParseDeleteOperation() { } + public object Value => null; // Updated to return null as the value for delete operations - public object Encode(IServiceHub serviceHub) => new Dictionary { ["__op"] = "Delete" }; + private ParseDeleteOperation() { } - public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) => this; + // Replaced Encode with ConvertToJSON + public IDictionary ConvertToJSON(IServiceHub serviceHub = default) + { + return new Dictionary + { + ["__op"] = "Delete" + }; + } - public object Apply(object oldValue, string key) => Token; + public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) + { + // Merging with any previous operation results in this delete operation + return this; } + + public object Apply(object oldValue, string key) + { + // When applied, delete the field by returning the delete token + return Token; + } + + } diff --git a/Parse/Infrastructure/Control/ParseFieldOperations.cs b/Parse/Infrastructure/Control/ParseFieldOperations.cs index 99f08155..9aeac840 100644 --- a/Parse/Infrastructure/Control/ParseFieldOperations.cs +++ b/Parse/Infrastructure/Control/ParseFieldOperations.cs @@ -2,48 +2,50 @@ using System.Collections.Generic; using Parse.Abstractions.Infrastructure.Control; -namespace Parse.Infrastructure.Control +namespace Parse.Infrastructure.Control; + +public class ParseObjectIdComparer : IEqualityComparer { - public class ParseObjectIdComparer : IEqualityComparer + bool IEqualityComparer.Equals(object p1, object p2) { - bool IEqualityComparer.Equals(object p1, object p2) + ParseObject parseObj1 = p1 as ParseObject; + ParseObject parseObj2 = p2 as ParseObject; + if (parseObj1 != null && parseObj2 != null) { - ParseObject parseObj1 = p1 as ParseObject; - ParseObject parseObj2 = p2 as ParseObject; - if (parseObj1 != null && parseObj2 != null) - { - return Equals(parseObj1.ObjectId, parseObj2.ObjectId); - } - return Equals(p1, p2); + return Equals(parseObj1.ObjectId, parseObj2.ObjectId); } + return Equals(p1, p2); + } - public int GetHashCode(object p) + public int GetHashCode(object p) + { + ParseObject parseObject = p as ParseObject; + if (parseObject != null) { - ParseObject parseObject = p as ParseObject; - if (parseObject != null) - { - return parseObject.ObjectId.GetHashCode(); - } - return p.GetHashCode(); + return parseObject.ObjectId.GetHashCode(); } + return p.GetHashCode(); } +} - static class ParseFieldOperations - { - private static ParseObjectIdComparer comparer; +static class ParseFieldOperations +{ + private static ParseObjectIdComparer comparer; - public static IParseFieldOperation Decode(IDictionary json) => throw new NotImplementedException(); + public static IParseFieldOperation Decode(IDictionary json) + { + throw new NotImplementedException(); + } - public static IEqualityComparer ParseObjectComparer + public static IEqualityComparer ParseObjectComparer + { + get { - get + if (comparer == null) { - if (comparer == null) - { - comparer = new ParseObjectIdComparer(); - } - return comparer; + comparer = new ParseObjectIdComparer(); } + return comparer; } } } diff --git a/Parse/Infrastructure/Control/ParseIncrementOperation.cs b/Parse/Infrastructure/Control/ParseIncrementOperation.cs index 18a784bd..036ef5ae 100644 --- a/Parse/Infrastructure/Control/ParseIncrementOperation.cs +++ b/Parse/Infrastructure/Control/ParseIncrementOperation.cs @@ -4,121 +4,145 @@ using Parse.Abstractions.Infrastructure; using Parse.Abstractions.Infrastructure.Control; -namespace Parse.Infrastructure.Control +namespace Parse.Infrastructure.Control; + +public class ParseIncrementOperation : IParseFieldOperation { - public class ParseIncrementOperation : IParseFieldOperation + // Defines adders for all of the implicit conversions: http://msdn.microsoft.com/en-US/library/y5b434w4(v=vs.80).aspx. + + static IDictionary, Func> Adders { get; } = new Dictionary, Func> { - // Defines adders for all of the implicit conversions: http://msdn.microsoft.com/en-US/library/y5b434w4(v=vs.80).aspx. + [new Tuple(typeof(sbyte), typeof(sbyte))] = (left, right) => (sbyte) left + (sbyte) right, + [new Tuple(typeof(sbyte), typeof(short))] = (left, right) => (sbyte) left + (short) right, + [new Tuple(typeof(sbyte), typeof(int))] = (left, right) => (sbyte) left + (int) right, + [new Tuple(typeof(sbyte), typeof(long))] = (left, right) => (sbyte) left + (long) right, + [new Tuple(typeof(sbyte), typeof(float))] = (left, right) => (sbyte) left + (float) right, + [new Tuple(typeof(sbyte), typeof(double))] = (left, right) => (sbyte) left + (double) right, + [new Tuple(typeof(sbyte), typeof(decimal))] = (left, right) => (sbyte) left + (decimal) right, + [new Tuple(typeof(byte), typeof(byte))] = (left, right) => (byte) left + (byte) right, + [new Tuple(typeof(byte), typeof(short))] = (left, right) => (byte) left + (short) right, + [new Tuple(typeof(byte), typeof(ushort))] = (left, right) => (byte) left + (ushort) right, + [new Tuple(typeof(byte), typeof(int))] = (left, right) => (byte) left + (int) right, + [new Tuple(typeof(byte), typeof(uint))] = (left, right) => (byte) left + (uint) right, + [new Tuple(typeof(byte), typeof(long))] = (left, right) => (byte) left + (long) right, + [new Tuple(typeof(byte), typeof(ulong))] = (left, right) => (byte) left + (ulong) right, + [new Tuple(typeof(byte), typeof(float))] = (left, right) => (byte) left + (float) right, + [new Tuple(typeof(byte), typeof(double))] = (left, right) => (byte) left + (double) right, + [new Tuple(typeof(byte), typeof(decimal))] = (left, right) => (byte) left + (decimal) right, + [new Tuple(typeof(short), typeof(short))] = (left, right) => (short) left + (short) right, + [new Tuple(typeof(short), typeof(int))] = (left, right) => (short) left + (int) right, + [new Tuple(typeof(short), typeof(long))] = (left, right) => (short) left + (long) right, + [new Tuple(typeof(short), typeof(float))] = (left, right) => (short) left + (float) right, + [new Tuple(typeof(short), typeof(double))] = (left, right) => (short) left + (double) right, + [new Tuple(typeof(short), typeof(decimal))] = (left, right) => (short) left + (decimal) right, + [new Tuple(typeof(ushort), typeof(ushort))] = (left, right) => (ushort) left + (ushort) right, + [new Tuple(typeof(ushort), typeof(int))] = (left, right) => (ushort) left + (int) right, + [new Tuple(typeof(ushort), typeof(uint))] = (left, right) => (ushort) left + (uint) right, + [new Tuple(typeof(ushort), typeof(long))] = (left, right) => (ushort) left + (long) right, + [new Tuple(typeof(ushort), typeof(ulong))] = (left, right) => (ushort) left + (ulong) right, + [new Tuple(typeof(ushort), typeof(float))] = (left, right) => (ushort) left + (float) right, + [new Tuple(typeof(ushort), typeof(double))] = (left, right) => (ushort) left + (double) right, + [new Tuple(typeof(ushort), typeof(decimal))] = (left, right) => (ushort) left + (decimal) right, + [new Tuple(typeof(int), typeof(int))] = (left, right) => (int) left + (int) right, + [new Tuple(typeof(int), typeof(long))] = (left, right) => (int) left + (long) right, + [new Tuple(typeof(int), typeof(float))] = (left, right) => (int) left + (float) right, + [new Tuple(typeof(int), typeof(double))] = (left, right) => (int) left + (double) right, + [new Tuple(typeof(int), typeof(decimal))] = (left, right) => (int) left + (decimal) right, + [new Tuple(typeof(uint), typeof(uint))] = (left, right) => (uint) left + (uint) right, + [new Tuple(typeof(uint), typeof(long))] = (left, right) => (uint) left + (long) right, + [new Tuple(typeof(uint), typeof(ulong))] = (left, right) => (uint) left + (ulong) right, + [new Tuple(typeof(uint), typeof(float))] = (left, right) => (uint) left + (float) right, + [new Tuple(typeof(uint), typeof(double))] = (left, right) => (uint) left + (double) right, + [new Tuple(typeof(uint), typeof(decimal))] = (left, right) => (uint) left + (decimal) right, + [new Tuple(typeof(long), typeof(long))] = (left, right) => (long) left + (long) right, + [new Tuple(typeof(long), typeof(float))] = (left, right) => (long) left + (float) right, + [new Tuple(typeof(long), typeof(double))] = (left, right) => (long) left + (double) right, + [new Tuple(typeof(long), typeof(decimal))] = (left, right) => (long) left + (decimal) right, + [new Tuple(typeof(char), typeof(char))] = (left, right) => (char) left + (char) right, + [new Tuple(typeof(char), typeof(ushort))] = (left, right) => (char) left + (ushort) right, + [new Tuple(typeof(char), typeof(int))] = (left, right) => (char) left + (int) right, + [new Tuple(typeof(char), typeof(uint))] = (left, right) => (char) left + (uint) right, + [new Tuple(typeof(char), typeof(long))] = (left, right) => (char) left + (long) right, + [new Tuple(typeof(char), typeof(ulong))] = (left, right) => (char) left + (ulong) right, + [new Tuple(typeof(char), typeof(float))] = (left, right) => (char) left + (float) right, + [new Tuple(typeof(char), typeof(double))] = (left, right) => (char) left + (double) right, + [new Tuple(typeof(char), typeof(decimal))] = (left, right) => (char) left + (decimal) right, + [new Tuple(typeof(float), typeof(float))] = (left, right) => (float) left + (float) right, + [new Tuple(typeof(float), typeof(double))] = (left, right) => (float) left + (double) right, + [new Tuple(typeof(ulong), typeof(ulong))] = (left, right) => (ulong) left + (ulong) right, + [new Tuple(typeof(ulong), typeof(float))] = (left, right) => (ulong) left + (float) right, + [new Tuple(typeof(ulong), typeof(double))] = (left, right) => (ulong) left + (double) right, + [new Tuple(typeof(ulong), typeof(decimal))] = (left, right) => (ulong) left + (decimal) right, + [new Tuple(typeof(double), typeof(double))] = (left, right) => (double) left + (double) right, + [new Tuple(typeof(decimal), typeof(decimal))] = (left, right) => (decimal) left + (decimal) right + }; - static IDictionary, Func> Adders { get; } = new Dictionary, Func> - { - [new Tuple(typeof(sbyte), typeof(sbyte))] = (left, right) => (sbyte) left + (sbyte) right, - [new Tuple(typeof(sbyte), typeof(short))] = (left, right) => (sbyte) left + (short) right, - [new Tuple(typeof(sbyte), typeof(int))] = (left, right) => (sbyte) left + (int) right, - [new Tuple(typeof(sbyte), typeof(long))] = (left, right) => (sbyte) left + (long) right, - [new Tuple(typeof(sbyte), typeof(float))] = (left, right) => (sbyte) left + (float) right, - [new Tuple(typeof(sbyte), typeof(double))] = (left, right) => (sbyte) left + (double) right, - [new Tuple(typeof(sbyte), typeof(decimal))] = (left, right) => (sbyte) left + (decimal) right, - [new Tuple(typeof(byte), typeof(byte))] = (left, right) => (byte) left + (byte) right, - [new Tuple(typeof(byte), typeof(short))] = (left, right) => (byte) left + (short) right, - [new Tuple(typeof(byte), typeof(ushort))] = (left, right) => (byte) left + (ushort) right, - [new Tuple(typeof(byte), typeof(int))] = (left, right) => (byte) left + (int) right, - [new Tuple(typeof(byte), typeof(uint))] = (left, right) => (byte) left + (uint) right, - [new Tuple(typeof(byte), typeof(long))] = (left, right) => (byte) left + (long) right, - [new Tuple(typeof(byte), typeof(ulong))] = (left, right) => (byte) left + (ulong) right, - [new Tuple(typeof(byte), typeof(float))] = (left, right) => (byte) left + (float) right, - [new Tuple(typeof(byte), typeof(double))] = (left, right) => (byte) left + (double) right, - [new Tuple(typeof(byte), typeof(decimal))] = (left, right) => (byte) left + (decimal) right, - [new Tuple(typeof(short), typeof(short))] = (left, right) => (short) left + (short) right, - [new Tuple(typeof(short), typeof(int))] = (left, right) => (short) left + (int) right, - [new Tuple(typeof(short), typeof(long))] = (left, right) => (short) left + (long) right, - [new Tuple(typeof(short), typeof(float))] = (left, right) => (short) left + (float) right, - [new Tuple(typeof(short), typeof(double))] = (left, right) => (short) left + (double) right, - [new Tuple(typeof(short), typeof(decimal))] = (left, right) => (short) left + (decimal) right, - [new Tuple(typeof(ushort), typeof(ushort))] = (left, right) => (ushort) left + (ushort) right, - [new Tuple(typeof(ushort), typeof(int))] = (left, right) => (ushort) left + (int) right, - [new Tuple(typeof(ushort), typeof(uint))] = (left, right) => (ushort) left + (uint) right, - [new Tuple(typeof(ushort), typeof(long))] = (left, right) => (ushort) left + (long) right, - [new Tuple(typeof(ushort), typeof(ulong))] = (left, right) => (ushort) left + (ulong) right, - [new Tuple(typeof(ushort), typeof(float))] = (left, right) => (ushort) left + (float) right, - [new Tuple(typeof(ushort), typeof(double))] = (left, right) => (ushort) left + (double) right, - [new Tuple(typeof(ushort), typeof(decimal))] = (left, right) => (ushort) left + (decimal) right, - [new Tuple(typeof(int), typeof(int))] = (left, right) => (int) left + (int) right, - [new Tuple(typeof(int), typeof(long))] = (left, right) => (int) left + (long) right, - [new Tuple(typeof(int), typeof(float))] = (left, right) => (int) left + (float) right, - [new Tuple(typeof(int), typeof(double))] = (left, right) => (int) left + (double) right, - [new Tuple(typeof(int), typeof(decimal))] = (left, right) => (int) left + (decimal) right, - [new Tuple(typeof(uint), typeof(uint))] = (left, right) => (uint) left + (uint) right, - [new Tuple(typeof(uint), typeof(long))] = (left, right) => (uint) left + (long) right, - [new Tuple(typeof(uint), typeof(ulong))] = (left, right) => (uint) left + (ulong) right, - [new Tuple(typeof(uint), typeof(float))] = (left, right) => (uint) left + (float) right, - [new Tuple(typeof(uint), typeof(double))] = (left, right) => (uint) left + (double) right, - [new Tuple(typeof(uint), typeof(decimal))] = (left, right) => (uint) left + (decimal) right, - [new Tuple(typeof(long), typeof(long))] = (left, right) => (long) left + (long) right, - [new Tuple(typeof(long), typeof(float))] = (left, right) => (long) left + (float) right, - [new Tuple(typeof(long), typeof(double))] = (left, right) => (long) left + (double) right, - [new Tuple(typeof(long), typeof(decimal))] = (left, right) => (long) left + (decimal) right, - [new Tuple(typeof(char), typeof(char))] = (left, right) => (char) left + (char) right, - [new Tuple(typeof(char), typeof(ushort))] = (left, right) => (char) left + (ushort) right, - [new Tuple(typeof(char), typeof(int))] = (left, right) => (char) left + (int) right, - [new Tuple(typeof(char), typeof(uint))] = (left, right) => (char) left + (uint) right, - [new Tuple(typeof(char), typeof(long))] = (left, right) => (char) left + (long) right, - [new Tuple(typeof(char), typeof(ulong))] = (left, right) => (char) left + (ulong) right, - [new Tuple(typeof(char), typeof(float))] = (left, right) => (char) left + (float) right, - [new Tuple(typeof(char), typeof(double))] = (left, right) => (char) left + (double) right, - [new Tuple(typeof(char), typeof(decimal))] = (left, right) => (char) left + (decimal) right, - [new Tuple(typeof(float), typeof(float))] = (left, right) => (float) left + (float) right, - [new Tuple(typeof(float), typeof(double))] = (left, right) => (float) left + (double) right, - [new Tuple(typeof(ulong), typeof(ulong))] = (left, right) => (ulong) left + (ulong) right, - [new Tuple(typeof(ulong), typeof(float))] = (left, right) => (ulong) left + (float) right, - [new Tuple(typeof(ulong), typeof(double))] = (left, right) => (ulong) left + (double) right, - [new Tuple(typeof(ulong), typeof(decimal))] = (left, right) => (ulong) left + (decimal) right, - [new Tuple(typeof(double), typeof(double))] = (left, right) => (double) left + (double) right, - [new Tuple(typeof(decimal), typeof(decimal))] = (left, right) => (decimal) left + (decimal) right - }; + static ParseIncrementOperation() + { + // Generate the adders in the other direction - static ParseIncrementOperation() + foreach (Tuple pair in Adders.Keys.ToList()) { - // Generate the adders in the other direction - - foreach (Tuple pair in Adders.Keys.ToList()) + if (pair.Item1.Equals(pair.Item2)) { - if (pair.Item1.Equals(pair.Item2)) - { - continue; - } - - Tuple reversePair = new Tuple(pair.Item2, pair.Item1); - Func func = Adders[pair]; - Adders[reversePair] = (left, right) => func(right, left); + continue; } + + Tuple reversePair = new Tuple(pair.Item2, pair.Item1); + Func func = Adders[pair]; + Adders[reversePair] = (left, right) => func(right, left); } + } - public ParseIncrementOperation(object amount) => Amount = amount; + public ParseIncrementOperation(object amount) => Amount = amount; - public object Encode(IServiceHub serviceHub) => new Dictionary + // Updated Encode to ConvertToJSON + public IDictionary ConvertToJSON(IServiceHub serviceHub = default) + { + // Updated to produce a JSON-compatible structure + return new Dictionary { - ["__op"] = "Increment", - ["amount"] = Amount + ["__op"] = "Increment", // Parse operation type + ["amount"] = Amount // Value to increment }; + } - static object Add(object first, object second) => Adders.TryGetValue(new Tuple(first.GetType(), second.GetType()), out Func adder) ? adder(first, second) : throw new InvalidCastException($"Could not add objects of type {first.GetType()} and {second.GetType()} to each other."); + static object Add(object first, object second) + { + // Handles type-safe addition using Adders + return Adders.TryGetValue(new Tuple(first.GetType(), second.GetType()), out Func adder) + ? adder(first, second) + : throw new InvalidCastException($"Could not add objects of type {first.GetType()} and {second.GetType()} to each other."); + } - public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) => previous switch + public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) + { + return previous switch { null => this, - ParseDeleteOperation _ => new ParseSetOperation(Amount), + ParseDeleteOperation _ => new ParseSetOperation(Amount), // Handles merging with delete - // This may be a bug, but it was in the original logic. + // Corrected the condition to properly handle non-number types + ParseSetOperation { Value: not null and not string } => new ParseSetOperation(Add(previous.Value, Amount)), + ParseSetOperation { Value: string } => throw new InvalidOperationException("Cannot increment a non-number type."), - ParseSetOperation { Value: string { } } => throw new InvalidOperationException("Cannot increment a non-number type."), - ParseSetOperation { Value: var value } => new ParseSetOperation(Add(value, Amount)), + // Merging with another increment operation ParseIncrementOperation { Amount: var amount } => new ParseIncrementOperation(Add(amount, Amount)), + _ => throw new InvalidOperationException("Operation is invalid after previous operation.") }; + } - public object Apply(object oldValue, string key) => oldValue is string ? throw new InvalidOperationException("Cannot increment a non-number type.") : Add(oldValue ?? 0, Amount); - - public object Amount { get; } + public object Apply(object oldValue, string key) + { + // Corrected logic to handle nulls and ensure only numeric types are processed + return oldValue is string + ? throw new InvalidOperationException("Cannot increment a non-number type.") + : Add(oldValue ?? 0, Amount); } + + public object Amount { get; } + + public object Value => Amount; } + diff --git a/Parse/Infrastructure/Control/ParseRelationOperation.cs b/Parse/Infrastructure/Control/ParseRelationOperation.cs index 503300ee..7960bfbd 100644 --- a/Parse/Infrastructure/Control/ParseRelationOperation.cs +++ b/Parse/Infrastructure/Control/ParseRelationOperation.cs @@ -7,63 +7,65 @@ using Parse.Abstractions.Platform.Objects; using Parse.Infrastructure.Data; -namespace Parse.Infrastructure.Control +namespace Parse.Infrastructure.Control; + +public class ParseRelationOperation : IParseFieldOperation { - public class ParseRelationOperation : IParseFieldOperation - { - IList Additions { get; } + IList Additions { get; } - IList Removals { get; } + IList Removals { get; } - IParseObjectClassController ClassController { get; } + IParseObjectClassController ClassController { get; } - ParseRelationOperation(IParseObjectClassController classController) => ClassController = classController; + ParseRelationOperation(IParseObjectClassController classController) => ClassController = classController; - ParseRelationOperation(IParseObjectClassController classController, IEnumerable adds, IEnumerable removes, string targetClassName) : this(classController) - { - TargetClassName = targetClassName; - Additions = new ReadOnlyCollection(adds.ToList()); - Removals = new ReadOnlyCollection(removes.ToList()); - } + ParseRelationOperation(IParseObjectClassController classController, IEnumerable adds, IEnumerable removes, string targetClassName) : this(classController) + { + TargetClassName = targetClassName; + Additions = new ReadOnlyCollection(adds.ToList()); + Removals = new ReadOnlyCollection(removes.ToList()); + } - public ParseRelationOperation(IParseObjectClassController classController, IEnumerable adds, IEnumerable removes) : this(classController) - { - adds ??= new ParseObject[0]; - removes ??= new ParseObject[0]; + public ParseRelationOperation(IParseObjectClassController classController, IEnumerable adds, IEnumerable removes) : this(classController) + { + adds ??= new ParseObject[0]; + removes ??= new ParseObject[0]; - TargetClassName = adds.Concat(removes).Select(entity => entity.ClassName).FirstOrDefault(); - Additions = new ReadOnlyCollection(GetIdsFromObjects(adds).ToList()); - Removals = new ReadOnlyCollection(GetIdsFromObjects(removes).ToList()); - } + TargetClassName = adds.Concat(removes).Select(entity => entity.ClassName).FirstOrDefault(); + Additions = new ReadOnlyCollection(GetIdsFromObjects(adds).ToList()); + Removals = new ReadOnlyCollection(GetIdsFromObjects(removes).ToList()); + } + + public object Encode(IServiceHub serviceHub) + { + List additions = Additions.Select(id => PointerOrLocalIdEncoder.Instance.Encode(ClassController.CreateObjectWithoutData(TargetClassName, id, serviceHub), serviceHub)).ToList(), removals = Removals.Select(id => PointerOrLocalIdEncoder.Instance.Encode(ClassController.CreateObjectWithoutData(TargetClassName, id, serviceHub), serviceHub)).ToList(); - public object Encode(IServiceHub serviceHub) + Dictionary addition = additions.Count == 0 ? default : new Dictionary { - List additions = Additions.Select(id => PointerOrLocalIdEncoder.Instance.Encode(ClassController.CreateObjectWithoutData(TargetClassName, id, serviceHub), serviceHub)).ToList(), removals = Removals.Select(id => PointerOrLocalIdEncoder.Instance.Encode(ClassController.CreateObjectWithoutData(TargetClassName, id, serviceHub), serviceHub)).ToList(); + ["__op"] = "AddRelation", + ["objects"] = additions + }; - Dictionary addition = additions.Count == 0 ? default : new Dictionary - { - ["__op"] = "AddRelation", - ["objects"] = additions - }; + Dictionary removal = removals.Count == 0 ? default : new Dictionary + { + ["__op"] = "RemoveRelation", + ["objects"] = removals + }; - Dictionary removal = removals.Count == 0 ? default : new Dictionary + if (addition is { } && removal is { }) + { + return new Dictionary { - ["__op"] = "RemoveRelation", - ["objects"] = removals + ["__op"] = "Batch", + ["ops"] = new[] { addition, removal } }; - - if (addition is { } && removal is { }) - { - return new Dictionary - { - ["__op"] = "Batch", - ["ops"] = new[] { addition, removal } - }; - } - return addition ?? removal; } + return addition ?? removal; + } - public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) => previous switch + public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) + { + return previous switch { null => this, ParseDeleteOperation { } => throw new InvalidOperationException("You can't modify a relation after deleting it."), @@ -71,8 +73,11 @@ public object Encode(IServiceHub serviceHub) ParseRelationOperation { ClassController: var classController } other => new ParseRelationOperation(classController, Additions.Union(other.Additions.Except(Removals)).ToList(), Removals.Union(other.Removals.Except(Additions)).ToList(), TargetClassName), _ => throw new InvalidOperationException("Operation is invalid after previous operation.") }; + } - public object Apply(object oldValue, string key) => oldValue switch + public object Apply(object oldValue, string key) + { + return oldValue switch { _ when Additions.Count == 0 && Removals.Count == 0 => default, null => ClassController.CreateRelation(null, key, TargetClassName), @@ -80,25 +85,29 @@ public object Encode(IServiceHub serviceHub) ParseRelationBase { } oldRelation => (Relation: oldRelation, oldRelation.TargetClassName = TargetClassName).Relation, _ => throw new InvalidOperationException("Operation is invalid after previous operation.") }; + } - public string TargetClassName { get; } + public string TargetClassName { get; } - IEnumerable GetIdsFromObjects(IEnumerable objects) + public object Value => throw new NotImplementedException(); + + IEnumerable GetIdsFromObjects(IEnumerable objects) + { + foreach (ParseObject entity in objects) { - foreach (ParseObject entity in objects) + if (entity.ObjectId is null) { - if (entity.ObjectId is null) - { - throw new ArgumentException("You can't add an unsaved ParseObject to a relation."); - } - - if (entity.ClassName != TargetClassName) - { - throw new ArgumentException($"Tried to create a ParseRelation with 2 different types: {TargetClassName} and {entity.ClassName}"); - } + throw new ArgumentException("You can't add an unsaved ParseObject to a relation."); } - return objects.Select(entity => entity.ObjectId).Distinct(); + if (entity.ClassName != TargetClassName) + { + throw new ArgumentException($"Tried to create a ParseRelation with 2 different types: {TargetClassName} and {entity.ClassName}"); + } } + + return objects.Select(entity => entity.ObjectId).Distinct(); } + + public IDictionary ConvertToJSON(IServiceHub serviceHub = null) => throw new NotImplementedException(); } diff --git a/Parse/Infrastructure/Control/ParseRemoveOperation.cs b/Parse/Infrastructure/Control/ParseRemoveOperation.cs index b3851844..899c6002 100644 --- a/Parse/Infrastructure/Control/ParseRemoveOperation.cs +++ b/Parse/Infrastructure/Control/ParseRemoveOperation.cs @@ -7,31 +7,52 @@ using Parse.Infrastructure.Data; using Parse.Infrastructure.Utilities; -namespace Parse.Infrastructure.Control -{ - public class ParseRemoveOperation : IParseFieldOperation - { - ReadOnlyCollection Data { get; } +namespace Parse.Infrastructure.Control; - public ParseRemoveOperation(IEnumerable objects) => Data = new ReadOnlyCollection(objects.Distinct().ToList()); +public class ParseRemoveOperation : IParseFieldOperation +{ + // Read-only collection to ensure immutability + ReadOnlyCollection Data { get; } - public object Encode(IServiceHub serviceHub) => new Dictionary - { - ["__op"] = "Remove", - ["objects"] = PointerOrLocalIdEncoder.Instance.Encode(Data, serviceHub) - }; + public ParseRemoveOperation(IEnumerable objects) => + Data = new ReadOnlyCollection(objects.Distinct().ToList()); // Ensure unique elements - public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) => previous switch + public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) + { + return previous switch { null => this, - ParseDeleteOperation _ => previous, - ParseSetOperation setOp => new ParseSetOperation(Apply(Conversion.As>(setOp.Value), default)), - ParseRemoveOperation oldOp => new ParseRemoveOperation(oldOp.Objects.Concat(Data)), + ParseDeleteOperation _ => previous, // Retain delete operation + ParseSetOperation setOp => new ParseSetOperation( + Apply(Conversion.As>(setOp.Value), default)), // Remove items from existing value + ParseRemoveOperation oldOp => new ParseRemoveOperation( + oldOp.Objects.Concat(Data).Distinct()), // Combine unique removals _ => throw new InvalidOperationException("Operation is invalid after previous operation.") }; + } - public object Apply(object oldValue, string key) => oldValue is { } ? Conversion.As>(oldValue).Except(Data, ParseFieldOperations.ParseObjectComparer).ToList() : new List { }; + public object Apply(object oldValue, string key) + { + // Remove the specified objects from the old value + return oldValue is { } + ? Conversion.As>(oldValue).Except(Data, ParseFieldOperations.ParseObjectComparer).ToList() + : new List { }; // Return empty list if no previous value + } + + public IDictionary ConvertToJSON(IServiceHub serviceHub = default) + { + // Convert data to a JSON-compatible structure + var encodedObjects = Data.Select(obj => PointerOrLocalIdEncoder.Instance.Encode(obj, serviceHub)).ToList(); - public IEnumerable Objects => Data; + return new Dictionary + { + ["__op"] = "Remove", // Parse operation type + ["objects"] = encodedObjects + }; } + + public IEnumerable Objects => Data; + + // Implemented Value property to expose the underlying data + public object Value => Data.ToList(); } diff --git a/Parse/Infrastructure/Control/ParseSetOperation.cs b/Parse/Infrastructure/Control/ParseSetOperation.cs index f55f09f9..4a15b367 100644 --- a/Parse/Infrastructure/Control/ParseSetOperation.cs +++ b/Parse/Infrastructure/Control/ParseSetOperation.cs @@ -1,19 +1,57 @@ +using System; +using System.Collections.Generic; using Parse.Abstractions.Infrastructure; using Parse.Abstractions.Infrastructure.Control; using Parse.Infrastructure.Data; -namespace Parse.Infrastructure.Control +namespace Parse.Infrastructure.Control; + +public class ParseSetOperation : IParseFieldOperation { - public class ParseSetOperation : IParseFieldOperation + public ParseSetOperation(object value) + { + Value = value; + } + + // Replace Encode with ConvertToJSON + public IDictionary ConvertToJSON(IServiceHub serviceHub = default) { - public ParseSetOperation(object value) => Value = value; + if (serviceHub == null) + { + throw new InvalidOperationException("ServiceHub is required to encode the value."); + } - public object Encode(IServiceHub serviceHub) => PointerOrLocalIdEncoder.Instance.Encode(Value, serviceHub); + var encodedValue = PointerOrLocalIdEncoder.Instance.Encode(Value, serviceHub); - public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) => this; + // For simple values, return them directly (avoid unnecessary __op) + if (Value != null && (Value.GetType().IsPrimitive || Value is string)) + { + return new Dictionary { ["value"] = Value }; + } - public object Apply(object oldValue, string key) => Value; + // If the encoded value is a dictionary, return it directly + if (encodedValue is IDictionary dictionary) + { + return dictionary; + } - public object Value { get; private set; } + // Default behavior for unsupported types + throw new ArgumentException($"Unsupported type for encoding: {Value?.GetType()?.FullName}"); } + + + + public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) + { + // Set operation always overrides previous operations + return this; + } + + public object Apply(object oldValue, string key) + { + // Set operation always sets the field to the specified value + return Value; + } + + public object Value { get; private set; } } diff --git a/Parse/Infrastructure/Data/NoObjectsEncoder.cs b/Parse/Infrastructure/Data/NoObjectsEncoder.cs index b49f6bf2..1aaa2a77 100644 --- a/Parse/Infrastructure/Data/NoObjectsEncoder.cs +++ b/Parse/Infrastructure/Data/NoObjectsEncoder.cs @@ -1,18 +1,20 @@ using System; using System.Collections.Generic; -namespace Parse.Infrastructure.Data +namespace Parse.Infrastructure.Data; + +// This class isn't really a Singleton, but since it has no state, it's more efficient to get the default instance. + +/// +/// A that throws an exception if it attempts to encode +/// a +/// +public class NoObjectsEncoder : ParseDataEncoder { - // This class isn't really a Singleton, but since it has no state, it's more efficient to get the default instance. + public static NoObjectsEncoder Instance { get; } = new NoObjectsEncoder(); - /// - /// A that throws an exception if it attempts to encode - /// a - /// - public class NoObjectsEncoder : ParseDataEncoder + protected override IDictionary EncodeObject(ParseObject value) { - public static NoObjectsEncoder Instance { get; } = new NoObjectsEncoder(); - - protected override IDictionary EncodeObject(ParseObject value) => throw new ArgumentException("ParseObjects not allowed here."); + throw new ArgumentException("ParseObjects not allowed here."); } } diff --git a/Parse/Infrastructure/Data/ParseDataDecoder.cs b/Parse/Infrastructure/Data/ParseDataDecoder.cs index fa38b49f..6eb5c2f0 100644 --- a/Parse/Infrastructure/Data/ParseDataDecoder.cs +++ b/Parse/Infrastructure/Data/ParseDataDecoder.cs @@ -8,41 +8,75 @@ using Parse.Infrastructure.Control; using Parse.Infrastructure.Utilities; -namespace Parse.Infrastructure.Data +namespace Parse.Infrastructure.Data; + +public class ParseDataDecoder : IParseDataDecoder { - public class ParseDataDecoder : IParseDataDecoder + IParseObjectClassController ClassController { get; } + + public ParseDataDecoder(IParseObjectClassController classController) => ClassController = classController; + + static string[] Types { get; } = { "Date", "Bytes", "Pointer", "File", "GeoPoint", "Object", "Relation" }; + + public object Decode(object data, IServiceHub serviceHub) { - // Prevent default constructor. + return data switch + { + null => default, + IDictionary { } dictionary when dictionary.ContainsKey("__op") => ParseFieldOperations.Decode(dictionary), + + IDictionary { } dictionary when dictionary.TryGetValue("__type", out var type) && Types.Contains(type) => type switch + { + "Date" => ParseDate(dictionary.TryGetValue("iso", out var iso) ? iso as string : throw new KeyNotFoundException("Missing 'iso' for Date type")), + + "Bytes" => Convert.FromBase64String(dictionary.TryGetValue("base64", out var base64) ? base64 as string : throw new KeyNotFoundException("Missing 'base64' for Bytes type")), + + "Pointer" => DecodePointer( + dictionary.TryGetValue("className", out var className) ? className as string : throw new KeyNotFoundException("Missing 'className' for Pointer type"), + dictionary.TryGetValue("objectId", out var objectId) ? objectId as string : throw new KeyNotFoundException("Missing 'objectId' for Pointer type"), + serviceHub), - IParseObjectClassController ClassController { get; } + "File" => new ParseFile( + dictionary.TryGetValue("name", out var name) ? name as string : throw new KeyNotFoundException("Missing 'name' for File type"), + new Uri(dictionary.TryGetValue("url", out var url) ? url as string : throw new KeyNotFoundException("Missing 'url' for File type"))), - public ParseDataDecoder(IParseObjectClassController classController) => ClassController = classController; + "GeoPoint" => new ParseGeoPoint( + Conversion.To(dictionary.TryGetValue("latitude", out var latitude) ? latitude : throw new KeyNotFoundException("Missing 'latitude' for GeoPoint type")), + Conversion.To(dictionary.TryGetValue("longitude", out var longitude) ? longitude : throw new KeyNotFoundException("Missing 'longitude' for GeoPoint type"))), - static string[] Types { get; } = { "Date", "Bytes", "Pointer", "File", "GeoPoint", "Object", "Relation" }; + "Object" => ClassController.GenerateObjectFromState( + ParseObjectCoder.Instance.Decode(dictionary, this, serviceHub), + dictionary.TryGetValue("className", out var objClassName) ? objClassName as string : throw new KeyNotFoundException("Missing 'className' for Object type"), + serviceHub), - public object Decode(object data, IServiceHub serviceHub) => data switch + "Relation" => serviceHub.CreateRelation(null, null, dictionary.TryGetValue("className", out var relClassName) ? relClassName as string : throw new KeyNotFoundException("Missing 'className' for Relation type")), + _ => throw new NotSupportedException($"Unsupported Parse type '{type}' encountered") + }, + + IDictionary { } dictionary => dictionary.ToDictionary(pair => pair.Key, pair => Decode(pair.Value, serviceHub)), + IList { } list => list.Select(item => Decode(item, serviceHub)).ToList(), + _ => data + }; + + } + + protected virtual object DecodePointer(string className, string objectId, IServiceHub serviceHub) => + ClassController.CreateObjectWithoutData(className, objectId, serviceHub); + + public static DateTime? ParseDate(string input) + { + if (string.IsNullOrEmpty(input)) + return null; + + foreach (var format in ParseClient.DateFormatStrings) { - null => default, - IDictionary { } dictionary when dictionary.ContainsKey("__op") => ParseFieldOperations.Decode(dictionary), - IDictionary { } dictionary when dictionary.TryGetValue("__type", out object type) && Types.Contains(type) => type switch + if (DateTime.TryParseExact(input, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsedDate)) { - "Date" => ParseDate(dictionary["iso"] as string), - "Bytes" => Convert.FromBase64String(dictionary["base64"] as string), - "Pointer" => DecodePointer(dictionary["className"] as string, dictionary["objectId"] as string, serviceHub), - "File" => new ParseFile(dictionary["name"] as string, new Uri(dictionary["url"] as string)), - "GeoPoint" => new ParseGeoPoint(Conversion.To(dictionary["latitude"]), Conversion.To(dictionary["longitude"])), - "Object" => ClassController.GenerateObjectFromState(ParseObjectCoder.Instance.Decode(dictionary, this, serviceHub), dictionary["className"] as string, serviceHub), - "Relation" => serviceHub.CreateRelation(null, null, dictionary["className"] as string) - }, - IDictionary { } dictionary => dictionary.ToDictionary(pair => pair.Key, pair => Decode(pair.Value, serviceHub)), - IList { } list => list.Select(item => Decode(item, serviceHub)).ToList(), - _ => data - }; - - protected virtual object DecodePointer(string className, string objectId, IServiceHub serviceHub) => ClassController.CreateObjectWithoutData(className, objectId, serviceHub); - - // TODO(hallucinogen): Figure out if we should be more flexible with the date formats we accept. - - public static DateTime ParseDate(string input) => DateTime.ParseExact(input, ParseClient.DateFormatStrings, CultureInfo.InvariantCulture, DateTimeStyles.None); + return parsedDate; + } + } + + return null; // Return null if no formats match } + } diff --git a/Parse/Infrastructure/Data/ParseDataEncoder.cs b/Parse/Infrastructure/Data/ParseDataEncoder.cs index 7fe99407..ef39b50f 100644 --- a/Parse/Infrastructure/Data/ParseDataEncoder.cs +++ b/Parse/Infrastructure/Data/ParseDataEncoder.cs @@ -1,71 +1,249 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Linq; using Parse.Abstractions.Infrastructure; using Parse.Abstractions.Infrastructure.Control; +using Parse.Infrastructure.Control; using Parse.Infrastructure.Utilities; -namespace Parse.Infrastructure.Data +namespace Parse.Infrastructure.Data; + +/// +/// A ParseEncoder can be used to transform objects such as into JSON +/// data structures. +/// +/// +public abstract class ParseDataEncoder { + private static readonly string[] SupportedDateFormats = ParseClient.DateFormatStrings; + + public static bool Validate(object value) + { + return value is null || + value.GetType().IsPrimitive|| + value is string || + value is ParseObject || + value is ParseACL || + value is ParseFile || + value is ParseGeoPoint || + value is ParseRelationBase || + value is DateTime || + value is byte[] || + value is Array || + Conversion.As>(value) is { } || + Conversion.As>(value) is { } || + Conversion.As>(value) is { } || + Conversion.As>(value) is { } || + Conversion.As>(value) is { } || + Conversion.As>(value) is { } || + Conversion.As>(value) is { } || + Conversion.As>(value) is { }; + } + /// - /// A ParseEncoder can be used to transform objects such as into JSON - /// data structures. + /// Encodes a given value into a JSON-compatible structure. /// - /// - public abstract class ParseDataEncoder + public object Encode(object value, IServiceHub serviceHub) { - public static bool Validate(object value) => value is null || value.GetType().IsPrimitive || value is string || value is ParseObject || value is ParseACL || value is ParseFile || value is ParseGeoPoint || value is ParseRelationBase || value is DateTime || value is byte[] || Conversion.As>(value) is { } || Conversion.As>(value) is { }; - - // If this object has a special encoding, encode it and return the encoded object. Otherwise, just return the original object. + if (value == null) + return null; - public object Encode(object value, IServiceHub serviceHub) => value switch + return value switch { - DateTime { } date => new Dictionary - { - ["iso"] = date.ToString(ParseClient.DateFormatStrings.First(), CultureInfo.InvariantCulture), - ["__type"] = "Date" - }, - byte[] { } bytes => new Dictionary - { - ["__type"] = "Bytes", - ["base64"] = Convert.ToBase64String(bytes) - }, - ParseObject { } entity => EncodeObject(entity), - IJsonConvertible { } jsonConvertible => jsonConvertible.ConvertToJSON(), - { } when Conversion.As>(value) is { } dictionary => dictionary.ToDictionary(pair => pair.Key, pair => Encode(pair.Value, serviceHub)), - { } when Conversion.As>(value) is { } list => EncodeList(list, serviceHub), - - // TODO (hallucinogen): convert IParseFieldOperation to IJsonConvertible - - IParseFieldOperation { } fieldOperation => fieldOperation.Encode(serviceHub), - _ => value + // DateTime encoding + DateTime date => EncodeDate(date), + + // Byte array encoding + byte[] bytes => EncodeBytes(bytes), + + // ParseObject encoding + ParseObject entity => EncodeObject(entity), + + // JSON-convertible types + IJsonConvertible jsonConvertible => jsonConvertible.ConvertToJSON(serviceHub), + + // Dictionary encoding + IDictionary dictionary => EncodeDictionary(dictionary, serviceHub), + IDictionary dictionary => EncodeDictionary(dictionary, serviceHub), + IDictionary dictionary => EncodeDictionary(dictionary, serviceHub), + IDictionary dictionary => EncodeDictionary(dictionary, serviceHub), + IDictionary dictionary => EncodeDictionary(dictionary, serviceHub), + IDictionary dictionary => EncodeDictionary(dictionary, serviceHub), + + // List or array encoding + IEnumerable list => EncodeList(list, serviceHub), + Array array => EncodeList(array.Cast(), serviceHub), + + // Parse field operations + + + // Primitive types or strings + _ when value.GetType().IsPrimitive || value is string => value, + + // Unsupported types + _ => throw new ArgumentException($"Unsupported type for encoding: {value?.GetType()?.FullName}") }; + } + + + /// + /// Encodes a ParseObject into a JSON-compatible structure. + /// + protected abstract IDictionary EncodeObject(ParseObject value); + + /// + /// Encodes a DateTime into a JSON-compatible structure. + /// + private static IDictionary EncodeDate(DateTime date) + { + return new Dictionary + { - protected abstract IDictionary EncodeObject(ParseObject value); + ["iso"] = date.ToString(SupportedDateFormats.First(), CultureInfo.InvariantCulture), + ["__type"] = "Date" + }; + } - object EncodeList(IList list, IServiceHub serviceHub) + /// + /// Encodes a byte array into a JSON-compatible structure. + /// + private static IDictionary EncodeBytes(byte[] bytes) + { + return new Dictionary { - List encoded = new List { }; + ["__type"] = "Bytes", + ["base64"] = Convert.ToBase64String(bytes) + }; + } - // We need to explicitly cast `list` to `List` rather than `IList` because IL2CPP is stricter than the usual Unity AOT compiler pipeline. - if (ParseClient.IL2CPPCompiled && list.GetType().IsArray) + //// + /// Encodes a dictionary into a JSON-compatible structure. + /// + private object EncodeDictionary(IDictionary dictionary, IServiceHub serviceHub) + { + var encodedDictionary = new Dictionary(); + if (dictionary.Count<1) + { + return encodedDictionary; + } + foreach (var pair in dictionary) + { + // Check if the value is a Dictionary + if (pair.Value is IDictionary stringDictionary) { - list = new List(list); + // If the value is a Dictionary, handle it separately + encodedDictionary[pair.Key] = stringDictionary.ToDictionary(k => k.Key, v => (object) v.Value); } + else + { + // Handle other types by encoding them recursively + encodedDictionary[pair.Key] = Encode(pair.Value, serviceHub); + } + } + + return encodedDictionary; + } - foreach (object item in list) + + // Add a specialized method to handle string-only dictionaries + private object EncodeDictionary(IDictionary dictionary, IServiceHub serviceHub) + { + + return dictionary.ToDictionary( + pair => pair.Key, + pair => Encode(pair.Value, serviceHub) // Encode string values as object + ); + } + + // Add a specialized method to handle int-only dictionaries + private object EncodeDictionary(IDictionary dictionary, IServiceHub serviceHub) + { + + + return dictionary.ToDictionary( + pair => pair.Key, + pair => Encode(pair.Value, serviceHub) // Encode int values as object + ); + } + + // Add a specialized method to handle long-only dictionaries + private object EncodeDictionary(IDictionary dictionary, IServiceHub serviceHub) + { + + + return dictionary.ToDictionary( + pair => pair.Key, + pair => Encode(pair.Value, serviceHub) // Encode long values as object + ); + } + + // Add a specialized method to handle float-only dictionaries + private object EncodeDictionary(IDictionary dictionary, IServiceHub serviceHub) + { + + + return dictionary.ToDictionary( + pair => pair.Key, + pair => Encode(pair.Value, serviceHub) // Encode float values as object + ); + } + + // Add a specialized method to handle double-only dictionaries + private object EncodeDictionary(IDictionary dictionary, IServiceHub serviceHub) + { + + + return dictionary.ToDictionary( + pair => pair.Key, + pair => Encode(pair.Value, serviceHub) // Encode double values as object + ); + } + + + + /// + /// Encodes a list into a JSON-compatible structure. + /// + private object EncodeList(IEnumerable list, IServiceHub serviceHub) + { + + + List encoded = new(); + foreach (var item in list) + { + if (item == null) { - if (!Validate(item)) - { - throw new ArgumentException("Invalid type for value in an array"); - } + encoded.Add(null); + continue; + } - encoded.Add(Encode(item, serviceHub)); + if (!Validate(item)) + { + throw new ArgumentException($"Invalid type for value in list: {item?.GetType().FullName}"); } - return encoded; + encoded.Add(Encode(item, serviceHub)); + } + + return encoded; + } + + + + + /// + /// Encodes a field operation into a JSON-compatible structure. + /// + private object EncodeFieldOperation(IParseFieldOperation fieldOperation, IServiceHub serviceHub) + { + if (fieldOperation is IJsonConvertible jsonConvertible) + { + return jsonConvertible.ConvertToJSON(); } + + throw new InvalidOperationException($"Cannot encode field operation of type {fieldOperation.GetType().Name}."); } } diff --git a/Parse/Infrastructure/Data/ParseObjectCoder.cs b/Parse/Infrastructure/Data/ParseObjectCoder.cs index 9475f181..04a58d1d 100644 --- a/Parse/Infrastructure/Data/ParseObjectCoder.cs +++ b/Parse/Infrastructure/Data/ParseObjectCoder.cs @@ -1,86 +1,139 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using Parse.Abstractions.Infrastructure; using Parse.Abstractions.Infrastructure.Control; using Parse.Abstractions.Infrastructure.Data; using Parse.Abstractions.Platform.Objects; using Parse.Platform.Objects; -namespace Parse.Infrastructure.Data -{ - // TODO: (richardross) refactor entire parse coder interfaces. +namespace Parse.Infrastructure.Data; - public class ParseObjectCoder - { - public static ParseObjectCoder Instance { get; } = new ParseObjectCoder { }; +// TODO: (richardross) refactor entire parse coder interfaces. +// Done: (YB) though, I wonder why Encode is never used in the ParseObjectCoder class. Might update if I find a use case. +//Got it now. The Encode method is used in ParseObjectController.cs - // Prevent default constructor. - ParseObjectCoder() { } +/// +/// Provides methods to encode and decode Parse objects. +/// +public class ParseObjectCoder +{ + /// + /// Gets the singleton instance of the ParseObjectCoder. + /// + public static ParseObjectCoder Instance { get; } = new ParseObjectCoder(); + + // Private constructor to prevent external instantiation + private ParseObjectCoder() { } + + /// + /// Encodes the object state and operations using the provided encoder. + /// + public IDictionary Encode( + T state, + IDictionary operations, + ParseDataEncoder encoder, + IServiceHub serviceHub + ) where T : IObjectState + { + var result = new Dictionary(); - public IDictionary Encode(T state, IDictionary operations, ParseDataEncoder encoder, IServiceHub serviceHub) where T : IObjectState + foreach (var pair in operations) { - Dictionary result = new Dictionary { }; - foreach (KeyValuePair pair in operations) - { - // Serialize the data - IParseFieldOperation operation = pair.Value; + var operation = pair.Value; + result[pair.Key] = encoder.Encode(operation, serviceHub); + } - result[pair.Key] = encoder.Encode(operation, serviceHub); - } + return result; + } + /// + /// Decodes raw server data into a mutable object state. + /// + public IObjectState Decode(IDictionary data, IParseDataDecoder decoder, IServiceHub serviceHub) + { - return result; - } + var serverData = new Dictionary(); + var mutableData = new Dictionary(data); - public IObjectState Decode(IDictionary data, IParseDataDecoder decoder, IServiceHub serviceHub) - { - IDictionary serverData = new Dictionary { }, mutableData = new Dictionary(data); + // Extract key properties (existing logic) + var objectId = Extract(mutableData, "objectId", obj => obj as string); + var email = Extract(mutableData, "email", obj => obj as string); + var username = Extract(mutableData, "username", obj => obj as string); + var sessionToken = Extract(mutableData, "sessionToken", obj => obj as string); + var error = Extract(mutableData, "error", obj => obj as string); + var code = Extract(mutableData, "code", obj => Convert.ToInt32(obj)); + var emailVerified = Extract(mutableData, "emailVerified", obj => obj is bool value && value); - string objectId = Extract(mutableData, "objectId", (obj) => obj as string); - DateTime? createdAt = Extract(mutableData, "createdAt", (obj) => ParseDataDecoder.ParseDate(obj as string)), updatedAt = Extract(mutableData, "updatedAt", (obj) => ParseDataDecoder.ParseDate(obj as string)); + var createdAt = Extract(mutableData, "createdAt", obj => ParseDataDecoder.ParseDate(obj as string)); + var updatedAt = Extract(mutableData, "updatedAt", obj => ParseDataDecoder.ParseDate(obj as string)) ?? createdAt; - if (mutableData.ContainsKey("ACL")) + // Handle ACL extraction + var acl = Extract(mutableData, "ACL", obj => + { + if (obj is IDictionary aclData) { - serverData["ACL"] = Extract(mutableData, "ACL", (obj) => new ParseACL(obj as IDictionary)); + return new ParseACL(aclData); // Return ParseACL if the format is correct } - if (createdAt != null && updatedAt == null) - { - updatedAt = createdAt; - } + return null; // If ACL is missing or in an incorrect format, return null + }); - // Bring in the new server data. + if (acl != null) + { + serverData["ACL"] = acl; // Add the decoded ACL back to serverData + } - foreach (KeyValuePair pair in mutableData) - { - if (pair.Key == "__type" || pair.Key == "className") - { - continue; - } - serverData[pair.Key] = decoder.Decode(pair.Value, serviceHub); - } + // Decode remaining fields + foreach (var pair in mutableData) + { + if (pair.Key == "__type" || pair.Key == "className") + continue; - return new MutableObjectState - { - ObjectId = objectId, - CreatedAt = createdAt, - UpdatedAt = updatedAt, - ServerData = serverData - }; + serverData[pair.Key] = decoder.Decode(pair.Value, serviceHub); } - T Extract(IDictionary data, string key, Func action) + // Populate server data with primary properties + PopulateServerData(serverData, "username", username); + PopulateServerData(serverData, "email", email); + PopulateServerData(serverData, "sessionToken", sessionToken); + PopulateServerData(serverData, "error", error); + PopulateServerData(serverData, "code", code); + PopulateServerData(serverData, "emailVerified", emailVerified); + + return new MutableObjectState { - T result = default; + ObjectId = objectId, + CreatedAt = createdAt, + UpdatedAt = updatedAt, + ServerData = serverData, + SessionToken = sessionToken + }; + } - if (data.ContainsKey(key)) - { - result = action(data[key]); - data.Remove(key); - } + /// + /// Extracts a value from a dictionary and removes the key. + /// + private static T Extract(IDictionary data, string key, Func action) +{ + if (data.TryGetValue(key, out var value)) + { + data.Remove(key); + return action(value); + } - return result; - } + return default; +} + +/// +/// Populates server data with a value if not already present. +/// +private static void PopulateServerData(IDictionary serverData, string key, object value) +{ + if (value != null && !serverData.ContainsKey(key)) + { + serverData[key] = value; } } +} diff --git a/Parse/Infrastructure/Data/PointerOrLocalIdEncoder.cs b/Parse/Infrastructure/Data/PointerOrLocalIdEncoder.cs index bd114567..997ad585 100644 --- a/Parse/Infrastructure/Data/PointerOrLocalIdEncoder.cs +++ b/Parse/Infrastructure/Data/PointerOrLocalIdEncoder.cs @@ -1,30 +1,29 @@ using System; using System.Collections.Generic; -namespace Parse.Infrastructure.Data +namespace Parse.Infrastructure.Data; + +/// +/// A that encodes as pointers. If the object does not have an , uses a local id. +/// +public class PointerOrLocalIdEncoder : ParseDataEncoder { - /// - /// A that encodes as pointers. If the object does not have an , uses a local id. - /// - public class PointerOrLocalIdEncoder : ParseDataEncoder - { - public static PointerOrLocalIdEncoder Instance { get; } = new PointerOrLocalIdEncoder { }; + public static PointerOrLocalIdEncoder Instance { get; } = new PointerOrLocalIdEncoder { }; - protected override IDictionary EncodeObject(ParseObject value) + protected override IDictionary EncodeObject(ParseObject value) + { + if (value.ObjectId is null) { - if (value.ObjectId is null) - { - // TODO (hallucinogen): handle local id. For now we throw. + // TODO (hallucinogen): handle local id. For now we throw. - throw new InvalidOperationException("Cannot create a pointer to an object without an objectId."); - } - - return new Dictionary - { - ["__type"] = "Pointer", - ["className"] = value.ClassName, - ["objectId"] = value.ObjectId - }; + throw new InvalidOperationException("Cannot create a pointer to an object without an objectId."); } + + return new Dictionary + { + ["__type"] = "Pointer", + ["className"] = value.ClassName, + ["objectId"] = value.ObjectId + }; } } diff --git a/Parse/Infrastructure/DataTransferLevel.cs b/Parse/Infrastructure/DataTransferLevel.cs index 6874e1d0..e40b766d 100644 --- a/Parse/Infrastructure/DataTransferLevel.cs +++ b/Parse/Infrastructure/DataTransferLevel.cs @@ -1,16 +1,15 @@ using System; using Parse.Abstractions.Infrastructure; -namespace Parse.Infrastructure +namespace Parse.Infrastructure; + +/// +/// Represents upload progress. +/// +public class DataTransferLevel : EventArgs, IDataTransferLevel { /// - /// Represents upload progress. + /// Gets the progress (a number between 0.0 and 1.0) of an upload or download. /// - public class DataTransferLevel : EventArgs, IDataTransferLevel - { - /// - /// Gets the progress (a number between 0.0 and 1.0) of an upload or download. - /// - public double Amount { get; set; } - } + public double Amount { get; set; } } diff --git a/Parse/Infrastructure/EnvironmentData.cs b/Parse/Infrastructure/EnvironmentData.cs index 3ec2061d..d5d3968a 100644 --- a/Parse/Infrastructure/EnvironmentData.cs +++ b/Parse/Infrastructure/EnvironmentData.cs @@ -2,36 +2,35 @@ using System.Runtime.InteropServices; using Parse.Abstractions.Infrastructure; -namespace Parse.Infrastructure +namespace Parse.Infrastructure; + +/// +/// Inferred data about the environment in which parse is operating. +/// +public class EnvironmentData : IEnvironmentData { /// - /// Inferred data about the environment in which parse is operating. + /// A instance that the Parse SDK will attempt to generate from environment metadata it should be able to access. /// - public class EnvironmentData : IEnvironmentData + public static EnvironmentData Inferred => new EnvironmentData { - /// - /// A instance that the Parse SDK will attempt to generate from environment metadata it should be able to access. - /// - public static EnvironmentData Inferred => new EnvironmentData - { - TimeZone = TimeZoneInfo.Local.StandardName, - OSVersion = RuntimeInformation.OSDescription ?? Environment.OSVersion.ToString(), - Platform = RuntimeInformation.FrameworkDescription ?? ".NET" - }; + TimeZone = TimeZoneInfo.Local.StandardName, + OSVersion = RuntimeInformation.OSDescription ?? Environment.OSVersion.ToString(), + Platform = RuntimeInformation.FrameworkDescription ?? ".NET" + }; - /// - /// The active time zone for the app and/or system. - /// - public string TimeZone { get; set; } + /// + /// The active time zone for the app and/or system. + /// + public string TimeZone { get; set; } - /// - /// The host operating system version of the platform the host application is operating in. - /// - public string OSVersion { get; set; } + /// + /// The host operating system version of the platform the host application is operating in. + /// + public string OSVersion { get; set; } - /// - /// The target platform the app is running on. Defaults to .NET. - /// - public string Platform { get; set; } - } + /// + /// The target platform the app is running on. Defaults to .NET. + /// + public string Platform { get; set; } } diff --git a/Parse/Infrastructure/Execution/ParseCommand.cs b/Parse/Infrastructure/Execution/ParseCommand.cs index 4e2148aa..f2db1772 100644 --- a/Parse/Infrastructure/Execution/ParseCommand.cs +++ b/Parse/Infrastructure/Execution/ParseCommand.cs @@ -5,50 +5,60 @@ using System.Text; using Parse.Infrastructure.Utilities; -namespace Parse.Infrastructure.Execution +namespace Parse.Infrastructure.Execution; + +/// +/// ParseCommand is an with pre-populated +/// headers. +/// +public class ParseCommand : WebRequest { - /// - /// ParseCommand is an with pre-populated - /// headers. - /// - public class ParseCommand : WebRequest - { - public IDictionary DataObject { get; private set; } + public IDictionary DataObject { get; private set; } - public override Stream Data + public override Stream Data + { + get { - get => base.Data ??= DataObject is { } ? new MemoryStream(Encoding.UTF8.GetBytes(JsonUtilities.Encode(DataObject))) : default; - set => base.Data = value; + if (DataObject is { }) + return base.Data ??= (new MemoryStream(Encoding.UTF8.GetBytes(JsonUtilities.Encode(DataObject)))); + else + return base.Data ??= default; } - public ParseCommand(string relativeUri, string method, string sessionToken = null, IList> headers = null, IDictionary data = null) : this(relativeUri: relativeUri, method: method, sessionToken: sessionToken, headers: headers, stream: null, contentType: data != null ? "application/json" : null) => DataObject = data; + set => base.Data = value; + } + + public ParseCommand(string relativeUri, string method, string sessionToken = null, IList> headers = null, IDictionary data = null) : this( + relativeUri: relativeUri, method: method, sessionToken: sessionToken, headers: headers, stream: null, contentType: data != null ? "application/json" : null) + { + DataObject = data; + } + + public ParseCommand(string relativeUri, string method, string sessionToken = null, IList> headers = null, Stream stream = null, string contentType = null) + { + Path = relativeUri; + Method = method; + Data = stream; + Headers = new List>(headers ?? Enumerable.Empty>()); - public ParseCommand(string relativeUri, string method, string sessionToken = null, IList> headers = null, Stream stream = null, string contentType = null) + if (!String.IsNullOrEmpty(sessionToken)) { - Path = relativeUri; - Method = method; - Data = stream; - Headers = new List>(headers ?? Enumerable.Empty>()); - - if (!String.IsNullOrEmpty(sessionToken)) - { - Headers.Add(new KeyValuePair("X-Parse-Session-Token", sessionToken)); - } - - if (!String.IsNullOrEmpty(contentType)) - { - Headers.Add(new KeyValuePair("Content-Type", contentType)); - } + Headers.Add(new KeyValuePair("X-Parse-Session-Token", sessionToken)); } - public ParseCommand(ParseCommand other) + if (!String.IsNullOrEmpty(contentType)) { - Resource = other.Resource; - Path = other.Path; - Method = other.Method; - DataObject = other.DataObject; - Headers = new List>(other.Headers); - Data = other.Data; + Headers.Add(new KeyValuePair("Content-Type", contentType)); } } + + public ParseCommand(ParseCommand other) + { + Resource = other.Resource; + Path = other.Path; + Method = other.Method; + DataObject = other.DataObject; + Headers = new List>(other.Headers); + Data = other.Data; + } } diff --git a/Parse/Infrastructure/Execution/ParseCommandRunner.cs b/Parse/Infrastructure/Execution/ParseCommandRunner.cs index 66e18220..630ed81a 100644 --- a/Parse/Infrastructure/Execution/ParseCommandRunner.cs +++ b/Parse/Infrastructure/Execution/ParseCommandRunner.cs @@ -9,150 +9,199 @@ using Parse.Abstractions.Platform.Users; using Parse.Infrastructure.Utilities; -namespace Parse.Infrastructure.Execution +namespace Parse.Infrastructure.Execution; + +/// +/// The command runner for all SDK operations that need to interact with the targeted deployment of Parse Server. +/// +public class ParseCommandRunner : IParseCommandRunner { + IWebClient WebClient { get; } + + IParseInstallationController InstallationController { get; } + + IMetadataController MetadataController { get; } + + IServerConnectionData ServerConnectionData { get; } + + Lazy UserController { get; } + + IWebClient GetWebClient() + { + return WebClient; + } + /// - /// The command runner for all SDK operations that need to interact with the targeted deployment of Parse Server. + /// Creates a new Parse SDK command runner. /// - public class ParseCommandRunner : IParseCommandRunner + /// The implementation instance to use. + /// The implementation instance to use. + public ParseCommandRunner(IWebClient webClient, IParseInstallationController installationController, IMetadataController metadataController, IServerConnectionData serverConnectionData, Lazy userController) { - IWebClient WebClient { get; } - - IParseInstallationController InstallationController { get; } + WebClient = webClient; + InstallationController = installationController; + MetadataController = metadataController; + ServerConnectionData = serverConnectionData; + UserController = userController; + } + /// + /// Runs a specified . + /// + /// The to run. + /// An instance to push upload progress data to. + /// An instance to push download progress data to. + /// An asynchronous operation cancellation token that dictates if and when the operation should be cancelled. + /// + public async Task>> RunCommandAsync( + ParseCommand command, + IProgress uploadProgress = null, + IProgress downloadProgress = null, + CancellationToken cancellationToken = default) + { + // Prepare the command + var preparedCommand = await PrepareCommand(command).ConfigureAwait(false); - IMetadataController MetadataController { get; } + // Execute the command + var response = await GetWebClient() + .ExecuteAsync(preparedCommand, uploadProgress, downloadProgress, cancellationToken) + .ConfigureAwait(false); - IServerConnectionData ServerConnectionData { get; } + cancellationToken.ThrowIfCancellationRequested(); - Lazy UserController { get; } + IDictionary contentJson = null; + // Extract response + var statusCode = response.Item1; + var content = response.Item2; + var responseCode = (int) statusCode; - IWebClient GetWebClient() => WebClient; - /// - /// Creates a new Parse SDK command runner. - /// - /// The implementation instance to use. - /// The implementation instance to use. - public ParseCommandRunner(IWebClient webClient, IParseInstallationController installationController, IMetadataController metadataController, IServerConnectionData serverConnectionData, Lazy userController) + if (responseCode == 200) { - WebClient = webClient; - InstallationController = installationController; - MetadataController = metadataController; - ServerConnectionData = serverConnectionData; - UserController = userController; + } - - /// - /// Runs a specified . - /// - /// The to run. - /// An instance to push upload progress data to. - /// An instance to push download progress data to. - /// An asynchronous operation cancellation token that dictates if and when the operation should be cancelled. - /// - public Task>> RunCommandAsync(ParseCommand command, IProgress uploadProgress = null, IProgress downloadProgress = null, CancellationToken cancellationToken = default) => PrepareCommand(command).ContinueWith(commandTask => GetWebClient().ExecuteAsync(commandTask.Result, uploadProgress, downloadProgress, cancellationToken).OnSuccess(task => + else if (responseCode == 201) { - cancellationToken.ThrowIfCancellationRequested(); + } + else if (responseCode == 404) + { + throw new ParseFailureException(ParseFailureException.ErrorCode.ERROR404, "Error 404"); + } + if (responseCode == 410) + { + return new Tuple>( + HttpStatusCode.Gone, + new Dictionary + { + { "error", "Page is no longer valid" } + } + ); + } + if (responseCode >= 500) + { + // Server error, return InternalServerError + throw new ParseFailureException(ParseFailureException.ErrorCode.InternalServerError, content); + } + if (string.IsNullOrEmpty(content)) + { + return new Tuple>(statusCode, null); + } + //else if(responseCode == ) - Tuple response = task.Result; - string content = response.Item2; - int responseCode = (int) response.Item1; + // Try to parse the content + try + { + contentJson = content.StartsWith("[") + ? new Dictionary { ["results"] = JsonUtilities.Parse(content) } + : JsonUtilities.Parse(content) as IDictionary; - if (responseCode >= 500) + // Add className if "username" exists + if (contentJson?.ContainsKey("username") == true) { - // Server error, return InternalServerError. - - throw new ParseFailureException(ParseFailureException.ErrorCode.InternalServerError, response.Item2); + contentJson["className"] = "_User"; } - else if (content is { }) - { - IDictionary contentJson = default; - - try - { - // TODO: Newer versions of Parse Server send the failure results back as HTML. - - contentJson = content.StartsWith("[") ? new Dictionary { ["results"] = JsonUtilities.Parse(content) } : JsonUtilities.Parse(content) as IDictionary; - } - catch (Exception e) + } + catch (Exception ex) + { + return new Tuple>( + HttpStatusCode.BadRequest, + new Dictionary { - throw new ParseFailureException(ParseFailureException.ErrorCode.OtherCause, "Invalid or alternatively-formatted response recieved from server.", e); + { "error", "Invalid or alternatively-formatted response received from server." }, + { "exception", ex.Message } } + ); + } - if (responseCode < 200 || responseCode > 299) - { - throw new ParseFailureException(contentJson.ContainsKey("code") ? (ParseFailureException.ErrorCode) (long) contentJson["code"] : ParseFailureException.ErrorCode.OtherCause, contentJson.ContainsKey("error") ? contentJson["error"] as string : content); - } - return new Tuple>(response.Item1, contentJson); - } - return new Tuple>(response.Item1, null); - })).Unwrap(); + // Return successful response + return new Tuple>(statusCode, contentJson); + } - Task PrepareCommand(ParseCommand command) + async Task PrepareCommand(ParseCommand command) + { + ParseCommand newCommand = new ParseCommand(command) { - ParseCommand newCommand = new ParseCommand(command) - { - Resource = ServerConnectionData.ServerURI - }; - - Task installationIdFetchTask = InstallationController.GetAsync().ContinueWith(task => - { - lock (newCommand.Headers) - { - newCommand.Headers.Add(new KeyValuePair("X-Parse-Installation-Id", task.Result.ToString())); - } + Resource = ServerConnectionData.ServerURI + }; - return newCommand; - }); + // Fetch Installation ID and add it to the headers + var installationId = await InstallationController.GetAsync(); + lock (newCommand.Headers) + { + newCommand.Headers.Add(new KeyValuePair("X-Parse-Installation-Id", installationId.ToString())); + } - // Locks needed due to installationFetchTask continuation newCommand.Headers.Add-call-related race condition (occurred once in Unity). - // TODO: Consider removal of installationFetchTask variable. + // Add application-specific headers + lock (newCommand.Headers) + { + newCommand.Headers.Add(new KeyValuePair("X-Parse-Application-Id", ServerConnectionData.ApplicationID)); + newCommand.Headers.Add(new KeyValuePair("X-Parse-Client-Version", ParseClient.Version.ToString())); - lock (newCommand.Headers) + // Add custom headers if available + if (ServerConnectionData.Headers != null) { - newCommand.Headers.Add(new KeyValuePair("X-Parse-Application-Id", ServerConnectionData.ApplicationID)); - newCommand.Headers.Add(new KeyValuePair("X-Parse-Client-Version", ParseClient.Version.ToString())); - - if (ServerConnectionData.Headers != null) + foreach (KeyValuePair header in ServerConnectionData.Headers) { - foreach (KeyValuePair header in ServerConnectionData.Headers) - { - newCommand.Headers.Add(header); - } - } - - if (!String.IsNullOrEmpty(MetadataController.HostManifestData.Version)) - { - newCommand.Headers.Add(new KeyValuePair("X-Parse-App-Build-Version", MetadataController.HostManifestData.Version)); + newCommand.Headers.Add(header); } + } - if (!String.IsNullOrEmpty(MetadataController.HostManifestData.ShortVersion)) - { - newCommand.Headers.Add(new KeyValuePair("X-Parse-App-Display-Version", MetadataController.HostManifestData.ShortVersion)); - } + // Add versioning headers if metadata is available + if (!string.IsNullOrEmpty(MetadataController.HostManifestData.Version)) + { + newCommand.Headers.Add(new KeyValuePair("X-Parse-App-Build-Version", MetadataController.HostManifestData.Version)); + } - if (!String.IsNullOrEmpty(MetadataController.EnvironmentData.OSVersion)) - { - newCommand.Headers.Add(new KeyValuePair("X-Parse-OS-Version", MetadataController.EnvironmentData.OSVersion)); - } + if (!string.IsNullOrEmpty(MetadataController.HostManifestData.ShortVersion)) + { + newCommand.Headers.Add(new KeyValuePair("X-Parse-App-Display-Version", MetadataController.HostManifestData.ShortVersion)); + } - if (!String.IsNullOrEmpty(ServerConnectionData.MasterKey)) - { - newCommand.Headers.Add(new KeyValuePair("X-Parse-Master-Key", ServerConnectionData.MasterKey)); - } - else - { - newCommand.Headers.Add(new KeyValuePair("X-Parse-Windows-Key", ServerConnectionData.Key)); - } + if (!string.IsNullOrEmpty(MetadataController.EnvironmentData.OSVersion)) + { + newCommand.Headers.Add(new KeyValuePair("X-Parse-OS-Version", MetadataController.EnvironmentData.OSVersion)); + } - if (UserController.Value.RevocableSessionEnabled) - { - newCommand.Headers.Add(new KeyValuePair("X-Parse-Revocable-Session", "1")); - } + // Add master key or windows key + if (!string.IsNullOrEmpty(ServerConnectionData.MasterKey)) + { + newCommand.Headers.Add(new KeyValuePair("X-Parse-Master-Key", ServerConnectionData.MasterKey)); + } + else + { + newCommand.Headers.Add(new KeyValuePair("X-Parse-Windows-Key", ServerConnectionData.Key)); } - return installationIdFetchTask; + // Add revocable session header if enabled + if (UserController.Value.RevocableSessionEnabled) + { + newCommand.Headers.Add(new KeyValuePair("X-Parse-Revocable-Session", "1")); + } } + + return newCommand; + + //by the way, The original installationFetchTask variable was removed, as the async/await pattern eliminates the need for it. } + } diff --git a/Parse/Infrastructure/Execution/UniversalWebClient.cs b/Parse/Infrastructure/Execution/UniversalWebClient.cs index d6af9f3d..ff5741e1 100644 --- a/Parse/Infrastructure/Execution/UniversalWebClient.cs +++ b/Parse/Infrastructure/Execution/UniversalWebClient.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Net; using System.Net.Http; @@ -8,130 +9,166 @@ using System.Threading.Tasks; using Parse.Abstractions.Infrastructure; using Parse.Abstractions.Infrastructure.Execution; -using Parse.Infrastructure.Utilities; using BCLWebClient = System.Net.Http.HttpClient; -namespace Parse.Infrastructure.Execution +namespace Parse.Infrastructure.Execution; + +/// +/// A universal implementation of . +/// +public class UniversalWebClient : IWebClient { - /// - /// A universal implementation of . - /// - public class UniversalWebClient : IWebClient + static HashSet ContentHeaders { get; } = new HashSet { - static HashSet ContentHeaders { get; } = new HashSet - { - { "Allow" }, - { "Content-Disposition" }, - { "Content-Encoding" }, - { "Content-Language" }, - { "Content-Length" }, - { "Content-Location" }, - { "Content-MD5" }, - { "Content-Range" }, - { "Content-Type" }, - { "Expires" }, - { "Last-Modified" } - }; - - public UniversalWebClient() : this(new BCLWebClient { }) { } - - public UniversalWebClient(BCLWebClient client) => Client = client; - - BCLWebClient Client { get; set; } - - public Task> ExecuteAsync(WebRequest httpRequest, IProgress uploadProgress, IProgress downloadProgress, CancellationToken cancellationToken) + { "Allow" }, + { "Content-Disposition" }, + { "Content-Encoding" }, + { "Content-Language" }, + { "Content-Length" }, + { "Content-Location" }, + { "Content-MD5" }, + { "Content-Range" }, + { "Content-Type" }, + { "Expires" }, + { "Last-Modified" } + }; + + public UniversalWebClient() : this(new BCLWebClient { }) { } + + public UniversalWebClient(BCLWebClient client) => Client = client; + + BCLWebClient Client { get; set; } + + public async Task> ExecuteAsync( +WebRequest httpRequest, +IProgress uploadProgress, +IProgress downloadProgress, +CancellationToken cancellationToken) + { + uploadProgress ??= new Progress(); + downloadProgress ??= new Progress(); + + using HttpRequestMessage message = new(new HttpMethod(httpRequest.Method), httpRequest.Target); + + Stream data = httpRequest.Data; + if (data != null || httpRequest.Method.Equals("POST", StringComparison.OrdinalIgnoreCase)) { - uploadProgress ??= new Progress { }; - downloadProgress ??= new Progress { }; + message.Content = new StreamContent(data ?? new MemoryStream(new byte[0])); + } - HttpRequestMessage message = new HttpRequestMessage(new HttpMethod(httpRequest.Method), httpRequest.Target); - // Fill in zero-length data if method is post. - if ((httpRequest.Data is null && httpRequest.Method.ToLower().Equals("post") ? new MemoryStream(new byte[0]) : httpRequest.Data) is Stream { } data) + // Add headers to the message + if (httpRequest.Headers != null) + { + foreach (var header in httpRequest.Headers) { - message.Content = new StreamContent(data); + if (ContentHeaders.Contains(header.Key)) + { + message.Content.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + else + { + message.Headers.TryAddWithoutValidation(header.Key, header.Value); + } } + } + + // Avoid aggressive caching + message.Headers.Add("Cache-Control", "no-cache"); + + message.Headers.IfModifiedSince = DateTimeOffset.UtcNow; + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); // Timeout after 30 seconds - if (httpRequest.Headers != null) + if (message.RequestUri.AbsoluteUri.EndsWith("/logout", StringComparison.OrdinalIgnoreCase)) + { + var handler = new HttpClientHandler { - foreach (KeyValuePair header in httpRequest.Headers) - { - if (ContentHeaders.Contains(header.Key)) - { - message.Content.Headers.Add(header.Key, header.Value); - } - else - { - message.Headers.Add(header.Key, header.Value); - } - } + AllowAutoRedirect = true, + UseCookies = false // Avoid unwanted cookies. + }; + + using var client = new HttpClient(handler) + { + Timeout = TimeSpan.FromSeconds(15) // Ensure timeout is respected. + }; + using var response = await client.SendAsync(message, cancellationToken).ConfigureAwait(false); + + // Read response content as a string + string responseContent = await response.Content.ReadAsStringAsync(); + + // Check if the status code indicates success + if (response.IsSuccessStatusCode) + { + Debug.WriteLine($"Logout succeeded. Status: {response.StatusCode}"); + } + else + { + // Log failure details for debugging + Debug.WriteLine($"Logout failed. Status: {response.StatusCode}, Error: {responseContent}"); } - // Avoid aggressive caching on Windows Phone 8.1. + // Return the status code and response content + return new Tuple(response.StatusCode, responseContent); - message.Headers.Add("Cache-Control", "no-cache"); - message.Headers.IfModifiedSince = DateTimeOffset.UtcNow; + } + else + { - // TODO: (richardross) investigate progress here, maybe there's something we're missing in order to support this. + using var response = await Client + .SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + + // Check if the status code indicates success + if (response.IsSuccessStatusCode) + { - uploadProgress.Report(new DataTransferLevel { Amount = 0 }); - return Client.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ContinueWith(httpMessageTask => + } + else { - HttpResponseMessage response = httpMessageTask.Result; - uploadProgress.Report(new DataTransferLevel { Amount = 1 }); + // Log failure details for debugging + var error = await response.Content.ReadAsStringAsync(cancellationToken); + Debug.WriteLine($"Logout failed. Status: {response.StatusCode}, Error: {error}"); + + } + using var responseStream = await response.Content.ReadAsStreamAsync(); + using var resultStream = new MemoryStream(); - return response.Content.ReadAsStreamAsync().ContinueWith(streamTask => + var buffer = new byte[4096]; + int bytesRead; + long totalLength = response.Content.Headers.ContentLength ?? -1; + long readSoFar = 0; + + // Read response stream and report progress + while ((bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0) + { + await resultStream.WriteAsync(buffer, 0, bytesRead, cancellationToken); + readSoFar += bytesRead; + + if (totalLength > 0) { - MemoryStream resultStream = new MemoryStream { }; - Stream responseStream = streamTask.Result; - - int bufferSize = 4096, bytesRead = 0; - byte[] buffer = new byte[bufferSize]; - long totalLength = -1, readSoFar = 0; - - try - { - totalLength = responseStream.Length; - } - catch { }; - - return InternalExtensions.WhileAsync(() => responseStream.ReadAsync(buffer, 0, bufferSize, cancellationToken).OnSuccess(readTask => (bytesRead = readTask.Result) > 0), () => - { - cancellationToken.ThrowIfCancellationRequested(); - - return resultStream.WriteAsync(buffer, 0, bytesRead, cancellationToken).OnSuccess(_ => - { - cancellationToken.ThrowIfCancellationRequested(); - readSoFar += bytesRead; - - if (totalLength > -1) - { - downloadProgress.Report(new DataTransferLevel { Amount = 1.0 * readSoFar / totalLength }); - } - }); - }).ContinueWith(_ => - { - responseStream.Dispose(); - return _; - }).Unwrap().OnSuccess(_ => - { - // If getting stream size is not supported, then report download only once. - - if (totalLength == -1) - { - downloadProgress.Report(new DataTransferLevel { Amount = 1.0 }); - } - - byte[] resultAsArray = resultStream.ToArray(); - resultStream.Dispose(); - - // Assume UTF-8 encoding. - - return new Tuple(response.StatusCode, Encoding.UTF8.GetString(resultAsArray, 0, resultAsArray.Length)); - }); - }); - }).Unwrap().Unwrap(); + downloadProgress.Report(new DataTransferLevel { Amount = 1.0 * readSoFar / totalLength }); + } + } + + // Report final progress if total length was unknown + if (totalLength == -1) + { + downloadProgress.Report(new DataTransferLevel { Amount = 1.0 }); + } + var encoding = response.Content.Headers.ContentType?.CharSet switch + { + "utf-8" => Encoding.UTF8, + "ascii" => Encoding.ASCII, + _ => Encoding.Default + }; + // Convert response to string (assuming UTF-8 encoding) + var resultAsArray = resultStream.ToArray(); + string responseContent = Encoding.UTF8.GetString(resultAsArray); + + return new Tuple(response.StatusCode, responseContent); + + } } + } diff --git a/Parse/Infrastructure/Execution/WebRequest.cs b/Parse/Infrastructure/Execution/WebRequest.cs index 24733022..d22f44a8 100644 --- a/Parse/Infrastructure/Execution/WebRequest.cs +++ b/Parse/Infrastructure/Execution/WebRequest.cs @@ -2,29 +2,28 @@ using System.Collections.Generic; using System.IO; -namespace Parse.Infrastructure.Execution +namespace Parse.Infrastructure.Execution; + +/// +/// IHttpRequest is an interface that provides an API to execute HTTP request data. +/// +public class WebRequest { - /// - /// IHttpRequest is an interface that provides an API to execute HTTP request data. - /// - public class WebRequest - { - public Uri Target => new Uri(new Uri(Resource), Path); + public Uri Target => new Uri(new Uri(Resource), Path); - public string Resource { get; set; } + public string Resource { get; set; } - public string Path { get; set; } + public string Path { get; set; } - public IList> Headers { get; set; } + public IList> Headers { get; set; } - /// - /// Data stream to be uploaded. - /// - public virtual Stream Data { get; set; } + /// + /// Data stream to be uploaded. + /// + public virtual Stream Data { get; set; } - /// - /// HTTP method. One of DELETE, GET, HEAD, POST or PUT - /// - public string Method { get; set; } - } + /// + /// HTTP method. One of DELETE, GET, HEAD, POST or PUT + /// + public string Method { get; set; } } diff --git a/Parse/Infrastructure/HostManifestData.cs b/Parse/Infrastructure/HostManifestData.cs index 75e38b86..1b651df5 100644 --- a/Parse/Infrastructure/HostManifestData.cs +++ b/Parse/Infrastructure/HostManifestData.cs @@ -2,61 +2,63 @@ using System.Reflection; using Parse.Abstractions.Infrastructure; -namespace Parse.Infrastructure +namespace Parse.Infrastructure; + +/// +/// In the event that you would like to use the Parse SDK +/// from a completely portable project, with no platform-specific library required, +/// to get full access to all of our features available on Parse Dashboard +/// (A/B testing, slow queries, etc.), you must set the values of this struct +/// to be appropriate for your platform. +/// +/// Any values set here will overwrite those that are automatically configured by +/// any platform-specific migration library your app includes. +/// +public class HostManifestData : IHostManifestData { /// - /// In the event that you would like to use the Parse SDK - /// from a completely portable project, with no platform-specific library required, - /// to get full access to all of our features available on Parse Dashboard - /// (A/B testing, slow queries, etc.), you must set the values of this struct - /// to be appropriate for your platform. - /// - /// Any values set here will overwrite those that are automatically configured by - /// any platform-specific migration library your app includes. + /// An instance of with inferred values based on the entry assembly. /// - public class HostManifestData : IHostManifestData + /// Should not be used with Unity. + public static HostManifestData Inferred => new HostManifestData { - /// - /// An instance of with inferred values based on the entry assembly. - /// - /// Should not be used with Unity. - public static HostManifestData Inferred => new HostManifestData - { - Version = Assembly.GetEntryAssembly().GetCustomAttribute().InformationalVersion, - Name = Assembly.GetEntryAssembly().GetCustomAttribute()?.Title ?? Assembly.GetEntryAssembly().GetCustomAttribute()?.Product ?? Assembly.GetEntryAssembly().GetName().Name, - ShortVersion = Assembly.GetEntryAssembly().GetName().Version.ToString(), - // TODO: For Xamarin, use manifest parsing, and for Unity, use some kind of package identifier API. - Identifier = AppDomain.CurrentDomain.FriendlyName - }; - - /// - /// The build version of your app. - /// - public string Version { get; set; } - - /// - /// The human-friendly display version number of your app. - /// - public string ShortVersion { get; set; } - - /// - /// The identifier of the application - /// - public string Identifier { get; set; } - - /// - /// The friendly name of your app. - /// - public string Name { get; set; } - - /// - /// Gets a value for whether or not this instance of is populated with default values. - /// - public bool IsDefault => Version is null && ShortVersion is null && Identifier is null && Name is null; - - /// - /// Gets a value for whether or not this instance of can currently be used for the generation of . - /// - public bool CanBeUsedForInference => !(IsDefault || String.IsNullOrWhiteSpace(ShortVersion)); - } + Version = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.InformationalVersion + ?? "1.0.0", // Default version if not available + Name = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.Title + ?? Assembly.GetEntryAssembly()?.GetCustomAttribute()?.Product + ?? AppDomain.CurrentDomain.FriendlyName, // Fallback for MAUI + ShortVersion = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "1.0", + Identifier = AppDomain.CurrentDomain.FriendlyName ?? "UnknownApp" + }; + + + /// + /// The build version of your app. + /// + public string Version { get; set; } + + /// + /// The human-friendly display version number of your app. + /// + public string ShortVersion { get; set; } + + /// + /// The identifier of the application + /// + public string Identifier { get; set; } + + /// + /// The friendly name of your app. + /// + public string Name { get; set; } + + /// + /// Gets a value for whether or not this instance of is populated with default values. + /// + public bool IsDefault => Version is null && ShortVersion is null && Identifier is null && Name is null; + + /// + /// Gets a value for whether or not this instance of can currently be used for the generation of . + /// + public bool CanBeUsedForInference => !(IsDefault || String.IsNullOrWhiteSpace(ShortVersion)); } diff --git a/Parse/Infrastructure/IdentifierBasedRelativeCacheLocationGenerator.cs b/Parse/Infrastructure/IdentifierBasedRelativeCacheLocationGenerator.cs index 0a076266..7ecad1a7 100644 --- a/Parse/Infrastructure/IdentifierBasedRelativeCacheLocationGenerator.cs +++ b/Parse/Infrastructure/IdentifierBasedRelativeCacheLocationGenerator.cs @@ -2,43 +2,45 @@ using System.IO; using Parse.Abstractions.Infrastructure; -namespace Parse.Infrastructure +namespace Parse.Infrastructure; + +/// +/// A configuration of the Parse SDK persistent storage location based on an identifier. +/// +public struct IdentifierBasedRelativeCacheLocationGenerator : IRelativeCacheLocationGenerator { + internal static IdentifierBasedRelativeCacheLocationGenerator Fallback { get; } = new IdentifierBasedRelativeCacheLocationGenerator { IsFallback = true }; + + /// + /// Dictates whether or not this instance should act as a fallback for when has not yet been initialized but the storage path is needed. + /// + internal bool IsFallback { get; set; } + + /// + /// The identifier that all Parse SDK cache files should be labelled with. + /// + public string Identifier { get; set; } + + /// + /// The corresponding relative path generated by this . + /// + /// This will cause a .cachefile file extension to be added to the cache file in order to prevent the creation of files with unwanted extensions due to the value of containing periods. + public string GetRelativeCacheFilePath(IServiceHub serviceHub) + { + FileInfo file; + + while ((file = serviceHub.CacheController.GetRelativeFile(GeneratePath())).Exists && IsFallback) + ; + + return file.FullName; + } + /// - /// A configuration of the Parse SDK persistent storage location based on an identifier. + /// Generates a path for use in the method. /// - public struct IdentifierBasedRelativeCacheLocationGenerator : IRelativeCacheLocationGenerator + /// A potential path to the cachefile + string GeneratePath() { - internal static IdentifierBasedRelativeCacheLocationGenerator Fallback { get; } = new IdentifierBasedRelativeCacheLocationGenerator { IsFallback = true }; - - /// - /// Dictates whether or not this instance should act as a fallback for when has not yet been initialized but the storage path is needed. - /// - internal bool IsFallback { get; set; } - - /// - /// The identifier that all Parse SDK cache files should be labelled with. - /// - public string Identifier { get; set; } - - /// - /// The corresponding relative path generated by this . - /// - /// This will cause a .cachefile file extension to be added to the cache file in order to prevent the creation of files with unwanted extensions due to the value of containing periods. - public string GetRelativeCacheFilePath(IServiceHub serviceHub) - { - FileInfo file; - - while ((file = serviceHub.CacheController.GetRelativeFile(GeneratePath())).Exists && IsFallback) - ; - - return file.FullName; - } - - /// - /// Generates a path for use in the method. - /// - /// A potential path to the cachefile - string GeneratePath() => Path.Combine(nameof(Parse), IsFallback ? "_fallback" : "_global", $"{(IsFallback ? new Random { }.Next().ToString() : Identifier)}.cachefile"); + return Path.Combine(nameof(Parse), IsFallback ? "_fallback" : "_global", $"{(IsFallback ? new Random { }.Next().ToString() : Identifier)}.cachefile"); } } diff --git a/Parse/Infrastructure/LateInitializedMutableServiceHub.cs b/Parse/Infrastructure/LateInitializedMutableServiceHub.cs index a5e58e4e..b5c671f4 100644 --- a/Parse/Infrastructure/LateInitializedMutableServiceHub.cs +++ b/Parse/Infrastructure/LateInitializedMutableServiceHub.cs @@ -26,140 +26,139 @@ using Parse.Infrastructure.Data; using Parse.Infrastructure.Utilities; -namespace Parse.Infrastructure +namespace Parse.Infrastructure; + +public class LateInitializedMutableServiceHub : IMutableServiceHub { - public class LateInitializedMutableServiceHub : IMutableServiceHub - { - LateInitializer LateInitializer { get; } = new LateInitializer { }; - - public IServiceHubCloner Cloner { get; set; } - - public IMetadataController MetadataController - { - get => LateInitializer.GetValue(() => new MetadataController { EnvironmentData = EnvironmentData.Inferred, HostManifestData = HostManifestData.Inferred }); - set => LateInitializer.SetValue(value); - } - - public IWebClient WebClient - { - get => LateInitializer.GetValue(() => new UniversalWebClient { }); - set => LateInitializer.SetValue(value); - } - - public ICacheController CacheController - { - get => LateInitializer.GetValue(() => new CacheController { }); - set => LateInitializer.SetValue(value); - } - - public IParseObjectClassController ClassController - { - get => LateInitializer.GetValue(() => new ParseObjectClassController { }); - set => LateInitializer.SetValue(value); - } - - public IParseInstallationController InstallationController - { - get => LateInitializer.GetValue(() => new ParseInstallationController(CacheController)); - set => LateInitializer.SetValue(value); - } - - public IParseCommandRunner CommandRunner - { - get => LateInitializer.GetValue(() => new ParseCommandRunner(WebClient, InstallationController, MetadataController, ServerConnectionData, new Lazy(() => UserController))); - set => LateInitializer.SetValue(value); - } - - public IParseCloudCodeController CloudCodeController - { - get => LateInitializer.GetValue(() => new ParseCloudCodeController(CommandRunner, Decoder)); - set => LateInitializer.SetValue(value); - } - - public IParseConfigurationController ConfigurationController - { - get => LateInitializer.GetValue(() => new ParseConfigurationController(CommandRunner, CacheController, Decoder)); - set => LateInitializer.SetValue(value); - } - - public IParseFileController FileController - { - get => LateInitializer.GetValue(() => new ParseFileController(CommandRunner)); - set => LateInitializer.SetValue(value); - } - - public IParseObjectController ObjectController - { - get => LateInitializer.GetValue(() => new ParseObjectController(CommandRunner, Decoder, ServerConnectionData)); - set => LateInitializer.SetValue(value); - } - - public IParseQueryController QueryController - { - get => LateInitializer.GetValue(() => new ParseQueryController(CommandRunner, Decoder)); - set => LateInitializer.SetValue(value); - } - - public IParseSessionController SessionController - { - get => LateInitializer.GetValue(() => new ParseSessionController(CommandRunner, Decoder)); - set => LateInitializer.SetValue(value); - } - - public IParseUserController UserController - { - get => LateInitializer.GetValue(() => new ParseUserController(CommandRunner, Decoder)); - set => LateInitializer.SetValue(value); - } - - public IParseCurrentUserController CurrentUserController - { - get => LateInitializer.GetValue(() => new ParseCurrentUserController(CacheController, ClassController, Decoder)); - set => LateInitializer.SetValue(value); - } - - public IParseAnalyticsController AnalyticsController - { - get => LateInitializer.GetValue(() => new ParseAnalyticsController(CommandRunner)); - set => LateInitializer.SetValue(value); - } - - public IParseInstallationCoder InstallationCoder - { - get => LateInitializer.GetValue(() => new ParseInstallationCoder(Decoder, ClassController)); - set => LateInitializer.SetValue(value); - } - - public IParsePushChannelsController PushChannelsController - { - get => LateInitializer.GetValue(() => new ParsePushChannelsController(CurrentInstallationController)); - set => LateInitializer.SetValue(value); - } - - public IParsePushController PushController - { - get => LateInitializer.GetValue(() => new ParsePushController(CommandRunner, CurrentUserController)); - set => LateInitializer.SetValue(value); - } - - public IParseCurrentInstallationController CurrentInstallationController - { - get => LateInitializer.GetValue(() => new ParseCurrentInstallationController(InstallationController, CacheController, InstallationCoder, ClassController)); - set => LateInitializer.SetValue(value); - } - - public IParseDataDecoder Decoder - { - get => LateInitializer.GetValue(() => new ParseDataDecoder(ClassController)); - set => LateInitializer.SetValue(value); - } - - public IParseInstallationDataFinalizer InstallationDataFinalizer - { - get => LateInitializer.GetValue(() => new ParseInstallationDataFinalizer { }); - set => LateInitializer.SetValue(value); - } - - public IServerConnectionData ServerConnectionData { get; set; } + LateInitializer LateInitializer { get; } = new LateInitializer { }; + + public IServiceHubCloner Cloner { get; set; } + + public IMetadataController MetadataController + { + get => LateInitializer.GetValue(() => new MetadataController { EnvironmentData = EnvironmentData.Inferred, HostManifestData = HostManifestData.Inferred }); + set => LateInitializer.SetValue(value); } + + public IWebClient WebClient + { + get => LateInitializer.GetValue(() => new UniversalWebClient { }); + set => LateInitializer.SetValue(value); + } + + public ICacheController CacheController + { + get => LateInitializer.GetValue(() => new CacheController { }); + set => LateInitializer.SetValue(value); + } + + public IParseObjectClassController ClassController + { + get => LateInitializer.GetValue(() => new ParseObjectClassController { }); + set => LateInitializer.SetValue(value); + } + + public IParseInstallationController InstallationController + { + get => LateInitializer.GetValue(() => new ParseInstallationController(CacheController)); + set => LateInitializer.SetValue(value); + } + + public IParseCommandRunner CommandRunner + { + get => LateInitializer.GetValue(() => new ParseCommandRunner(WebClient, InstallationController, MetadataController, ServerConnectionData, new Lazy(() => UserController))); + set => LateInitializer.SetValue(value); + } + + public IParseCloudCodeController CloudCodeController + { + get => LateInitializer.GetValue(() => new ParseCloudCodeController(CommandRunner, Decoder)); + set => LateInitializer.SetValue(value); + } + + public IParseConfigurationController ConfigurationController + { + get => LateInitializer.GetValue(() => new ParseConfigurationController(CommandRunner, CacheController, Decoder)); + set => LateInitializer.SetValue(value); + } + + public IParseFileController FileController + { + get => LateInitializer.GetValue(() => new ParseFileController(CommandRunner)); + set => LateInitializer.SetValue(value); + } + + public IParseObjectController ObjectController + { + get => LateInitializer.GetValue(() => new ParseObjectController(CommandRunner, Decoder, ServerConnectionData)); + set => LateInitializer.SetValue(value); + } + + public IParseQueryController QueryController + { + get => LateInitializer.GetValue(() => new ParseQueryController(CommandRunner, Decoder)); + set => LateInitializer.SetValue(value); + } + + public IParseSessionController SessionController + { + get => LateInitializer.GetValue(() => new ParseSessionController(CommandRunner, Decoder)); + set => LateInitializer.SetValue(value); + } + + public IParseUserController UserController + { + get => LateInitializer.GetValue(() => new ParseUserController(CommandRunner, Decoder)); + set => LateInitializer.SetValue(value); + } + + public IParseCurrentUserController CurrentUserController + { + get => LateInitializer.GetValue(() => new ParseCurrentUserController(CacheController, ClassController, Decoder)); + set => LateInitializer.SetValue(value); + } + + public IParseAnalyticsController AnalyticsController + { + get => LateInitializer.GetValue(() => new ParseAnalyticsController(CommandRunner)); + set => LateInitializer.SetValue(value); + } + + public IParseInstallationCoder InstallationCoder + { + get => LateInitializer.GetValue(() => new ParseInstallationCoder(Decoder, ClassController)); + set => LateInitializer.SetValue(value); + } + + public IParsePushChannelsController PushChannelsController + { + get => LateInitializer.GetValue(() => new ParsePushChannelsController(CurrentInstallationController)); + set => LateInitializer.SetValue(value); + } + + public IParsePushController PushController + { + get => LateInitializer.GetValue(() => new ParsePushController(CommandRunner, CurrentUserController)); + set => LateInitializer.SetValue(value); + } + + public IParseCurrentInstallationController CurrentInstallationController + { + get => LateInitializer.GetValue(() => new ParseCurrentInstallationController(InstallationController, CacheController, InstallationCoder, ClassController)); + set => LateInitializer.SetValue(value); + } + + public IParseDataDecoder Decoder + { + get => LateInitializer.GetValue(() => new ParseDataDecoder(ClassController)); + set => LateInitializer.SetValue(value); + } + + public IParseInstallationDataFinalizer InstallationDataFinalizer + { + get => LateInitializer.GetValue(() => new ParseInstallationDataFinalizer { }); + set => LateInitializer.SetValue(value); + } + + public IServerConnectionData ServerConnectionData { get; set; } } diff --git a/Parse/Infrastructure/MetadataBasedRelativeCacheLocationGenerator.cs b/Parse/Infrastructure/MetadataBasedRelativeCacheLocationGenerator.cs index 9eadc5d1..82636e0c 100644 --- a/Parse/Infrastructure/MetadataBasedRelativeCacheLocationGenerator.cs +++ b/Parse/Infrastructure/MetadataBasedRelativeCacheLocationGenerator.cs @@ -2,36 +2,38 @@ using System.Reflection; using Parse.Abstractions.Infrastructure; -namespace Parse.Infrastructure +namespace Parse.Infrastructure; + +/// +/// A configuration of the Parse SDK persistent storage location based on product metadata such as company name and product name. +/// +public struct MetadataBasedRelativeCacheLocationGenerator : IRelativeCacheLocationGenerator { /// - /// A configuration of the Parse SDK persistent storage location based on product metadata such as company name and product name. + /// An instance of with inferred values based on the entry assembly. Should be used with and . /// - public struct MetadataBasedRelativeCacheLocationGenerator : IRelativeCacheLocationGenerator + /// Should not be used with Unity. + public static MetadataBasedRelativeCacheLocationGenerator Inferred => new MetadataBasedRelativeCacheLocationGenerator { - /// - /// An instance of with inferred values based on the entry assembly. Should be used with and . - /// - /// Should not be used with Unity. - public static MetadataBasedRelativeCacheLocationGenerator Inferred => new MetadataBasedRelativeCacheLocationGenerator - { - Company = Assembly.GetExecutingAssembly()?.GetCustomAttribute()?.Company, - Product = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.Product ?? Assembly.GetEntryAssembly()?.GetName()?.Name - }; + Company = Assembly.GetExecutingAssembly()?.GetCustomAttribute()?.Company, + Product = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.Product ?? Assembly.GetEntryAssembly()?.GetName()?.Name + }; - /// - /// The name of the company that owns the product specified by . - /// - public string Company { get; set; } + /// + /// The name of the company that owns the product specified by . + /// + public string Company { get; set; } - /// - /// The name of the product that is using the Parse .NET SDK. - /// - public string Product { get; set; } + /// + /// The name of the product that is using the Parse .NET SDK. + /// + public string Product { get; set; } - /// - /// The corresponding relative path generated by this . - /// - public string GetRelativeCacheFilePath(IServiceHub serviceHub) => Path.Combine(Company ?? nameof(Parse), Product ?? "_global", $"{serviceHub.MetadataController.HostManifestData.ShortVersion ?? "1.0.0.0"}.pc"); + /// + /// The corresponding relative path generated by this . + /// + public string GetRelativeCacheFilePath(IServiceHub serviceHub) + { + return Path.Combine(Company ?? nameof(Parse), Product ?? "_global", $"{serviceHub.MetadataController.HostManifestData.ShortVersion ?? "1.0.0.0"}.pc"); } } diff --git a/Parse/Infrastructure/MetadataController.cs b/Parse/Infrastructure/MetadataController.cs index 6d8b83b3..e373542f 100644 --- a/Parse/Infrastructure/MetadataController.cs +++ b/Parse/Infrastructure/MetadataController.cs @@ -1,17 +1,16 @@ using Parse.Abstractions.Infrastructure; -namespace Parse.Infrastructure +namespace Parse.Infrastructure; + +public class MetadataController : IMetadataController { - public class MetadataController : IMetadataController - { - /// - /// Information about your app. - /// - public IHostManifestData HostManifestData { get; set; } + /// + /// Information about your app. + /// + public IHostManifestData HostManifestData { get; set; } - /// - /// Information about the environment the library is operating in. - /// - public IEnvironmentData EnvironmentData { get; set; } - } + /// + /// Information about the environment the library is operating in. + /// + public IEnvironmentData EnvironmentData { get; set; } } diff --git a/Parse/Infrastructure/MetadataMutator.cs b/Parse/Infrastructure/MetadataMutator.cs index 351f779b..004bbaaf 100644 --- a/Parse/Infrastructure/MetadataMutator.cs +++ b/Parse/Infrastructure/MetadataMutator.cs @@ -1,22 +1,24 @@ using Parse.Abstractions.Infrastructure; -namespace Parse.Infrastructure +namespace Parse.Infrastructure; + +/// +/// An for setting metadata information manually. +/// +public class MetadataMutator : MetadataController, IServiceHubMutator { /// - /// An for setting metadata information manually. + /// A value representing whether or not all of the required metadata information has been provided. /// - public class MetadataMutator : MetadataController, IServiceHubMutator - { - /// - /// A value representing whether or not all of the required metadata information has been provided. - /// - public bool Valid => this is { EnvironmentData: { OSVersion: { }, Platform: { }, TimeZone: { } }, HostManifestData: { Identifier: { }, Name: { }, ShortVersion: { }, Version: { } } }; + public bool Valid => this is { EnvironmentData: { OSVersion: { }, Platform: { }, TimeZone: { } }, HostManifestData: { Identifier: { }, Name: { }, ShortVersion: { }, Version: { } } }; - /// - /// Sets the to the instance. - /// - /// The to compose the information onto. - /// Thhe to use if a default service instance is required. - public void Mutate(ref IMutableServiceHub target, in IServiceHub referenceHub) => target.MetadataController = this; + /// + /// Sets the to the instance. + /// + /// The to compose the information onto. + /// Thhe to use if a default service instance is required. + public void Mutate(ref IMutableServiceHub target, in IServiceHub referenceHub) + { + target.MetadataController = this; } } diff --git a/Parse/Infrastructure/MutableServiceHub.cs b/Parse/Infrastructure/MutableServiceHub.cs index 4c63669b..3cf50a0d 100644 --- a/Parse/Infrastructure/MutableServiceHub.cs +++ b/Parse/Infrastructure/MutableServiceHub.cs @@ -25,85 +25,84 @@ using Parse.Platform.Sessions; using Parse.Platform.Users; -namespace Parse.Infrastructure +namespace Parse.Infrastructure; + +/// +/// A service hub that is mutable. +/// +/// This class is not thread safe; the mutability is allowed for the purposes of overriding values before it is used, as opposed to modifying it while it is in use. +public class MutableServiceHub : IMutableServiceHub { - /// - /// A service hub that is mutable. - /// - /// This class is not thread safe; the mutability is allowed for the purposes of overriding values before it is used, as opposed to modifying it while it is in use. - public class MutableServiceHub : IMutableServiceHub - { - public IServerConnectionData ServerConnectionData { get; set; } - public IMetadataController MetadataController { get; set; } + public IServerConnectionData ServerConnectionData { get; set; } + public IMetadataController MetadataController { get; set; } - public IServiceHubCloner Cloner { get; set; } + public IServiceHubCloner Cloner { get; set; } - public IWebClient WebClient { get; set; } - public ICacheController CacheController { get; set; } - public IParseObjectClassController ClassController { get; set; } + public IWebClient WebClient { get; set; } + public ICacheController CacheController { get; set; } + public IParseObjectClassController ClassController { get; set; } - public IParseDataDecoder Decoder { get; set; } + public IParseDataDecoder Decoder { get; set; } - public IParseInstallationController InstallationController { get; set; } - public IParseCommandRunner CommandRunner { get; set; } + public IParseInstallationController InstallationController { get; set; } + public IParseCommandRunner CommandRunner { get; set; } - public IParseCloudCodeController CloudCodeController { get; set; } - public IParseConfigurationController ConfigurationController { get; set; } - public IParseFileController FileController { get; set; } - public IParseObjectController ObjectController { get; set; } - public IParseQueryController QueryController { get; set; } - public IParseSessionController SessionController { get; set; } - public IParseUserController UserController { get; set; } - public IParseCurrentUserController CurrentUserController { get; set; } + public IParseCloudCodeController CloudCodeController { get; set; } + public IParseConfigurationController ConfigurationController { get; set; } + public IParseFileController FileController { get; set; } + public IParseObjectController ObjectController { get; set; } + public IParseQueryController QueryController { get; set; } + public IParseSessionController SessionController { get; set; } + public IParseUserController UserController { get; set; } + public IParseCurrentUserController CurrentUserController { get; set; } - public IParseAnalyticsController AnalyticsController { get; set; } + public IParseAnalyticsController AnalyticsController { get; set; } - public IParseInstallationCoder InstallationCoder { get; set; } + public IParseInstallationCoder InstallationCoder { get; set; } - public IParsePushChannelsController PushChannelsController { get; set; } - public IParsePushController PushController { get; set; } - public IParseCurrentInstallationController CurrentInstallationController { get; set; } - public IParseInstallationDataFinalizer InstallationDataFinalizer { get; set; } + public IParsePushChannelsController PushChannelsController { get; set; } + public IParsePushController PushController { get; set; } + public IParseCurrentInstallationController CurrentInstallationController { get; set; } + public IParseInstallationDataFinalizer InstallationDataFinalizer { get; set; } - public MutableServiceHub SetDefaults(IServerConnectionData connectionData = default) + public MutableServiceHub SetDefaults(IServerConnectionData connectionData = default) + { + ServerConnectionData ??= connectionData; + MetadataController ??= new MetadataController { - ServerConnectionData ??= connectionData; - MetadataController ??= new MetadataController - { - EnvironmentData = EnvironmentData.Inferred, - HostManifestData = HostManifestData.Inferred - }; + EnvironmentData = EnvironmentData.Inferred, + HostManifestData = HostManifestData.Inferred + }; - Cloner ??= new ConcurrentUserServiceHubCloner { }; + Cloner ??= new ConcurrentUserServiceHubCloner { }; - WebClient ??= new UniversalWebClient { }; - CacheController ??= new CacheController { }; - ClassController ??= new ParseObjectClassController { }; + WebClient ??= new UniversalWebClient { }; + CacheController ??= new CacheController { }; + ClassController ??= new ParseObjectClassController { }; - Decoder ??= new ParseDataDecoder(ClassController); + Decoder ??= new ParseDataDecoder(ClassController); - InstallationController ??= new ParseInstallationController(CacheController); - CommandRunner ??= new ParseCommandRunner(WebClient, InstallationController, MetadataController, ServerConnectionData, new Lazy(() => UserController)); + InstallationController ??= new ParseInstallationController(CacheController); + CommandRunner ??= new ParseCommandRunner(WebClient, InstallationController, MetadataController, ServerConnectionData, new Lazy(() => UserController)); - CloudCodeController ??= new ParseCloudCodeController(CommandRunner, Decoder); - ConfigurationController ??= new ParseConfigurationController(CommandRunner, CacheController, Decoder); - FileController ??= new ParseFileController(CommandRunner); - ObjectController ??= new ParseObjectController(CommandRunner, Decoder, ServerConnectionData); - QueryController ??= new ParseQueryController(CommandRunner, Decoder); - SessionController ??= new ParseSessionController(CommandRunner, Decoder); - UserController ??= new ParseUserController(CommandRunner, Decoder); - CurrentUserController ??= new ParseCurrentUserController(CacheController, ClassController, Decoder); + CloudCodeController ??= new ParseCloudCodeController(CommandRunner, Decoder); + ConfigurationController ??= new ParseConfigurationController(CommandRunner, CacheController, Decoder); + FileController ??= new ParseFileController(CommandRunner); + ObjectController ??= new ParseObjectController(CommandRunner, Decoder, ServerConnectionData); + QueryController ??= new ParseQueryController(CommandRunner, Decoder); + SessionController ??= new ParseSessionController(CommandRunner, Decoder); + UserController ??= new ParseUserController(CommandRunner, Decoder); + CurrentUserController ??= new ParseCurrentUserController(CacheController, ClassController, Decoder); - AnalyticsController ??= new ParseAnalyticsController(CommandRunner); + AnalyticsController ??= new ParseAnalyticsController(CommandRunner); - InstallationCoder ??= new ParseInstallationCoder(Decoder, ClassController); + InstallationCoder ??= new ParseInstallationCoder(Decoder, ClassController); - PushController ??= new ParsePushController(CommandRunner, CurrentUserController); - CurrentInstallationController ??= new ParseCurrentInstallationController(InstallationController, CacheController, InstallationCoder, ClassController); - PushChannelsController ??= new ParsePushChannelsController(CurrentInstallationController); - InstallationDataFinalizer ??= new ParseInstallationDataFinalizer { }; + PushController ??= new ParsePushController(CommandRunner, CurrentUserController); + CurrentInstallationController ??= new ParseCurrentInstallationController(InstallationController, CacheController, InstallationCoder, ClassController); + PushChannelsController ??= new ParsePushChannelsController(CurrentInstallationController); + InstallationDataFinalizer ??= new ParseInstallationDataFinalizer { }; - return this; - } + return this; } } diff --git a/Parse/Infrastructure/OrchestrationServiceHub.cs b/Parse/Infrastructure/OrchestrationServiceHub.cs index 8aa99a9e..d8079425 100644 --- a/Parse/Infrastructure/OrchestrationServiceHub.cs +++ b/Parse/Infrastructure/OrchestrationServiceHub.cs @@ -12,58 +12,57 @@ using Parse.Abstractions.Platform.Sessions; using Parse.Abstractions.Platform.Users; -namespace Parse.Infrastructure +namespace Parse.Infrastructure; + +public class OrchestrationServiceHub : IServiceHub { - public class OrchestrationServiceHub : IServiceHub - { - public IServiceHub Default { get; set; } + public IServiceHub Default { get; set; } - public IServiceHub Custom { get; set; } + public IServiceHub Custom { get; set; } - public IServiceHubCloner Cloner => Custom.Cloner ?? Default.Cloner; + public IServiceHubCloner Cloner => Custom.Cloner ?? Default.Cloner; - public IMetadataController MetadataController => Custom.MetadataController ?? Default.MetadataController; + public IMetadataController MetadataController => Custom.MetadataController ?? Default.MetadataController; - public IWebClient WebClient => Custom.WebClient ?? Default.WebClient; + public IWebClient WebClient => Custom.WebClient ?? Default.WebClient; - public ICacheController CacheController => Custom.CacheController ?? Default.CacheController; + public ICacheController CacheController => Custom.CacheController ?? Default.CacheController; - public IParseObjectClassController ClassController => Custom.ClassController ?? Default.ClassController; + public IParseObjectClassController ClassController => Custom.ClassController ?? Default.ClassController; - public IParseInstallationController InstallationController => Custom.InstallationController ?? Default.InstallationController; + public IParseInstallationController InstallationController => Custom.InstallationController ?? Default.InstallationController; - public IParseCommandRunner CommandRunner => Custom.CommandRunner ?? Default.CommandRunner; + public IParseCommandRunner CommandRunner => Custom.CommandRunner ?? Default.CommandRunner; - public IParseCloudCodeController CloudCodeController => Custom.CloudCodeController ?? Default.CloudCodeController; + public IParseCloudCodeController CloudCodeController => Custom.CloudCodeController ?? Default.CloudCodeController; - public IParseConfigurationController ConfigurationController => Custom.ConfigurationController ?? Default.ConfigurationController; + public IParseConfigurationController ConfigurationController => Custom.ConfigurationController ?? Default.ConfigurationController; - public IParseFileController FileController => Custom.FileController ?? Default.FileController; + public IParseFileController FileController => Custom.FileController ?? Default.FileController; - public IParseObjectController ObjectController => Custom.ObjectController ?? Default.ObjectController; + public IParseObjectController ObjectController => Custom.ObjectController ?? Default.ObjectController; - public IParseQueryController QueryController => Custom.QueryController ?? Default.QueryController; + public IParseQueryController QueryController => Custom.QueryController ?? Default.QueryController; - public IParseSessionController SessionController => Custom.SessionController ?? Default.SessionController; + public IParseSessionController SessionController => Custom.SessionController ?? Default.SessionController; - public IParseUserController UserController => Custom.UserController ?? Default.UserController; + public IParseUserController UserController => Custom.UserController ?? Default.UserController; - public IParseCurrentUserController CurrentUserController => Custom.CurrentUserController ?? Default.CurrentUserController; + public IParseCurrentUserController CurrentUserController => Custom.CurrentUserController ?? Default.CurrentUserController; - public IParseAnalyticsController AnalyticsController => Custom.AnalyticsController ?? Default.AnalyticsController; + public IParseAnalyticsController AnalyticsController => Custom.AnalyticsController ?? Default.AnalyticsController; - public IParseInstallationCoder InstallationCoder => Custom.InstallationCoder ?? Default.InstallationCoder; + public IParseInstallationCoder InstallationCoder => Custom.InstallationCoder ?? Default.InstallationCoder; - public IParsePushChannelsController PushChannelsController => Custom.PushChannelsController ?? Default.PushChannelsController; + public IParsePushChannelsController PushChannelsController => Custom.PushChannelsController ?? Default.PushChannelsController; - public IParsePushController PushController => Custom.PushController ?? Default.PushController; + public IParsePushController PushController => Custom.PushController ?? Default.PushController; - public IParseCurrentInstallationController CurrentInstallationController => Custom.CurrentInstallationController ?? Default.CurrentInstallationController; + public IParseCurrentInstallationController CurrentInstallationController => Custom.CurrentInstallationController ?? Default.CurrentInstallationController; - public IServerConnectionData ServerConnectionData => Custom.ServerConnectionData ?? Default.ServerConnectionData; + public IServerConnectionData ServerConnectionData => Custom.ServerConnectionData ?? Default.ServerConnectionData; - public IParseDataDecoder Decoder => Custom.Decoder ?? Default.Decoder; + public IParseDataDecoder Decoder => Custom.Decoder ?? Default.Decoder; - public IParseInstallationDataFinalizer InstallationDataFinalizer => Custom.InstallationDataFinalizer ?? Default.InstallationDataFinalizer; - } + public IParseInstallationDataFinalizer InstallationDataFinalizer => Custom.InstallationDataFinalizer ?? Default.InstallationDataFinalizer; } diff --git a/Parse/Infrastructure/ParseClassNameAttribute.cs b/Parse/Infrastructure/ParseClassNameAttribute.cs index fb406975..cd55309d 100644 --- a/Parse/Infrastructure/ParseClassNameAttribute.cs +++ b/Parse/Infrastructure/ParseClassNameAttribute.cs @@ -1,22 +1,21 @@ using System; -namespace Parse +namespace Parse; + +/// +/// Defines the class name for a subclass of ParseObject. +/// +[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)] +public sealed class ParseClassNameAttribute : Attribute { /// - /// Defines the class name for a subclass of ParseObject. + /// Constructs a new ParseClassName attribute. /// - [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)] - public sealed class ParseClassNameAttribute : Attribute - { - /// - /// Constructs a new ParseClassName attribute. - /// - /// The class name to associate with the ParseObject subclass. - public ParseClassNameAttribute(string className) => ClassName = className; + /// The class name to associate with the ParseObject subclass. + public ParseClassNameAttribute(string className) => ClassName = className; - /// - /// Gets the class name to associate with the ParseObject subclass. - /// - public string ClassName { get; private set; } - } + /// + /// Gets the class name to associate with the ParseObject subclass. + /// + public string ClassName { get; private set; } } diff --git a/Parse/Infrastructure/ParseFailureException.cs b/Parse/Infrastructure/ParseFailureException.cs index fca939fd..086b7b33 100644 --- a/Parse/Infrastructure/ParseFailureException.cs +++ b/Parse/Infrastructure/ParseFailureException.cs @@ -1,266 +1,269 @@ using System; -namespace Parse.Infrastructure +namespace Parse.Infrastructure; + +/// +/// Exceptions that may occur when sending requests to Parse. +/// +public class ParseFailureException : Exception { /// - /// Exceptions that may occur when sending requests to Parse. + /// Error codes that may be delivered in response to requests to Parse. /// - public class ParseFailureException : Exception + public enum ErrorCode { /// - /// Error codes that may be delivered in response to requests to Parse. - /// - public enum ErrorCode - { - /// - /// Error code indicating that an unknown error or an error unrelated to Parse - /// occurred. - /// - OtherCause = -1, - - /// - /// Error code indicating that something has gone wrong with the server. - /// If you get this error code, it is Parse's fault. Please report the bug. - /// - InternalServerError = 1, - - /// - /// Error code indicating the connection to the Parse servers failed. - /// - ConnectionFailed = 100, - - /// - /// Error code indicating the specified object doesn't exist. - /// - ObjectNotFound = 101, - - /// - /// Error code indicating you tried to query with a datatype that doesn't - /// support it, like exact matching an array or object. - /// - InvalidQuery = 102, - - /// - /// Error code indicating a missing or invalid classname. Classnames are - /// case-sensitive. They must start with a letter, and a-zA-Z0-9_ are the - /// only valid characters. - /// - InvalidClassName = 103, - - /// - /// Error code indicating an unspecified object id. - /// - MissingObjectId = 104, - - /// - /// Error code indicating an invalid key name. Keys are case-sensitive. They - /// must start with a letter, and a-zA-Z0-9_ are the only valid characters. - /// - InvalidKeyName = 105, - - /// - /// Error code indicating a malformed pointer. You should not see this unless - /// you have been mucking about changing internal Parse code. - /// - InvalidPointer = 106, - - /// - /// Error code indicating that badly formed JSON was received upstream. This - /// either indicates you have done something unusual with modifying how - /// things encode to JSON, or the network is failing badly. - /// - InvalidJSON = 107, - - /// - /// Error code indicating that the feature you tried to access is only - /// available internally for testing purposes. - /// - CommandUnavailable = 108, - - /// - /// You must call Parse.initialize before using the Parse library. - /// - NotInitialized = 109, - - /// - /// Error code indicating that a field was set to an inconsistent type. - /// - IncorrectType = 111, - - /// - /// Error code indicating an invalid channel name. A channel name is either - /// an empty string (the broadcast channel) or contains only a-zA-Z0-9_ - /// characters and starts with a letter. - /// - InvalidChannelName = 112, - - /// - /// Error code indicating that push is misconfigured. - /// - PushMisconfigured = 115, - - /// - /// Error code indicating that the object is too large. - /// - ObjectTooLarge = 116, - - /// - /// Error code indicating that the operation isn't allowed for clients. - /// - OperationForbidden = 119, - - /// - /// Error code indicating the result was not found in the cache. - /// - CacheMiss = 120, - - /// - /// Error code indicating that an invalid key was used in a nested - /// JSONObject. - /// - InvalidNestedKey = 121, - - /// - /// Error code indicating that an invalid filename was used for ParseFile. - /// A valid file name contains only a-zA-Z0-9_. characters and is between 1 - /// and 128 characters. - /// - InvalidFileName = 122, - - /// - /// Error code indicating an invalid ACL was provided. - /// - InvalidACL = 123, - - /// - /// Error code indicating that the request timed out on the server. Typically - /// this indicates that the request is too expensive to run. - /// - Timeout = 124, - - /// - /// Error code indicating that the email address was invalid. - /// - InvalidEmailAddress = 125, - - /// - /// Error code indicating that a unique field was given a value that is - /// already taken. - /// - DuplicateValue = 137, - - /// - /// Error code indicating that a role's name is invalid. - /// - InvalidRoleName = 139, - - /// - /// Error code indicating that an application quota was exceeded. Upgrade to - /// resolve. - /// - ExceededQuota = 140, - - /// - /// Error code indicating that a Cloud Code script failed. - /// - ScriptFailed = 141, - - /// - /// Error code indicating that a Cloud Code validation failed. - /// - ValidationFailed = 142, - - /// - /// Error code indicating that deleting a file failed. - /// - FileDeleteFailed = 153, - - /// - /// Error code indicating that the application has exceeded its request limit. - /// - RequestLimitExceeded = 155, - - /// - /// Error code indicating that the provided event name is invalid. - /// - InvalidEventName = 160, - - /// - /// Error code indicating that the username is missing or empty. - /// - UsernameMissing = 200, - - /// - /// Error code indicating that the password is missing or empty. - /// - PasswordMissing = 201, - - /// - /// Error code indicating that the username has already been taken. - /// - UsernameTaken = 202, - - /// - /// Error code indicating that the email has already been taken. - /// - EmailTaken = 203, - - /// - /// Error code indicating that the email is missing, but must be specified. - /// - EmailMissing = 204, - - /// - /// Error code indicating that a user with the specified email was not found. - /// - EmailNotFound = 205, - - /// - /// Error code indicating that a user object without a valid session could - /// not be altered. - /// - SessionMissing = 206, - - /// - /// Error code indicating that a user can only be created through signup. - /// - MustCreateUserThroughSignup = 207, - - /// - /// Error code indicating that an an account being linked is already linked - /// to another user. - /// - AccountAlreadyLinked = 208, - - /// - /// Error code indicating that the current session token is invalid. - /// - InvalidSessionToken = 209, - - /// - /// Error code indicating that a user cannot be linked to an account because - /// that account's id could not be found. - /// - LinkedIdMissing = 250, - - /// - /// Error code indicating that a user with a linked (e.g. Facebook) account - /// has an invalid session. - /// - InvalidLinkedSession = 251, - - /// - /// Error code indicating that a service being linked (e.g. Facebook or - /// Twitter) is unsupported. - /// - UnsupportedService = 252 - } - - internal ParseFailureException(ErrorCode code, string message, Exception cause = null) : base(message, cause) => Code = code; - - /// - /// The Parse error code associated with the exception. - /// - public ErrorCode Code { get; private set; } + /// Error code indicating that an unknown error or an error unrelated to Parse + /// occurred. + /// + OtherCause = -1, + + /// + /// Error code indicating that something has gone wrong with the server. + /// If you get this error code, it is Parse's fault. Please report the bug. + /// + InternalServerError = 1, + + /// + /// Error code indicating the connection to the Parse servers failed. + /// + ConnectionFailed = 100, + + /// + /// Error code indicating the specified object doesn't exist. + /// + ObjectNotFound = 101, + + /// + /// Error code indicating you tried to query with a datatype that doesn't + /// support it, like exact matching an array or object. + /// + InvalidQuery = 102, + + /// + /// Error code indicating a missing or invalid classname. Classnames are + /// case-sensitive. They must start with a letter, and a-zA-Z0-9_ are the + /// only valid characters. + /// + InvalidClassName = 103, + + /// + /// Error code indicating an unspecified object id. + /// + MissingObjectId = 104, + + /// + /// Error code indicating an invalid key name. Keys are case-sensitive. They + /// must start with a letter, and a-zA-Z0-9_ are the only valid characters. + /// + InvalidKeyName = 105, + + /// + /// Error code indicating a malformed pointer. You should not see this unless + /// you have been mucking about changing internal Parse code. + /// + InvalidPointer = 106, + + /// + /// Error code indicating that badly formed JSON was received upstream. This + /// either indicates you have done something unusual with modifying how + /// things encode to JSON, or the network is failing badly. + /// + InvalidJSON = 107, + + /// + /// Error code indicating that the feature you tried to access is only + /// available internally for testing purposes. + /// + CommandUnavailable = 108, + + /// + /// You must call Parse.initialize before using the Parse library. + /// + NotInitialized = 109, + + /// + /// Error code indicating that a field was set to an inconsistent type. + /// + IncorrectType = 111, + + /// + /// Error code indicating an invalid channel name. A channel name is either + /// an empty string (the broadcast channel) or contains only a-zA-Z0-9_ + /// characters and starts with a letter. + /// + InvalidChannelName = 112, + + /// + /// Error code indicating that push is misconfigured. + /// + PushMisconfigured = 115, + + /// + /// Error code indicating that the object is too large. + /// + ObjectTooLarge = 116, + + /// + /// Error code indicating that the operation isn't allowed for clients. + /// + OperationForbidden = 119, + + /// + /// Error code indicating the result was not found in the cache. + /// + CacheMiss = 120, + + /// + /// Error code indicating that an invalid key was used in a nested + /// JSONObject. + /// + InvalidNestedKey = 121, + + /// + /// Error code indicating that an invalid filename was used for ParseFile. + /// A valid file name contains only a-zA-Z0-9_. characters and is between 1 + /// and 128 characters. + /// + InvalidFileName = 122, + + /// + /// Error code indicating an invalid ACL was provided. + /// + InvalidACL = 123, + + /// + /// Error code indicating that the request timed out on the server. Typically + /// this indicates that the request is too expensive to run. + /// + Timeout = 124, + + /// + /// Error code indicating that the email address was invalid. + /// + InvalidEmailAddress = 125, + + /// + /// Error code indicating that a unique field was given a value that is + /// already taken. + /// + DuplicateValue = 137, + + /// + /// Error code indicating that a role's name is invalid. + /// + InvalidRoleName = 139, + + /// + /// Error code indicating that an application quota was exceeded. Upgrade to + /// resolve. + /// + ExceededQuota = 140, + + /// + /// Error code indicating that a Cloud Code script failed. + /// + ScriptFailed = 141, + + /// + /// Error code indicating that a Cloud Code validation failed. + /// + ValidationFailed = 142, + + /// + /// Error code indicating that deleting a file failed. + /// + FileDeleteFailed = 153, + + /// + /// Error code indicating that the application has exceeded its request limit. + /// + RequestLimitExceeded = 155, + + /// + /// Error code indicating that the provided event name is invalid. + /// + InvalidEventName = 160, + + /// + /// Error code indicating that the username is missing or empty. + /// + UsernameMissing = 200, + + /// + /// Error code indicating that the password is missing or empty. + /// + PasswordMissing = 201, + + /// + /// Error code indicating that the username has already been taken. + /// + UsernameTaken = 202, + + /// + /// Error code indicating that the email has already been taken. + /// + EmailTaken = 203, + + /// + /// Error code indicating that the email is missing, but must be specified. + /// + EmailMissing = 204, + + /// + /// Error code indicating that a user with the specified email was not found. + /// + EmailNotFound = 205, + + /// + /// Error code indicating that a user object without a valid session could + /// not be altered. + /// + SessionMissing = 206, + + /// + /// Error code indicating that a user can only be created through signup. + /// + MustCreateUserThroughSignup = 207, + + /// + /// Error code indicating that an an account being linked is already linked + /// to another user. + /// + AccountAlreadyLinked = 208, + + /// + /// Error code indicating that the current session token is invalid. + /// + InvalidSessionToken = 209, + + /// + /// Error code indicating that a user cannot be linked to an account because + /// that account's id could not be found. + /// + LinkedIdMissing = 250, + + /// + /// Error code indicating that a user with a linked (e.g. Facebook) account + /// has an invalid session. + /// + InvalidLinkedSession = 251, + + /// + /// Error code indicating that a service being linked (e.g. Facebook or + /// Twitter) is unsupported. + /// + UnsupportedService = 252, + /// + /// ERROR 404 + /// + ERROR404 = 404 } + + internal ParseFailureException(ErrorCode code, string message, Exception cause = null) : base(message, cause) => Code = code; + + /// + /// The Parse error code associated with the exception. + /// + public ErrorCode Code { get; private set; } } diff --git a/Parse/Infrastructure/ParseFieldNameAttribute.cs b/Parse/Infrastructure/ParseFieldNameAttribute.cs index 3b5c20df..d8e74e9b 100644 --- a/Parse/Infrastructure/ParseFieldNameAttribute.cs +++ b/Parse/Infrastructure/ParseFieldNameAttribute.cs @@ -1,23 +1,22 @@ using System; -namespace Parse +namespace Parse; + +/// +/// Specifies a field name for a property on a ParseObject subclass. +/// +[AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] +public sealed class ParseFieldNameAttribute : Attribute { /// - /// Specifies a field name for a property on a ParseObject subclass. + /// Constructs a new ParseFieldName attribute. /// - [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] - public sealed class ParseFieldNameAttribute : Attribute - { - /// - /// Constructs a new ParseFieldName attribute. - /// - /// The name of the field on the ParseObject that the - /// property represents. - public ParseFieldNameAttribute(string fieldName) => FieldName = fieldName; + /// The name of the field on the ParseObject that the + /// property represents. + public ParseFieldNameAttribute(string fieldName) => FieldName = fieldName; - /// - /// Gets the name of the field represented by this property. - /// - public string FieldName { get; private set; } - } + /// + /// Gets the name of the field represented by this property. + /// + public string FieldName { get; private set; } } diff --git a/Parse/Infrastructure/RelativeCacheLocationMutator.cs b/Parse/Infrastructure/RelativeCacheLocationMutator.cs index fe332424..ea4094de 100644 --- a/Parse/Infrastructure/RelativeCacheLocationMutator.cs +++ b/Parse/Infrastructure/RelativeCacheLocationMutator.cs @@ -1,28 +1,30 @@ using Parse.Abstractions.Infrastructure; -namespace Parse.Infrastructure +namespace Parse.Infrastructure; + +/// +/// An for the relative cache file location. This should be used if the relative cache file location is not created correctly by the SDK, such as platforms on which it is not possible to gather metadata about the client assembly, or ones on which is inaccsessible. +/// +public class RelativeCacheLocationMutator : IServiceHubMutator { /// - /// An for the relative cache file location. This should be used if the relative cache file location is not created correctly by the SDK, such as platforms on which it is not possible to gather metadata about the client assembly, or ones on which is inaccsessible. + /// An implementation instance which creates a path that should be used as the -relative cache location. /// - public class RelativeCacheLocationMutator : IServiceHubMutator - { - /// - /// An implementation instance which creates a path that should be used as the -relative cache location. - /// - public IRelativeCacheLocationGenerator RelativeCacheLocationGenerator { get; set; } + public IRelativeCacheLocationGenerator RelativeCacheLocationGenerator { get; set; } - /// - /// - /// - public bool Valid => RelativeCacheLocationGenerator is { }; + /// + /// + /// + public bool Valid => RelativeCacheLocationGenerator is { }; - /// - /// - /// - /// - /// - public void Mutate(ref IMutableServiceHub target, in IServiceHub referenceHub) => target.CacheController = (target as IServiceHub).CacheController switch + /// + /// + /// + /// + /// + public void Mutate(ref IMutableServiceHub target, in IServiceHub referenceHub) + { + target.CacheController = (target as IServiceHub).CacheController switch { null => new CacheController { RelativeCacheFilePath = RelativeCacheLocationGenerator.GetRelativeCacheFilePath(referenceHub) }, IDiskFileCacheController { } controller => (Controller: controller, controller.RelativeCacheFilePath = RelativeCacheLocationGenerator.GetRelativeCacheFilePath(referenceHub)).Controller, diff --git a/Parse/Infrastructure/ServerConnectionData.cs b/Parse/Infrastructure/ServerConnectionData.cs index 6cedaccc..7b69ff7d 100644 --- a/Parse/Infrastructure/ServerConnectionData.cs +++ b/Parse/Infrastructure/ServerConnectionData.cs @@ -1,43 +1,42 @@ using System.Collections.Generic; using Parse.Abstractions.Infrastructure; -namespace Parse.Infrastructure +namespace Parse.Infrastructure; + +/// +/// Represents the configuration of the Parse SDK. +/// +public struct ServerConnectionData : IServerConnectionData { + + // TODO: Move Test property elsewhere. + + internal bool Test { get; set; } + + /// + /// The App ID of your app. + /// + public string ApplicationID { get; set; } + + /// + /// A URI pointing to the target Parse Server instance hosting the app targeted by . + /// + public string ServerURI { get; set; } + + /// + /// The .NET Key for the Parse app targeted by . + /// + public string Key { get; set; } + + /// + /// The Master Key for the Parse app targeted by . + /// + public string MasterKey { get; set; } + + // ALTERNATE NAME: AuxiliaryHeaders, AdditionalHeaders + /// - /// Represents the configuration of the Parse SDK. + /// Additional HTTP headers to be sent with network requests from the SDK. /// - public struct ServerConnectionData : IServerConnectionData - { - // TODO: Consider simplification of names: ApplicationID => Application | Target, ServerURI => Server, MasterKey => Master. - // TODO: Move Test property elsewhere. - - internal bool Test { get; set; } - - /// - /// The App ID of your app. - /// - public string ApplicationID { get; set; } - - /// - /// A URI pointing to the target Parse Server instance hosting the app targeted by . - /// - public string ServerURI { get; set; } - - /// - /// The .NET Key for the Parse app targeted by . - /// - public string Key { get; set; } - - /// - /// The Master Key for the Parse app targeted by . - /// - public string MasterKey { get; set; } - - // ALTERNATE NAME: AuxiliaryHeaders, AdditionalHeaders - - /// - /// Additional HTTP headers to be sent with network requests from the SDK. - /// - public IDictionary Headers { get; set; } - } + public IDictionary Headers { get; set; } } diff --git a/Parse/Infrastructure/ServiceHub.cs b/Parse/Infrastructure/ServiceHub.cs index 564b7bf6..dbff4b24 100644 --- a/Parse/Infrastructure/ServiceHub.cs +++ b/Parse/Infrastructure/ServiceHub.cs @@ -26,48 +26,50 @@ using Parse.Platform.Sessions; using Parse.Platform.Users; -namespace Parse.Infrastructure -{ +namespace Parse.Infrastructure; - /// - /// A service hub that uses late initialization to efficiently provide controllers and other dependencies to internal Parse SDK systems. - /// - public class ServiceHub : IServiceHub - { - LateInitializer LateInitializer { get; } = new LateInitializer { }; - public IServerConnectionData ServerConnectionData { get; set; } - public IMetadataController MetadataController => LateInitializer.GetValue(() => new MetadataController { HostManifestData = HostManifestData.Inferred, EnvironmentData = EnvironmentData.Inferred }); +/// +/// A service hub that uses late initialization to efficiently provide controllers and other dependencies to internal Parse SDK systems. +/// +public class ServiceHub : IServiceHub +{ + LateInitializer LateInitializer { get; } = new LateInitializer { }; - public IServiceHubCloner Cloner => LateInitializer.GetValue(() => new { } as object as IServiceHubCloner); + public IServerConnectionData ServerConnectionData { get; set; } + public IMetadataController MetadataController => LateInitializer.GetValue(() => new MetadataController { HostManifestData = HostManifestData.Inferred, EnvironmentData = EnvironmentData.Inferred }); - public IWebClient WebClient => LateInitializer.GetValue(() => new UniversalWebClient { }); - public ICacheController CacheController => LateInitializer.GetValue(() => new CacheController { }); - public IParseObjectClassController ClassController => LateInitializer.GetValue(() => new ParseObjectClassController { }); + public IServiceHubCloner Cloner => LateInitializer.GetValue(() => new { } as object as IServiceHubCloner); - public IParseDataDecoder Decoder => LateInitializer.GetValue(() => new ParseDataDecoder(ClassController)); + public IWebClient WebClient => LateInitializer.GetValue(() => new UniversalWebClient { }); + public ICacheController CacheController => LateInitializer.GetValue(() => new CacheController { }); + public IParseObjectClassController ClassController => LateInitializer.GetValue(() => new ParseObjectClassController { }); - public IParseInstallationController InstallationController => LateInitializer.GetValue(() => new ParseInstallationController(CacheController)); - public IParseCommandRunner CommandRunner => LateInitializer.GetValue(() => new ParseCommandRunner(WebClient, InstallationController, MetadataController, ServerConnectionData, new Lazy(() => UserController))); + public IParseDataDecoder Decoder => LateInitializer.GetValue(() => new ParseDataDecoder(ClassController)); - public IParseCloudCodeController CloudCodeController => LateInitializer.GetValue(() => new ParseCloudCodeController(CommandRunner, Decoder)); - public IParseConfigurationController ConfigurationController => LateInitializer.GetValue(() => new ParseConfigurationController(CommandRunner, CacheController, Decoder)); - public IParseFileController FileController => LateInitializer.GetValue(() => new ParseFileController(CommandRunner)); - public IParseObjectController ObjectController => LateInitializer.GetValue(() => new ParseObjectController(CommandRunner, Decoder, ServerConnectionData)); - public IParseQueryController QueryController => LateInitializer.GetValue(() => new ParseQueryController(CommandRunner, Decoder)); - public IParseSessionController SessionController => LateInitializer.GetValue(() => new ParseSessionController(CommandRunner, Decoder)); - public IParseUserController UserController => LateInitializer.GetValue(() => new ParseUserController(CommandRunner, Decoder)); - public IParseCurrentUserController CurrentUserController => LateInitializer.GetValue(() => new ParseCurrentUserController(CacheController, ClassController, Decoder)); + public IParseInstallationController InstallationController => LateInitializer.GetValue(() => new ParseInstallationController(CacheController)); + public IParseCommandRunner CommandRunner => LateInitializer.GetValue(() => new ParseCommandRunner(WebClient, InstallationController, MetadataController, ServerConnectionData, new Lazy(() => UserController))); - public IParseAnalyticsController AnalyticsController => LateInitializer.GetValue(() => new ParseAnalyticsController(CommandRunner)); + public IParseCloudCodeController CloudCodeController => LateInitializer.GetValue(() => new ParseCloudCodeController(CommandRunner, Decoder)); + public IParseConfigurationController ConfigurationController => LateInitializer.GetValue(() => new ParseConfigurationController(CommandRunner, CacheController, Decoder)); + public IParseFileController FileController => LateInitializer.GetValue(() => new ParseFileController(CommandRunner)); + public IParseObjectController ObjectController => LateInitializer.GetValue(() => new ParseObjectController(CommandRunner, Decoder, ServerConnectionData)); + public IParseQueryController QueryController => LateInitializer.GetValue(() => new ParseQueryController(CommandRunner, Decoder)); + public IParseSessionController SessionController => LateInitializer.GetValue(() => new ParseSessionController(CommandRunner, Decoder)); + public IParseUserController UserController => LateInitializer.GetValue(() => new ParseUserController(CommandRunner, Decoder)); + public IParseCurrentUserController CurrentUserController => LateInitializer.GetValue(() => new ParseCurrentUserController(CacheController, ClassController, Decoder)); - public IParseInstallationCoder InstallationCoder => LateInitializer.GetValue(() => new ParseInstallationCoder(Decoder, ClassController)); + public IParseAnalyticsController AnalyticsController => LateInitializer.GetValue(() => new ParseAnalyticsController(CommandRunner)); - public IParsePushChannelsController PushChannelsController => LateInitializer.GetValue(() => new ParsePushChannelsController(CurrentInstallationController)); - public IParsePushController PushController => LateInitializer.GetValue(() => new ParsePushController(CommandRunner, CurrentUserController)); - public IParseCurrentInstallationController CurrentInstallationController => LateInitializer.GetValue(() => new ParseCurrentInstallationController(InstallationController, CacheController, InstallationCoder, ClassController)); - public IParseInstallationDataFinalizer InstallationDataFinalizer => LateInitializer.GetValue(() => new ParseInstallationDataFinalizer { }); + public IParseInstallationCoder InstallationCoder => LateInitializer.GetValue(() => new ParseInstallationCoder(Decoder, ClassController)); - public bool Reset() => LateInitializer.Used && LateInitializer.Reset(); + public IParsePushChannelsController PushChannelsController => LateInitializer.GetValue(() => new ParsePushChannelsController(CurrentInstallationController)); + public IParsePushController PushController => LateInitializer.GetValue(() => new ParsePushController(CommandRunner, CurrentUserController)); + public IParseCurrentInstallationController CurrentInstallationController => LateInitializer.GetValue(() => new ParseCurrentInstallationController(InstallationController, CacheController, InstallationCoder, ClassController)); + public IParseInstallationDataFinalizer InstallationDataFinalizer => LateInitializer.GetValue(() => new ParseInstallationDataFinalizer { }); + + public bool Reset() + { + return LateInitializer.Used && LateInitializer.Reset(); } } diff --git a/Parse/Infrastructure/TransientCacheController.cs b/Parse/Infrastructure/TransientCacheController.cs index ccfd62f7..2a1108cb 100644 --- a/Parse/Infrastructure/TransientCacheController.cs +++ b/Parse/Infrastructure/TransientCacheController.cs @@ -5,43 +5,54 @@ using Parse.Abstractions.Infrastructure; using static Parse.Resources; -namespace Parse.Infrastructure +namespace Parse.Infrastructure; + +public class TransientCacheController : ICacheController { - public class TransientCacheController : ICacheController + class VirtualCache : Dictionary, IDataCache { - class VirtualCache : Dictionary, IDataCache + public Task AddAsync(string key, object value) + { + Add(key, value); + return Task.CompletedTask; + } + + public Task RemoveAsync(string key) { - public Task AddAsync(string key, object value) - { - Add(key, value); - return Task.CompletedTask; - } - - public Task RemoveAsync(string key) - { - Remove(key); - return Task.CompletedTask; - } + Remove(key); + return Task.CompletedTask; } + } - VirtualCache Cache { get; } = new VirtualCache { }; + VirtualCache Cache { get; } = new VirtualCache { }; - public void Clear() => Cache.Clear(); + public void Clear() + { + Cache.Clear(); + } - public FileInfo GetRelativeFile(string path) => throw new NotSupportedException(TransientCacheControllerDiskFileOperationNotSupportedMessage); + public FileInfo GetRelativeFile(string path) + { + throw new NotSupportedException(TransientCacheControllerDiskFileOperationNotSupportedMessage); + } - public Task> LoadAsync() => Task.FromResult>(Cache); + public Task> LoadAsync() + { + return Task.FromResult>(Cache); + } - public Task> SaveAsync(IDictionary contents) + public Task> SaveAsync(IDictionary contents) + { + foreach (KeyValuePair pair in contents) { - foreach (KeyValuePair pair in contents) - { - ((IDictionary) Cache).Add(pair); - } - - return Task.FromResult>(Cache); + ((IDictionary) Cache).Add(pair); } - public Task TransferAsync(string originFilePath, string targetFilePath) => Task.FromException(new NotSupportedException(TransientCacheControllerDiskFileOperationNotSupportedMessage)); + return Task.FromResult>(Cache); + } + + public Task TransferAsync(string originFilePath, string targetFilePath) + { + return Task.FromException(new NotSupportedException(TransientCacheControllerDiskFileOperationNotSupportedMessage)); } } diff --git a/Parse/Infrastructure/Utilities/AssemblyLister.cs b/Parse/Infrastructure/Utilities/AssemblyLister.cs index c3105903..c62757f5 100644 --- a/Parse/Infrastructure/Utilities/AssemblyLister.cs +++ b/Parse/Infrastructure/Utilities/AssemblyLister.cs @@ -3,45 +3,44 @@ using System.Linq; using System.Reflection; -namespace Parse.Infrastructure.Utilities +namespace Parse.Infrastructure.Utilities; + +/// +/// A class that lets you list all loaded assemblies in a PCL-compliant way. +/// +public static class Lister { /// - /// A class that lets you list all loaded assemblies in a PCL-compliant way. + /// Get all of the assemblies used by this application. /// - public static class Lister + public static IEnumerable AllAssemblies { - /// - /// Get all of the assemblies used by this application. - /// - public static IEnumerable AllAssemblies + get { - get - { - // For each of the loaded assemblies, deeply walk all of their references. - HashSet seen = new HashSet(); - return AppDomain.CurrentDomain.GetAssemblies().SelectMany(asm => asm.DeepWalkReferences(seen)); - } + // For each of the loaded assemblies, deeply walk all of their references. + HashSet seen = new HashSet(); + return AppDomain.CurrentDomain.GetAssemblies().SelectMany(asm => asm.DeepWalkReferences(seen)); } + } - private static IEnumerable DeepWalkReferences(this Assembly assembly, HashSet seen = null) - { - seen ??= new HashSet(); - - if (!seen.Add(assembly.FullName)) - return Enumerable.Empty(); + private static IEnumerable DeepWalkReferences(this Assembly assembly, HashSet seen = null) + { + seen ??= new HashSet(); - List assemblies = new List { assembly }; + if (!seen.Add(assembly.FullName)) + return Enumerable.Empty(); - foreach (AssemblyName reference in assembly.GetReferencedAssemblies()) - { - if (seen.Contains(reference.FullName)) - continue; + List assemblies = new List { assembly }; - Assembly referencedAsm = Assembly.Load(reference); - assemblies.AddRange(referencedAsm.DeepWalkReferences(seen)); - } + foreach (AssemblyName reference in assembly.GetReferencedAssemblies()) + { + if (seen.Contains(reference.FullName)) + continue; - return assemblies; + Assembly referencedAsm = Assembly.Load(reference); + assemblies.AddRange(referencedAsm.DeepWalkReferences(seen)); } + + return assemblies; } } diff --git a/Parse/Infrastructure/Utilities/Conversion.cs b/Parse/Infrastructure/Utilities/Conversion.cs index 6cfa698a..2cf3f627 100644 --- a/Parse/Infrastructure/Utilities/Conversion.cs +++ b/Parse/Infrastructure/Utilities/Conversion.cs @@ -1,96 +1,126 @@ using System; using System.Collections.Generic; +using System.Diagnostics; -namespace Parse.Infrastructure.Utilities -{ +namespace Parse.Infrastructure.Utilities; + +#pragma warning disable CS1030 // #warning directive #warning Possibly should be refactored. +/// +/// A set of utilities for converting generic types between each other. +/// +public static class Conversion +#pragma warning restore CS1030 // #warning directive +{ /// - /// A set of utilities for converting generic types between each other. + /// Converts a value to the requested type -- coercing primitives to + /// the desired type, wrapping lists and dictionaries appropriately, + /// or else returning null. + /// + /// This should be used on any containers that might be coming from a + /// user to normalize the collection types. Collection types coming from + /// JSON deserialization can be safely assumed to be lists or dictionaries of + /// objects. /// - public static class Conversion + public static T As(object value) where T : class { - /// - /// Converts a value to the requested type -- coercing primitives to - /// the desired type, wrapping lists and dictionaries appropriately, - /// or else returning null. - /// - /// This should be used on any containers that might be coming from a - /// user to normalize the collection types. Collection types coming from - /// JSON deserialization can be safely assumed to be lists or dictionaries of - /// objects. - /// - public static T As(object value) where T : class => ConvertTo(value) as T; - - /// - /// Converts a value to the requested type -- coercing primitives to - /// the desired type, wrapping lists and dictionaries appropriately, - /// or else throwing an exception. - /// - /// This should be used on any containers that might be coming from a - /// user to normalize the collection types. Collection types coming from - /// JSON deserialization can be safely assumed to be lists or dictionaries of - /// objects. - /// - public static T To(object value) => (T) ConvertTo(value); - - /// - /// Converts a value to the requested type -- coercing primitives to - /// the desired type, wrapping lists and dictionaries appropriately, - /// or else passing the object along to the caller unchanged. - /// - /// This should be used on any containers that might be coming from a - /// user to normalize the collection types. Collection types coming from - /// JSON deserialization can be safely assumed to be lists or dictionaries of - /// objects. - /// - internal static object ConvertTo(object value) - { - if (value is T || value == null) - return value; + return ConvertTo(value) as T; + } - if (typeof(T).IsPrimitive) - return (T) Convert.ChangeType(value, typeof(T), System.Globalization.CultureInfo.InvariantCulture); + /// + /// Converts a value to the requested type -- coercing primitives to + /// the desired type, wrapping lists and dictionaries appropriately, + /// or else throwing an exception. + /// + /// This should be used on any containers that might be coming from a + /// user to normalize the collection types. Collection types coming from + /// JSON deserialization can be safely assumed to be lists or dictionaries of + /// objects. + /// + public static T To(object value) + { + return (T) ConvertTo(value); + } + internal static object ConvertTo(object value) + { + if (value is T || value == null) + return value; - if (typeof(T).IsConstructedGenericType) + if (typeof(T).IsPrimitive) + { + // Special case for JSON deserialized strings that represent numbers + if (value is string stringValue) { - // Add lifting for nullables. Only supports conversions between primitives. + if (typeof(T) == typeof(float) && float.TryParse(stringValue, out float floatValue)) + return floatValue; + + if (typeof(T) == typeof(double) && double.TryParse(stringValue, out double doubleValue)) + return doubleValue; + + if (typeof(T) == typeof(int) && int.TryParse(stringValue, out int intValue)) + return intValue; + + if (typeof(T) == typeof(long) && long.TryParse(stringValue, out long longValue)) + return longValue; - if (typeof(T).CheckWrappedWithNullable() && typeof(T).GenericTypeArguments[0] is { IsPrimitive: true } innerType) - return (T) Convert.ChangeType(value, innerType, System.Globalization.CultureInfo.InvariantCulture); + if (typeof(T) == typeof(decimal) && decimal.TryParse(stringValue, out decimal decimalValue)) + return decimalValue; - if (GetInterfaceType(value.GetType(), typeof(IList<>)) is { } listType && typeof(T).GetGenericTypeDefinition() == typeof(IList<>)) - return Activator.CreateInstance(typeof(FlexibleListWrapper<,>).MakeGenericType(typeof(T).GenericTypeArguments[0], listType.GenericTypeArguments[0]), value); + if (typeof(T) == typeof(short) && short.TryParse(stringValue, out short shortValue)) + return shortValue; - if (GetInterfaceType(value.GetType(), typeof(IDictionary<,>)) is { } dictType && typeof(T).GetGenericTypeDefinition() == typeof(IDictionary<,>)) - return Activator.CreateInstance(typeof(FlexibleDictionaryWrapper<,>).MakeGenericType(typeof(T).GenericTypeArguments[1], dictType.GenericTypeArguments[1]), value); + if (typeof(T) == typeof(byte) && byte.TryParse(stringValue, out byte byteValue)) + return byteValue; + + if (typeof(T) == typeof(sbyte) && SByte.TryParse(stringValue, out sbyte sbyteValue)) + return sbyteValue; + + if (typeof(T) == typeof(bool) && Boolean.TryParse(stringValue, out bool boolValue)) + return boolValue; + + if (typeof(T) == typeof(char) && stringValue.Length == 1) + return stringValue[0]; // Returns the first character if the string length is 1 } + + return (T) Convert.ChangeType(value, typeof(T), System.Globalization.CultureInfo.InvariantCulture); + } + if (typeof(T).IsConstructedGenericType) + { + if (typeof(T).CheckWrappedWithNullable() && typeof(T).GenericTypeArguments[0] is { IsPrimitive: true } innerType) + return (T) Convert.ChangeType(value, innerType, System.Globalization.CultureInfo.InvariantCulture); - return value; + if (GetInterfaceType(value.GetType(), typeof(IList<>)) is { } listType && typeof(T).GetGenericTypeDefinition() == typeof(IList<>)) + return Activator.CreateInstance(typeof(FlexibleListWrapper<,>).MakeGenericType(typeof(T).GenericTypeArguments[0], listType.GenericTypeArguments[0]), value); + + if (GetInterfaceType(value.GetType(), typeof(IDictionary<,>)) is { } dictType && typeof(T).GetGenericTypeDefinition() == typeof(IDictionary<,>)) + return Activator.CreateInstance(typeof(FlexibleDictionaryWrapper<,>).MakeGenericType(typeof(T).GenericTypeArguments[1], dictType.GenericTypeArguments[1]), value); } - /// - /// Holds a dictionary that maps a cache of interface types for related concrete types. - /// The lookup is slow the first time for each type because it has to enumerate all interface - /// on the object type, but made fast by the cache. - /// - /// The map is: - /// (object type, generic interface type) => constructed generic type - /// - static Dictionary, Type> InterfaceLookupCache { get; } = new Dictionary, Type>(); - - static Type GetInterfaceType(Type objType, Type genericInterfaceType) - { - Tuple cacheKey = new Tuple(objType, genericInterfaceType); + return value; + } + + /// + /// Holds a dictionary that maps a cache of interface types for related concrete types. + /// The lookup is slow the first time for each type because it has to enumerate all interface + /// on the object type, but made fast by the cache. + /// + /// The map is: + /// (object type, generic interface type) => constructed generic type + /// + static Dictionary, Type> InterfaceLookupCache { get; } = new Dictionary, Type>(); + + static Type GetInterfaceType(Type objType, Type genericInterfaceType) + { + Tuple cacheKey = new Tuple(objType, genericInterfaceType); - if (InterfaceLookupCache.ContainsKey(cacheKey)) - return InterfaceLookupCache[cacheKey]; + if (InterfaceLookupCache.ContainsKey(cacheKey)) + return InterfaceLookupCache[cacheKey]; - foreach (Type type in objType.GetInterfaces()) - if (type.IsConstructedGenericType && type.GetGenericTypeDefinition() == genericInterfaceType) - return InterfaceLookupCache[cacheKey] = type; + foreach (Type type in objType.GetInterfaces()) + if (type.IsConstructedGenericType && type.GetGenericTypeDefinition() == genericInterfaceType) + return InterfaceLookupCache[cacheKey] = type; - return default; - } + return default; } } \ No newline at end of file diff --git a/Parse/Infrastructure/Utilities/FileUtilities.cs b/Parse/Infrastructure/Utilities/FileUtilities.cs index 43280b2b..d9ed9c6d 100644 --- a/Parse/Infrastructure/Utilities/FileUtilities.cs +++ b/Parse/Infrastructure/Utilities/FileUtilities.cs @@ -2,35 +2,34 @@ using System.Text; using System.Threading.Tasks; -namespace Parse.Infrastructure.Utilities +namespace Parse.Infrastructure.Utilities; + +/// +/// A collection of utility methods and properties for writing to the app-specific persistent storage folder. +/// +internal static class FileUtilities { /// - /// A collection of utility methods and properties for writing to the app-specific persistent storage folder. + /// Asynchronously read all of the little-endian 16-bit character units (UTF-16) contained within the file wrapped by the provided instance. /// - internal static class FileUtilities + /// The instance wrapping the target file that string content is to be read from + /// A task that should contain the little-endian 16-bit character string (UTF-16) extracted from the if the read completes successfully + public static async Task ReadAllTextAsync(this FileInfo file) { - /// - /// Asynchronously read all of the little-endian 16-bit character units (UTF-16) contained within the file wrapped by the provided instance. - /// - /// The instance wrapping the target file that string content is to be read from - /// A task that should contain the little-endian 16-bit character string (UTF-16) extracted from the if the read completes successfully - public static async Task ReadAllTextAsync(this FileInfo file) - { - using StreamReader reader = new StreamReader(file.OpenRead(), Encoding.Unicode); - return await reader.ReadToEndAsync(); - } + using StreamReader reader = new StreamReader(file.OpenRead(), Encoding.Unicode); + return await reader.ReadToEndAsync(); + } - /// - /// Asynchronously writes the provided little-endian 16-bit character string to the file wrapped by the provided instance. - /// - /// The instance wrapping the target file that is to be written to - /// The little-endian 16-bit Unicode character string (UTF-16) that is to be written to the - /// A task that completes once the write operation to the completes - public static async Task WriteContentAsync(this FileInfo file, string content) - { - using FileStream stream = new FileStream(Path.GetFullPath(file.FullName), FileMode.Create, FileAccess.Write, FileShare.Read, 4096, FileOptions.SequentialScan | FileOptions.Asynchronous); - byte[] data = Encoding.Unicode.GetBytes(content); - await stream.WriteAsync(data, 0, data.Length); - } + /// + /// Asynchronously writes the provided little-endian 16-bit character string to the file wrapped by the provided instance. + /// + /// The instance wrapping the target file that is to be written to + /// The little-endian 16-bit Unicode character string (UTF-16) that is to be written to the + /// A task that completes once the write operation to the completes + public static async Task WriteContentAsync(this FileInfo file, string content) + { + using FileStream stream = new FileStream(Path.GetFullPath(file.FullName), FileMode.Create, FileAccess.Write, FileShare.Read, 4096, FileOptions.SequentialScan | FileOptions.Asynchronous); + byte[] data = Encoding.Unicode.GetBytes(content); + await stream.WriteAsync(data, 0, data.Length); } } diff --git a/Parse/Infrastructure/Utilities/FlexibleDictionaryWrapper.cs b/Parse/Infrastructure/Utilities/FlexibleDictionaryWrapper.cs index f68be7b8..5ae8b12b 100644 --- a/Parse/Infrastructure/Utilities/FlexibleDictionaryWrapper.cs +++ b/Parse/Infrastructure/Utilities/FlexibleDictionaryWrapper.cs @@ -1,75 +1,98 @@ using System.Collections.Generic; using System.Linq; -namespace Parse.Infrastructure.Utilities +namespace Parse.Infrastructure.Utilities; + +/// +/// Provides a Dictionary implementation that can delegate to any other +/// dictionary, regardless of its value type. Used for coercion of +/// dictionaries when returning them to users. +/// +/// The resulting type of value in the dictionary. +/// The original type of value in the dictionary. +[Preserve(AllMembers = true, Conditional = false)] +public class FlexibleDictionaryWrapper : IDictionary { - /// - /// Provides a Dictionary implementation that can delegate to any other - /// dictionary, regardless of its value type. Used for coercion of - /// dictionaries when returning them to users. - /// - /// The resulting type of value in the dictionary. - /// The original type of value in the dictionary. - [Preserve(AllMembers = true, Conditional = false)] - public class FlexibleDictionaryWrapper : IDictionary - { - private readonly IDictionary toWrap; - public FlexibleDictionaryWrapper(IDictionary toWrap) => this.toWrap = toWrap; + private readonly IDictionary toWrap; + public FlexibleDictionaryWrapper(IDictionary toWrap) => this.toWrap = toWrap; - public void Add(string key, TOut value) => toWrap.Add(key, (TIn) Conversion.ConvertTo(value)); + public void Add(string key, TOut value) + { + toWrap.Add(key, (TIn) Conversion.ConvertTo(value)); + } - public bool ContainsKey(string key) => toWrap.ContainsKey(key); + public bool ContainsKey(string key) + { + return toWrap.ContainsKey(key); + } - public ICollection Keys => toWrap.Keys; + public ICollection Keys => toWrap.Keys; - public bool Remove(string key) => toWrap.Remove(key); + public bool Remove(string key) + { + return toWrap.Remove(key); + } - public bool TryGetValue(string key, out TOut value) - { - bool result = toWrap.TryGetValue(key, out TIn outValue); - value = (TOut) Conversion.ConvertTo(outValue); - return result; - } + public bool TryGetValue(string key, out TOut value) + { + bool result = toWrap.TryGetValue(key, out TIn outValue); + value = (TOut) Conversion.ConvertTo(outValue); + return result; + } - public ICollection Values => toWrap.Values - .Select(item => (TOut) Conversion.ConvertTo(item)).ToList(); + public ICollection Values => toWrap.Values + .Select(item => (TOut) Conversion.ConvertTo(item)).ToList(); - public TOut this[string key] - { - get => (TOut) Conversion.ConvertTo(toWrap[key]); - set => toWrap[key] = (TIn) Conversion.ConvertTo(value); - } + public TOut this[string key] + { + get => (TOut) Conversion.ConvertTo(toWrap[key]); + set => toWrap[key] = (TIn) Conversion.ConvertTo(value); + } - public void Add(KeyValuePair item) => toWrap.Add(new KeyValuePair(item.Key, - (TIn) Conversion.ConvertTo(item.Value))); + public void Add(KeyValuePair item) + { + toWrap.Add(new KeyValuePair(item.Key, + (TIn) Conversion.ConvertTo(item.Value))); + } - public void Clear() => toWrap.Clear(); + public void Clear() + { + toWrap.Clear(); + } - public bool Contains(KeyValuePair item) => toWrap.Contains(new KeyValuePair(item.Key, - (TIn) Conversion.ConvertTo(item.Value))); + public bool Contains(KeyValuePair item) + { + return toWrap.Contains(new KeyValuePair(item.Key, + (TIn) Conversion.ConvertTo(item.Value))); + } - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - IEnumerable> converted = from pair in toWrap - select new KeyValuePair(pair.Key, - (TOut) Conversion.ConvertTo(pair.Value)); - converted.ToList().CopyTo(array, arrayIndex); - } + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + IEnumerable> converted = from pair in toWrap + select new KeyValuePair(pair.Key, + (TOut) Conversion.ConvertTo(pair.Value)); + converted.ToList().CopyTo(array, arrayIndex); + } - public int Count => toWrap.Count; + public int Count => toWrap.Count; - public bool IsReadOnly => toWrap.IsReadOnly; + public bool IsReadOnly => toWrap.IsReadOnly; - public bool Remove(KeyValuePair item) => toWrap.Remove(new KeyValuePair(item.Key, - (TIn) Conversion.ConvertTo(item.Value))); + public bool Remove(KeyValuePair item) + { + return toWrap.Remove(new KeyValuePair(item.Key, + (TIn) Conversion.ConvertTo(item.Value))); + } - public IEnumerator> GetEnumerator() - { - foreach (KeyValuePair pair in toWrap) - yield return new KeyValuePair(pair.Key, - (TOut) Conversion.ConvertTo(pair.Value)); - } + public IEnumerator> GetEnumerator() + { + foreach (KeyValuePair pair in toWrap) + yield return new KeyValuePair(pair.Key, + (TOut) Conversion.ConvertTo(pair.Value)); + } - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); } } diff --git a/Parse/Infrastructure/Utilities/FlexibleListWrapper.cs b/Parse/Infrastructure/Utilities/FlexibleListWrapper.cs index 875b2806..adfe2890 100644 --- a/Parse/Infrastructure/Utilities/FlexibleListWrapper.cs +++ b/Parse/Infrastructure/Utilities/FlexibleListWrapper.cs @@ -2,54 +2,80 @@ using System.Collections.Generic; using System.Linq; -namespace Parse.Infrastructure.Utilities +namespace Parse.Infrastructure.Utilities; + +/// +/// Provides a List implementation that can delegate to any other +/// list, regardless of its value type. Used for coercion of +/// lists when returning them to users. +/// +/// The resulting type of value in the list. +/// The original type of value in the list. +[Preserve(AllMembers = true, Conditional = false)] +public class FlexibleListWrapper : IList { - /// - /// Provides a List implementation that can delegate to any other - /// list, regardless of its value type. Used for coercion of - /// lists when returning them to users. - /// - /// The resulting type of value in the list. - /// The original type of value in the list. - [Preserve(AllMembers = true, Conditional = false)] - public class FlexibleListWrapper : IList - { - private IList toWrap; - public FlexibleListWrapper(IList toWrap) => this.toWrap = toWrap; + private IList toWrap; + public FlexibleListWrapper(IList toWrap) => this.toWrap = toWrap; - public int IndexOf(TOut item) => toWrap.IndexOf((TIn) Conversion.ConvertTo(item)); + public int IndexOf(TOut item) + { + return toWrap.IndexOf((TIn) Conversion.ConvertTo(item)); + } - public void Insert(int index, TOut item) => toWrap.Insert(index, (TIn) Conversion.ConvertTo(item)); + public void Insert(int index, TOut item) + { + toWrap.Insert(index, (TIn) Conversion.ConvertTo(item)); + } - public void RemoveAt(int index) => toWrap.RemoveAt(index); + public void RemoveAt(int index) + { + toWrap.RemoveAt(index); + } - public TOut this[int index] - { - get => (TOut) Conversion.ConvertTo(toWrap[index]); - set => toWrap[index] = (TIn) Conversion.ConvertTo(value); - } + public TOut this[int index] + { + get => (TOut) Conversion.ConvertTo(toWrap[index]); + set => toWrap[index] = (TIn) Conversion.ConvertTo(value); + } - public void Add(TOut item) => toWrap.Add((TIn) Conversion.ConvertTo(item)); + public void Add(TOut item) + { + toWrap.Add((TIn) Conversion.ConvertTo(item)); + } - public void Clear() => toWrap.Clear(); + public void Clear() + { + toWrap.Clear(); + } - public bool Contains(TOut item) => toWrap.Contains((TIn) Conversion.ConvertTo(item)); + public bool Contains(TOut item) + { + return toWrap.Contains((TIn) Conversion.ConvertTo(item)); + } - public void CopyTo(TOut[] array, int arrayIndex) => toWrap.Select(item => (TOut) Conversion.ConvertTo(item)) - .ToList().CopyTo(array, arrayIndex); + public void CopyTo(TOut[] array, int arrayIndex) + { + toWrap.Select(item => (TOut) Conversion.ConvertTo(item)) + .ToList().CopyTo(array, arrayIndex); + } - public int Count => toWrap.Count; + public int Count => toWrap.Count; - public bool IsReadOnly => toWrap.IsReadOnly; + public bool IsReadOnly => toWrap.IsReadOnly; - public bool Remove(TOut item) => toWrap.Remove((TIn) Conversion.ConvertTo(item)); + public bool Remove(TOut item) + { + return toWrap.Remove((TIn) Conversion.ConvertTo(item)); + } - public IEnumerator GetEnumerator() - { - foreach (object item in (IEnumerable) toWrap) - yield return (TOut) Conversion.ConvertTo(item); - } + public IEnumerator GetEnumerator() + { + foreach (object item in (IEnumerable) toWrap) + yield return (TOut) Conversion.ConvertTo(item); + } - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); } } diff --git a/Parse/Infrastructure/Utilities/IdentityEqualityComparer.cs b/Parse/Infrastructure/Utilities/IdentityEqualityComparer.cs index 6eadf010..8743f3b8 100644 --- a/Parse/Infrastructure/Utilities/IdentityEqualityComparer.cs +++ b/Parse/Infrastructure/Utilities/IdentityEqualityComparer.cs @@ -1,17 +1,22 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; -namespace Parse.Infrastructure.Utilities +namespace Parse.Infrastructure.Utilities; + +/// +/// An equality comparer that uses the object identity (i.e. ReferenceEquals) +/// rather than .Equals, allowing identity to be used for checking equality in +/// ISets and IDictionaries. +/// +public class IdentityEqualityComparer : IEqualityComparer { - /// - /// An equality comparer that uses the object identity (i.e. ReferenceEquals) - /// rather than .Equals, allowing identity to be used for checking equality in - /// ISets and IDictionaries. - /// - public class IdentityEqualityComparer : IEqualityComparer + public bool Equals(T x, T y) { - public bool Equals(T x, T y) => ReferenceEquals(x, y); + return ReferenceEquals(x, y); + } - public int GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj); + public int GetHashCode(T obj) + { + return RuntimeHelpers.GetHashCode(obj); } } diff --git a/Parse/Infrastructure/Utilities/InternalExtensions.cs b/Parse/Infrastructure/Utilities/InternalExtensions.cs index 5607e987..3e3c8794 100644 --- a/Parse/Infrastructure/Utilities/InternalExtensions.cs +++ b/Parse/Infrastructure/Utilities/InternalExtensions.cs @@ -4,89 +4,129 @@ using System.Runtime.ExceptionServices; using System.Threading.Tasks; -namespace Parse.Infrastructure.Utilities +namespace Parse.Infrastructure.Utilities; +/// +/// Provides helper methods that allow us to use terser code elsewhere. +/// +public static class InternalExtensions { /// - /// Provides helper methods that allow us to use terser code elsewhere. + /// Ensures a task (even null) is awaitable. /// - public static class InternalExtensions + public static Task Safe(this Task task) => + task ?? Task.FromResult(default(T)); + + /// + /// Ensures a task (even null) is awaitable. + /// + public static Task Safe(this Task task) => + task ?? Task.CompletedTask; + + public delegate void PartialAccessor(ref T arg); + + /// + /// Gets the value from a dictionary or returns the default value if the key is not found. + /// + public static TValue GetOrDefault(this IDictionary self, TKey key, TValue defaultValue) => + self.TryGetValue(key, out var value) ? value : defaultValue; + + /// + /// Compares two collections for equality. + /// + public static bool CollectionsEqual(this IEnumerable a, IEnumerable b) => + ReferenceEquals(a, b) || (a != null && b != null && a.SequenceEqual(b)); + + /// + /// Executes a continuation on a task that returns a result on success. + /// + public static async Task OnSuccess(this Task task, Func> continuation) { - /// - /// Ensures a task (even null) is awaitable. - /// - /// - /// - /// - public static Task Safe(this Task task) => task ?? Task.FromResult(default(T)); + if (task.IsFaulted) + { + var ex = task.Exception?.Flatten(); + ExceptionDispatchInfo.Capture(ex?.InnerExceptions[0] ?? ex).Throw(); + } + else if (task.IsCanceled) + { + var tcs = new TaskCompletionSource(); + tcs.SetCanceled(); + return await tcs.Task.ConfigureAwait(false); + } - /// - /// Ensures a task (even null) is awaitable. - /// - /// - /// - public static Task Safe(this Task task) => task ?? Task.FromResult(null); + // Ensure continuation returns a Task, then await with ConfigureAwait + var resultTask = continuation(task); + return await resultTask.ConfigureAwait(false); + } - public delegate void PartialAccessor(ref T arg); - public static TValue GetOrDefault(this IDictionary self, - TKey key, - TValue defaultValue) + /// + /// Executes a continuation on a task that has a result type. + /// + public static async Task OnSuccess(this Task task, Func, Task> continuation) + { + if (task.IsFaulted) { - if (self.TryGetValue(key, out TValue value)) - return value; - return defaultValue; + var ex = task.Exception?.Flatten(); + ExceptionDispatchInfo.Capture(ex?.InnerExceptions[0] ?? ex).Throw(); + } + else if (task.IsCanceled) + { + var tcs = new TaskCompletionSource(); + tcs.SetCanceled(); + return await tcs.Task.ConfigureAwait(false); } - public static bool CollectionsEqual(this IEnumerable a, IEnumerable b) => Equals(a, b) || - a != null && b != null && - a.SequenceEqual(b); + // Ensure continuation returns a Task, then await with ConfigureAwait + var resultTask = continuation(task); + return await resultTask.ConfigureAwait(false); + } - public static Task OnSuccess(this Task task, Func, TResult> continuation) => ((Task) task).OnSuccess(t => continuation((Task) t)); + /// + /// Executes a continuation on a task and returns void. + /// + public static async Task OnSuccess(this Task task, Action continuation) + { + if (task.IsFaulted) + { + var ex = task.Exception?.Flatten(); + ExceptionDispatchInfo.Capture(ex?.InnerExceptions[0] ?? ex).Throw(); + } + else if (task.IsCanceled) + { + task = Task.CompletedTask; + } - public static Task OnSuccess(this Task task, Action> continuation) => task.OnSuccess((Func, object>) (t => - { - continuation(t); - return null; - })); + continuation(task); + await task.ConfigureAwait(false); + } - public static Task OnSuccess(this Task task, Func continuation) => task.ContinueWith(t => - { - if (t.IsFaulted) - { - AggregateException ex = t.Exception.Flatten(); - if (ex.InnerExceptions.Count == 1) - ExceptionDispatchInfo.Capture(ex.InnerExceptions[0]).Throw(); - else - ExceptionDispatchInfo.Capture(ex).Throw(); - // Unreachable - return Task.FromResult(default(TResult)); - } - else if (t.IsCanceled) - { - TaskCompletionSource tcs = new TaskCompletionSource(); - tcs.SetCanceled(); - return tcs.Task; - } - else - return Task.FromResult(continuation(t)); - }).Unwrap(); + /// + /// Executes a continuation on a task and returns void, for tasks with result. + /// + public static async Task OnSuccess(this Task task, Action> continuation) + { + if (task.IsFaulted) + { + var ex = task.Exception?.Flatten(); + ExceptionDispatchInfo.Capture(ex?.InnerExceptions[0] ?? ex).Throw(); + } + else if (task.IsCanceled) + { + task = Task.FromResult(default); // Handle canceled task by returning a completed Task + } - public static Task OnSuccess(this Task task, Action continuation) => task.OnSuccess((Func) (t => - { - continuation(t); - return null; - })); + continuation(task); + await task.ConfigureAwait(false); + } - public static Task WhileAsync(Func> predicate, Func body) + /// + /// Executes an asynchronous loop until the predicate evaluates to false. + /// + public static async Task WhileAsync(Func> predicate, Func body) + { + while (await predicate().ConfigureAwait(false)) { - Func iterate = null; - iterate = () => predicate().OnSuccess(t => - { - if (!t.Result) - return Task.FromResult(0); - return body().OnSuccess(_ => iterate()).Unwrap(); - }).Unwrap(); - return iterate(); + await body().ConfigureAwait(false); } } } diff --git a/Parse/Infrastructure/Utilities/JsonUtilities.cs b/Parse/Infrastructure/Utilities/JsonUtilities.cs index 585e2110..c7621d6e 100644 --- a/Parse/Infrastructure/Utilities/JsonUtilities.cs +++ b/Parse/Infrastructure/Utilities/JsonUtilities.cs @@ -1,254 +1,276 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Reflection; using System.Text; using System.Text.RegularExpressions; +using Parse.Platform.Objects; -namespace Parse.Infrastructure.Utilities +namespace Parse.Infrastructure.Utilities; + +/// +/// A simple recursive-descent JSON Parser based on the grammar defined at http://www.json.org +/// and http://tools.ietf.org/html/rfc4627 +/// +public class JsonUtilities { /// - /// A simple recursive-descent JSON Parser based on the grammar defined at http://www.json.org - /// and http://tools.ietf.org/html/rfc4627 + /// Place at the start of a regex to force the match to begin wherever the search starts (i.e. + /// anchored at the index of the first character of the search, even when that search starts + /// in the middle of the string). /// - public class JsonUtilities + private static readonly string startOfString = "\\G"; + private static readonly char startObject = '{'; + private static readonly char endObject = '}'; + private static readonly char startArray = '['; + private static readonly char endArray = ']'; + private static readonly char valueSeparator = ','; + private static readonly char nameSeparator = ':'; + private static readonly char[] falseValue = "false".ToCharArray(); + private static readonly char[] trueValue = "true".ToCharArray(); + private static readonly char[] nullValue = "null".ToCharArray(); + private static readonly Regex numberValue = new Regex(startOfString + @"-?(?:0|[1-9]\d*)(?\.\d+)?(?(?:e|E)(?:-|\+)?\d+)?"); + private static readonly Regex stringValue = new Regex(startOfString + "\"(?(?:[^\\\\\"]|(?\\\\(?:[\\\\\"/bfnrt]|u[0-9a-fA-F]{4})))*)\"", RegexOptions.Multiline); + + private static readonly Regex escapePattern = new Regex("\\\\|\"|[\u0000-\u001F]"); + + private class JsonStringParser { + public string Input { get; private set; } + + public char[] InputAsArray { get; private set; } + public int CurrentIndex { get; private set; } + + public void Skip(int skip) + { + CurrentIndex += skip; + } + + public JsonStringParser(string input) + { + Input = input; + InputAsArray = input.ToCharArray(); + } + /// - /// Place at the start of a regex to force the match to begin wherever the search starts (i.e. - /// anchored at the index of the first character of the search, even when that search starts - /// in the middle of the string). + /// Parses JSON object syntax (e.g. '{}') /// - private static readonly string startOfString = "\\G"; - private static readonly char startObject = '{'; - private static readonly char endObject = '}'; - private static readonly char startArray = '['; - private static readonly char endArray = ']'; - private static readonly char valueSeparator = ','; - private static readonly char nameSeparator = ':'; - private static readonly char[] falseValue = "false".ToCharArray(); - private static readonly char[] trueValue = "true".ToCharArray(); - private static readonly char[] nullValue = "null".ToCharArray(); - private static readonly Regex numberValue = new Regex(startOfString + @"-?(?:0|[1-9]\d*)(?\.\d+)?(?(?:e|E)(?:-|\+)?\d+)?"); - private static readonly Regex stringValue = new Regex(startOfString + "\"(?(?:[^\\\\\"]|(?\\\\(?:[\\\\\"/bfnrt]|u[0-9a-fA-F]{4})))*)\"", RegexOptions.Multiline); - - private static readonly Regex escapePattern = new Regex("\\\\|\"|[\u0000-\u001F]"); - - private class JsonStringParser + internal bool ParseObject(out object output) { - public string Input { get; private set; } + output = null; + int initialCurrentIndex = CurrentIndex; + if (!Accept(startObject)) + return false; - public char[] InputAsArray { get; private set; } - public int CurrentIndex { get; private set; } + Dictionary dict = new Dictionary { }; + while (true) + { + if (!ParseMember(out object pairValue)) + break; - public void Skip(int skip) => CurrentIndex += skip; + Tuple pair = pairValue as Tuple; + dict[pair.Item1] = pair.Item2; + if (!Accept(valueSeparator)) + break; + } + if (!Accept(endObject)) + return false; + output = dict; + return true; + } - public JsonStringParser(string input) + /// + /// Parses JSON member syntax (e.g. '"keyname" : null') + /// + private bool ParseMember(out object output) + { + output = null; + if (!ParseString(out object key)) + return false; + if (!Accept(nameSeparator)) + return false; + if (!ParseValue(out object value)) + return false; + output = new Tuple((string) key, value); + return true; + } + + /// + /// Parses JSON array syntax (e.g. '[]') + /// + internal bool ParseArray(out object output) + { + output = null; + if (!Accept(startArray)) + return false; + List list = new List(); + while (true) { - Input = input; - InputAsArray = input.ToCharArray(); + if (!ParseValue(out object value)) + break; + list.Add(value); + if (!Accept(valueSeparator)) + break; } + if (!Accept(endArray)) + return false; + output = list; + return true; + } - /// - /// Parses JSON object syntax (e.g. '{}') - /// - internal bool ParseObject(out object output) + /// + /// Parses a value (i.e. the right-hand side of an object member assignment or + /// an element in an array) + /// + private bool ParseValue(out object output) + { + if (Accept(falseValue)) { - output = null; - int initialCurrentIndex = CurrentIndex; - if (!Accept(startObject)) - return false; - - Dictionary dict = new Dictionary { }; - while (true) - { - if (!ParseMember(out object pairValue)) - break; - - Tuple pair = pairValue as Tuple; - dict[pair.Item1] = pair.Item2; - if (!Accept(valueSeparator)) - break; - } - if (!Accept(endObject)) - return false; - output = dict; + output = false; return true; } - - /// - /// Parses JSON member syntax (e.g. '"keyname" : null') - /// - private bool ParseMember(out object output) + else if (Accept(nullValue)) { output = null; - if (!ParseString(out object key)) - return false; - if (!Accept(nameSeparator)) - return false; - if (!ParseValue(out object value)) - return false; - output = new Tuple((string) key, value); return true; } - - /// - /// Parses JSON array syntax (e.g. '[]') - /// - internal bool ParseArray(out object output) + else if (Accept(trueValue)) { - output = null; - if (!Accept(startArray)) - return false; - List list = new List(); - while (true) - { - if (!ParseValue(out object value)) - break; - list.Add(value); - if (!Accept(valueSeparator)) - break; - } - if (!Accept(endArray)) - return false; - output = list; + output = true; return true; } + return ParseObject(out output) || + ParseArray(out output) || + ParseNumber(out output) || + ParseString(out output); + } - /// - /// Parses a value (i.e. the right-hand side of an object member assignment or - /// an element in an array) - /// - private bool ParseValue(out object output) + /// + /// Parses a JSON string (e.g. '"foo\u1234bar\n"') + /// + private bool ParseString(out object output) + { + output = null; + if (!Accept(stringValue, out Match m)) + return false; + // handle escapes: + int offset = 0; + Group contentCapture = m.Groups["content"]; + StringBuilder builder = new StringBuilder(contentCapture.Value); + foreach (Capture escape in m.Groups["escape"].Captures) { - if (Accept(falseValue)) - { - output = false; - return true; - } - else if (Accept(nullValue)) - { - output = null; - return true; - } - else if (Accept(trueValue)) + int index = escape.Index - contentCapture.Index - offset; + offset += escape.Length - 1; + builder.Remove(index + 1, escape.Length - 1); + switch (escape.Value[1]) { - output = true; - return true; + case '\"': + builder[index] = '\"'; + break; + case '\\': + builder[index] = '\\'; + break; + case '/': + builder[index] = '/'; + break; + case 'b': + builder[index] = '\b'; + break; + case 'f': + builder[index] = '\f'; + break; + case 'n': + builder[index] = '\n'; + break; + case 'r': + builder[index] = '\r'; + break; + case 't': + builder[index] = '\t'; + break; + case 'u': + builder[index] = (char) UInt16.Parse(escape.Value.Substring(2), NumberStyles.AllowHexSpecifier); + break; + default: + throw new ArgumentException("Unexpected escape character in string: " + escape.Value); } - return ParseObject(out output) || - ParseArray(out output) || - ParseNumber(out output) || - ParseString(out output); } + output = builder.ToString(); + return true; + } - /// - /// Parses a JSON string (e.g. '"foo\u1234bar\n"') - /// - private bool ParseString(out object output) + /// + /// Parses a number. Returns a long if the number is an integer or has an exponent, + /// otherwise returns a double. + /// + private bool ParseNumber(out object output) + { + output = null; + if (!Accept(numberValue, out Match m)) + return false; + if (m.Groups["frac"].Length > 0 || m.Groups["exp"].Length > 0) { - output = null; - if (!Accept(stringValue, out Match m)) - return false; - // handle escapes: - int offset = 0; - Group contentCapture = m.Groups["content"]; - StringBuilder builder = new StringBuilder(contentCapture.Value); - foreach (Capture escape in m.Groups["escape"].Captures) - { - int index = escape.Index - contentCapture.Index - offset; - offset += escape.Length - 1; - builder.Remove(index + 1, escape.Length - 1); - switch (escape.Value[1]) - { - case '\"': - builder[index] = '\"'; - break; - case '\\': - builder[index] = '\\'; - break; - case '/': - builder[index] = '/'; - break; - case 'b': - builder[index] = '\b'; - break; - case 'f': - builder[index] = '\f'; - break; - case 'n': - builder[index] = '\n'; - break; - case 'r': - builder[index] = '\r'; - break; - case 't': - builder[index] = '\t'; - break; - case 'u': - builder[index] = (char) UInt16.Parse(escape.Value.Substring(2), NumberStyles.AllowHexSpecifier); - break; - default: - throw new ArgumentException("Unexpected escape character in string: " + escape.Value); - } - } - output = builder.ToString(); + // It's a double. + output = Double.Parse(m.Value, CultureInfo.InvariantCulture); return true; } - - /// - /// Parses a number. Returns a long if the number is an integer or has an exponent, - /// otherwise returns a double. - /// - private bool ParseNumber(out object output) + else { - output = null; - if (!Accept(numberValue, out Match m)) - return false; - if (m.Groups["frac"].Length > 0 || m.Groups["exp"].Length > 0) + // try to parse to a long assuming it is an integer value (this might fail due to value range differences when storing as double without decimal point or exponent) + if (Int64.TryParse(m.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out long longValue)) { - // It's a double. - output = Double.Parse(m.Value, CultureInfo.InvariantCulture); + output = longValue; return true; } - else + // try to parse as double again (most likely due to value range exceeding long type + else if (Double.TryParse(m.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out double doubleValue)) { - // try to parse to a long assuming it is an integer value (this might fail due to value range differences when storing as double without decimal point or exponent) - if (Int64.TryParse(m.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out long longValue)) - { - output = longValue; - return true; - } - // try to parse as double again (most likely due to value range exceeding long type - else if (Double.TryParse(m.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out double doubleValue)) - { - output = doubleValue; - return true; - } - else - return false; + output = doubleValue; + return true; } + else + return false; } + } + + /// + /// Matches the string to a regex, consuming part of the string and returning the match. + /// + private bool Accept(Regex matcher, out Match match) + { + match = matcher.Match(Input, CurrentIndex); + if (match.Success) + Skip(match.Length); + return match.Success; + } + + /// + /// Find the first occurrences of a character, consuming part of the string. + /// + private bool Accept(char condition) + { + int step = 0; + int strLen = InputAsArray.Length; + int currentStep = CurrentIndex; + char currentChar; - /// - /// Matches the string to a regex, consuming part of the string and returning the match. - /// - private bool Accept(Regex matcher, out Match match) + // Remove whitespace + while (currentStep < strLen && + ((currentChar = InputAsArray[currentStep]) == ' ' || + currentChar == '\r' || + currentChar == '\t' || + currentChar == '\n')) { - match = matcher.Match(Input, CurrentIndex); - if (match.Success) - Skip(match.Length); - return match.Success; + ++step; + ++currentStep; } - /// - /// Find the first occurrences of a character, consuming part of the string. - /// - private bool Accept(char condition) + bool match = currentStep < strLen && InputAsArray[currentStep] == condition; + if (match) { - int step = 0; - int strLen = InputAsArray.Length; - int currentStep = CurrentIndex; - char currentChar; + ++step; + ++currentStep; // Remove whitespace while (currentStep < strLen && @@ -261,167 +283,226 @@ private bool Accept(char condition) ++currentStep; } - bool match = currentStep < strLen && InputAsArray[currentStep] == condition; - if (match) - { - ++step; - ++currentStep; - - // Remove whitespace - while (currentStep < strLen && - ((currentChar = InputAsArray[currentStep]) == ' ' || - currentChar == '\r' || - currentChar == '\t' || - currentChar == '\n')) - { - ++step; - ++currentStep; - } - - Skip(step); - } - return match; + Skip(step); } + return match; + } + + /// + /// Find the first occurrences of a string, consuming part of the string. + /// + private bool Accept(char[] condition) + { + int step = 0; + int strLen = InputAsArray.Length; + int currentStep = CurrentIndex; + char currentChar; - /// - /// Find the first occurrences of a string, consuming part of the string. - /// - private bool Accept(char[] condition) + // Remove whitespace + while (currentStep < strLen && + ((currentChar = InputAsArray[currentStep]) == ' ' || + currentChar == '\r' || + currentChar == '\t' || + currentChar == '\n')) { - int step = 0; - int strLen = InputAsArray.Length; - int currentStep = CurrentIndex; - char currentChar; + ++step; + ++currentStep; + } - // Remove whitespace - while (currentStep < strLen && - ((currentChar = InputAsArray[currentStep]) == ' ' || - currentChar == '\r' || - currentChar == '\t' || - currentChar == '\n')) + bool strMatch = true; + for (int i = 0; currentStep < strLen && i < condition.Length; ++i, ++currentStep) + if (InputAsArray[currentStep] != condition[i]) { - ++step; - ++currentStep; + strMatch = false; + break; } - bool strMatch = true; - for (int i = 0; currentStep < strLen && i < condition.Length; ++i, ++currentStep) - if (InputAsArray[currentStep] != condition[i]) - { - strMatch = false; - break; - } - - bool match = currentStep < strLen && strMatch; - if (match) - Skip(step + condition.Length); - return match; - } + bool match = currentStep < strLen && strMatch; + if (match) + Skip(step + condition.Length); + return match; + } + } + /// + /// Parses a JSON-text as defined in http://tools.ietf.org/html/rfc4627, returning an + /// IDictionary<string, object> or an IList<object> depending on whether + /// the value was an array or dictionary. Nested objects also match these types. + /// Gracefully handles invalid JSON or HTML responses. + /// + public static object Parse(string input) + { + // Gracefully handle empty or whitespace input + if (string.IsNullOrWhiteSpace(input)) + { + // Return an empty JSON object `{}` as a Dictionary + return new Dictionary(); } - /// - /// Parses a JSON-text as defined in http://tools.ietf.org/html/rfc4627, returning an - /// IDictionary<string, object> or an IList<object> depending on whether - /// the value was an array or dictionary. Nested objects also match these types. - /// - public static object Parse(string input) + input = input.Trim(); + + try { - input = input.Trim(); JsonStringParser parser = new JsonStringParser(input); - if ((parser.ParseObject(out object output) || - parser.ParseArray(out output)) && + if ((parser.ParseObject(out object output) || parser.ParseArray(out output)) && parser.CurrentIndex == input.Length) + { return output; - throw new ArgumentException("Input JSON was invalid."); + } + } + catch + { + // Fallback handling for non-JSON input } - /// - /// Encodes a dictionary into a JSON string. Supports values that are - /// IDictionary<string, object>, IList<object>, strings, - /// nulls, and any of the primitive types. - /// - public static string Encode(IDictionary dict) + // Detect HTML responses + if (input.StartsWith(" pair in dict) - { - builder.Append(Encode(pair.Key)); - builder.Append(":"); - builder.Append(Encode(pair.Value)); - builder.Append(","); - } - builder[builder.Length - 1] = '}'; - return builder.ToString(); + return new Dictionary + { + { "error", "Non-JSON response" }, + { "type", "HTML" }, + { "content", ExtractTextFromHtml(input) } + }; } - /// - /// Encodes a list into a JSON string. Supports values that are - /// IDictionary<string, object>, IList<object>, strings, - /// nulls, and any of the primitive types. - /// - public static string Encode(IList list) + // If input is not JSON or HTML, throw an exception + throw new ArgumentException("Input data is neither valid JSON nor recognizable HTML."); + } + + /// + /// Extracts meaningful text from an HTML response, such as the contents of
 tags.
+    /// 
+ private static string ExtractTextFromHtml(string html) + { + try { - if (list == null) - throw new ArgumentNullException(); - if (list.Count == 0) - return "[]"; - StringBuilder builder = new StringBuilder("["); - foreach (object item in list) + int startIndex = html.IndexOf("
", StringComparison.OrdinalIgnoreCase);
+            int endIndex = html.IndexOf("
", StringComparison.OrdinalIgnoreCase); + + if (startIndex != -1 && endIndex != -1) { - builder.Append(Encode(item)); - builder.Append(","); + startIndex += 5; // Skip "
"
+                return html.Substring(startIndex, endIndex - startIndex).Trim();
             }
-            builder[builder.Length - 1] = ']';
-            return builder.ToString();
+
+            // If no 
 tags, return raw HTML as fallback
+            return html;
         }
+        catch
+        {
+            return "Unable to extract meaningful content from HTML.";
+        }
+    }
 
-        /// 
-        /// Encodes an object into a JSON string.
-        /// 
-        public static string Encode(object obj)
+
+
+    /// 
+    /// Encodes a dictionary into a JSON string. Supports values that are
+    /// IDictionary<string, object>, IList<object>, strings,
+    /// nulls, and any of the primitive types.
+    /// 
+    public static string Encode(IDictionary dict)
+    {
+        if (dict == null)
+            throw new ArgumentNullException();
+        if (dict.Count == 0)
+            return "{}";
+        StringBuilder builder = new StringBuilder("{");
+        foreach (KeyValuePair pair in dict)
+        {
+            builder.Append(Encode(pair.Key));
+            builder.Append(":");
+            builder.Append(Encode(pair.Value));
+            builder.Append(",");
+        }
+        builder[builder.Length - 1] = '}';
+        return builder.ToString();
+    }
+
+    /// 
+    /// Encodes a list into a JSON string. Supports values that are
+    /// IDictionary<string, object>, IList<object>, strings,
+    /// nulls, and any of the primitive types.
+    /// 
+    public static string Encode(IList list)
+    {
+        if (list == null)
+            throw new ArgumentNullException();
+        if (list.Count == 0)
+            return "[]";
+        StringBuilder builder = new StringBuilder("[");
+        foreach (object item in list)
+        {
+            builder.Append(Encode(item));
+            builder.Append(",");
+        }
+        builder[builder.Length - 1] = ']';
+        return builder.ToString();
+    }
+
+    /// 
+    /// Encodes an object into a JSON string.
+    /// 
+    public static string Encode(object obj)
+    {
+        if (obj is IDictionary dict)
+            return Encode(dict);
+        if (obj is IList list)
+            return Encode(list);
+        if (obj is string str)
         {
-            if (obj is IDictionary dict)
-                return Encode(dict);
-            if (obj is IList list)
-                return Encode(list);
-            if (obj is string str)
+            str = escapePattern.Replace(str, m =>
             {
-                str = escapePattern.Replace(str, m =>
+                switch (m.Value[0])
                 {
-                    switch (m.Value[0])
-                    {
-                        case '\\':
-                            return "\\\\";
-                        case '\"':
-                            return "\\\"";
-                        case '\b':
-                            return "\\b";
-                        case '\f':
-                            return "\\f";
-                        case '\n':
-                            return "\\n";
-                        case '\r':
-                            return "\\r";
-                        case '\t':
-                            return "\\t";
-                        default:
-                            return "\\u" + ((ushort) m.Value[0]).ToString("x4");
-                    }
-                });
-                return "\"" + str + "\"";
+                    case '\\':
+                        return "\\\\";
+                    case '\"':
+                        return "\\\"";
+                    case '\b':
+                        return "\\b";
+                    case '\f':
+                        return "\\f";
+                    case '\n':
+                        return "\\n";
+                    case '\r':
+                        return "\\r";
+                    case '\t':
+                        return "\\t";
+                    default:
+                        return "\\u" + ((ushort) m.Value[0]).ToString("x4");
+                }
+            });
+            return "\"" + str + "\"";
+        }
+        if (obj is null)
+            return "null";
+        if (obj is bool)
+            return (bool) obj ? "true" : "false";
+        if (!obj.GetType().GetTypeInfo().IsPrimitive)
+        {
+            if (obj is MutableObjectState state)
+            {
+                // Convert MutableObjectState to a dictionary
+                var stateDict = new Dictionary
+            {
+                { "ObjectId", state.ObjectId },
+                { "ClassName", state.ClassName },
+                { "CreatedAt", state.CreatedAt },
+                { "UpdatedAt", state.UpdatedAt },
+                { "ServerData", state.ServerData }
+            };
+
+                // Encode the dictionary recursively
+                return Encode(stateDict);
             }
-            if (obj is null)
-                return "null";
-            if (obj is bool)
-                return (bool) obj ? "true" : "false";
-            if (!obj.GetType().GetTypeInfo().IsPrimitive)
-                throw new ArgumentException("Unable to encode objects of type " + obj.GetType());
-            return Convert.ToString(obj, CultureInfo.InvariantCulture);
+
+            return "null"; // Return "null" for unsupported types
         }
+
+        return Convert.ToString(obj, CultureInfo.InvariantCulture);
     }
+
 }
diff --git a/Parse/Infrastructure/Utilities/LateInitializer.cs b/Parse/Infrastructure/Utilities/LateInitializer.cs
index 6c0d0b4d..07ae32ec 100644
--- a/Parse/Infrastructure/Utilities/LateInitializer.cs
+++ b/Parse/Infrastructure/Utilities/LateInitializer.cs
@@ -6,86 +6,85 @@
 [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Parse.Tests")]
 #endif
 
-namespace Parse.Infrastructure.Utilities
+namespace Parse.Infrastructure.Utilities;
+
+/// 
+/// A wrapper over a dictionary from value generator to value. Uses the fact that lambda expressions in a specific location are cached, so the cost of instantiating a generator delegate is only incurred once at the call site of  and subsequent calls look up the result of the first generation from the dictionary based on the hash of the generator delegate. This is effectively a lazy initialization mechanism that allows the member type to remain unchanged.
+/// 
+internal class LateInitializer
 {
-    /// 
-    /// A wrapper over a dictionary from value generator to value. Uses the fact that lambda expressions in a specific location are cached, so the cost of instantiating a generator delegate is only incurred once at the call site of  and subsequent calls look up the result of the first generation from the dictionary based on the hash of the generator delegate. This is effectively a lazy initialization mechanism that allows the member type to remain unchanged.
-    /// 
-    internal class LateInitializer
-    {
-        Lazy, object>> Storage { get; set; } = new Lazy, object>> { };
+    Lazy, object>> Storage { get; set; } = new Lazy, object>> { };
 
-        public TData GetValue(Func generator)
+    public TData GetValue(Func generator)
+    {
+        lock (generator)
         {
-            lock (generator)
+            if (Storage.IsValueCreated && Storage.Value.Keys.OfType>().FirstOrDefault() is { } key && Storage.Value.TryGetValue(key as Func, out object data))
             {
-                if (Storage.IsValueCreated && Storage.Value.Keys.OfType>().FirstOrDefault() is { } key && Storage.Value.TryGetValue(key as Func, out object data))
-                {
-                    return (TData) data;
-                }
-                else
-                {
-                    TData result = generator.Invoke();
+                return (TData) data;
+            }
+            else
+            {
+                TData result = generator.Invoke();
 
-                    Storage.Value.Add(generator as Func, result);
-                    return result;
-                }
+                Storage.Value.Add(generator as Func, result);
+                return result;
             }
         }
+    }
 
-        public bool ClearValue()
+    public bool ClearValue()
+    {
+        lock (Storage)
         {
-            lock (Storage)
+            if (Storage.IsValueCreated && Storage.Value.Keys.OfType>().FirstOrDefault() is { } key)
             {
-                if (Storage.IsValueCreated && Storage.Value.Keys.OfType>().FirstOrDefault() is { } key)
+                lock (key)
                 {
-                    lock (key)
-                    {
-                        Storage.Value.Remove(key as Func);
-                        return true;
-                    }
+                    Storage.Value.Remove(key as Func);
+                    return true;
                 }
             }
-
-            return false;
         }
 
-        public bool SetValue(TData value, bool initialize = true)
+        return false;
+    }
+
+    public bool SetValue(TData value, bool initialize = true)
+    {
+        lock (Storage)
         {
-            lock (Storage)
+            if (Storage.IsValueCreated && Storage.Value.Keys.OfType>().FirstOrDefault() is { } key)
             {
-                if (Storage.IsValueCreated && Storage.Value.Keys.OfType>().FirstOrDefault() is { } key)
-                {
-                    lock (key)
-                    {
-                        Storage.Value[key as Func] = value;
-                        return true;
-                    }
-                }
-                else if (initialize)
+                lock (key)
                 {
-                    Storage.Value[new Func(() => value) as Func] = value;
+                    Storage.Value[key as Func] = value;
                     return true;
                 }
             }
-
-            return false;
+            else if (initialize)
+            {
+                Storage.Value[new Func(() => value) as Func] = value;
+                return true;
+            }
         }
 
-        public bool Reset()
+        return false;
+    }
+
+    public bool Reset()
+    {
+        lock (Storage)
         {
-            lock (Storage)
+            if (Storage.IsValueCreated)
             {
-                if (Storage.IsValueCreated)
-                {
-                    Storage.Value.Clear();
-                    return true;
-                }
+                Storage.Value.Clear();
+                return true;
             }
-
-            return false;
         }
 
-        public bool Used => Storage.IsValueCreated;
+        return false;
     }
+
+    public bool Used => Storage.IsValueCreated;
 }
diff --git a/Parse/Infrastructure/Utilities/LockSet.cs b/Parse/Infrastructure/Utilities/LockSet.cs
index 659c538d..99c2d86f 100644
--- a/Parse/Infrastructure/Utilities/LockSet.cs
+++ b/Parse/Infrastructure/Utilities/LockSet.cs
@@ -4,33 +4,32 @@
 using System.Runtime.CompilerServices;
 using System.Threading;
 
-namespace Parse.Infrastructure.Utilities
+namespace Parse.Infrastructure.Utilities;
+
+public class LockSet
 {
-    public class LockSet
-    {
-        private static readonly ConditionalWeakTable stableIds = new ConditionalWeakTable();
-        private static long nextStableId = 0;
+    private static readonly ConditionalWeakTable stableIds = new ConditionalWeakTable();
+    private static long nextStableId = 0;
 
-        private readonly IEnumerable mutexes;
+    private readonly IEnumerable mutexes;
 
-        public LockSet(IEnumerable mutexes) => this.mutexes = (from mutex in mutexes orderby GetStableId(mutex) select mutex).ToList();
+    public LockSet(IEnumerable mutexes) => this.mutexes = (from mutex in mutexes orderby GetStableId(mutex) select mutex).ToList();
 
-        public void Enter()
-        {
-            foreach (object mutex in mutexes)
-                Monitor.Enter(mutex);
-        }
+    public void Enter()
+    {
+        foreach (object mutex in mutexes)
+            Monitor.Enter(mutex);
+    }
 
-        public void Exit()
-        {
-            foreach (object mutex in mutexes)
-                Monitor.Exit(mutex);
-        }
+    public void Exit()
+    {
+        foreach (object mutex in mutexes)
+            Monitor.Exit(mutex);
+    }
 
-        private static IComparable GetStableId(object mutex)
-        {
-            lock (stableIds)
-                return stableIds.GetValue(mutex, k => nextStableId++);
-        }
+    private static IComparable GetStableId(object mutex)
+    {
+        lock (stableIds)
+            return stableIds.GetValue(mutex, k => nextStableId++);
     }
 }
diff --git a/Parse/Infrastructure/Utilities/ReflectionUtilities.cs b/Parse/Infrastructure/Utilities/ReflectionUtilities.cs
index 830f21d6..ed150026 100644
--- a/Parse/Infrastructure/Utilities/ReflectionUtilities.cs
+++ b/Parse/Infrastructure/Utilities/ReflectionUtilities.cs
@@ -3,39 +3,50 @@
 using System.Linq;
 using System.Reflection;
 
-namespace Parse.Infrastructure.Utilities
+namespace Parse.Infrastructure.Utilities;
+
+public static class ReflectionUtilities
 {
-    public static class ReflectionUtilities
+    /// 
+    /// Gets all of the defined constructors that aren't static on a given  instance.
+    /// 
+    /// 
+    /// 
+    public static IEnumerable GetInstanceConstructors(this Type type)
     {
-        /// 
-        /// Gets all of the defined constructors that aren't static on a given  instance.
-        /// 
-        /// 
-        /// 
-        public static IEnumerable GetInstanceConstructors(this Type type) => type.GetTypeInfo().DeclaredConstructors.Where(constructor => (constructor.Attributes & MethodAttributes.Static) == 0);
+        return type.GetTypeInfo().DeclaredConstructors.Where(constructor => (constructor.Attributes & MethodAttributes.Static) == 0);
+    }
 
-        /// 
-        /// This method helps simplify the process of getting a constructor for a type.
-        /// A method like this exists in .NET but is not allowed in a Portable Class Library,
-        /// so we've built our own.
-        /// 
-        /// 
-        /// 
-        /// 
-        public static ConstructorInfo FindConstructor(this Type self, params Type[] parameterTypes) => self.GetConstructors().Where(constructor => constructor.GetParameters().Select(parameter => parameter.ParameterType).SequenceEqual(parameterTypes)).SingleOrDefault();
+    /// 
+    /// This method helps simplify the process of getting a constructor for a type.
+    /// A method like this exists in .NET but is not allowed in a Portable Class Library,
+    /// so we've built our own.
+    /// 
+    /// 
+    /// 
+    /// 
+    public static ConstructorInfo FindConstructor(this Type self, params Type[] parameterTypes)
+    {
+        return self.GetConstructors().Where(constructor => constructor.GetParameters().Select(parameter => parameter.ParameterType).SequenceEqual(parameterTypes)).SingleOrDefault();
+    }
 
-        /// 
-        /// Checks if a  instance is another  instance wrapped with .
-        /// 
-        /// 
-        /// 
-        public static bool CheckWrappedWithNullable(this Type type) => type.IsConstructedGenericType && type.GetGenericTypeDefinition().Equals(typeof(Nullable<>));
+    /// 
+    /// Checks if a  instance is another  instance wrapped with .
+    /// 
+    /// 
+    /// 
+    public static bool CheckWrappedWithNullable(this Type type)
+    {
+        return type.IsConstructedGenericType && type.GetGenericTypeDefinition().Equals(typeof(Nullable<>));
+    }
 
-        /// 
-        /// Gets the value of  if the type has a custom attribute of type .
-        /// 
-        /// 
-        /// 
-        public static string GetParseClassName(this Type type) => type.GetCustomAttribute()?.ClassName;
+    /// 
+    /// Gets the value of  if the type has a custom attribute of type .
+    /// 
+    /// 
+    /// 
+    public static string GetParseClassName(this Type type)
+    {
+        return type.GetCustomAttribute()?.ClassName;
     }
 }
diff --git a/Parse/Infrastructure/Utilities/SynchronizedEventHandler.cs b/Parse/Infrastructure/Utilities/SynchronizedEventHandler.cs
index 8466098f..9c9aae2d 100644
--- a/Parse/Infrastructure/Utilities/SynchronizedEventHandler.cs
+++ b/Parse/Infrastructure/Utilities/SynchronizedEventHandler.cs
@@ -4,68 +4,67 @@
 using System.Threading;
 using System.Threading.Tasks;
 
-namespace Parse.Infrastructure.Utilities
+namespace Parse.Infrastructure.Utilities;
+
+/// 
+/// Represents an event handler that calls back from the synchronization context
+/// that subscribed.
+/// Should look like an EventArgs, but may not inherit EventArgs if T is implemented by the Windows team.
+/// 
+public class SynchronizedEventHandler
 {
-    /// 
-    /// Represents an event handler that calls back from the synchronization context
-    /// that subscribed.
-    /// Should look like an EventArgs, but may not inherit EventArgs if T is implemented by the Windows team.
-    /// 
-    public class SynchronizedEventHandler
-    {
-        LinkedList> Callbacks { get; } = new LinkedList> { };
+    LinkedList> Callbacks { get; } = new LinkedList> { };
 
-        public void Add(Delegate target)
+    public void Add(Delegate target)
+    {
+        lock (Callbacks)
         {
-            lock (Callbacks)
-            {
-                TaskFactory factory = SynchronizationContext.Current is { } ? new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.FromCurrentSynchronizationContext()) : Task.Factory;
+            TaskFactory factory = SynchronizationContext.Current is { } ? new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.FromCurrentSynchronizationContext()) : Task.Factory;
 
-                foreach (Delegate invocation in target.GetInvocationList())
-                {
-                    Callbacks.AddLast(new Tuple(invocation, factory));
-                }
+            foreach (Delegate invocation in target.GetInvocationList())
+            {
+                Callbacks.AddLast(new Tuple(invocation, factory));
             }
         }
+    }
 
-        public void Remove(Delegate target)
+    public void Remove(Delegate target)
+    {
+        lock (Callbacks)
         {
-            lock (Callbacks)
+            if (Callbacks.Count == 0)
             {
-                if (Callbacks.Count == 0)
-                {
-                    return;
-                }
+                return;
+            }
 
-                foreach (Delegate invocation in target.GetInvocationList())
-                {
-                    LinkedListNode> node = Callbacks.First;
+            foreach (Delegate invocation in target.GetInvocationList())
+            {
+                LinkedListNode> node = Callbacks.First;
 
-                    while (node != null)
+                while (node != null)
+                {
+                    if (node.Value.Item1 == invocation)
                     {
-                        if (node.Value.Item1 == invocation)
-                        {
-                            Callbacks.Remove(node);
-                            break;
-                        }
-                        node = node.Next;
+                        Callbacks.Remove(node);
+                        break;
                     }
+                    node = node.Next;
                 }
             }
         }
+    }
 
-        public Task Invoke(object sender, T args)
-        {
-            IEnumerable> toInvoke;
-            Task[] toContinue = new[] { Task.FromResult(0) };
-
-            lock (Callbacks)
-            {
-                toInvoke = Callbacks.ToList();
-            }
+    public Task Invoke(object sender, T args)
+    {
+        IEnumerable> toInvoke;
+        Task[] toContinue = new[] { Task.FromResult(0) };
 
-            List> invocations = toInvoke.Select(callback => callback.Item2.ContinueWhenAll(toContinue, _ => callback.Item1.DynamicInvoke(sender, args))).ToList();
-            return Task.WhenAll(invocations);
+        lock (Callbacks)
+        {
+            toInvoke = Callbacks.ToList();
         }
+
+        List> invocations = toInvoke.Select(callback => callback.Item2.ContinueWhenAll(toContinue, _ => callback.Item1.DynamicInvoke(sender, args))).ToList();
+        return Task.WhenAll(invocations);
     }
 }
diff --git a/Parse/Infrastructure/Utilities/TaskQueue.cs b/Parse/Infrastructure/Utilities/TaskQueue.cs
index 475343eb..1fee0f55 100644
--- a/Parse/Infrastructure/Utilities/TaskQueue.cs
+++ b/Parse/Infrastructure/Utilities/TaskQueue.cs
@@ -2,71 +2,64 @@
 using System.Threading;
 using System.Threading.Tasks;
 
-namespace Parse.Infrastructure.Utilities
+namespace Parse.Infrastructure.Utilities;
+
+/// 
+/// A helper class for enqueuing tasks
+/// 
+public class TaskQueue
 {
     /// 
-    /// A helper class for enqueuing tasks
+    /// We only need to keep the tail of the queue. Cancelled tasks will
+    /// just complete normally/immediately when their turn arrives.
     /// 
-    public class TaskQueue
-    {
-        /// 
-        /// We only need to keep the tail of the queue. Cancelled tasks will
-        /// just complete normally/immediately when their turn arrives.
-        /// 
-        Task Tail { get; set; }
+    private Task? Tail { get; set; } = Task.CompletedTask; // Start with a completed task to simplify logic.
 
-        /// 
-        /// Gets a cancellable task that can be safely awaited and is dependent
-        /// on the current tail of the queue. This essentially gives us a proxy
-        /// for the tail end of the queue whose awaiting can be cancelled.
-        /// 
-        /// A cancellation token that cancels
-        /// the task even if the task is still in the queue. This allows the
-        /// running task to return immediately without breaking the dependency
-        /// chain. It also ensures that errors do not propagate.
-        /// A new task that should be awaited by enqueued tasks.
-        private Task GetTaskToAwait(CancellationToken cancellationToken)
+    /// 
+    /// Gets a task that can be awaited and is dependent on the current queue's tail.
+    /// 
+    /// A cancellation token to cancel waiting for the task.
+    /// A task representing the tail of the queue.
+    private Task GetTaskToAwait(CancellationToken cancellationToken)
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
-            {
-                return (Tail ?? Task.FromResult(true)).ContinueWith(task => { }, cancellationToken);
-            }
+            // Ensure the returned task is cancellable even if it's already completed.
+            return Tail?.ContinueWith(
+                _ => { },
+                cancellationToken,
+                TaskContinuationOptions.ExecuteSynchronously,
+                TaskScheduler.Default) ?? Task.CompletedTask;
         }
+    }
 
-        /// 
-        /// Enqueues a task created by . If the task is
-        /// cancellable (or should be able to be cancelled while it is waiting in the
-        /// queue), pass a cancellationToken.
-        /// 
-        /// The type of task.
-        /// A function given a task to await once state is
-        /// snapshotted (e.g. after capturing session tokens at the time of the save call).
-        /// Awaiting this task will wait for the created task's turn in the queue.
-        /// A cancellation token that can be used to
-        /// cancel waiting in the queue.
-        /// The task created by the taskStart function.
-        public T Enqueue(Func taskStart, CancellationToken cancellationToken = default) where T : Task
-        {
-            Task oldTail;
-            T task;
+    /// 
+    /// Enqueues a task to be executed after the current tail of the queue.
+    /// 
+    /// The type of task.
+    /// A function that creates a new task dependent on the current queue state.
+    /// A cancellation token to cancel the waiting task.
+    /// The newly created task.
+    public T Enqueue(Func taskStart, CancellationToken cancellationToken = default) where T : Task
+    {
+        T task;
 
-            lock (Mutex)
-            {
-                oldTail = Tail ?? Task.FromResult(true);
+        lock (Mutex)
+        {
+            var oldTail = Tail ?? Task.CompletedTask;
 
-                // The task created by taskStart is responsible for waiting the
-                // task passed to it before doing its work (this gives it an opportunity
-                // to do startup work or save state before waiting for its turn in the queue
-                task = taskStart(GetTaskToAwait(cancellationToken));
+            // Create the new task using the tail task as a dependency.
+            task = taskStart(GetTaskToAwait(cancellationToken));
 
-                // The tail task should be dependent on the old tail as well as the newly-created
-                // task. This prevents cancellation of the new task from causing the queue to run
-                // out of order.
-                Tail = Task.WhenAll(oldTail, task);
-            }
-            return task;
+            // Update the tail to include the newly created task.
+            Tail = Task.WhenAll(oldTail, task);
         }
 
-        public object Mutex { get; } = new object { };
+        return task;
     }
+    /// 
+    /// Synchronization object to protect shared state.
+    /// 
+    public readonly object Mutex = new();
+
 }
diff --git a/Parse/Infrastructure/Utilities/ThreadingUtilities.cs b/Parse/Infrastructure/Utilities/ThreadingUtilities.cs
index 90ecd537..31a000da 100644
--- a/Parse/Infrastructure/Utilities/ThreadingUtilities.cs
+++ b/Parse/Infrastructure/Utilities/ThreadingUtilities.cs
@@ -1,22 +1,21 @@
 using System;
 
-namespace Parse.Infrastructure.Utilities
+namespace Parse.Infrastructure.Utilities;
+
+internal static class ThreadingUtilities
 {
-    internal static class ThreadingUtilities
+    public static void Lock(ref object @lock, Action operationToLock)
     {
-        public static void Lock(ref object @lock, Action operationToLock)
-        {
-            lock (@lock)
-                operationToLock();
-        }
+        lock (@lock)
+            operationToLock();
+    }
 
-        public static TResult Lock(ref object @lock, Func operationToLock)
-        {
-            TResult result = default;
-            lock (@lock)
-                result = operationToLock();
+    public static TResult Lock(ref object @lock, Func operationToLock)
+    {
+        TResult result = default;
+        lock (@lock)
+            result = operationToLock();
 
-            return result;
-        }
+        return result;
     }
 }
diff --git a/Parse/Infrastructure/Utilities/XamarinAttributes.cs b/Parse/Infrastructure/Utilities/XamarinAttributes.cs
index 108bba23..36c280dd 100644
--- a/Parse/Infrastructure/Utilities/XamarinAttributes.cs
+++ b/Parse/Infrastructure/Utilities/XamarinAttributes.cs
@@ -1,407 +1,406 @@
 using System;
 using System.Collections.Generic;
 
-namespace Parse.Infrastructure.Utilities
+namespace Parse.Infrastructure.Utilities;
+
+/// 
+/// A reimplementation of Xamarin's PreserveAttribute.
+/// This allows us to support AOT and linking for Xamarin platforms.
+/// 
+[AttributeUsage(AttributeTargets.All)]
+internal class PreserveAttribute : Attribute
 {
-    /// 
-    /// A reimplementation of Xamarin's PreserveAttribute.
-    /// This allows us to support AOT and linking for Xamarin platforms.
-    /// 
-    [AttributeUsage(AttributeTargets.All)]
-    internal class PreserveAttribute : Attribute
-    {
-        public bool AllMembers;
-        public bool Conditional;
-    }
+    public bool AllMembers;
+    public bool Conditional;
+}
 
-    [AttributeUsage(AttributeTargets.All)]
-    internal class LinkerSafeAttribute : Attribute
-    {
-        public LinkerSafeAttribute() { }
-    }
+[AttributeUsage(AttributeTargets.All)]
+internal class LinkerSafeAttribute : Attribute
+{
+    public LinkerSafeAttribute() { }
+}
 
-    [Preserve(AllMembers = true)]
-    internal class PreserveWrapperTypes
+[Preserve(AllMembers = true)]
+internal class PreserveWrapperTypes
+{
+    /// 
+    /// Exists to ensure that generic types are AOT-compiled for the conversions we support.
+    /// Any new value types that we add support for will need to be registered here.
+    /// The method itself is never called, but by virtue of the Preserve attribute being set
+    /// on the class, these types will be AOT-compiled.
+    ///
+    /// This also applies to Unity.
+    /// 
+    static List AOTPreservations => new List
     {
-        /// 
-        /// Exists to ensure that generic types are AOT-compiled for the conversions we support.
-        /// Any new value types that we add support for will need to be registered here.
-        /// The method itself is never called, but by virtue of the Preserve attribute being set
-        /// on the class, these types will be AOT-compiled.
-        ///
-        /// This also applies to Unity.
-        /// 
-        static List AOTPreservations => new List
-        {
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
 
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
 
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
 
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
 
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
 
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
 
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
 
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
 
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
 
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
 
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
 
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
 
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
 
-            typeof(FlexibleListWrapper),
-            typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
+        typeof(FlexibleListWrapper),
 
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
 
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
 
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
 
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
 
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
 
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
 
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
 
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
 
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
 
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
 
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
 
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
 
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper),
 
-            typeof(FlexibleDictionaryWrapper),
-            typeof(FlexibleDictionaryWrapper)
-        };
-    }
+        typeof(FlexibleDictionaryWrapper),
+        typeof(FlexibleDictionaryWrapper)
+    };
 }
diff --git a/Parse/Parse.csproj b/Parse/Parse.csproj
index 0df4edc6..d411ce88 100644
--- a/Parse/Parse.csproj
+++ b/Parse/Parse.csproj
@@ -1,11 +1,11 @@
 
 
     
-        netstandard2.0
+        net6.0
         bin\Release\netstandard2.0\Parse.xml
         3.0.2
         latest
-        
+
         Parse
         https://parseplatform.org/
         https://github.com/parse-community/Parse-SDK-dotNET/
@@ -19,10 +19,12 @@
         true
         parse-logo.png
         LICENSE
+
+        True
     
 
     
-        
+        
     
 
     
@@ -33,14 +35,6 @@
       
     
 
-    
-      
-        True
-        True
-        Resources.resx
-      
-    
-
     
       
         ResXFileCodeGenerator
@@ -55,4 +49,12 @@
       
     
 
+    
+      
+        True
+        True
+        Resources.resx
+      
+    
+
 
diff --git a/Parse/Platform/Analytics/ParseAnalyticsController.cs b/Parse/Platform/Analytics/ParseAnalyticsController.cs
index c1abb784..09ef2b8e 100644
--- a/Parse/Platform/Analytics/ParseAnalyticsController.cs
+++ b/Parse/Platform/Analytics/ParseAnalyticsController.cs
@@ -8,60 +8,59 @@
 using Parse.Infrastructure.Data;
 using Parse.Infrastructure.Execution;
 
-namespace Parse.Platform.Analytics
+namespace Parse.Platform.Analytics;
+
+/// 
+/// The controller for the Parse Analytics API.
+/// 
+public class ParseAnalyticsController : IParseAnalyticsController
 {
+    IParseCommandRunner Runner { get; }
+
     /// 
-    /// The controller for the Parse Analytics API.
+    /// Creates an instance of the Parse Analytics API controller.
     /// 
-    public class ParseAnalyticsController : IParseAnalyticsController
-    {
-        IParseCommandRunner Runner { get; }
-
-        /// 
-        /// Creates an instance of the Parse Analytics API controller.
-        /// 
-        /// A  to use.
-        public ParseAnalyticsController(IParseCommandRunner commandRunner) => Runner = commandRunner;
+    /// A  to use.
+    public ParseAnalyticsController(IParseCommandRunner commandRunner) => Runner = commandRunner;
 
-        /// 
-        /// Tracks an event matching the specified details.
-        /// 
-        /// The name of the event.
-        /// The parameters of the event.
-        /// The session token for the event.
-        /// The asynchonous cancellation token.
-        /// A  that will complete successfully once the event has been set to be tracked.
-        public Task TrackEventAsync(string name, IDictionary dimensions, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default)
+    /// 
+    /// Tracks an event matching the specified details.
+    /// 
+    /// The name of the event.
+    /// The parameters of the event.
+    /// The session token for the event.
+    /// The asynchonous cancellation token.
+    /// A  that will complete successfully once the event has been set to be tracked.
+    public Task TrackEventAsync(string name, IDictionary dimensions, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default)
+    {
+        IDictionary data = new Dictionary
         {
-            IDictionary data = new Dictionary
-            {
-                ["at"] = DateTime.Now,
-                [nameof(name)] = name,
-            };
-
-            if (dimensions != null)
-            {
-                data[nameof(dimensions)] = dimensions;
-            }
+            ["at"] = DateTime.Now,
+            [nameof(name)] = name,
+        };
 
-            return Runner.RunCommandAsync(new ParseCommand($"events/{name}", "POST", sessionToken, data: PointerOrLocalIdEncoder.Instance.Encode(data, serviceHub) as IDictionary), cancellationToken: cancellationToken);
+        if (dimensions != null)
+        {
+            data[nameof(dimensions)] = dimensions;
         }
 
-        /// 
-        /// Tracks an app open for the specified event.
-        /// 
-        /// The hash for the target push notification.
-        /// The token of the current session.
-        /// The asynchronous cancellation token.
-        /// A  the will complete successfully once app openings for the target push notification have been set to be tracked.
-        public Task TrackAppOpenedAsync(string pushHash, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default)
-        {
-            IDictionary data = new Dictionary { ["at"] = DateTime.Now };
+        return Runner.RunCommandAsync(new ParseCommand($"events/{name}", "POST", sessionToken, data: PointerOrLocalIdEncoder.Instance.Encode(data, serviceHub) as IDictionary), cancellationToken: cancellationToken);
+    }
+
+    /// 
+    /// Tracks an app open for the specified event.
+    /// 
+    /// The hash for the target push notification.
+    /// The token of the current session.
+    /// The asynchronous cancellation token.
+    /// A  the will complete successfully once app openings for the target push notification have been set to be tracked.
+    public Task TrackAppOpenedAsync(string pushHash, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default)
+    {
+        IDictionary data = new Dictionary { ["at"] = DateTime.Now };
 
-            if (pushHash != null)
-                data["push_hash"] = pushHash;
+        if (pushHash != null)
+            data["push_hash"] = pushHash;
 
-            return Runner.RunCommandAsync(new ParseCommand("events/AppOpened", "POST", sessionToken, data: PointerOrLocalIdEncoder.Instance.Encode(data, serviceHub) as IDictionary), cancellationToken: cancellationToken);
-        }
+        return Runner.RunCommandAsync(new ParseCommand("events/AppOpened", "POST", sessionToken, data: PointerOrLocalIdEncoder.Instance.Encode(data, serviceHub) as IDictionary), cancellationToken: cancellationToken);
     }
 }
diff --git a/Parse/Platform/Cloud/ParseCloudCodeController.cs b/Parse/Platform/Cloud/ParseCloudCodeController.cs
index 286e47e8..c1610d8f 100644
--- a/Parse/Platform/Cloud/ParseCloudCodeController.cs
+++ b/Parse/Platform/Cloud/ParseCloudCodeController.cs
@@ -9,21 +9,88 @@
 using Parse.Infrastructure.Utilities;
 using Parse.Infrastructure.Data;
 using Parse.Infrastructure.Execution;
+using Parse.Infrastructure;
+using System.Diagnostics;
 
-namespace Parse.Platform.Cloud
+namespace Parse.Platform.Cloud;
+
+public class ParseCloudCodeController : IParseCloudCodeController
 {
-    public class ParseCloudCodeController : IParseCloudCodeController
+    IParseCommandRunner CommandRunner { get; }
+    IParseDataDecoder Decoder { get; }
+
+    public ParseCloudCodeController(IParseCommandRunner commandRunner, IParseDataDecoder decoder) =>
+        (CommandRunner, Decoder) = (commandRunner, decoder);
+    public async Task CallFunctionAsync(
+    string name,
+    IDictionary parameters,
+    string sessionToken,
+    IServiceHub serviceHub,
+    CancellationToken cancellationToken = default,
+    IProgress uploadProgress = null,
+    IProgress downloadProgress = null)
     {
-        IParseCommandRunner CommandRunner { get; }
+        if (string.IsNullOrWhiteSpace(name))
+            throw new ArgumentException("Function name cannot be null or empty.", nameof(name));
+
+        try
+        {
+            // Prepare the command
+            var command = new ParseCommand(
+                $"functions/{Uri.EscapeUriString(name)}",
+                method: "POST",
+                sessionToken: sessionToken,
+                data: NoObjectsEncoder.Instance.Encode(parameters, serviceHub) as IDictionary);
+
+            // Execute the command with progress tracking
+            var commandResult = await CommandRunner.RunCommandAsync(
+                command,
+                uploadProgress,
+                downloadProgress,
+                cancellationToken).ConfigureAwait(false);
+
+            // Ensure the command result is valid
+            if (commandResult.Item2 == null)
+            {
+                throw new ParseFailureException(ParseFailureException.ErrorCode.OtherCause, "Cloud function returned no data.");
+            }
 
-        IParseDataDecoder Decoder { get; }
+            // Decode the result
+            var decoded = Decoder.Decode(commandResult.Item2, serviceHub) as IDictionary;
 
-        public ParseCloudCodeController(IParseCommandRunner commandRunner, IParseDataDecoder decoder) => (CommandRunner, Decoder) = (commandRunner, decoder);
+            if (decoded == null)
+            {
+                throw new ParseFailureException(ParseFailureException.ErrorCode.OtherCause, "Failed to decode cloud function response.");
+            }
 
-        public Task CallFunctionAsync(string name, IDictionary parameters, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand($"functions/{Uri.EscapeUriString(name)}", method: "POST", sessionToken: sessionToken, data: NoObjectsEncoder.Instance.Encode(parameters, serviceHub) as IDictionary), cancellationToken: cancellationToken).OnSuccess(task =>
+            // Extract the result key
+            if (decoded.TryGetValue("result", out var result))
+            {
+                try
+                {
+                    return Conversion.To(result);
+                }
+                catch (Exception ex)
+                {
+                    throw new ParseFailureException(ParseFailureException.ErrorCode.OtherCause, "Failed to convert cloud function result to expected type.", ex);
+                }
+            }
+
+
+            // Handle missing result key
+            throw new ParseFailureException(ParseFailureException.ErrorCode.OtherCause, "Cloud function did not return a result.");
+        }
+        catch (ParseFailureException)
+        {
+            // Rethrow known Parse exceptions
+            throw;
+        }
+        catch (Exception ex)
         {
-            IDictionary decoded = Decoder.Decode(task.Result.Item2, serviceHub) as IDictionary;
-            return !decoded.ContainsKey("result") ? default : Conversion.To(decoded["result"]);
-        });
+            // Wrap unexpected exceptions
+            throw new ParseFailureException(ParseFailureException.ErrorCode.OtherCause, "An unexpected error occurred while calling the cloud function.", ex);
+        }
     }
+
 }
+
diff --git a/Parse/Platform/Configuration/ParseConfiguration.cs b/Parse/Platform/Configuration/ParseConfiguration.cs
index 90b43a1f..7cc76950 100644
--- a/Parse/Platform/Configuration/ParseConfiguration.cs
+++ b/Parse/Platform/Configuration/ParseConfiguration.cs
@@ -1,76 +1,114 @@
+using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using Parse.Abstractions.Infrastructure;
 using Parse.Abstractions.Infrastructure.Data;
 using Parse.Infrastructure.Data;
 using Parse.Infrastructure.Utilities;
 
-namespace Parse.Platform.Configuration
+namespace Parse.Platform.Configuration;
+
+/// 
+/// The ParseConfig is a representation of the remote configuration object,
+/// that enables you to add things like feature gating, a/b testing or simple "Message of the day".
+/// 
+public class ParseConfiguration : IJsonConvertible
 {
-    /// 
-    /// The ParseConfig is a representation of the remote configuration object,
-    /// that enables you to add things like feature gating, a/b testing or simple "Message of the day".
-    /// 
-    public class ParseConfiguration : IJsonConvertible
-    {
-        IDictionary Properties { get; } = new Dictionary { };
+    IDictionary Properties { get; } = new Dictionary { };
 
-        IServiceHub Services { get; }
+    IServiceHub Services { get; }
 
-        internal ParseConfiguration(IServiceHub serviceHub) => Services = serviceHub;
+    internal ParseConfiguration(IServiceHub serviceHub) => Services = serviceHub;
 
-        ParseConfiguration(IDictionary properties, IServiceHub serviceHub) : this(serviceHub) => Properties = properties;
+    ParseConfiguration(IDictionary properties, IServiceHub serviceHub) : this(serviceHub) => Properties = properties;
 
-        internal static ParseConfiguration Create(IDictionary configurationData, IParseDataDecoder decoder, IServiceHub serviceHub) => new ParseConfiguration(decoder.Decode(configurationData["params"], serviceHub) as IDictionary, serviceHub);
+    internal static ParseConfiguration Create(IDictionary configurationData, IParseDataDecoder decoder, IServiceHub serviceHub)
+    {
+        return new ParseConfiguration(decoder.Decode(configurationData["params"], serviceHub) as IDictionary, serviceHub);
+    }
 
-        /// 
-        /// Gets a value for the key of a particular type.
-        /// 
-        /// The type to convert the value to. Supported types are
-        /// ParseObject and its descendents, Parse types such as ParseRelation and ParseGeopoint,
-        /// primitive types,IList<T>, IDictionary<string, T> and strings.
-        /// The key of the element to get.
-        /// The property is retrieved
-        /// and  is not found.
-        /// The property under this 
-        /// key was found, but of a different type.
-        public T Get(string key) => Conversion.To(Properties[key]);
+    /// 
+    /// Gets a value for the key of a particular type.
+    /// 
+    /// The type to convert the value to. Supported types are
+    /// ParseObject and its descendents, Parse types such as ParseRelation and ParseGeopoint,
+    /// primitive types,IList<T>, IDictionary<string, T> and strings.
+    /// The key of the element to get.
+    /// The property is retrieved
+    /// and  is not found.
+    /// The property under this 
+    /// key was found, but of a different type.
+    public T Get(string key)
+    {
+        try
+        {
+            // Check if the key exists in the Properties dictionary
+            if (!Properties.ContainsKey(key))
+            {
+                throw new KeyNotFoundException($"The key '{key}' was not found in the configuration.");
+            }
 
-        /// 
-        /// Populates result with the value for the key, if possible.
-        /// 
-        /// The desired type for the value.
-        /// The key to retrieve a value for.
-        /// The value for the given key, converted to the
-        /// requested type, or null if unsuccessful.
-        /// true if the lookup and conversion succeeded, otherwise false.
-        public bool TryGetValue(string key, out T result)
+            // Try to convert the value to the desired type
+            return Conversion.To(Properties[key]);
+        }
+        catch (KeyNotFoundException)
         {
+            // Handle case where the key is not found in the dictionary
+            throw;
+        }
+        catch (Exception ex)
+        {
+            // Handle any other exception, such as a FormatException when conversion fails
+            throw new FormatException($"Error converting value for key '{key}' to type '{typeof(T)}'.", ex);
+        }
+    }
+
+    /// 
+    /// Populates result with the value for the key, if possible.
+    /// 
+    /// The desired type for the value.
+    /// The key to retrieve a value for.
+    /// The value for the given key, converted to the
+    /// requested type, or null if unsuccessful.
+    /// true if the lookup and conversion succeeded, otherwise false.
+    public bool TryGetValue(string key, out T result)
+    {
+        result = default;
+
+        try
+        {
+            // Check if the key exists in the Properties dictionary
             if (Properties.ContainsKey(key))
-                try
-                {
-                    T temp = Conversion.To(Properties[key]);
-                    result = temp;
-                    return true;
-                }
-                catch
-                {
-                    // Could not convert, do nothing.
-                }
+            {
+                // Attempt to convert the value to the requested type
+                result = Conversion.To(Properties[key]);
+                return true;
+            }
 
-            result = default;
+            // If the key does not exist, return false
+            return false;
+        }
+        catch (Exception ex)
+        {
+            // Log the exception if needed or just return false
+            Debug.WriteLine($"Error converting value for key '{key}': {ex.Message}");
             return false;
         }
+    }
 
-        /// 
-        /// Gets a value on the config.
-        /// 
-        /// The key for the parameter.
-        /// The property is
-        /// retrieved and  is not found.
-        /// The value for the key.
-        virtual public object this[string key] => Properties[key];
 
-        IDictionary IJsonConvertible.ConvertToJSON() => new Dictionary
+    /// 
+    /// Gets a value on the config.
+    /// 
+    /// The key for the parameter.
+    /// The property is
+    /// retrieved and  is not found.
+    /// The value for the key.
+    virtual public object this[string key] => Properties[key];
+
+    public IDictionary ConvertToJSON(IServiceHub serviceHub = default)
+    {
+        return new Dictionary
         {
             ["params"] = NoObjectsEncoder.Instance.Encode(Properties, Services)
         };
diff --git a/Parse/Platform/Configuration/ParseConfigurationController.cs b/Parse/Platform/Configuration/ParseConfigurationController.cs
index d34912ad..9b3c25a3 100644
--- a/Parse/Platform/Configuration/ParseConfigurationController.cs
+++ b/Parse/Platform/Configuration/ParseConfigurationController.cs
@@ -4,42 +4,43 @@
 using Parse.Abstractions.Infrastructure.Execution;
 using Parse.Abstractions.Infrastructure;
 using Parse.Abstractions.Platform.Configuration;
-using Parse.Infrastructure.Utilities;
-using Parse;
 using Parse.Infrastructure.Execution;
 
-namespace Parse.Platform.Configuration
+namespace Parse.Platform.Configuration;
+
+/// 
+/// Config controller.
+/// 
+internal class ParseConfigurationController : IParseConfigurationController
 {
+    private IParseCommandRunner CommandRunner { get; }
+    private IParseDataDecoder Decoder { get; }
+    public IParseCurrentConfigurationController CurrentConfigurationController { get; }
+
     /// 
-    /// Config controller.
+    /// Initializes a new instance of the  class.
     /// 
-    internal class ParseConfigurationController : IParseConfigurationController
+    public ParseConfigurationController(IParseCommandRunner commandRunner, ICacheController storageController, IParseDataDecoder decoder)
+    {
+        CommandRunner = commandRunner;
+        CurrentConfigurationController = new ParseCurrentConfigurationController(storageController, decoder);
+        Decoder = decoder;
+    }
+
+    public async Task FetchConfigAsync(string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default)
     {
-        IParseCommandRunner CommandRunner { get; }
-
-        IParseDataDecoder Decoder { get; }
-
-        public IParseCurrentConfigurationController CurrentConfigurationController { get; }
-
-        /// 
-        /// Initializes a new instance of the  class.
-        /// 
-        public ParseConfigurationController(IParseCommandRunner commandRunner, ICacheController storageController, IParseDataDecoder decoder)
-        {
-            CommandRunner = commandRunner;
-            CurrentConfigurationController = new ParseCurrentConfigurationController(storageController, decoder);
-            Decoder = decoder;
-        }
-
-        public Task FetchConfigAsync(string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand("config", method: "GET", sessionToken: sessionToken, data: default), cancellationToken: cancellationToken).OnSuccess(task =>
-        {
-            cancellationToken.ThrowIfCancellationRequested();
-            return Decoder.BuildConfiguration(task.Result.Item2, serviceHub);
-        }).OnSuccess(task =>
-        {
-            cancellationToken.ThrowIfCancellationRequested();
-            CurrentConfigurationController.SetCurrentConfigAsync(task.Result);
-            return task;
-        }).Unwrap();
+        cancellationToken.ThrowIfCancellationRequested();
+
+        // Fetch configuration via the command runner (returns a Task)
+        var commandResult = await CommandRunner.RunCommandAsync(new ParseCommand("config", method: "GET", sessionToken: sessionToken, null, null),null, null).ConfigureAwait(false);
+
+        // Build the configuration using the decoder (assuming BuildConfiguration is async)
+        var config = Decoder.BuildConfiguration(commandResult.Item2, serviceHub);
+
+        // Set the current configuration (assuming SetCurrentConfigAsync is async)
+        await CurrentConfigurationController.SetCurrentConfigAsync(config).ConfigureAwait(false);
+
+        return config;
     }
+
 }
diff --git a/Parse/Platform/Configuration/ParseCurrentConfigurationController.cs b/Parse/Platform/Configuration/ParseCurrentConfigurationController.cs
index 8870c1b1..70717e75 100644
--- a/Parse/Platform/Configuration/ParseCurrentConfigurationController.cs
+++ b/Parse/Platform/Configuration/ParseCurrentConfigurationController.cs
@@ -1,55 +1,57 @@
-using System.Threading;
 using System.Threading.Tasks;
 using Parse.Abstractions.Infrastructure.Data;
 using Parse.Abstractions.Infrastructure;
 using Parse.Abstractions.Platform.Configuration;
-using Parse.Infrastructure.Utilities;
 
-namespace Parse.Platform.Configuration
+namespace Parse.Platform.Configuration;
+
+/// 
+/// Parse current config controller.
+/// 
+internal class ParseCurrentConfigurationController : IParseCurrentConfigurationController
 {
-    /// 
-    /// Parse current config controller.
-    /// 
-    internal class ParseCurrentConfigurationController : IParseCurrentConfigurationController
-    {
-        static string CurrentConfigurationKey { get; } = "CurrentConfig";
+    private static readonly string CurrentConfigurationKey = "CurrentConfig";
 
-        TaskQueue TaskQueue { get; }
+    private ParseConfiguration _currentConfiguration;
+    private readonly ICacheController _storageController;
+    private readonly IParseDataDecoder _decoder;
 
-        ParseConfiguration CurrentConfiguration { get; set; }
+    public ParseCurrentConfigurationController(ICacheController storageController, IParseDataDecoder decoder)
+    {
+        _storageController = storageController;
+        _decoder = decoder;
+    }
 
-        ICacheController StorageController { get; }
+    public async Task GetCurrentConfigAsync(IServiceHub serviceHub)
+    {
+        if (_currentConfiguration != null)
+            return _currentConfiguration;
 
-        IParseDataDecoder Decoder { get; }
+        var data = await _storageController.LoadAsync();
+        data.TryGetValue(CurrentConfigurationKey, out var storedData);
 
-        /// 
-        /// Initializes a new instance of the  class.
-        /// 
-        public ParseCurrentConfigurationController(ICacheController storageController, IParseDataDecoder decoder)
-        {
-            StorageController = storageController;
-            Decoder = decoder;
-            TaskQueue = new TaskQueue { };
-        }
+        _currentConfiguration = storedData is string configString
+            ? _decoder.BuildConfiguration(ParseClient.DeserializeJsonString(configString), serviceHub)
+            : new ParseConfiguration(serviceHub);
 
-        public Task GetCurrentConfigAsync(IServiceHub serviceHub) => TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => CurrentConfiguration is { } ? Task.FromResult(CurrentConfiguration) : StorageController.LoadAsync().OnSuccess(task =>
-        {
-            task.Result.TryGetValue(CurrentConfigurationKey, out object data);
-            return CurrentConfiguration = data is string { } configuration ? Decoder.BuildConfiguration(ParseClient.DeserializeJsonString(configuration), serviceHub) : new ParseConfiguration(serviceHub);
-        })), CancellationToken.None).Unwrap();
+        return _currentConfiguration;
+    }
 
-        public Task SetCurrentConfigAsync(ParseConfiguration target) => TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ =>
-        {
-            CurrentConfiguration = target;
-            return StorageController.LoadAsync().OnSuccess(task => task.Result.AddAsync(CurrentConfigurationKey, ParseClient.SerializeJsonString(((IJsonConvertible) target).ConvertToJSON())));
-        }).Unwrap().Unwrap(), CancellationToken.None);
+    public async Task SetCurrentConfigAsync(ParseConfiguration target)
+    {
+        _currentConfiguration = target;
 
-        public Task ClearCurrentConfigAsync() => TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ =>
-        {
-            CurrentConfiguration = null;
-            return StorageController.LoadAsync().OnSuccess(task => task.Result.RemoveAsync(CurrentConfigurationKey));
-        }).Unwrap().Unwrap(), CancellationToken.None);
+        var data = await _storageController.LoadAsync();
+        await data.AddAsync(CurrentConfigurationKey, ParseClient.SerializeJsonString(((IJsonConvertible) target).ConvertToJSON()));
+    }
 
-        public Task ClearCurrentConfigInMemoryAsync() => TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => CurrentConfiguration = null), CancellationToken.None);
+    public async Task ClearCurrentConfigAsync()
+    {
+        _currentConfiguration = null;
+
+        var data = await _storageController.LoadAsync();
+        await data.RemoveAsync(CurrentConfigurationKey);
     }
+
+    public Task ClearCurrentConfigInMemoryAsync() => Task.Run(() => _currentConfiguration = null);
 }
diff --git a/Parse/Platform/Files/FileState.cs b/Parse/Platform/Files/FileState.cs
index 4430d673..304d4b25 100644
--- a/Parse/Platform/Files/FileState.cs
+++ b/Parse/Platform/Files/FileState.cs
@@ -1,30 +1,48 @@
 using System;
 
-namespace Parse.Platform.Files
+namespace Parse.Platform.Files;
+
+public class FileState
 {
-    public class FileState
-    {
-        static string SecureHyperTextTransferScheme { get; } = "https";
+    static string SecureHyperTextTransferScheme { get; } = "https";
 
-        public string Name { get; set; }
+    public string Name { get; set; }
 
-        public string MediaType { get; set; }
+    public string MediaType { get; set; }
 
-        public Uri Location { get; set; }
+    public Uri Location { get; set; }
 
-        public Uri SecureLocation => Location switch
+    /// 
+    /// Converts the file's location to a secure HTTPS location if applicable.
+    /// 
+    public Uri SecureLocation
+    {
+        get
         {
-#warning Investigate if the first branch of this swhich expression should be removed or an explicit failure case when not testing.
+            if (Location == null)
+                throw new InvalidOperationException("Location is not set.");
 
-            { Host: "files.parsetfss.com" } location => new UriBuilder(location)
-            {
-                Scheme = SecureHyperTextTransferScheme,
+            return IsParseHostedFile(Location) ? GetSecureUri(Location) : Location;
+        }
+    }
 
-                // This makes URIBuilder assign the default port for the URL scheme.
+    /// 
+    /// Checks if the file is hosted on a supported Parse file server.
+    /// 
+    private static bool IsParseHostedFile(Uri location)
+    {
+        return location.Host.EndsWith("parsetfss.com", StringComparison.OrdinalIgnoreCase);
+    }
 
-                Port = -1,
-            }.Uri,
-            _ => Location
-        };
+    /// 
+    /// Converts a URI to a secure HTTPS URI.
+    /// 
+    private static Uri GetSecureUri(Uri location)
+    {
+        return new UriBuilder(location)
+        {
+            Scheme = SecureHyperTextTransferScheme,
+            Port = -1, // Default port for HTTPS
+        }.Uri;
     }
 }
diff --git a/Parse/Platform/Files/ParseFile.cs b/Parse/Platform/Files/ParseFile.cs
index d7871ccf..3559512c 100644
--- a/Parse/Platform/Files/ParseFile.cs
+++ b/Parse/Platform/Files/ParseFile.cs
@@ -7,150 +7,176 @@
 using Parse.Infrastructure.Utilities;
 using Parse.Platform.Files;
 
-namespace Parse
+namespace Parse;
+
+public static class FileServiceExtensions
 {
-    public static class FileServiceExtensions
+    /// 
+    /// Saves the file to the Parse cloud.
+    /// 
+    /// The cancellation token.
+    public static Task SaveFileAsync(this IServiceHub serviceHub, ParseFile file, CancellationToken cancellationToken = default)
     {
-        /// 
-        /// Saves the file to the Parse cloud.
-        /// 
-        /// The cancellation token.
-        public static Task SaveFileAsync(this IServiceHub serviceHub, ParseFile file, CancellationToken cancellationToken = default) => serviceHub.SaveFileAsync(file, default, cancellationToken);
-
-        /// 
-        /// Saves the file to the Parse cloud.
-        /// 
-        /// The progress callback.
-        /// The cancellation token.
-        public static Task SaveFileAsync(this IServiceHub serviceHub, ParseFile file, IProgress progress, CancellationToken cancellationToken = default) => file.TaskQueue.Enqueue(toAwait => serviceHub.FileController.SaveAsync(file.State, file.DataStream, serviceHub.GetCurrentSessionToken(), progress, cancellationToken), cancellationToken).OnSuccess(task => file.State = task.Result);
+        return serviceHub.SaveFileAsync(file, default, cancellationToken);
+    }
+
+    /// 
+    /// Saves the file to the Parse cloud.
+    /// 
+    /// The progress callback.
+    /// The cancellation token.
+    public static async Task SaveFileAsync(this IServiceHub serviceHub,ParseFile file,
+        IProgress progress,CancellationToken cancellationToken = default)
+    {
+        var result = await file.TaskQueue.Enqueue(
+            async toAwait => await serviceHub.FileController.SaveAsync(file.State,file.DataStream,
+                await serviceHub.GetCurrentSessionToken(),progress,cancellationToken)
+            .ConfigureAwait(false),cancellationToken)
+            .ConfigureAwait(false);
+
+        file.State = result; // Update the file state with the result
+    }
+
 
+#pragma warning disable CS1030 // #warning directive
 #warning Make serviceHub null by default once dependents properly inject it when needed.
 
-        /// 
-        /// Saves the file to the Parse cloud.
-        /// 
-        /// The cancellation token.
-        public static Task SaveAsync(this ParseFile file, IServiceHub serviceHub, CancellationToken cancellationToken = default) => serviceHub.SaveFileAsync(file, cancellationToken);
-
-        /// 
-        /// Saves the file to the Parse cloud.
-        /// 
-        /// The progress callback.
-        /// The cancellation token.
-        public static Task SaveAsync(this ParseFile file, IServiceHub serviceHub, IProgress progress, CancellationToken cancellationToken = default) => serviceHub.SaveFileAsync(file, progress, cancellationToken);
+    /// 
+    /// Saves the file to the Parse cloud.
+    /// 
+    /// The cancellation token.
+    public static Task SaveAsync(this ParseFile file, IServiceHub serviceHub, CancellationToken cancellationToken = default)
+    {
+        return serviceHub.SaveFileAsync(file, cancellationToken);
     }
+#pragma warning restore CS1030 // #warning directive
 
     /// 
-    /// ParseFile is a local representation of a file that is saved to the Parse cloud.
+    /// Saves the file to the Parse cloud.
     /// 
-    /// 
-    /// The workflow is to construct a  with data and a filename,
-    /// then save it and set it as a field on a ParseObject:
-    ///
-    /// 
-    /// var file = new ParseFile("hello.txt",
-    ///     new MemoryStream(Encoding.UTF8.GetBytes("hello")));
-    /// await file.SaveAsync();
-    /// var obj = new ParseObject("TestObject");
-    /// obj["file"] = file;
-    /// await obj.SaveAsync();
-    /// 
-    /// 
-    public class ParseFile : IJsonConvertible
+    /// The progress callback.
+    /// The cancellation token.
+    public static Task SaveAsync(this ParseFile file, IServiceHub serviceHub, IProgress progress, CancellationToken cancellationToken = default)
     {
-        internal FileState State { get; set; }
+        return serviceHub.SaveFileAsync(file, progress, cancellationToken);
+    }
+}
 
-        internal Stream DataStream { get; }
+/// 
+/// ParseFile is a local representation of a file that is saved to the Parse cloud.
+/// 
+/// 
+/// The workflow is to construct a  with data and a filename,
+/// then save it and set it as a field on a ParseObject:
+///
+/// 
+/// var file = new ParseFile("hello.txt",
+///     new MemoryStream(Encoding.UTF8.GetBytes("hello")));
+/// await file.SaveAsync();
+/// var obj = new ParseObject("TestObject");
+/// obj["file"] = file;
+/// await obj.SaveAsync();
+/// 
+/// 
+public class ParseFile : IJsonConvertible
+{
+    internal FileState State { get; set; }
 
-        internal TaskQueue TaskQueue { get; } = new TaskQueue { };
+    internal Stream DataStream { get; }
 
-        #region Constructor
+    internal TaskQueue TaskQueue { get; } = new TaskQueue { };
 
+    #region Constructor
+
+#pragma warning disable CS1030 // #warning directive
 #warning Make IServiceHub optionally null once all dependents are injecting it if necessary.
 
-        internal ParseFile(string name, Uri uri, string mimeType = null) => State = new FileState
+    internal ParseFile(string name, Uri uri, string mimeType = null)
+    {
+        State = new FileState
         {
             Name = name,
             Location = uri,
             MediaType = mimeType
         };
+    }
+#pragma warning restore CS1030 // #warning directive
 
-        /// 
-        /// Creates a new file from a byte array and a name.
-        /// 
-        /// The file's name, ideally with an extension. The file name
-        /// must begin with an alphanumeric character, and consist of alphanumeric
-        /// characters, periods, spaces, underscores, or dashes.
-        /// The file's data.
-        /// To specify the content-type used when uploading the
-        /// file, provide this parameter.
-        public ParseFile(string name, byte[] data, string mimeType = null) : this(name, new MemoryStream(data), mimeType) { }
-
-        /// 
-        /// Creates a new file from a stream and a name.
-        /// 
-        /// The file's name, ideally with an extension. The file name
-        /// must begin with an alphanumeric character, and consist of alphanumeric
-        /// characters, periods, spaces, underscores, or dashes.
-        /// The file's data.
-        /// To specify the content-type used when uploading the
-        /// file, provide this parameter.
-        public ParseFile(string name, Stream data, string mimeType = null)
+    /// 
+    /// Creates a new file from a byte array and a name.
+    /// 
+    /// The file's name, ideally with an extension. The file name
+    /// must begin with an alphanumeric character, and consist of alphanumeric
+    /// characters, periods, spaces, underscores, or dashes.
+    /// The file's data.
+    /// To specify the content-type used when uploading the
+    /// file, provide this parameter.
+    public ParseFile(string name, byte[] data, string mimeType = null) : this(name, new MemoryStream(data), mimeType) { }
+
+    /// 
+    /// Creates a new file from a stream and a name.
+    /// 
+    /// The file's name, ideally with an extension. The file name
+    /// must begin with an alphanumeric character, and consist of alphanumeric
+    /// characters, periods, spaces, underscores, or dashes.
+    /// The file's data.
+    /// To specify the content-type used when uploading the
+    /// file, provide this parameter.
+    public ParseFile(string name, Stream data, string mimeType = null)
+    {
+        State = new FileState
         {
-            State = new FileState
-            {
-                Name = name,
-                MediaType = mimeType
-            };
+            Name = name,
+            MediaType = mimeType
+        };
 
-            DataStream = data;
-        }
+        DataStream = data;
+    }
 
-        #endregion
+    #endregion
 
-        #region Properties
+    #region Properties
 
-        /// 
-        /// Gets whether the file still needs to be saved.
-        /// 
-        public bool IsDirty => State.Location == null;
+    /// 
+    /// Gets whether the file still needs to be saved.
+    /// 
+    public bool IsDirty => State.Location == null;
 
-        /// 
-        /// Gets the name of the file. Before save is called, this is the filename given by
-        /// the user. After save is called, that name gets prefixed with a unique identifier.
-        /// 
-        [ParseFieldName("name")]
-        public string Name => State.Name;
+    /// 
+    /// Gets the name of the file. Before save is called, this is the filename given by
+    /// the user. After save is called, that name gets prefixed with a unique identifier.
+    /// 
+    [ParseFieldName("name")]
+    public string Name => State.Name;
 
-        /// 
-        /// Gets the MIME type of the file. This is either passed in to the constructor or
-        /// inferred from the file extension. "unknown/unknown" will be used if neither is
-        /// available.
-        /// 
-        public string MimeType => State.MediaType;
+    /// 
+    /// Gets the MIME type of the file. This is either passed in to the constructor or
+    /// inferred from the file extension. "unknown/unknown" will be used if neither is
+    /// available.
+    /// 
+    public string MimeType => State.MediaType;
 
-        /// 
-        /// Gets the url of the file. It is only available after you save the file or after
-        /// you get the file from a .
-        /// 
-        [ParseFieldName("url")]
-        public Uri Url => State.SecureLocation;
+    /// 
+    /// Gets the url of the file. It is only available after you save the file or after
+    /// you get the file from a .
+    /// 
+    [ParseFieldName("url")]
+    public Uri Url => State.SecureLocation;
 
-        #endregion
+    #endregion
 
-        IDictionary IJsonConvertible.ConvertToJSON()
+    public IDictionary ConvertToJSON(IServiceHub serviceHub = default)
+    {
+        if (IsDirty)
         {
-            if (IsDirty)
-            {
-                throw new InvalidOperationException("ParseFile must be saved before it can be serialized.");
-            }
-
-            return new Dictionary
-            {
-                ["__type"] = "File",
-                ["name"] = Name,
-                ["url"] = Url.AbsoluteUri
-            };
+            throw new InvalidOperationException("ParseFile must be saved before it can be serialized.");
         }
+
+        return new Dictionary
+        {
+            ["__type"] = "File",
+            ["name"] = Name,
+            ["url"] = Url.AbsoluteUri
+        };
     }
 }
diff --git a/Parse/Platform/Files/ParseFileController.cs b/Parse/Platform/Files/ParseFileController.cs
index 0545f8e4..c852a9ed 100644
--- a/Parse/Platform/Files/ParseFileController.cs
+++ b/Parse/Platform/Files/ParseFileController.cs
@@ -1,56 +1,75 @@
 using System;
-using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
-using System.Net;
 using System.Threading;
 using System.Threading.Tasks;
 using Parse.Abstractions.Infrastructure;
 using Parse.Abstractions.Infrastructure.Execution;
 using Parse.Abstractions.Platform.Files;
 using Parse.Infrastructure.Execution;
-using Parse.Infrastructure.Utilities;
 
-namespace Parse.Platform.Files
+namespace Parse.Platform.Files;
+
+public class ParseFileController : IParseFileController
 {
-    public class ParseFileController : IParseFileController
+    private IParseCommandRunner CommandRunner { get; }
+
+    public ParseFileController(IParseCommandRunner commandRunner) => CommandRunner = commandRunner;
+
+    public async Task SaveAsync(FileState state, Stream dataStream, string sessionToken, IProgress progress, CancellationToken cancellationToken = default)
     {
-        IParseCommandRunner CommandRunner { get; }
+        // If the file is already uploaded, no need to re-upload.
+        if (state.Location != null)
+            return state;
 
-        public ParseFileController(IParseCommandRunner commandRunner) => CommandRunner = commandRunner;
+        if (cancellationToken.IsCancellationRequested)
+            return await Task.FromCanceled(cancellationToken);
 
-        public Task SaveAsync(FileState state, Stream dataStream, string sessionToken, IProgress progress, CancellationToken cancellationToken = default)
+        long oldPosition = dataStream.Position;
+
+        try
         {
-            if (state.Location != null)
-                // !isDirty
+            // Execute the file upload command
+            var result = await CommandRunner.RunCommandAsync(
+                new ParseCommand($"files/{state.Name}", method: "POST", sessionToken: sessionToken, contentType: state.MediaType, stream: dataStream),
+                uploadProgress: progress,
+                cancellationToken: cancellationToken).ConfigureAwait(false);
 
-                return Task.FromResult(state);
+            // Extract the result
+            var jsonData = result.Item2;
 
-            if (cancellationToken.IsCancellationRequested)
-                return Task.FromCanceled(cancellationToken);
+            // Ensure the cancellation token hasn't been triggered during processing
+            cancellationToken.ThrowIfCancellationRequested();
 
-            long oldPosition = dataStream.Position;
+            var name = jsonData["name"] as string;
+            var url = jsonData["url"] as string;
 
-            return CommandRunner.RunCommandAsync(new ParseCommand($"files/{state.Name}", method: "POST", sessionToken: sessionToken, contentType: state.MediaType, stream: dataStream), uploadProgress: progress, cancellationToken: cancellationToken).OnSuccess(uploadTask =>
+            if (name == null || url == null)
             {
-                Tuple> result = uploadTask.Result;
-                IDictionary jsonData = result.Item2;
-                cancellationToken.ThrowIfCancellationRequested();
-
-                return new FileState
-                {
-                    Name = jsonData["name"] as string,
-                    Location = new Uri(jsonData["url"] as string, UriKind.Absolute),
-                    MediaType = state.MediaType
-                };
-            }).ContinueWith(task =>
+                throw new Exception("Incomplete result: missing 'name' or 'url'.");
+            }
+            return new FileState
             {
-                // Rewind the stream on failure or cancellation (if possible).
-
-                if ((task.IsFaulted || task.IsCanceled) && dataStream.CanSeek)
-                    dataStream.Seek(oldPosition, SeekOrigin.Begin);
-
-                return task;
-            }).Unwrap();
+                Name = jsonData["name"] as string,
+                Location = new Uri(jsonData["url"] as string, UriKind.Absolute),
+                MediaType = state.MediaType
+            };
+        }
+        catch (OperationCanceledException)
+        {
+            // Handle the cancellation properly, resetting the stream if it can seek
+            if (dataStream.CanSeek)
+                dataStream.Seek(oldPosition, SeekOrigin.Begin);
+            
+            throw; // Re-throw to allow the caller to handle the cancellation
+        }
+        catch (Exception)
+        {
+            // If an error occurs, reset the stream position and rethrow
+            if (dataStream.CanSeek)
+                dataStream.Seek(oldPosition, SeekOrigin.Begin);
+            
+            throw; // Re-throw to allow the caller to handle the error
         }
     }
 }
diff --git a/Parse/Platform/Installations/ParseCurrentInstallationController.cs b/Parse/Platform/Installations/ParseCurrentInstallationController.cs
index c3f68365..2ab8d1c4 100644
--- a/Parse/Platform/Installations/ParseCurrentInstallationController.cs
+++ b/Parse/Platform/Installations/ParseCurrentInstallationController.cs
@@ -6,100 +6,122 @@
 using Parse.Abstractions.Platform.Objects;
 using Parse.Infrastructure.Utilities;
 
-namespace Parse.Platform.Installations
+namespace Parse.Platform.Installations;
+internal class ParseCurrentInstallationController : IParseCurrentInstallationController
 {
-    internal class ParseCurrentInstallationController : IParseCurrentInstallationController
-    {
-        static string ParseInstallationKey { get; } = nameof(CurrentInstallation);
-
-        object Mutex { get; } = new object { };
-
-        TaskQueue TaskQueue { get; } = new TaskQueue { };
+    private static readonly string ParseInstallationKey = nameof(CurrentInstallation);
+    private readonly object Mutex = new object();
+    private readonly TaskQueue TaskQueue = new TaskQueue();
 
-        IParseInstallationController InstallationController { get; }
+    private readonly IParseInstallationController InstallationController;
+    private readonly ICacheController StorageController;
+    private readonly IParseInstallationCoder InstallationCoder;
+    private readonly IParseObjectClassController ClassController;
 
-        ICacheController StorageController { get; }
+    private ParseInstallation CurrentInstallationValue { get; set; }
 
-        IParseInstallationCoder InstallationCoder { get; }
-
-        IParseObjectClassController ClassController { get; }
-
-        public ParseCurrentInstallationController(IParseInstallationController installationIdController, ICacheController storageController, IParseInstallationCoder installationCoder, IParseObjectClassController classController)
+    internal ParseInstallation CurrentInstallation
+    {
+        get
         {
-            InstallationController = installationIdController;
-            StorageController = storageController;
-            InstallationCoder = installationCoder;
-            ClassController = classController;
+            lock (Mutex)
+            {
+                return CurrentInstallationValue;
+            }
         }
+        set
+        {
+            lock (Mutex)
+            {
+                CurrentInstallationValue = value;
+            }
+        }
+    }
 
-        ParseInstallation CurrentInstallationValue { get; set; }
+    public ParseCurrentInstallationController(
+        IParseInstallationController installationIdController,
+        ICacheController storageController,
+        IParseInstallationCoder installationCoder,
+        IParseObjectClassController classController)
+    {
+        InstallationController = installationIdController;
+        StorageController = storageController;
+        InstallationCoder = installationCoder;
+        ClassController = classController;
+    }
 
-        internal ParseInstallation CurrentInstallation
+    public async Task SetAsync(ParseInstallation installation, CancellationToken cancellationToken)
+    {
+        // Update the current installation in memory and disk asynchronously
+        var inst = await TaskQueue.Enqueue>(async (toAwait) =>
         {
-            get
+            var storage = await StorageController.LoadAsync().ConfigureAwait(false);
+            if (installation != null)
             {
-                lock (Mutex)
-                {
-                    return CurrentInstallationValue;
-                }
+                await storage.AddAsync(ParseInstallationKey, JsonUtilities.Encode(InstallationCoder.Encode(installation))).ConfigureAwait(false);
             }
-            set
+            else
             {
-                lock (Mutex)
-                {
-                    CurrentInstallationValue = value;
-                }
+                await storage.RemoveAsync(ParseInstallationKey).ConfigureAwait(false);
             }
+            return installation;
+        }, cancellationToken).ConfigureAwait(false);
+        return inst;
+    }
+
+    public async Task GetAsync(IServiceHub serviceHub, CancellationToken cancellationToken = default)
+    {
+        // Check if the installation is already cached
+        var cachedCurrent = CurrentInstallation;
+        if (cachedCurrent != null)
+        {
+            return cachedCurrent;
         }
 
-        public Task SetAsync(ParseInstallation installation, CancellationToken cancellationToken) => TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ =>
+        var storage = await StorageController.LoadAsync().ConfigureAwait(false);
+        if (storage.TryGetValue(ParseInstallationKey, out object temp) && temp is string installationDataString)
         {
-            Task saveTask = StorageController.LoadAsync().OnSuccess(storage => installation is { } ? storage.Result.AddAsync(ParseInstallationKey, JsonUtilities.Encode(InstallationCoder.Encode(installation))) : storage.Result.RemoveAsync(ParseInstallationKey)).Unwrap();
+            var installationData = JsonUtilities.Parse(installationDataString) as IDictionary;
+            var installation = InstallationCoder.Decode(installationData, serviceHub);
             CurrentInstallation = installation;
-
-            return saveTask;
-        }).Unwrap(), cancellationToken);
-
-        public Task GetAsync(IServiceHub serviceHub, CancellationToken cancellationToken = default)
+            return installation;
+        }
+        else
         {
-            ParseInstallation cachedCurrent;
-            cachedCurrent = CurrentInstallation;
-
-            return cachedCurrent is { } ? Task.FromResult(cachedCurrent) : TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => StorageController.LoadAsync().OnSuccess(stroage =>
-            {
-                Task fetchTask;
-                stroage.Result.TryGetValue(ParseInstallationKey, out object temp);
-                ParseInstallation installation = default;
-
-                if (temp is string installationDataString)
-                {
-                    IDictionary installationData = JsonUtilities.Parse(installationDataString) as IDictionary;
-                    installation = InstallationCoder.Decode(installationData, serviceHub);
-
-                    fetchTask = Task.FromResult(null);
-                }
-                else
-                {
-                    installation = ClassController.CreateObject(serviceHub);
-                    fetchTask = InstallationController.GetAsync().ContinueWith(t => installation.SetIfDifferent("installationId", t.Result.ToString()));
-                }
-
-                CurrentInstallation = installation;
-                return fetchTask.ContinueWith(task => installation);
-            })).Unwrap().Unwrap(), cancellationToken);
+            var installation = ClassController.CreateObject(serviceHub);
+            var installationId = await InstallationController.GetAsync().ConfigureAwait(false);
+            installation.SetIfDifferent("installationId", installationId.ToString());
+            CurrentInstallation = installation;
+            return installation;
         }
+    }
 
-        public Task ExistsAsync(CancellationToken cancellationToken) => CurrentInstallation is { } ? Task.FromResult(true) : TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => StorageController.LoadAsync().OnSuccess(storageTask => storageTask.Result.ContainsKey(ParseInstallationKey))).Unwrap(), cancellationToken);
+    public async Task ExistsAsync(CancellationToken cancellationToken)
+    {
+        // Check if the current installation exists in memory or storage
+        if (CurrentInstallation != null)
+        {
+            return true;
+        }
 
-        public bool IsCurrent(ParseInstallation installation) => CurrentInstallation == installation;
+        var storage = await StorageController.LoadAsync().ConfigureAwait(false);
+        return storage.ContainsKey(ParseInstallationKey);
+    }
 
-        public void ClearFromMemory() => CurrentInstallation = default;
+    public bool IsCurrent(ParseInstallation installation)
+    {
+        return CurrentInstallation == installation;
+    }
 
-        public void ClearFromDisk()
-        {
-            ClearFromMemory();
+    public void ClearFromMemory()
+    {
+        CurrentInstallation = null;
+    }
 
-            TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => StorageController.LoadAsync().OnSuccess(storage => storage.Result.RemoveAsync(ParseInstallationKey))).Unwrap().Unwrap(), CancellationToken.None);
-        }
+    public async Task ClearFromDiskAsync()
+    {
+        ClearFromMemory();
+        var storage = await StorageController.LoadAsync().ConfigureAwait(false);
+        await storage.RemoveAsync(ParseInstallationKey).ConfigureAwait(false);
     }
-}
+}
\ No newline at end of file
diff --git a/Parse/Platform/Installations/ParseInstallation.cs b/Parse/Platform/Installations/ParseInstallation.cs
index a266cf55..0a68f8a5 100644
--- a/Parse/Platform/Installations/ParseInstallation.cs
+++ b/Parse/Platform/Installations/ParseInstallation.cs
@@ -5,341 +5,337 @@
 using System.Threading.Tasks;
 using Parse.Infrastructure.Utilities;
 
-namespace Parse
+namespace Parse;
+
+/// 
+///  Represents this app installed on this device. Use this class to track information you want
+///  to sample from (i.e. if you update a field on app launch, you can issue a query to see
+///  the number of devices which were active in the last N hours).
+/// 
+[ParseClassName("_Installation")]
+public partial class ParseInstallation : ParseObject
 {
+    static HashSet ImmutableKeys { get; } = new HashSet { "deviceType", "deviceUris", "installationId", "timeZone", "localeIdentifier", "parseVersion", "appName", "appIdentifier", "appVersion", "pushType" };
+
     /// 
-    ///  Represents this app installed on this device. Use this class to track information you want
-    ///  to sample from (i.e. if you update a field on app launch, you can issue a query to see
-    ///  the number of devices which were active in the last N hours).
+    /// Constructs a new ParseInstallation. Generally, you should not need to construct
+    /// ParseInstallations yourself. Instead use .
     /// 
-    [ParseClassName("_Installation")]
-    public partial class ParseInstallation : ParseObject
-    {
-        static HashSet ImmutableKeys { get; } = new HashSet { "deviceType", "deviceUris", "installationId", "timeZone", "localeIdentifier", "parseVersion", "appName", "appIdentifier", "appVersion", "pushType" };
-
-        /// 
-        /// Constructs a new ParseInstallation. Generally, you should not need to construct
-        /// ParseInstallations yourself. Instead use .
-        /// 
-        public ParseInstallation() : base() { }
+    public ParseInstallation() : base() { }
 
-        /// 
-        /// A GUID that uniquely names this app installed on this device.
-        /// 
-        [ParseFieldName("installationId")]
-        public Guid InstallationId
+    /// 
+    /// A GUID that uniquely names this app installed on this device.
+    /// 
+    [ParseFieldName("installationId")]
+    public Guid InstallationId
+    {
+        get
         {
-            get
+            string installationIdString = GetProperty(nameof(InstallationId));
+            if (Guid.TryParse(installationIdString, out Guid installationId))
             {
-                string installationIdString = GetProperty(nameof(InstallationId));
-                Guid? installationId = null;
-
-                try
-                {
-                    installationId = new Guid(installationIdString);
-                }
-                catch (Exception)
-                {
-                    // Do nothing.
-                }
-
-                return installationId.Value;
-            }
-            internal set
-            {
-                Guid installationId = value;
-                SetProperty(installationId.ToString(), nameof(InstallationId));
+                return installationId;
             }
+            return Guid.Empty; // Return a default value
         }
+        internal set => SetProperty(value.ToString(), nameof(InstallationId));
+    }
 
-        /// 
-        /// The runtime target of this installation object.
-        /// 
-        [ParseFieldName("deviceType")]
-        public string DeviceType
-        {
-            get => GetProperty(nameof(DeviceType));
-            internal set => SetProperty(value, nameof(DeviceType));
-        }
+    /// 
+    /// The runtime target of this installation object.
+    /// 
+    [ParseFieldName("deviceType")]
+    public string DeviceType
+    {
+        get => GetProperty(nameof(DeviceType));
+        internal set => SetProperty(value, nameof(DeviceType));
+    }
 
-        /// 
-        /// The user-friendly display name of this application.
-        /// 
-        [ParseFieldName("appName")]
-        public string AppName
-        {
-            get => GetProperty(nameof(AppName));
-            internal set => SetProperty(value, nameof(AppName));
-        }
+    /// 
+    /// The user-friendly display name of this application.
+    /// 
+    [ParseFieldName("appName")]
+    public string AppName
+    {
+        get => GetProperty(nameof(AppName));
+        internal set => SetProperty(value, nameof(AppName));
+    }
 
-        /// 
-        /// A version string consisting of Major.Minor.Build.Revision.
-        /// 
-        [ParseFieldName("appVersion")]
-        public string AppVersion
+    /// 
+    /// A version string consisting of Major.Minor.Build.Revision.
+    /// 
+    [ParseFieldName("appVersion")]
+    public string AppVersion
+    {
+        get => GetProperty(nameof(AppVersion));
+        internal set => SetProperty(value, nameof(AppVersion));
+    }
+
+    /// 
+    /// The system-dependent unique identifier of this installation. This identifier should be
+    /// sufficient to distinctly name an app on stores which may allow multiple apps with the
+    /// same display name.
+    /// 
+    [ParseFieldName("appIdentifier")]
+    public string AppIdentifier
+    {
+        get => GetProperty(nameof(AppIdentifier));
+        internal set => SetProperty(value, nameof(AppIdentifier));
+    }
+
+    /// 
+    /// The time zone in which this device resides. This string is in the tz database format
+    /// Parse uses for local-time pushes. Due to platform restrictions, the mapping is less
+    /// granular on Windows than it may be on other systems. E.g. The zones
+    /// America/Vancouver America/Dawson America/Whitehorse, America/Tijuana, PST8PDT, and
+    /// America/Los_Angeles are all reported as America/Los_Angeles.
+    /// 
+    [ParseFieldName("timeZone")]
+    public string TimeZone
+    {
+        get => GetProperty(nameof(TimeZone));
+        private set => SetProperty(value, nameof(TimeZone));
+    }
+
+    /// 
+    /// The users locale. This field gets automatically populated by the SDK.
+    /// Can be null (Parse Push uses default language in this case).
+    /// 
+    [ParseFieldName("localeIdentifier")]
+    public string LocaleIdentifier
+    {
+        get => GetProperty(nameof(LocaleIdentifier));
+        private set => SetProperty(value, nameof(LocaleIdentifier));
+    }
+
+    /// 
+    /// Gets the locale identifier in the format: [language code]-[COUNTRY CODE].
+    /// 
+    /// The locale identifier in the format: [language code]-[COUNTRY CODE].
+    private string GetLocaleIdentifier()
+    {
+        string languageCode = null;
+        string countryCode = null;
+
+        if (CultureInfo.CurrentCulture != null)
         {
-            get => GetProperty(nameof(AppVersion));
-            internal set => SetProperty(value, nameof(AppVersion));
+            languageCode = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
         }
-
-        /// 
-        /// The system-dependent unique identifier of this installation. This identifier should be
-        /// sufficient to distinctly name an app on stores which may allow multiple apps with the
-        /// same display name.
-        /// 
-        [ParseFieldName("appIdentifier")]
-        public string AppIdentifier
+        if (RegionInfo.CurrentRegion != null)
         {
-            get => GetProperty(nameof(AppIdentifier));
-            internal set => SetProperty(value, nameof(AppIdentifier));
+            countryCode = RegionInfo.CurrentRegion.TwoLetterISORegionName;
         }
-
-        /// 
-        /// The time zone in which this device resides. This string is in the tz database format
-        /// Parse uses for local-time pushes. Due to platform restrictions, the mapping is less
-        /// granular on Windows than it may be on other systems. E.g. The zones
-        /// America/Vancouver America/Dawson America/Whitehorse, America/Tijuana, PST8PDT, and
-        /// America/Los_Angeles are all reported as America/Los_Angeles.
-        /// 
-        [ParseFieldName("timeZone")]
-        public string TimeZone
+        if (String.IsNullOrEmpty(countryCode))
         {
-            get => GetProperty(nameof(TimeZone));
-            private set => SetProperty(value, nameof(TimeZone));
+            return languageCode;
         }
-
-        /// 
-        /// The users locale. This field gets automatically populated by the SDK.
-        /// Can be null (Parse Push uses default language in this case).
-        /// 
-        [ParseFieldName("localeIdentifier")]
-        public string LocaleIdentifier
+        else
         {
-            get => GetProperty(nameof(LocaleIdentifier));
-            private set => SetProperty(value, nameof(LocaleIdentifier));
+            return String.Format("{0}-{1}", languageCode, countryCode);
         }
+    }
 
-        /// 
-        /// Gets the locale identifier in the format: [language code]-[COUNTRY CODE].
-        /// 
-        /// The locale identifier in the format: [language code]-[COUNTRY CODE].
-        private string GetLocaleIdentifier()
+    /// 
+    /// The version of the Parse SDK used to build this application.
+    /// 
+    [ParseFieldName("parseVersion")]
+    public Version ParseVersion
+    {
+        get
         {
-            string languageCode = null;
-            string countryCode = null;
-
-            if (CultureInfo.CurrentCulture != null)
-            {
-                languageCode = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
-            }
-            if (RegionInfo.CurrentRegion != null)
-            {
-                countryCode = RegionInfo.CurrentRegion.TwoLetterISORegionName;
-            }
-            if (String.IsNullOrEmpty(countryCode))
-            {
-                return languageCode;
-            }
+            string versionString = GetProperty(nameof(ParseVersion));
+            if (Version.TryParse(versionString, out var version))
+                return  version; // Return a default  version
             else
-            {
-                return String.Format("{0}-{1}", languageCode, countryCode);
-            }
+                return  new Version(0, 0); // Return a default version
         }
+        private set => SetProperty(value.ToString(), nameof(ParseVersion));
+    }
 
-        /// 
-        /// The version of the Parse SDK used to build this application.
-        /// 
-        [ParseFieldName("parseVersion")]
-        public Version ParseVersion
-        {
-            get
-            {
-                string versionString = GetProperty(nameof(ParseVersion));
-                Version version = null;
-                try
-                {
-                    version = new Version(versionString);
-                }
-                catch (Exception)
-                {
-                    // Do nothing.
-                }
+    /// 
+    /// A sequence of arbitrary strings which are used to identify this installation for push notifications.
+    /// By convention, the empty string is known as the "Broadcast" channel.
+    /// 
+    [ParseFieldName("channels")]
+    public IList Channels
+    {
+        get => GetProperty>(nameof(Channels));
+        set => SetProperty(value, nameof(Channels));
+    }
 
-                return version;
-            }
-            private set
-            {
-                Version version = value;
-                SetProperty(version.ToString(), nameof(ParseVersion));
-            }
-        }
+    protected override bool CheckKeyMutable(string key)
+    {
+        return !ImmutableKeys.Contains(key);
+    }
+
+    protected override async Task SaveAsync(Task toAwait, CancellationToken cancellationToken)
+    {
+        if (Services.CurrentInstallationController.IsCurrent(this))
 
-        /// 
-        /// A sequence of arbitrary strings which are used to identify this installation for push notifications.
-        /// By convention, the empty string is known as the "Broadcast" channel.
-        /// 
-        [ParseFieldName("channels")]
-        public IList Channels
         {
-            get => GetProperty>(nameof(Channels));
-            set => SetProperty(value, nameof(Channels));
+            SetIfDifferent("deviceType", Services.MetadataController.EnvironmentData.Platform);
+            SetIfDifferent("timeZone", Services.MetadataController.EnvironmentData.TimeZone);
+            SetIfDifferent("localeIdentifier", GetLocaleIdentifier());
+            SetIfDifferent("parseVersion", ParseClient.Version);
+            SetIfDifferent("appVersion", Services.MetadataController.HostManifestData.Version);
+            SetIfDifferent("appIdentifier", Services.MetadataController.HostManifestData.Identifier);
+            SetIfDifferent("appName", Services.MetadataController.HostManifestData.Name);
+
         }
 
-        protected override bool CheckKeyMutable(string key) => !ImmutableKeys.Contains(key);
+        Task platformHookTask = ParseClient.Instance.InstallationDataFinalizer.FinalizeAsync(this); 
 
-        protected override Task SaveAsync(Task toAwait, CancellationToken cancellationToken)
+        // Wait for the platform task, then proceed with saving the main task.
+        try
         {
-            Task platformHookTask = null;
-
-            if (Services.CurrentInstallationController.IsCurrent(this))
+            _ = platformHookTask.Safe().ConfigureAwait(false);
+            _ = base.SaveAsync(toAwait, cancellationToken).ConfigureAwait(false);
+            if (!Services.CurrentInstallationController.IsCurrent(this))
             {
-                SetIfDifferent("deviceType", Services.MetadataController.EnvironmentData.Platform);
-                SetIfDifferent("timeZone", Services.MetadataController.EnvironmentData.TimeZone);
-                SetIfDifferent("localeIdentifier", GetLocaleIdentifier());
-                SetIfDifferent("parseVersion", ParseClient.Version);
-                SetIfDifferent("appVersion", Services.MetadataController.HostManifestData.Version);
-                SetIfDifferent("appIdentifier", Services.MetadataController.HostManifestData.Identifier);
-                SetIfDifferent("appName", Services.MetadataController.HostManifestData.Name);
-
-#warning InstallationDataFinalizer needs to be injected here somehow or removed.
-
-                //platformHookTask = Client.InstallationDataFinalizer.FinalizeAsync(this);
+                _ = Services.CurrentInstallationController.SetAsync(this, cancellationToken).ConfigureAwait(false);
             }
-
-            return platformHookTask.Safe().OnSuccess(_ => base.SaveAsync(toAwait, cancellationToken)).Unwrap().OnSuccess(_ => Services.CurrentInstallationController.IsCurrent(this) ? Task.CompletedTask : Services.CurrentInstallationController.SetAsync(this, cancellationToken)).Unwrap();
         }
-
-        /// 
-        /// This mapping of Windows names to a standard everyone else uses is maintained
-        /// by the Unicode consortium, which makes this officially the first helpful
-        /// interaction between Unicode and Microsoft.
-        /// Unfortunately this is a little lossy in that we only store the first mapping in each zone because
-        /// Microsoft does not give us more granular location information.
-        /// Built from http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/zone_tzid.html
-        /// 
-        internal static Dictionary TimeZoneNameMap { get; } = new Dictionary
+        catch (Exception ex)
         {
-            ["Dateline Standard Time"] = "Etc/GMT+12",
-            ["UTC-11"] = "Etc/GMT+11",
-            ["Hawaiian Standard Time"] = "Pacific/Honolulu",
-            ["Alaskan Standard Time"] = "America/Anchorage",
-            ["Pacific Standard Time (Mexico)"] = "America/Santa_Isabel",
-            ["Pacific Standard Time"] = "America/Los_Angeles",
-            ["US Mountain Standard Time"] = "America/Phoenix",
-            ["Mountain Standard Time (Mexico)"] = "America/Chihuahua",
-            ["Mountain Standard Time"] = "America/Denver",
-            ["Central America Standard Time"] = "America/Guatemala",
-            ["Central Standard Time"] = "America/Chicago",
-            ["Central Standard Time (Mexico)"] = "America/Mexico_City",
-            ["Canada Central Standard Time"] = "America/Regina",
-            ["SA Pacific Standard Time"] = "America/Bogota",
-            ["Eastern Standard Time"] = "America/New_York",
-            ["US Eastern Standard Time"] = "America/Indianapolis",
-            ["Venezuela Standard Time"] = "America/Caracas",
-            ["Paraguay Standard Time"] = "America/Asuncion",
-            ["Atlantic Standard Time"] = "America/Halifax",
-            ["Central Brazilian Standard Time"] = "America/Cuiaba",
-            ["SA Western Standard Time"] = "America/La_Paz",
-            ["Pacific SA Standard Time"] = "America/Santiago",
-            ["Newfoundland Standard Time"] = "America/St_Johns",
-            ["E. South America Standard Time"] = "America/Sao_Paulo",
-            ["Argentina Standard Time"] = "America/Buenos_Aires",
-            ["SA Eastern Standard Time"] = "America/Cayenne",
-            ["Greenland Standard Time"] = "America/Godthab",
-            ["Montevideo Standard Time"] = "America/Montevideo",
-            ["Bahia Standard Time"] = "America/Bahia",
-            ["UTC-02"] = "Etc/GMT+2",
-            ["Azores Standard Time"] = "Atlantic/Azores",
-            ["Cape Verde Standard Time"] = "Atlantic/Cape_Verde",
-            ["Morocco Standard Time"] = "Africa/Casablanca",
-            ["UTC"] = "Etc/GMT",
-            ["GMT Standard Time"] = "Europe/London",
-            ["Greenwich Standard Time"] = "Atlantic/Reykjavik",
-            ["W. Europe Standard Time"] = "Europe/Berlin",
-            ["Central Europe Standard Time"] = "Europe/Budapest",
-            ["Romance Standard Time"] = "Europe/Paris",
-            ["Central European Standard Time"] = "Europe/Warsaw",
-            ["W. Central Africa Standard Time"] = "Africa/Lagos",
-            ["Namibia Standard Time"] = "Africa/Windhoek",
-            ["GTB Standard Time"] = "Europe/Bucharest",
-            ["Middle East Standard Time"] = "Asia/Beirut",
-            ["Egypt Standard Time"] = "Africa/Cairo",
-            ["Syria Standard Time"] = "Asia/Damascus",
-            ["E. Europe Standard Time"] = "Asia/Nicosia",
-            ["South Africa Standard Time"] = "Africa/Johannesburg",
-            ["FLE Standard Time"] = "Europe/Kiev",
-            ["Turkey Standard Time"] = "Europe/Istanbul",
-            ["Israel Standard Time"] = "Asia/Jerusalem",
-            ["Jordan Standard Time"] = "Asia/Amman",
-            ["Arabic Standard Time"] = "Asia/Baghdad",
-            ["Kaliningrad Standard Time"] = "Europe/Kaliningrad",
-            ["Arab Standard Time"] = "Asia/Riyadh",
-            ["E. Africa Standard Time"] = "Africa/Nairobi",
-            ["Iran Standard Time"] = "Asia/Tehran",
-            ["Arabian Standard Time"] = "Asia/Dubai",
-            ["Azerbaijan Standard Time"] = "Asia/Baku",
-            ["Russian Standard Time"] = "Europe/Moscow",
-            ["Mauritius Standard Time"] = "Indian/Mauritius",
-            ["Georgian Standard Time"] = "Asia/Tbilisi",
-            ["Caucasus Standard Time"] = "Asia/Yerevan",
-            ["Afghanistan Standard Time"] = "Asia/Kabul",
-            ["Pakistan Standard Time"] = "Asia/Karachi",
-            ["West Asia Standard Time"] = "Asia/Tashkent",
-            ["India Standard Time"] = "Asia/Calcutta",
-            ["Sri Lanka Standard Time"] = "Asia/Colombo",
-            ["Nepal Standard Time"] = "Asia/Katmandu",
-            ["Central Asia Standard Time"] = "Asia/Almaty",
-            ["Bangladesh Standard Time"] = "Asia/Dhaka",
-            ["Ekaterinburg Standard Time"] = "Asia/Yekaterinburg",
-            ["Myanmar Standard Time"] = "Asia/Rangoon",
-            ["SE Asia Standard Time"] = "Asia/Bangkok",
-            ["N. Central Asia Standard Time"] = "Asia/Novosibirsk",
-            ["China Standard Time"] = "Asia/Shanghai",
-            ["North Asia Standard Time"] = "Asia/Krasnoyarsk",
-            ["Singapore Standard Time"] = "Asia/Singapore",
-            ["W. Australia Standard Time"] = "Australia/Perth",
-            ["Taipei Standard Time"] = "Asia/Taipei",
-            ["Ulaanbaatar Standard Time"] = "Asia/Ulaanbaatar",
-            ["North Asia East Standard Time"] = "Asia/Irkutsk",
-            ["Tokyo Standard Time"] = "Asia/Tokyo",
-            ["Korea Standard Time"] = "Asia/Seoul",
-            ["Cen. Australia Standard Time"] = "Australia/Adelaide",
-            ["AUS Central Standard Time"] = "Australia/Darwin",
-            ["E. Australia Standard Time"] = "Australia/Brisbane",
-            ["AUS Eastern Standard Time"] = "Australia/Sydney",
-            ["West Pacific Standard Time"] = "Pacific/Port_Moresby",
-            ["Tasmania Standard Time"] = "Australia/Hobart",
-            ["Yakutsk Standard Time"] = "Asia/Yakutsk",
-            ["Central Pacific Standard Time"] = "Pacific/Guadalcanal",
-            ["Vladivostok Standard Time"] = "Asia/Vladivostok",
-            ["New Zealand Standard Time"] = "Pacific/Auckland",
-            ["UTC+12"] = "Etc/GMT-12",
-            ["Fiji Standard Time"] = "Pacific/Fiji",
-            ["Magadan Standard Time"] = "Asia/Magadan",
-            ["Tonga Standard Time"] = "Pacific/Tongatapu",
-            ["Samoa Standard Time"] = "Pacific/Apia"
-        };
+            // Log or handle the exception
+            // You can log it or rethrow if necessary
+            Console.Error.WriteLine(ex);
+        }
 
-        /// 
-        /// This is a mapping of odd TimeZone offsets to their respective IANA codes across the world.
-        /// This list was compiled from painstakingly pouring over the information available at
-        /// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.
-        /// 
-        internal static Dictionary TimeZoneOffsetMap { get; } = new Dictionary
-        {
-            [new TimeSpan(12, 45, 0)] = "Pacific/Chatham",
-            [new TimeSpan(10, 30, 0)] = "Australia/Lord_Howe",
-            [new TimeSpan(9, 30, 0)] = "Australia/Adelaide",
-            [new TimeSpan(8, 45, 0)] = "Australia/Eucla",
-            [new TimeSpan(8, 30, 0)] = "Asia/Pyongyang", // Parse in North Korea confirmed.
-            [new TimeSpan(6, 30, 0)] = "Asia/Rangoon",
-            [new TimeSpan(5, 45, 0)] = "Asia/Kathmandu",
-            [new TimeSpan(5, 30, 0)] = "Asia/Colombo",
-            [new TimeSpan(4, 30, 0)] = "Asia/Kabul",
-            [new TimeSpan(3, 30, 0)] = "Asia/Tehran",
-            [new TimeSpan(-3, 30, 0)] = "America/St_Johns",
-            [new TimeSpan(-4, 30, 0)] = "America/Caracas",
-            [new TimeSpan(-9, 30, 0)] = "Pacific/Marquesas",
-        };
     }
+
+    /// 
+    /// This mapping of Windows names to a standard everyone else uses is maintained
+    /// by the Unicode consortium, which makes this officially the first helpful
+    /// interaction between Unicode and Microsoft.
+    /// Unfortunately this is a little lossy in that we only store the first mapping in each zone because
+    /// Microsoft does not give us more granular location information.
+    /// Built from http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/zone_tzid.html
+    /// 
+    internal static Dictionary TimeZoneNameMap { get; } = new Dictionary
+    {
+        ["Dateline Standard Time"] = "Etc/GMT+12",
+        ["UTC-11"] = "Etc/GMT+11",
+        ["Hawaiian Standard Time"] = "Pacific/Honolulu",
+        ["Alaskan Standard Time"] = "America/Anchorage",
+        ["Pacific Standard Time (Mexico)"] = "America/Tijuana",
+        ["Pacific Standard Time"] = "America/Los_Angeles",
+        ["US Mountain Standard Time"] = "America/Phoenix",
+        ["Mountain Standard Time (Mexico)"] = "America/Chihuahua",
+        ["Mountain Standard Time"] = "America/Denver",
+        ["Central America Standard Time"] = "America/Guatemala",
+        ["Central Standard Time"] = "America/Chicago",
+        ["Central Standard Time (Mexico)"] = "America/Mexico_City",
+        ["Canada Central Standard Time"] = "America/Regina",
+        ["SA Pacific Standard Time"] = "America/Bogota",
+        ["Eastern Standard Time"] = "America/New_York",
+        ["US Eastern Standard Time"] = "America/Indianapolis",
+        ["Venezuela Standard Time"] = "America/Caracas",
+        ["Paraguay Standard Time"] = "America/Asuncion",
+        ["Atlantic Standard Time"] = "America/Halifax",
+        ["Central Brazilian Standard Time"] = "America/Cuiaba",
+        ["SA Western Standard Time"] = "America/La_Paz",
+        ["Pacific SA Standard Time"] = "America/Santiago",
+        ["Newfoundland Standard Time"] = "America/St_Johns",
+        ["E. South America Standard Time"] = "America/Sao_Paulo",
+        ["Argentina Standard Time"] = "America/Buenos_Aires",
+        ["SA Eastern Standard Time"] = "America/Cayenne",
+        ["Greenland Standard Time"] = "America/Nuuk",
+        ["Montevideo Standard Time"] = "America/Montevideo",
+        ["Bahia Standard Time"] = "America/Bahia",
+        ["UTC-02"] = "Etc/GMT+2",
+        ["Azores Standard Time"] = "Atlantic/Azores",
+        ["Cape Verde Standard Time"] = "Atlantic/Cape_Verde",
+        ["Morocco Standard Time"] = "Africa/Casablanca",
+        ["UTC"] = "Etc/GMT",
+        ["GMT Standard Time"] = "Europe/London",
+        ["Greenwich Standard Time"] = "Atlantic/Reykjavik",
+        ["W. Europe Standard Time"] = "Europe/Berlin",
+        ["Central Europe Standard Time"] = "Europe/Budapest",
+        ["Romance Standard Time"] = "Europe/Paris",
+        ["Central European Standard Time"] = "Europe/Warsaw",
+        ["W. Central Africa Standard Time"] = "Africa/Lagos",
+        ["Namibia Standard Time"] = "Africa/Windhoek",
+        ["GTB Standard Time"] = "Europe/Bucharest",
+        ["Middle East Standard Time"] = "Asia/Beirut",
+        ["Egypt Standard Time"] = "Africa/Cairo",
+        ["Syria Standard Time"] = "Asia/Damascus",
+        ["E. Europe Standard Time"] = "Europe/Minsk",
+        ["South Africa Standard Time"] = "Africa/Johannesburg",
+        ["FLE Standard Time"] = "Europe/Kiev",
+        ["Turkey Standard Time"] = "Europe/Istanbul",
+        ["Israel Standard Time"] = "Asia/Jerusalem",
+        ["Jordan Standard Time"] = "Asia/Amman",
+        ["Arabic Standard Time"] = "Asia/Baghdad",
+        ["Kaliningrad Standard Time"] = "Europe/Kaliningrad",
+        ["Arab Standard Time"] = "Asia/Riyadh",
+        ["E. Africa Standard Time"] = "Africa/Nairobi",
+        ["Iran Standard Time"] = "Asia/Tehran",
+        ["Arabian Standard Time"] = "Asia/Dubai",
+        ["Azerbaijan Standard Time"] = "Asia/Baku",
+        ["Russian Standard Time"] = "Europe/Moscow",
+        ["Mauritius Standard Time"] = "Indian/Mauritius",
+        ["Georgian Standard Time"] = "Asia/Tbilisi",
+        ["Caucasus Standard Time"] = "Asia/Yerevan",
+        ["Afghanistan Standard Time"] = "Asia/Kabul",
+        ["Pakistan Standard Time"] = "Asia/Karachi",
+        ["West Asia Standard Time"] = "Asia/Tashkent",
+        ["India Standard Time"] = "Asia/Kolkata",
+        ["Sri Lanka Standard Time"] = "Asia/Colombo",
+        ["Nepal Standard Time"] = "Asia/Kathmandu",
+        ["Central Asia Standard Time"] = "Asia/Almaty",
+        ["Bangladesh Standard Time"] = "Asia/Dhaka",
+        ["Ekaterinburg Standard Time"] = "Asia/Yekaterinburg",
+        ["Myanmar Standard Time"] = "Asia/Yangon",
+        ["SE Asia Standard Time"] = "Asia/Bangkok",
+        ["N. Central Asia Standard Time"] = "Asia/Novosibirsk",
+        ["China Standard Time"] = "Asia/Shanghai",
+        ["North Asia Standard Time"] = "Asia/Krasnoyarsk",
+        ["Singapore Standard Time"] = "Asia/Singapore",
+        ["W. Australia Standard Time"] = "Australia/Perth",
+        ["Taipei Standard Time"] = "Asia/Taipei",
+        ["Ulaanbaatar Standard Time"] = "Asia/Ulaanbaatar",
+        ["North Asia East Standard Time"] = "Asia/Irkutsk",
+        ["Tokyo Standard Time"] = "Asia/Tokyo",
+        ["Korea Standard Time"] = "Asia/Seoul",
+        ["Cen. Australia Standard Time"] = "Australia/Adelaide",
+        ["AUS Central Standard Time"] = "Australia/Darwin",
+        ["E. Australia Standard Time"] = "Australia/Brisbane",
+        ["AUS Eastern Standard Time"] = "Australia/Sydney",
+        ["West Pacific Standard Time"] = "Pacific/Port_Moresby",
+        ["Tasmania Standard Time"] = "Australia/Hobart",
+        ["Yakutsk Standard Time"] = "Asia/Yakutsk",
+        ["Central Pacific Standard Time"] = "Pacific/Guadalcanal",
+        ["Vladivostok Standard Time"] = "Asia/Vladivostok",
+        ["New Zealand Standard Time"] = "Pacific/Auckland",
+        ["UTC+12"] = "Etc/GMT-12",
+        ["Fiji Standard Time"] = "Pacific/Fiji",
+        ["Magadan Standard Time"] = "Asia/Magadan",
+        ["Tonga Standard Time"] = "Pacific/Tongatapu",
+        ["Samoa Standard Time"] = "Pacific/Apia"
+    };
+
+
+    /// 
+    /// This is a mapping of odd TimeZone offsets to their respective IANA codes across the world.
+    /// This list was compiled from painstakingly pouring over the information available at
+    /// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.
+    /// 
+    internal static Dictionary TimeZoneOffsetMap { get; } = new Dictionary
+    {
+        [new TimeSpan(12, 45, 0)] = "Pacific/Chatham",//new one
+        [new TimeSpan(10, 30, 0)] = "Australia/Lord_Howe",
+        [new TimeSpan(9, 30, 0)] = "Australia/Adelaide",
+        [new TimeSpan(8, 45, 0)] = "Australia/Eucla",
+        [new TimeSpan(8, 30, 0)] = "Asia/Pyongyang",
+        [new TimeSpan(6, 30, 0)] = "Asia/Yangon",
+        [new TimeSpan(5, 45, 0)] = "Asia/Kathmandu",
+        [new TimeSpan(5, 30, 0)] = "Asia/Colombo",
+        [new TimeSpan(4, 30, 0)] = "Asia/Kabul",
+        [new TimeSpan(3, 30, 0)] = "Asia/Tehran",
+        [new TimeSpan(-3, 30, 0)] = "America/St_Johns",
+        [new TimeSpan(-4, 30, 0)] = "America/Caracas",
+        [new TimeSpan(-9, 30, 0)] = "Pacific/Marquesas"
+    };
+
 }
diff --git a/Parse/Platform/Installations/ParseInstallationCoder.cs b/Parse/Platform/Installations/ParseInstallationCoder.cs
index 25263db6..4cd40166 100644
--- a/Parse/Platform/Installations/ParseInstallationCoder.cs
+++ b/Parse/Platform/Installations/ParseInstallationCoder.cs
@@ -6,34 +6,36 @@
 using Parse.Abstractions.Platform.Objects;
 using Parse.Infrastructure.Data;
 
-namespace Parse.Platform.Installations
+namespace Parse.Platform.Installations;
+
+public class ParseInstallationCoder : IParseInstallationCoder
 {
-    public class ParseInstallationCoder : IParseInstallationCoder
-    {
-        IParseDataDecoder Decoder { get; }
+    IParseDataDecoder Decoder { get; }
 
-        IParseObjectClassController ClassController { get; }
+    IParseObjectClassController ClassController { get; }
 
-        public ParseInstallationCoder(IParseDataDecoder decoder, IParseObjectClassController classController) => (Decoder, ClassController) = (decoder, classController);
+    public ParseInstallationCoder(IParseDataDecoder decoder, IParseObjectClassController classController) => (Decoder, ClassController) = (decoder, classController);
 
-        public IDictionary Encode(ParseInstallation installation)
-        {
-            IObjectState state = installation.State;
-            IDictionary data = PointerOrLocalIdEncoder.Instance.Encode(state.ToDictionary(pair => pair.Key, pair => pair.Value), installation.Services) as IDictionary;
+    public IDictionary Encode(ParseInstallation installation)
+    {
+        IObjectState state = installation.State;
+        IDictionary data = PointerOrLocalIdEncoder.Instance.Encode(state.ToDictionary(pair => pair.Key, pair => pair.Value), installation.Services) as IDictionary;
 
-            data["objectId"] = state.ObjectId;
+        data["objectId"] = state.ObjectId;
 
-            // The following operations use the date and time serialization format defined by ISO standard 8601.
+        // The following operations use the date and time serialization format defined by ISO standard 8601.
 
-            if (state.CreatedAt is { })
-                data["createdAt"] = state.CreatedAt.Value.ToString(ParseClient.DateFormatStrings[0]);
+        if (state.CreatedAt is { })
+            data["createdAt"] = state.CreatedAt.Value.ToString(ParseClient.DateFormatStrings[0]);
 
-            if (state.UpdatedAt is { })
-                data["updatedAt"] = state.UpdatedAt.Value.ToString(ParseClient.DateFormatStrings[0]);
+        if (state.UpdatedAt is { })
+            data["updatedAt"] = state.UpdatedAt.Value.ToString(ParseClient.DateFormatStrings[0]);
 
-            return data;
-        }
+        return data;
+    }
 
-        public ParseInstallation Decode(IDictionary data, IServiceHub serviceHub) => ClassController.GenerateObjectFromState(ParseObjectCoder.Instance.Decode(data, Decoder, serviceHub), "_Installation", serviceHub);
+    public ParseInstallation Decode(IDictionary data, IServiceHub serviceHub)
+    {
+        return ClassController.GenerateObjectFromState(ParseObjectCoder.Instance.Decode(data, Decoder, serviceHub), "_Installation", serviceHub);
     }
 }
\ No newline at end of file
diff --git a/Parse/Platform/Installations/ParseInstallationController.cs b/Parse/Platform/Installations/ParseInstallationController.cs
index 23d4ac0f..6251a3ca 100644
--- a/Parse/Platform/Installations/ParseInstallationController.cs
+++ b/Parse/Platform/Installations/ParseInstallationController.cs
@@ -2,59 +2,77 @@
 using System.Threading.Tasks;
 using Parse.Abstractions.Infrastructure;
 using Parse.Abstractions.Platform.Installations;
-using Parse.Infrastructure.Utilities;
 
-namespace Parse.Platform.Installations
+namespace Parse.Platform.Installations;
+
+public class ParseInstallationController : IParseInstallationController
 {
-    public class ParseInstallationController : IParseInstallationController
-    {
-        static string InstallationIdKey { get; } = "InstallationId";
+    static string InstallationIdKey { get; } = "InstallationId";
+
+    object Mutex { get; } = new object { };
 
-        object Mutex { get; } = new object { };
+    Guid? InstallationId { get; set; }
 
-        Guid? InstallationId { get; set; }
+    ICacheController StorageController { get; }
 
-        ICacheController StorageController { get; }
+    public ParseInstallationController(ICacheController storageController) => StorageController = storageController;
 
-        public ParseInstallationController(ICacheController storageController) => StorageController = storageController;
+    public async Task SetAsync(Guid? installationId)
+    {
+        // Directly handle the async calls without using locks
+        var storage = await StorageController.LoadAsync().ConfigureAwait(false);
 
-        public Task SetAsync(Guid? installationId)
+        // Update the installationId and modify storage accordingly
+        if (installationId.HasValue)
         {
-            lock (Mutex)
-            {
-#warning Should refactor here if this operates correctly.
+            await storage.AddAsync(InstallationIdKey, installationId.Value.ToString()).ConfigureAwait(false);
+        }
+        else
+        {
+            await storage.RemoveAsync(InstallationIdKey).ConfigureAwait(false);
+        }
+
+        // Set the current installationId
+        InstallationId = installationId;
+    }
 
-                Task saveTask = installationId is { } ? StorageController.LoadAsync().OnSuccess(storage => storage.Result.AddAsync(InstallationIdKey, installationId.ToString())).Unwrap() : StorageController.LoadAsync().OnSuccess(storage => storage.Result.RemoveAsync(InstallationIdKey)).Unwrap();
 
-                InstallationId = installationId;
-                return saveTask;
+    public async Task GetAsync()
+    {
+        lock (Mutex)
+        {
+            if (InstallationId != null)
+            {
+                return InstallationId;
             }
         }
 
-        public Task GetAsync()
+        // Await the asynchronous storage loading task
+        var storageResult = await StorageController.LoadAsync();
+
+        // Try to get the installation ID from the storage result
+        if (storageResult.TryGetValue(InstallationIdKey, out object id) && id is string idString && Guid.TryParse(idString, out Guid parsedId))
         {
             lock (Mutex)
-                if (InstallationId is { })
-                    return Task.FromResult(InstallationId);
-
-            return StorageController.LoadAsync().OnSuccess(storageTask =>
             {
-                storageTask.Result.TryGetValue(InstallationIdKey, out object id);
-
-                try
-                {
-                    lock (Mutex)
-                        return Task.FromResult(InstallationId = new Guid(id as string));
-                }
-                catch (Exception)
-                {
-                    Guid newInstallationId = Guid.NewGuid();
-                    return SetAsync(newInstallationId).OnSuccess(_ => newInstallationId);
-                }
-            })
-            .Unwrap();
+                InstallationId = parsedId; // Cache the parsed ID
+                return InstallationId;
+            }
         }
 
-        public Task ClearAsync() => SetAsync(null);
+        // If no valid ID is found, generate a new one
+        Guid newInstallationId = Guid.NewGuid();
+        await SetAsync(newInstallationId); // Save the new ID
+
+        lock (Mutex)
+        {
+            InstallationId = newInstallationId; // Cache the new ID
+            return InstallationId;
+        }
+    }
+
+    public Task ClearAsync()
+    {
+        return SetAsync(null);
     }
 }
diff --git a/Parse/Platform/Installations/ParseInstallationDataFinalizer.cs b/Parse/Platform/Installations/ParseInstallationDataFinalizer.cs
index 4f6d335f..464853f3 100644
--- a/Parse/Platform/Installations/ParseInstallationDataFinalizer.cs
+++ b/Parse/Platform/Installations/ParseInstallationDataFinalizer.cs
@@ -1,15 +1,17 @@
 using System.Threading.Tasks;
 using Parse.Abstractions.Platform.Installations;
 
-namespace Parse.Platform.Installations
+namespace Parse.Platform.Installations;
+
+/// 
+/// Controls the device information.
+/// 
+public class ParseInstallationDataFinalizer : IParseInstallationDataFinalizer
 {
-    /// 
-    /// Controls the device information.
-    /// 
-    public class ParseInstallationDataFinalizer : IParseInstallationDataFinalizer
+    public Task FinalizeAsync(ParseInstallation installation)
     {
-        public Task FinalizeAsync(ParseInstallation installation) => Task.FromResult(null);
-
-        public void Initialize() { }
+        return Task.FromResult(null);
     }
+
+    public void Initialize() { }
 }
\ No newline at end of file
diff --git a/Parse/Platform/Location/ParseGeoDistance.cs b/Parse/Platform/Location/ParseGeoDistance.cs
index 843e5cd6..cf5b6725 100644
--- a/Parse/Platform/Location/ParseGeoDistance.cs
+++ b/Parse/Platform/Location/ParseGeoDistance.cs
@@ -1,54 +1,62 @@
-namespace Parse
+namespace Parse;
+
+/// 
+/// Represents a distance between two ParseGeoPoints.
+/// 
+public struct ParseGeoDistance
 {
+    private const double EarthMeanRadiusKilometers = 6371.0;
+    private const double EarthMeanRadiusMiles = 3958.8;
+
+    /// 
+    /// Creates a ParseGeoDistance.
+    /// 
+    /// The distance in radians.
+    public ParseGeoDistance(double radians)
+      : this() => Radians = radians;
+
+    /// 
+    /// Gets the distance in radians.
+    /// 
+    public double Radians { get; private set; }
+
+    /// 
+    /// Gets the distance in miles.
+    /// 
+    public double Miles => Radians * EarthMeanRadiusMiles;
+
+    /// 
+    /// Gets the distance in kilometers.
+    /// 
+    public double Kilometers => Radians * EarthMeanRadiusKilometers;
+
+    /// 
+    /// Gets a ParseGeoDistance from a number of miles.
+    /// 
+    /// The number of miles.
+    /// A ParseGeoDistance for the given number of miles.
+    public static ParseGeoDistance FromMiles(double miles)
+    {
+        return new ParseGeoDistance(miles / EarthMeanRadiusMiles);
+    }
+
+    /// 
+    /// Gets a ParseGeoDistance from a number of kilometers.
+    /// 
+    /// The number of kilometers.
+    /// A ParseGeoDistance for the given number of kilometers.
+    public static ParseGeoDistance FromKilometers(double kilometers)
+    {
+        return new ParseGeoDistance(kilometers / EarthMeanRadiusKilometers);
+    }
+
     /// 
-    /// Represents a distance between two ParseGeoPoints.
+    /// Gets a ParseGeoDistance from a number of radians.
     /// 
-    public struct ParseGeoDistance
+    /// The number of radians.
+    /// A ParseGeoDistance for the given number of radians.
+    public static ParseGeoDistance FromRadians(double radians)
     {
-        private const double EarthMeanRadiusKilometers = 6371.0;
-        private const double EarthMeanRadiusMiles = 3958.8;
-
-        /// 
-        /// Creates a ParseGeoDistance.
-        /// 
-        /// The distance in radians.
-        public ParseGeoDistance(double radians)
-          : this() => Radians = radians;
-
-        /// 
-        /// Gets the distance in radians.
-        /// 
-        public double Radians { get; private set; }
-
-        /// 
-        /// Gets the distance in miles.
-        /// 
-        public double Miles => Radians * EarthMeanRadiusMiles;
-
-        /// 
-        /// Gets the distance in kilometers.
-        /// 
-        public double Kilometers => Radians * EarthMeanRadiusKilometers;
-
-        /// 
-        /// Gets a ParseGeoDistance from a number of miles.
-        /// 
-        /// The number of miles.
-        /// A ParseGeoDistance for the given number of miles.
-        public static ParseGeoDistance FromMiles(double miles) => new ParseGeoDistance(miles / EarthMeanRadiusMiles);
-
-        /// 
-        /// Gets a ParseGeoDistance from a number of kilometers.
-        /// 
-        /// The number of kilometers.
-        /// A ParseGeoDistance for the given number of kilometers.
-        public static ParseGeoDistance FromKilometers(double kilometers) => new ParseGeoDistance(kilometers / EarthMeanRadiusKilometers);
-
-        /// 
-        /// Gets a ParseGeoDistance from a number of radians.
-        /// 
-        /// The number of radians.
-        /// A ParseGeoDistance for the given number of radians.
-        public static ParseGeoDistance FromRadians(double radians) => new ParseGeoDistance(radians);
+        return new ParseGeoDistance(radians);
     }
 }
diff --git a/Parse/Platform/Location/ParseGeoPoint.cs b/Parse/Platform/Location/ParseGeoPoint.cs
index da1b6190..e9aa5ead 100644
--- a/Parse/Platform/Location/ParseGeoPoint.cs
+++ b/Parse/Platform/Location/ParseGeoPoint.cs
@@ -1,97 +1,102 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using Parse.Abstractions.Infrastructure;
 
-namespace Parse
+namespace Parse;
+
+/// 
+/// ParseGeoPoint represents a latitude / longitude point that may be associated
+/// with a key in a ParseObject or used as a reference point for geo queries.
+/// This allows proximity-based queries on the key.
+///
+/// Only one key in a class may contain a GeoPoint.
+/// 
+public struct ParseGeoPoint : IJsonConvertible
 {
     /// 
-    /// ParseGeoPoint represents a latitude / longitude point that may be associated
-    /// with a key in a ParseObject or used as a reference point for geo queries.
-    /// This allows proximity-based queries on the key.
-    ///
-    /// Only one key in a class may contain a GeoPoint.
+    /// Constructs a ParseGeoPoint with the specified latitude and longitude.
     /// 
-    public struct ParseGeoPoint : IJsonConvertible
+    /// The point's latitude.
+    /// The point's longitude.
+    public ParseGeoPoint(double latitude, double longitude)
+      : this()
     {
-        /// 
-        /// Constructs a ParseGeoPoint with the specified latitude and longitude.
-        /// 
-        /// The point's latitude.
-        /// The point's longitude.
-        public ParseGeoPoint(double latitude, double longitude)
-          : this()
-        {
-            Latitude = latitude;
-            Longitude = longitude;
-        }
+        Latitude = latitude;
+        Longitude = longitude;
+    }
 
-        private double latitude;
-        /// 
-        /// Gets or sets the latitude of the GeoPoint. Valid range is [-90, 90].
-        /// Extremes should not be used.
-        /// 
-        public double Latitude
+    private double latitude;
+    /// 
+    /// Gets or sets the latitude of the GeoPoint. Valid range is [-90, 90].
+    /// Extremes should not be used.
+    /// 
+    public double Latitude
+    {
+        get => latitude;
+        set
         {
-            get => latitude;
-            set
+            if (value > 90 || value < -90)
             {
-                if (value > 90 || value < -90)
-                {
-                    throw new ArgumentOutOfRangeException("value",
-                      "Latitude must be within the range [-90, 90]");
-                }
-                latitude = value;
+                Debug.WriteLine($"Invalid Latitude: {value}");
+                throw new ArgumentOutOfRangeException("value",
+                  "Latitude must be within the range [-90, 90]");
             }
+            latitude = value;
         }
+    }
 
-        private double longitude;
-        /// 
-        /// Gets or sets the longitude. Valid range is [-180, 180].
-        /// Extremes should not be used.
-        /// 
-        public double Longitude
+    private double longitude;
+    /// 
+    /// Gets or sets the longitude. Valid range is [-180, 180].
+    /// Extremes should not be used.
+    /// 
+    public double Longitude
+    {
+        get => longitude;
+        set
         {
-            get => longitude;
-            set
+            if (value > 180 || value < -180)
             {
-                if (value > 180 || value < -180)
-                {
-                    throw new ArgumentOutOfRangeException("value",
-                      "Longitude must be within the range [-180, 180]");
-                }
-                longitude = value;
+                Debug.WriteLine($"Invalid Latitude: {value}");
+                throw new ArgumentOutOfRangeException("value",
+                  "Longitude must be within the range [-180, 180]");
             }
+            longitude = value;
         }
+    }
 
-        /// 
-        /// Get the distance in radians between this point and another GeoPoint. This is the smallest angular
-        /// distance between the two points.
-        /// 
-        /// GeoPoint describing the other point being measured against.
-        /// The distance in between the two points.
-        public ParseGeoDistance DistanceTo(ParseGeoPoint point)
-        {
-            double d2r = Math.PI / 180; // radian conversion factor
-            double lat1rad = Latitude * d2r;
-            double long1rad = longitude * d2r;
-            double lat2rad = point.Latitude * d2r;
-            double long2rad = point.Longitude * d2r;
-            double deltaLat = lat1rad - lat2rad;
-            double deltaLong = long1rad - long2rad;
-            double sinDeltaLatDiv2 = Math.Sin(deltaLat / 2);
-            double sinDeltaLongDiv2 = Math.Sin(deltaLong / 2);
-            // Square of half the straight line chord distance between both points.
-            // [0.0, 1.0]
-            double a = sinDeltaLatDiv2 * sinDeltaLatDiv2 +
-              Math.Cos(lat1rad) * Math.Cos(lat2rad) * sinDeltaLongDiv2 * sinDeltaLongDiv2;
-            a = Math.Min(1.0, a);
-            return new ParseGeoDistance(2 * Math.Asin(Math.Sqrt(a)));
-        }
+    /// 
+    /// Get the distance in radians between this point and another GeoPoint. This is the smallest angular
+    /// distance between the two points.
+    /// 
+    /// GeoPoint describing the other point being measured against.
+    /// The distance in between the two points.
+    public ParseGeoDistance DistanceTo(ParseGeoPoint point)
+    {
+        double d2r = Math.PI / 180; // radian conversion factor
+        double lat1rad = Latitude * d2r;
+        double long1rad = longitude * d2r;
+        double lat2rad = point.Latitude * d2r;
+        double long2rad = point.Longitude * d2r;
+        double deltaLat = lat1rad - lat2rad;
+        double deltaLong = long1rad - long2rad;
+        double sinDeltaLatDiv2 = Math.Sin(deltaLat / 2);
+        double sinDeltaLongDiv2 = Math.Sin(deltaLong / 2);
+        // Square of half the straight line chord distance between both points.
+        // [0.0, 1.0]
+        double a = sinDeltaLatDiv2 * sinDeltaLatDiv2 +
+          Math.Cos(lat1rad) * Math.Cos(lat2rad) * sinDeltaLongDiv2 * sinDeltaLongDiv2;
+        a = Math.Min(1.0, a);
+        return new ParseGeoDistance(2 * Math.Asin(Math.Sqrt(a)));
+    }
 
-        IDictionary IJsonConvertible.ConvertToJSON() => new Dictionary {
+    public IDictionary ConvertToJSON(IServiceHub serviceHub = default)
+    {
+        return new Dictionary {
         {"__type", "GeoPoint"},
-        {nameof(latitude), Latitude},
-        {nameof(longitude), Longitude}
-      };
+        {"latitude", Latitude},
+        {"longitude", Longitude}
+  };
     }
 }
diff --git a/Parse/Platform/Objects/MutableObjectState.cs b/Parse/Platform/Objects/MutableObjectState.cs
index 9b71a9ed..306e37a6 100644
--- a/Parse/Platform/Objects/MutableObjectState.cs
+++ b/Parse/Platform/Objects/MutableObjectState.cs
@@ -1,73 +1,183 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
 using System.Linq;
+using Parse.Abstractions.Infrastructure;
 using Parse.Abstractions.Infrastructure.Control;
 using Parse.Abstractions.Platform.Objects;
 using Parse.Infrastructure.Control;
 
-namespace Parse.Platform.Objects
+namespace Parse.Platform.Objects;
+
+public class MutableObjectState : IObjectState
 {
-    public class MutableObjectState : IObjectState
+    public bool IsNew { get; set; }
+    public string ClassName { get; set; }
+    public string ObjectId { get; set; }
+    public DateTime? UpdatedAt { get; set; }
+    public DateTime? CreatedAt { get; set; }
+    public string SessionToken { get; set; } // Added
+
+    public IDictionary ServerData { get; set; } = new Dictionary();
+    public object this[string key]
     {
-        public bool IsNew { get; set; }
-        public string ClassName { get; set; }
-        public string ObjectId { get; set; }
-        public DateTime? UpdatedAt { get; set; }
-        public DateTime? CreatedAt { get; set; }
+        get => ServerData.ContainsKey(key) ? ServerData[key] : null;
+        set
+        {
+            if (!Equals(ServerData[key], value))
+            {
+                ServerData[key] = value;
+                OnPropertyChanged(key); // Raise PropertyChanged for the updated key
+            }
+        }
+    }
 
-        public IDictionary ServerData { get; set; } = new Dictionary { };
+    public event PropertyChangedEventHandler PropertyChanged;
 
-        public object this[string key] => ServerData[key];
+    protected virtual void OnPropertyChanged(string propertyName)
+    {
+        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs( propertyName));
+    }
+
+    public bool ContainsKey(string key)
+    {
+        return ServerData.ContainsKey(key);
+    }
 
-        public bool ContainsKey(string key) => ServerData.ContainsKey(key);
 
-        public void Apply(IDictionary operationSet)
+
+    public void Apply(IDictionary operationSet)
+    {
+        foreach (var pair in operationSet)
         {
-            // Apply operationSet
-            foreach (KeyValuePair pair in operationSet)
+            try
             {
-                ServerData.TryGetValue(pair.Key, out object oldValue);
-                object newValue = pair.Value.Apply(oldValue, pair.Key);
+                ServerData.TryGetValue(pair.Key, out var oldValue);
+                var newValue = pair.Value.Apply(oldValue, pair.Key);
                 if (newValue != ParseDeleteOperation.Token)
                     ServerData[pair.Key] = newValue;
                 else
                     ServerData.Remove(pair.Key);
             }
+            catch
+            {
+                // Log and skip incompatible field updates
+                Debug.WriteLine($"Skipped incompatible operation for key: {pair.Key}");
+            }
         }
+    }
+
+    public void Apply(IObjectState other)
+    {
+        IsNew = other.IsNew;
 
-        public void Apply(IObjectState other)
+        if (other.ObjectId != null)
+            ObjectId = other.ObjectId;
+        if (other.UpdatedAt != null)
+            UpdatedAt = other.UpdatedAt;
+        if (other.CreatedAt != null)
+            CreatedAt = other.CreatedAt;
+
+        foreach (var pair in other)
         {
-            IsNew = other.IsNew;
-            if (other.ObjectId != null)
-                ObjectId = other.ObjectId;
-            if (other.UpdatedAt != null)
-                UpdatedAt = other.UpdatedAt;
-            if (other.CreatedAt != null)
-                CreatedAt = other.CreatedAt;
-
-            foreach (KeyValuePair pair in other)
+            try
+            {
                 ServerData[pair.Key] = pair.Value;
+            }
+            catch
+            {
+                // Log and skip incompatible fields
+                Debug.WriteLine($"Skipped incompatible field: {pair.Key}");
+            }
         }
-
-        public IObjectState MutatedClone(Action func)
+    }
+    public IObjectState MutatedClone(Action func)
+    {
+        var clone = MutableClone();
+        try
         {
-            MutableObjectState clone = MutableClone();
+            // Apply the mutation function to the clone
             func(clone);
-            return clone;
         }
-
-        protected virtual MutableObjectState MutableClone() => new MutableObjectState
+        catch (Exception ex)
+        {
+            // Log the failure and continue
+            Debug.WriteLine($"Skipped incompatible mutation during clone: {ex.Message}");
+        }
+        return clone;
+    }
+    protected virtual MutableObjectState MutableClone()
+    {
+        return new MutableObjectState
         {
             IsNew = IsNew,
             ClassName = ClassName,
             ObjectId = ObjectId,
             CreatedAt = CreatedAt,
             UpdatedAt = UpdatedAt,
-            ServerData = this.ToDictionary(t => t.Key, t => t.Value)
+            
+            ServerData = ServerData.ToDictionary(entry => entry.Key, entry => entry.Value)
         };
+    }
+
+    IEnumerator> IEnumerable>.GetEnumerator()
+    {
+        return ServerData.GetEnumerator();
+    }
+
+    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+    {
+        return ServerData.GetEnumerator();
+    }
+
+    public static MutableObjectState Decode(object data, IServiceHub serviceHub)
+    {
+        if (data is IDictionary dictionary)
+        {
+            try
+            {
+                var state = new MutableObjectState
+                {
+                    ClassName = dictionary.ContainsKey("className") ? dictionary["className"]?.ToString() : null,
+                    ObjectId = dictionary.ContainsKey("objectId") ? dictionary["objectId"]?.ToString() : null,
+                    CreatedAt = dictionary.ContainsKey("createdAt") ? DecodeDateTime(dictionary["createdAt"]) : null,
+                    UpdatedAt = dictionary.ContainsKey("updatedAt") ? DecodeDateTime(dictionary["updatedAt"]) : null,
+                    IsNew = dictionary.ContainsKey("isNew") && Convert.ToBoolean(dictionary["isNew"]),
+                    ServerData = dictionary
+                        .Where(pair => IsValidField(pair.Key, pair.Value))
+                        .ToDictionary(pair => pair.Key, pair => pair.Value)
+                };
 
-        IEnumerator> IEnumerable>.GetEnumerator() => ServerData.GetEnumerator();
+                return state;
+            }
+            catch (Exception ex)
+            {
+                Debug.WriteLine($"Failed to decode MutableObjectState: {ex.Message}");
+                return null; // Graceful failure
+            }
+        }
+
+        Debug.WriteLine("Data is not a compatible object for decoding.");
+        return null;
+    }
+
+    private static DateTime? DecodeDateTime(object value)
+    {
+        try
+        {
+            return value is DateTime dateTime ? dateTime : DateTime.Parse(value.ToString());
+        }
+        catch
+        {
+            Debug.WriteLine($"Failed to decode DateTime value: {value}");
+            return null; // Graceful fallback
+        }
+    }
 
-        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => ((IEnumerable>) this).GetEnumerator();
+    private static bool IsValidField(string key, object value)
+    {
+        // Add any validation logic for fields if needed
+        return !string.IsNullOrEmpty(key); // Example: Ignore null/empty keys
     }
 }
diff --git a/Parse/Platform/Objects/ParseObject.cs b/Parse/Platform/Objects/ParseObject.cs
index 25768796..910e94bc 100644
--- a/Parse/Platform/Objects/ParseObject.cs
+++ b/Parse/Platform/Objects/ParseObject.cs
@@ -13,1112 +13,1285 @@
 using Parse.Infrastructure.Utilities;
 using Parse.Platform.Objects;
 using Parse.Infrastructure.Data;
-
-namespace Parse
+using System.Diagnostics;
+
+namespace Parse;
+
+/// 
+/// The ParseObject is a local representation of data that can be saved and
+/// retrieved from the Parse cloud.
+/// 
+/// 
+/// The basic workflow for creating new data is to construct a new ParseObject,
+/// use the indexer to fill it with data, and then use SaveAsync() to persist to the
+/// database.
+/// 
+/// 
+/// The basic workflow for accessing existing data is to use a ParseQuery
+/// to specify which existing data to retrieve.
+/// 
+/// 
+public class ParseObject : IEnumerable>, INotifyPropertyChanged
 {
-    /// 
-    /// The ParseObject is a local representation of data that can be saved and
-    /// retrieved from the Parse cloud.
-    /// 
-    /// 
-    /// The basic workflow for creating new data is to construct a new ParseObject,
-    /// use the indexer to fill it with data, and then use SaveAsync() to persist to the
-    /// database.
-    /// 
-    /// 
-    /// The basic workflow for accessing existing data is to use a ParseQuery
-    /// to specify which existing data to retrieve.
-    /// 
-    /// 
-    public class ParseObject : IEnumerable>, INotifyPropertyChanged
-    {
-        internal static string AutoClassName { get; } = "_Automatic";
+    internal static string AutoClassName { get; } = "_Automatic";
 
-        internal static ThreadLocal CreatingPointer { get; } = new ThreadLocal(() => false);
+    internal static ThreadLocal CreatingPointer { get; } = new ThreadLocal(() => false);
 
-        internal TaskQueue TaskQueue { get; } = new TaskQueue { };
+    internal TaskQueue TaskQueue { get; } = new TaskQueue { };
 
-        /// 
-        /// The  instance being targeted. This should generally not be set except when an object is being constructed, as otherwise race conditions may occur. The preferred method to set this property is via calling .
-        /// 
-        public IServiceHub Services { get; set; }
+    /// 
+    /// The  instance being targeted. This should generally not be set except when an object is being constructed, as otherwise race conditions may occur. The preferred method to set this property is via calling .
+    /// 
+    public IServiceHub Services { get; set; }
 
-        /// 
-        /// Constructs a new ParseObject with no data in it. A ParseObject constructed in this way will
-        /// not have an ObjectId and will not persist to the database until 
-        /// is called.
-        /// 
-        /// 
-        /// Class names must be alphanumerical plus underscore, and start with a letter. It is recommended
-        /// to name classes in PascalCase.
-        /// 
-        /// The className for this ParseObject.
-        /// The  implementation instance to target for any resources. This paramater can be effectively set after construction via .
-        public ParseObject(string className, IServiceHub serviceHub = default)
+    /// 
+    /// Constructs a new ParseObject with no data in it. A ParseObject constructed in this way will
+    /// not have an ObjectId and will not persist to the database until 
+    /// is called.
+    /// 
+    /// 
+    /// Class names must be alphanumerical plus underscore, and start with a letter. It is recommended
+    /// to name classes in PascalCase.
+    /// 
+    /// The className for this ParseObject.
+    /// The  implementation instance to target for any resources. This paramater can be effectively set after construction via .
+    public ParseObject(string className, IServiceHub serviceHub = default)
+    {
+        // Validate serviceHub
+        if (serviceHub == null && ParseClient.Instance == null)
         {
-            // We use a ThreadLocal rather than passing a parameter so that createWithoutData can do the
-            // right thing with subclasses. It's ugly and terrible, but it does provide the development
-            // experience we generally want, so... yeah. Sorry to whomever has to deal with this in the
-            // future. I pinky-swear we won't make a habit of this -- you believe me, don't you?
+            throw new InvalidOperationException("A valid IServiceHub or ParseClient.Instance must be available to construct a ParseObject.");
+        }
 
-            bool isPointer = CreatingPointer.Value;
-            CreatingPointer.Value = false;
+        Services = serviceHub ?? ParseClient.Instance.Services;
 
-            if (AutoClassName.Equals(className ?? throw new ArgumentException("You must specify a Parse class name when creating a new ParseObject.")))
-            {
-                className = GetType().GetParseClassName();
-            }
+        // Validate and set className
+        if (string.IsNullOrWhiteSpace(className))
+        {
+            throw new ArgumentException("You must specify a Parse class name when creating a new ParseObject.");
+        }
 
-            // Technically, an exception should be thrown here for when both serviceHub and ParseClient.Instance is null, but it is not possible because ParseObjectClass.Constructor for derived classes is a reference to this constructor, and it will be called with a null serviceHub, then Bind will be called on the constructed object, so this needs to fail softly, unfortunately.
-            // Services = ... ?? throw new InvalidOperationException("A ParseClient needs to be initialized as the configured default instance before any ParseObjects can be instantiated.")
+        if (AutoClassName.Equals(className))
+        {
+            className = GetType().GetParseClassName() ?? throw new ArgumentException("Unable to determine class name for ParseObject.");
+        }
 
-            if ((Services = serviceHub ?? ParseClient.Instance) is { })
+        if (Services is not null)
+        {
+            // Validate against factory requirements
+            if (!Services.ClassController.GetClassMatch(className, GetType()))
             {
-                // If this is supposed to be created by a factory but wasn't, throw an exception.
-
-                if (!Services.ClassController.GetClassMatch(className, GetType()))
+                if (!typeof(ParseObject).IsAssignableFrom(GetType()))
                 {
+                    // Allow subclasses of ParseObject (like ParseUser, ParseSession, etc.)
+                    
                     throw new ArgumentException("You must create this type of ParseObject using ParseObject.Create() or the proper subclass.");
                 }
             }
 
-            State = new MutableObjectState { ClassName = className };
-            OnPropertyChanged(nameof(ClassName));
+        }
 
-            OperationSetQueue.AddLast(new Dictionary());
+        OperationSetQueue.AddLast(new Dictionary());
 
-            if (!isPointer)
-            {
-                Fetched = true;
-                IsDirty = true;
+        // Handle pointer creation
+        bool isPointer = CreatingPointer.Value;
+        CreatingPointer.Value = false;
 
-                SetDefaultValues();
-            }
-            else
+        Fetched = !isPointer;
+        IsDirty = !isPointer;
+
+        if (!isPointer)
+        {
+            SetDefaultValues();
+        }
+        // Initialize state
+        State = new MutableObjectState { ClassName = className };
+        OnPropertyChanged(nameof(ClassName));
+        
+    }
+
+    #region ParseObject Creation
+    public static T Create() where T : ParseObject, new()
+    {
+        try
+        {
+            
+            if (ParseClient.Instance.Services == null)
             {
-                IsDirty = false;
-                Fetched = false;
+                throw new InvalidOperationException("ParseClient.Services must be initialized before creating objects.");
             }
+
+            var instance = new T();
+            instance.Bind(ParseClient.Instance.Services); // Ensure the ServiceHub is attached
+            
+            return instance;
+        }
+        catch (Exception ex)
+        {
+
+            throw new Exception("Error when Creating parse Object..");
         }
+    }
+
 
-        #region ParseObject Creation
 
-        /// 
-        /// Constructor for use in ParseObject subclasses. Subclasses must specify a ParseClassName attribute. Subclasses that do not implement a constructor accepting  will need to be bond to an implementation instance via  after construction.
-        /// 
-        protected ParseObject(IServiceHub serviceHub = default) : this(AutoClassName, serviceHub) { }
+    /// 
+    /// Constructor for use in ParseObject subclasses. Subclasses must specify a ParseClassName attribute. Subclasses that do not implement a constructor accepting  will need to be bond to an implementation instance via  after construction.
+    /// 
+    protected ParseObject(IServiceHub serviceHub = default) : this(AutoClassName, serviceHub) { }
 
-        /// 
-        /// Attaches the given  implementation instance to this  or -derived class instance.
-        /// 
-        /// The serviceHub to use for all operations.
-        /// The instance which was mutated.
-        public ParseObject Bind(IServiceHub serviceHub) => (Instance: this, Services = serviceHub).Instance;
+    /// 
+    /// Attaches the given  implementation instance to this  or -derived class instance.
+    /// 
+    /// The serviceHub to use for all operations.
+    /// The instance which was mutated.
+    public ParseObject Bind(IServiceHub serviceHub)
+    {
+        return (Instance: this, Services = serviceHub).Instance;
+    }
 
-        /// 
-        /// Occurs when a property value changes.
-        /// 
-        public event PropertyChangedEventHandler PropertyChanged
+    /// 
+    /// Occurs when a property value changes.
+    /// 
+    public event PropertyChangedEventHandler PropertyChanged
+    {
+        add
         {
-            add
-            {
-                PropertyChangedHandler.Add(value);
-            }
-            remove
-            {
-                PropertyChangedHandler.Remove(value);
-            }
+            PropertyChangedHandler.Add(value);
         }
-
-        /// 
-        /// Gets or sets the ParseACL governing this object.
-        /// 
-        [ParseFieldName("ACL")]
-        public ParseACL ACL
+        remove
         {
-            get => GetProperty(default, nameof(ACL));
-            set => SetProperty(value, nameof(ACL));
+            PropertyChangedHandler.Remove(value);
         }
+    }
+
+    /// 
+    /// Gets or sets the ParseACL governing this object.
+    /// 
+    [ParseFieldName("ACL")]
+    public ParseACL ACL
+    {
+        get => GetProperty(default, nameof(ACL));
+        set => SetProperty(value, nameof(ACL));
+    }
 
-        /// 
-        /// Gets the class name for the ParseObject.
-        /// 
-        public string ClassName => State.ClassName;
+    /// 
+    /// Gets the class name for the ParseObject.
+    /// 
+    public string ClassName => State.ClassName;
 
-        /// 
-        /// Gets the first time this object was saved as the server sees it, so that if you create a
-        /// ParseObject, then wait a while, and then call , the
-        /// creation time will be the time of the first  call rather than
-        /// the time the object was created locally.
-        /// 
-        [ParseFieldName("createdAt")]
-        public DateTime? CreatedAt => State.CreatedAt;
+    /// 
+    /// Gets the first time this object was saved as the server sees it, so that if you create a
+    /// ParseObject, then wait a while, and then call , the
+    /// creation time will be the time of the first  call rather than
+    /// the time the object was created locally.
+    /// 
+    [ParseFieldName("createdAt")]
+    public DateTime? CreatedAt => State.CreatedAt;
 
-        /// 
-        /// Gets whether the ParseObject has been fetched.
-        /// 
-        public bool IsDataAvailable
+    /// 
+    /// Gets whether the ParseObject has been fetched.
+    /// 
+    public bool IsDataAvailable
+    {
+        get
         {
-            get
+            lock (Mutex)
             {
-                lock (Mutex)
-                {
-                    return Fetched;
-                }
+                return Fetched;
             }
         }
+    }
 
-        /// 
-        /// Indicates whether this ParseObject has unsaved changes.
-        /// 
-        public bool IsDirty
+    /// 
+    /// Indicates whether this ParseObject has unsaved changes.
+    /// 
+    public bool IsDirty
+    {
+        get
         {
-            get
-            {
-                lock (Mutex)
-                {
-                    return CheckIsDirty(true);
-                }
-            }
-            internal set
+            lock (Mutex)
             {
-                lock (Mutex)
-                {
-                    Dirty = value;
-                    OnPropertyChanged(nameof(IsDirty));
-                }
+                return CheckIsDirty(true);
             }
         }
-
-        /// 
-        /// Returns true if this object was created by the Parse server when the
-        /// object might have already been there (e.g. in the case of a Facebook
-        /// login)
-        /// 
-        public bool IsNew
+        internal set
         {
-            get => State.IsNew;
-            internal set
+            lock (Mutex)
             {
-                MutateState(mutableClone => mutableClone.IsNew = value);
-                OnPropertyChanged(nameof(IsNew));
+                Dirty = value;
+                OnPropertyChanged(nameof(IsDirty));
             }
         }
+    }
 
-        /// 
-        /// Gets a set view of the keys contained in this object. This does not include createdAt,
-        /// updatedAt, or objectId. It does include things like username and ACL.
-        /// 
-        public ICollection Keys
+    /// 
+    /// Returns true if this object was created by the Parse server when the
+    /// object might have already been there (e.g. in the case of a Facebook
+    /// login)
+    /// 
+    public bool IsNew
+    {
+        get => State.IsNew;
+        internal set
         {
-            get
-            {
-                lock (Mutex)
-                {
-                    return EstimatedData.Keys;
-                }
-            }
+            MutateState(mutableClone => mutableClone.IsNew = value);
+            OnPropertyChanged(nameof(IsNew));
         }
+    }
 
-        /// 
-        /// Gets or sets the object id. An object id is assigned as soon as an object is
-        /// saved to the server. The combination of a  and an
-        ///  uniquely identifies an object in your application.
-        /// 
-        [ParseFieldName("objectId")]
-        public string ObjectId
+    /// 
+    /// Gets a set view of the keys contained in this object. This does not include createdAt,
+    /// updatedAt, or objectId. It does include things like username and ACL.
+    /// 
+    public ICollection Keys
+    {
+        get
         {
-            get => State.ObjectId;
-            set
+            lock (Mutex)
             {
-                IsDirty = true;
-                SetObjectIdInternal(value);
+                return EstimatedData.Keys;
             }
         }
+    }
 
-        /// 
-        /// Gets the last time this object was updated as the server sees it, so that if you make changes
-        /// to a ParseObject, then wait a while, and then call , the updated time
-        /// will be the time of the  call rather than the time the object was
-        /// changed locally.
-        /// 
-        [ParseFieldName("updatedAt")]
-        public DateTime? UpdatedAt => State.UpdatedAt;
+    /// 
+    /// Gets or sets the object id. An object id is assigned as soon as an object is
+    /// saved to the server. The combination of a  and an
+    ///  uniquely identifies an object in your application.
+    /// 
+    [ParseFieldName("objectId")]
+    public string ObjectId
+    {
+        get => State.ObjectId;
+        set
+        {
+            IsDirty = true;
+            SetObjectIdInternal(value);
+        }
+    }
 
-        public IDictionary CurrentOperations
+    /// 
+    /// Gets the last time this object was updated as the server sees it, so that if you make changes
+    /// to a ParseObject, then wait a while, and then call , the updated time
+    /// will be the time of the  call rather than the time the object was
+    /// changed locally.
+    /// 
+    [ParseFieldName("updatedAt")]
+    public DateTime? UpdatedAt => State.UpdatedAt;
+
+    public IDictionary CurrentOperations
+    {
+        get
         {
-            get
+            lock (Mutex)
             {
-                lock (Mutex)
-                {
-                    return OperationSetQueue.Last.Value;
-                }
+                return OperationSetQueue.Last.Value;
             }
         }
+    }
 
-        internal object Mutex { get; } = new object { };
+    internal object Mutex { get; } = new object { };
 
-        public IObjectState State { get; private set; }
+    public IObjectState State { get; private set; }
 
-        internal bool CanBeSerialized
+    internal bool CanBeSerialized
+    {
+        get
         {
-            get
-            {
-                // This method is only used for batching sets of objects for saveAll
-                // and when saving children automatically. Since it's only used to
-                // determine whether or not save should be called on them, it only
-                // needs to examine their current values, so we use estimatedData.
+            // This method is only used for batching sets of objects for saveAll
+            // and when saving children automatically. Since it's only used to
+            // determine whether or not save should be called on them, it only
+            // needs to examine their current values, so we use estimatedData.
 
-                lock (Mutex)
-                {
-                    return Services.CanBeSerializedAsValue(EstimatedData);
-                }
+            lock (Mutex)
+            {
+                return Services.CanBeSerializedAsValue(EstimatedData);
             }
         }
+    }
 
-        bool Dirty { get; set; }
+    bool Dirty { get; set; }
 
-        internal IDictionary EstimatedData { get; } = new Dictionary { };
+    internal IDictionary EstimatedData { get; } = new Dictionary { };
 
-        internal bool Fetched { get; set; }
+    internal bool Fetched { get; set; }
 
-        bool HasDirtyChildren
+    bool HasDirtyChildren
+    {
+        get
         {
-            get
+            lock (Mutex)
             {
-                lock (Mutex)
-                {
-                    return FindUnsavedChildren().FirstOrDefault() != null;
-                }
+                return FindUnsavedChildren().FirstOrDefault() != null;
             }
         }
+    }
 
-        LinkedList> OperationSetQueue { get; } = new LinkedList>();
+    LinkedList> OperationSetQueue { get; } = new LinkedList>();
 
-        SynchronizedEventHandler PropertyChangedHandler { get; } = new SynchronizedEventHandler();
+    SynchronizedEventHandler PropertyChangedHandler { get; } = new SynchronizedEventHandler();
 
-        /// 
-        /// Gets or sets a value on the object. It is recommended to name
-        /// keys in partialCamelCaseLikeThis.
-        /// 
-        /// The key for the object. Keys must be alphanumeric plus underscore
-        /// and start with a letter.
-        /// The property is
-        /// retrieved and  is not found.
-        /// The value for the key.
-        virtual public object this[string key]
+    /// 
+    /// Gets or sets a value on the object. It is recommended to name
+    /// keys in partialCamelCaseLikeThis.
+    /// 
+    /// The key for the object. Keys must be alphanumeric plus underscore
+    /// and start with a letter.
+    /// The property is
+    /// retrieved and  is not found.
+    /// The value for the key.
+    public virtual object this[string key]
+    {
+        get
         {
-            get
+            lock (Mutex)
             {
-                lock (Mutex)
-                {
-                    CheckGetAccess(key);
-                    object value = EstimatedData[key];
-
-                    // A relation may be deserialized without a parent or key. Either way,
-                    // make sure it's consistent.
-
-                    if (value is ParseRelationBase relation)
-                    {
-                        relation.EnsureParentAndKey(this, key);
-                    }
+                CheckGetAccess(key);
 
-                    return value;
-                }
-            }
-            set
-            {
-                lock (Mutex)
-                {
-                    CheckKeyIsMutable(key);
-                    Set(key, value);
+                if (!EstimatedData.TryGetValue(key, out var value))
+                {   
+                    return null; // Return null, do NOT throw exception. Parse official doesn't.
                 }
-            }
-        }
 
-        /// 
-        /// Adds a value for the given key, throwing an Exception if the key
-        /// already has a value.
-        /// 
-        /// 
-        /// This allows you to use collection initialization syntax when creating ParseObjects,
-        /// such as:
-        /// 
-        /// var obj = new ParseObject("MyType")
-        /// {
-        ///     {"name", "foo"},
-        ///     {"count", 10},
-        ///     {"found", false}
-        /// };
-        /// 
-        /// 
-        /// The key for which a value should be set.
-        /// The value for the key.
-        public void Add(string key, object value)
-        {
-            lock (Mutex)
-            {
-                if (ContainsKey(key))
+                // Ensure ParseRelationBase consistency
+                if (value is ParseRelationBase relation && (relation.Parent== null || relation.Key == null))
                 {
-                    throw new ArgumentException("Key already exists", key);
+                    relation.EnsureParentAndKey(this, key);
                 }
 
-                this[key] = value;
+                return value;
             }
         }
-
-        /// 
-        /// Atomically adds objects to the end of the list associated with the given key.
-        /// 
-        /// The key.
-        /// The objects to add.
-        public void AddRangeToList(string key, IEnumerable values)
+        set
         {
             lock (Mutex)
             {
                 CheckKeyIsMutable(key);
-                PerformOperation(key, new ParseAddOperation(values.Cast()));
+                Set(key, value);
             }
         }
+    }
 
-        /// 
-        /// Atomically adds objects to the end of the list associated with the given key,
-        /// only if they are not already present in the list. The position of the inserts are not
-        /// guaranteed.
-        /// 
-        /// The key.
-        /// The objects to add.
-        public void AddRangeUniqueToList(string key, IEnumerable values)
+    /// 
+    /// Adds a value for the given key, throwing an Exception if the key
+    /// already has a value.
+    /// 
+    /// 
+    /// This allows you to use collection initialization syntax when creating ParseObjects,
+    /// such as:
+    /// 
+    /// var obj = new ParseObject("MyType")
+    /// {
+    ///     {"name", "foo"},
+    ///     {"count", 10},
+    ///     {"found", false}
+    /// };
+    /// 
+    /// 
+    /// The key for which a value should be set.
+    /// The value for the key.
+    public void Add(string key, object value)
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
+            if (ContainsKey(key))
             {
-                CheckKeyIsMutable(key);
-                PerformOperation(key, new ParseAddUniqueOperation(values.Cast()));
+                throw new ArgumentException("Key already exists", key);
             }
-        }
 
-        #endregion
-
-        /// 
-        /// Atomically adds an object to the end of the list associated with the given key.
-        /// 
-        /// The key.
-        /// The object to add.
-        public void AddToList(string key, object value) => AddRangeToList(key, new[] { value });
+            this[key] = value;
+        }
+    }
 
-        /// 
-        /// Atomically adds an object to the end of the list associated with the given key,
-        /// only if it is not already present in the list. The position of the insert is not
-        /// guaranteed.
-        /// 
-        /// The key.
-        /// The object to add.
-        public void AddUniqueToList(string key, object value) => AddRangeUniqueToList(key, new object[] { value });
+    /// 
+    /// Atomically adds objects to the end of the list associated with the given key.
+    /// 
+    /// The key.
+    /// The objects to add.
+    public void AddRangeToList(string key, IEnumerable values)
+    {
+        lock (Mutex)
+        {
+            CheckKeyIsMutable(key);
+            PerformOperation(key, new ParseAddOperation(values.Cast()));
+        }
+    }
 
-        /// 
-        /// Returns whether this object has a particular key.
-        /// 
-        /// The key to check for
-        public bool ContainsKey(string key)
+    /// 
+    /// Atomically adds objects to the end of the list associated with the given key,
+    /// only if they are not already present in the list. The position of the inserts are not
+    /// guaranteed.
+    /// 
+    /// The key.
+    /// The objects to add.
+    public void AddRangeUniqueToList(string key, IEnumerable values)
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
-            {
-                return EstimatedData.ContainsKey(key);
-            }
+            CheckKeyIsMutable(key);
+            PerformOperation(key, new ParseAddUniqueOperation(values.Cast()));
         }
+    }
 
-        /// 
-        /// Deletes this object on the server.
-        /// 
-        /// The cancellation token.
-        public Task DeleteAsync(CancellationToken cancellationToken = default) => TaskQueue.Enqueue(toAwait => DeleteAsync(toAwait, cancellationToken), cancellationToken);
+    #endregion
+
+    /// 
+    /// Atomically adds an object to the end of the list associated with the given key.
+    /// 
+    /// The key.
+    /// The object to add.
+    public void AddToList(string key, object value)
+    {
+        AddRangeToList(key, new[] { value });
+    }
 
-        /// 
-        /// Gets a value for the key of a particular type.
-        /// The type to convert the value to. Supported types are
-        /// ParseObject and its descendents, Parse types such as ParseRelation and ParseGeopoint,
-        /// primitive types,IList<T>, IDictionary<string, T>, and strings.
-        /// The key of the element to get.
-        /// The property is
-        /// retrieved and  is not found.
-        /// 
-        public T Get(string key) => Conversion.To(this[key]);
+    /// 
+    /// Atomically adds an object to the end of the list associated with the given key,
+    /// only if it is not already present in the list. The position of the insert is not
+    /// guaranteed.
+    /// 
+    /// The key.
+    /// The object to add.
+    public void AddUniqueToList(string key, object value)
+    {
+        AddRangeUniqueToList(key, new object[] { value });
+    }
 
-        /// 
-        /// Access or create a Relation value for a key.
-        /// 
-        /// The type of object to create a relation for.
-        /// The key for the relation field.
-        /// A ParseRelation for the key.
-        public ParseRelation GetRelation(string key) where T : ParseObject
+    /// 
+    /// Returns whether this object has a particular key.
+    /// 
+    /// The key to check for
+    public bool ContainsKey(string key)
+    {
+        lock (Mutex)
         {
-            // All the sanity checking is done when add or remove is called.
-
-            TryGetValue(key, out ParseRelation relation);
-            return relation ?? new ParseRelation(this, key);
+            return EstimatedData.ContainsKey(key);
         }
+    }
+
+    /// 
+    /// Gets a value for the key of a particular type.
+    /// The type to convert the value to. Supported types are
+    /// ParseObject and its descendents, Parse types such as ParseRelation and ParseGeopoint,
+    /// primitive types,IList<T>, IDictionary<string, T>, and strings.
+    /// The key of the element to get.
+    /// The property is
+    /// retrieved and  is not found.
+    /// 
+    public T Get(string key)
+    {
 
-        /// 
-        /// A helper function for checking whether two ParseObjects point to
-        /// the same object in the cloud.
-        /// 
-        public bool HasSameId(ParseObject other)
+        try
         {
-            lock (Mutex)
+            // Try to get the value
+            if (!ContainsKey(key))
             {
-                return other is { } && Equals(ClassName, other.ClassName) && Equals(ObjectId, other.ObjectId);
+                // If the key doesn't exist, throw a KeyNotFoundException
+                throw new KeyNotFoundException($"The key '{key}' was not found in the object.");
             }
+            // If the key exists, attempt to convert the value
+            return Conversion.To(this[key]);
+        }
+        catch (KeyNotFoundException)
+        {
+            
+            // Handle missing key explicitly - better than a NullReferenceException
+            throw; // Rethrow the KeyNotFoundException
+        }
+        catch (Exception ex)
+        {
+            
+            // Optionally to catch other exceptions or rethrow a more specific exception
+            throw new InvalidCastException($"Error converting value for key '{key}'", ex);
         }
+    }
 
-        #region Atomic Increment
 
-        /// 
-        /// Atomically increments the given key by 1.
-        /// 
-        /// The key to increment.
-        public void Increment(string key) => Increment(key, 1);
+    /// 
+    /// Access or create a Relation value for a key.
+    /// 
+    /// The type of object to create a relation for.
+    /// The key for the relation field.
+    /// A ParseRelation for the key.
+    public ParseRelation GetRelation(string key) where T : ParseObject
+    {
+        // All the sanity checking is done when add or remove is called.
+
+        TryGetValue(key, out ParseRelation relation);
+        return relation ?? new ParseRelation(this, key);
+    }
 
-        /// 
-        /// Atomically increments the given key by the given number.
-        /// 
-        /// The key to increment.
-        /// The amount to increment by.
-        public void Increment(string key, long amount)
+    /// 
+    /// A helper function for checking whether two ParseObjects point to
+    /// the same object in the cloud.
+    /// 
+    public bool HasSameId(ParseObject other)
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
-            {
-                CheckKeyIsMutable(key);
-                PerformOperation(key, new ParseIncrementOperation(amount));
-            }
+            return other is { } && Equals(ClassName, other.ClassName) && Equals(ObjectId, other.ObjectId);
         }
+    }
+
+    #region Atomic Increment
+
+    /// 
+    /// Atomically increments the given key by 1.
+    /// 
+    /// The key to increment.
+    public void Increment(string key)
+    {
+        Increment(key, 1);
+    }
 
-        /// 
-        /// Atomically increments the given key by the given number.
-        /// 
-        /// The key to increment.
-        /// The amount to increment by.
-        public void Increment(string key, double amount)
+    /// 
+    /// Atomically increments the given key by the given number.
+    /// 
+    /// The key to increment.
+    /// The amount to increment by.
+    public void Increment(string key, long amount)
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
-            {
-                CheckKeyIsMutable(key);
-                PerformOperation(key, new ParseIncrementOperation(amount));
-            }
+            CheckKeyIsMutable(key);
+            PerformOperation(key, new ParseIncrementOperation(amount));
         }
+    }
 
-        /// 
-        /// Indicates whether key is unsaved for this ParseObject.
-        /// 
-        /// The key to check for.
-        /// true if the key has been altered and not saved yet, otherwise
-        /// false.
-        public bool IsKeyDirty(string key)
+    /// 
+    /// Atomically increments the given key by the given number.
+    /// 
+    /// The key to increment.
+    /// The amount to increment by.
+    public void Increment(string key, double amount)
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
-            {
-                return CurrentOperations.ContainsKey(key);
-            }
+            CheckKeyIsMutable(key);
+            PerformOperation(key, new ParseIncrementOperation(amount));
         }
+    }
 
-        /// 
-        /// Removes a key from the object's data if it exists.
-        /// 
-        /// The key to remove.
-        public virtual void Remove(string key)
+    /// 
+    /// Indicates whether key is unsaved for this ParseObject.
+    /// 
+    /// The key to check for.
+    /// true if the key has been altered and not saved yet, otherwise
+    /// false.
+    public bool IsKeyDirty(string key)
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
-            {
-                CheckKeyIsMutable(key);
-                PerformOperation(key, ParseDeleteOperation.Instance);
-            }
+            return CurrentOperations.ContainsKey(key);
+        }
+    }
+
+    /// 
+    /// Removes a key from the object's data if it exists.
+    /// 
+    /// The key to remove.
+    public virtual void Remove(string key)
+    {
+        lock (Mutex)
+        {
+            CheckKeyIsMutable(key);
+            PerformOperation(key, ParseDeleteOperation.Instance);
         }
+    }
 
-        /// 
-        /// Atomically removes all instances of the objects in 
-        /// from the list associated with the given key.
-        /// 
-        /// The key.
-        /// The objects to remove.
-        public void RemoveAllFromList(string key, IEnumerable values)
+    /// 
+    /// Atomically removes all instances of the objects in 
+    /// from the list associated with the given key.
+    /// 
+    /// The key.
+    /// The objects to remove.
+    public void RemoveAllFromList(string key, IEnumerable values)
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
-            {
-                CheckKeyIsMutable(key);
-                PerformOperation(key, new ParseRemoveOperation(values.Cast()));
-            }
+            CheckKeyIsMutable(key);
+            PerformOperation(key, new ParseRemoveOperation(values.Cast()));
         }
+    }
 
-        /// 
-        /// Clears any changes to this object made since the last call to .
-        /// 
-        public void Revert()
+    /// 
+    /// Clears any changes to this object made since the last call to .
+    /// 
+    public void Revert()
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
+            if (CurrentOperations.Count > 0)
             {
-                if (CurrentOperations.Count > 0)
-                {
-                    CurrentOperations.Clear();
-                    RebuildEstimatedData();
-                    OnPropertyChanged(nameof(IsDirty));
-                }
+                CurrentOperations.Clear();
+                RebuildEstimatedData();
+                OnPropertyChanged(nameof(IsDirty));
             }
         }
+    }
 
-        /// 
-        /// Saves this object to the server.
-        /// 
-        /// The cancellation token.
-        public Task SaveAsync(CancellationToken cancellationToken = default) => TaskQueue.Enqueue(toAwait => SaveAsync(toAwait, cancellationToken), cancellationToken);
+    /// 
+    /// Saves this object to the server.
+    /// 
+    /// The cancellation token.
+    public Task SaveAsync(CancellationToken cancellationToken = default)
+    {
+        return TaskQueue.Enqueue(toAwait => SaveAsync(toAwait, cancellationToken), cancellationToken);
+    }
 
-        /// 
-        /// Populates result with the value for the key, if possible.
-        /// 
-        /// The desired type for the value.
-        /// The key to retrieve a value for.
-        /// The value for the given key, converted to the
-        /// requested type, or null if unsuccessful.
-        /// true if the lookup and conversion succeeded, otherwise
-        /// false.
-        public bool TryGetValue(string key, out T result)
+    /// 
+    /// Populates result with the value for the key, if possible.
+    /// 
+    /// The desired type for the value.
+    /// The key to retrieve a value for.
+    /// The value for the given key, converted to the
+    /// requested type, or null if unsuccessful.
+    /// true if the lookup and conversion succeeded, otherwise
+    /// false.
+    public bool TryGetValue(string key, out T result)
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
+            if (ContainsKey(key))
             {
-                if (ContainsKey(key))
+                try
                 {
-                    try
-                    {
-                        T temp = Conversion.To(this[key]);
-                        result = temp;
-                        return true;
-                    }
-                    catch
-                    {
-                        result = default;
-                        return false;
-                    }
+                    T temp = Conversion.To(this[key]);
+                    result = temp;
+                    return true;
+                }
+                catch
+                {
+                    result = default;
+                    return false;
                 }
-
-                result = default;
-                return false;
             }
+
+            result = default;
+            return false;
         }
+    }
 
-        #endregion
+    #endregion
 
-        #region Delete Object
+    #region Delete Object
 
-        internal Task DeleteAsync(Task toAwait, CancellationToken cancellationToken)
+    /// 
+    /// Deletes this object on the server.
+    /// 
+    /// The cancellation token.
+    public Task DeleteAsync(CancellationToken cancellationToken = default)
+    {
+        return TaskQueue.Enqueue(async toAwait =>
         {
-            if (ObjectId == null)
-            {
-                return Task.FromResult(0);
-            }
-
-            string sessionToken = Services.GetCurrentSessionToken();
-
-            return toAwait.OnSuccess(_ => Services.ObjectController.DeleteAsync(State, sessionToken, cancellationToken)).Unwrap().OnSuccess(_ => IsDirty = true);
-        }
+            await DeleteAsyncInternal(cancellationToken).ConfigureAwait(false);
+            return toAwait;  // Ensure the task is returned to the queue
+        }, cancellationToken);
+    }
 
-        internal virtual Task FetchAsyncInternal(Task toAwait, CancellationToken cancellationToken) => toAwait.OnSuccess(_ => ObjectId == null ? throw new InvalidOperationException("Cannot refresh an object that hasn't been saved to the server.") : Services.ObjectController.FetchAsync(State, Services.GetCurrentSessionToken(), Services, cancellationToken)).Unwrap().OnSuccess(task =>
+    internal async Task DeleteAsyncInternal(CancellationToken cancellationToken)
+    {
+        if (ObjectId == null)
         {
-            HandleFetchResult(task.Result);
-            return this;
-        });
-
-        #endregion
+            return; // No need to delete if the object doesn't have an ID
+        }
 
-        #region Fetch Object(s)
+        var sessionToken = await Services.GetCurrentSessionToken();
+        await Services.ObjectController.DeleteAsync(State, sessionToken, cancellationToken).ConfigureAwait(false);
+        IsDirty = true;
+    }
 
-        /// 
-        /// Fetches this object with the data from the server.
-        /// 
-        /// The cancellation token.
-        internal Task FetchAsyncInternal(CancellationToken cancellationToken) => TaskQueue.Enqueue(toAwait => FetchAsyncInternal(toAwait, cancellationToken), cancellationToken);
 
-        internal Task FetchIfNeededAsyncInternal(Task toAwait, CancellationToken cancellationToken) => !IsDataAvailable ? FetchAsyncInternal(toAwait, cancellationToken) : Task.FromResult(this);
+    #region Fetch Object(s)
+    /// 
+    /// Fetches this object with the data from the server.
+    /// 
+    /// The cancellation token.
+    internal Task FetchAsync(CancellationToken cancellationToken)
+    {
+        return TaskQueue.Enqueue(async toAwait =>
+        {
+            await FetchAsyncInternal(cancellationToken).ConfigureAwait(false);
+            return this;  // Ensures the task is returned to the queue after fetch
+        }, cancellationToken);
+    }
+
+    internal async Task FetchIfNeededAsync(CancellationToken cancellationToken)
+    {
+        if (!IsDataAvailable)
+        {
+            return await FetchAsyncInternal(cancellationToken).ConfigureAwait(false);
+        }
+        return this;
+    }
+    internal async Task FetchIfNeededAsyncInternal(Task toAwait, CancellationToken cancellationToken)
+    {
+        if (IsDataAvailable)
+        {
+            return this;
+        }
 
-        /// 
-        /// If this ParseObject has not been fetched (i.e.  returns
-        /// false), fetches this object with the data from the server.
-        /// 
-        /// The cancellation token.
-        internal Task FetchIfNeededAsyncInternal(CancellationToken cancellationToken) => TaskQueue.Enqueue(toAwait => FetchIfNeededAsyncInternal(toAwait, cancellationToken), cancellationToken);
+        await toAwait.ConfigureAwait(false);
+        return await FetchAsyncInternal(cancellationToken).ConfigureAwait(false);
+    }
 
-        internal void HandleFailedSave(IDictionary operationsBeforeSave)
+    /// 
+    /// If this ParseObject has not been fetched (i.e.  returns
+    /// false), fetches this object with the data from the server.
+    /// 
+    /// The cancellation token.
+    internal Task FetchIfNeededAsyncInternal(CancellationToken cancellationToken)
+    {
+        return TaskQueue.Enqueue(async toAwait =>
         {
-            lock (Mutex)
-            {
-                LinkedListNode> opNode = OperationSetQueue.Find(operationsBeforeSave);
-                IDictionary nextOperations = opNode.Next.Value;
-                bool wasDirty = nextOperations.Count > 0;
-                OperationSetQueue.Remove(opNode);
+            return await FetchIfNeededAsyncInternal(toAwait, cancellationToken).ConfigureAwait(false);
+        }, cancellationToken);
+    }
 
-                // Merge the data from the failed save into the next save.
+    internal virtual async Task FetchAsyncInternal(CancellationToken cancellationToken)
+    {
+        if (ObjectId == null)
+        {
+            throw new InvalidOperationException("Cannot refresh an object that hasn't been saved to the server.");
+        }
 
-                foreach (KeyValuePair pair in operationsBeforeSave)
-                {
-                    IParseFieldOperation operation1 = pair.Value;
+        var sessionToken = await Services.GetCurrentSessionToken();
+        var result = await Services.ObjectController.FetchAsync(State, sessionToken, Services, cancellationToken).ConfigureAwait(false);
+        HandleFetchResult(result);
+        return this;
+    }
 
-                    nextOperations.TryGetValue(pair.Key, out IParseFieldOperation operation2);
-                    nextOperations[pair.Key] = operation2 is { } ? operation2.MergeWithPrevious(operation1) : operation1;
-                }
+    #endregion
 
-                if (!wasDirty && nextOperations == CurrentOperations && operationsBeforeSave.Count > 0)
-                {
-                    OnPropertyChanged(nameof(IsDirty));
-                }
-            }
-        }
 
-        public virtual void HandleFetchResult(IObjectState serverState)
+    internal void HandleFailedSave(IDictionary operationsBeforeSave)
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
+            // Attempt to find the node in the OperationSetQueue
+            LinkedListNode> opNode = OperationSetQueue.Find(operationsBeforeSave);
+            if (opNode == null)
             {
-                MergeFromServer(serverState);
+                // If not found, gracefully exit or perform cleanup as needed
+                return; // Gracefully exit
             }
-        }
 
-        internal virtual void HandleSave(IObjectState serverState)
-        {
-            lock (Mutex)
+            IDictionary nextOperations = opNode.Next.Value;
+            bool wasDirty = nextOperations.Count > 0;
+            OperationSetQueue.Remove(opNode);
+
+            // Merge the data from the failed save into the next save.
+
+            foreach (KeyValuePair pair in operationsBeforeSave)
             {
-                IDictionary operationsBeforeSave = OperationSetQueue.First.Value;
-                OperationSetQueue.RemoveFirst();
+                IParseFieldOperation operation1 = pair.Value;
 
-                // Merge the data from the save and the data from the server into serverData.
+                nextOperations.TryGetValue(pair.Key, out IParseFieldOperation operation2);
+                nextOperations[pair.Key] = operation2 is { } ? operation2.MergeWithPrevious(operation1) : operation1;
+            }
 
-                MutateState(mutableClone => mutableClone.Apply(operationsBeforeSave));
-                MergeFromServer(serverState);
+            if (!wasDirty && nextOperations == CurrentOperations && operationsBeforeSave.Count > 0)
+            {
+                OnPropertyChanged(nameof(IsDirty));
             }
         }
+    }
 
-        internal void MergeFromObject(ParseObject other)
+    public virtual void HandleFetchResult(IObjectState serverState)
+    {
+        lock (Mutex)
         {
-            // If they point to the same instance, we don't need to merge
+            MergeFromServer(serverState);
+        }
+    }
 
-            lock (Mutex)
-            {
-                if (this == other)
-                {
-                    return;
-                }
-            }
+    internal virtual void HandleSave(IObjectState serverState)
+    {
+        if (serverState == null)
+        {
+            throw new InvalidOperationException("Server state cannot be null in HandleSave.");
+        }
+        lock (Mutex)
+        {
+            IDictionary operationsBeforeSave = OperationSetQueue.First.Value;
+            OperationSetQueue.RemoveFirst();
 
-            // Clear out any changes on this object.
+            // Merge the data from the save and the data from the server into serverData.
 
-            if (OperationSetQueue.Count != 1)
-            {
-                throw new InvalidOperationException("Attempt to MergeFromObject during save.");
-            }
+            MutateState(mutableClone => mutableClone.Apply(operationsBeforeSave));
+            MergeFromServer(serverState);
+        }
+    }
 
-            OperationSetQueue.Clear();
+    internal void MergeFromObject(ParseObject other)
+    {
+        // If they point to the same instance, we don't need to merge
 
-            foreach (IDictionary operationSet in other.OperationSetQueue)
+        lock (Mutex)
+        {
+            if (this == other)
             {
-                OperationSetQueue.AddLast(operationSet.ToDictionary(entry => entry.Key, entry => entry.Value));
+                return;
             }
+        }
 
-            lock (Mutex)
-            {
-                State = other.State;
-            }
+        // Clear out any changes on this object.
 
-            RebuildEstimatedData();
+        if (OperationSetQueue.Count != 1)
+        {
+            throw new InvalidOperationException("Attempt to MergeFromObject during save.");
         }
 
-        internal virtual void MergeFromServer(IObjectState serverState)
+        OperationSetQueue.Clear();
+
+        foreach (IDictionary operationSet in other.OperationSetQueue)
         {
-            // Make a new serverData with fetched values.
+            OperationSetQueue.AddLast(operationSet.ToDictionary(entry => entry.Key, entry => entry.Value));
+        }
 
-            Dictionary newServerData = serverState.ToDictionary(t => t.Key, t => t.Value);
+        lock (Mutex)
+        {
+            State = other.State;
+        }
 
-            lock (Mutex)
+        RebuildEstimatedData();
+    }
+
+    internal virtual void MergeFromServer(IObjectState serverState)
+    {
+        // Make a new serverData with fetched values.
+
+        Dictionary newServerData = serverState.ToDictionary(t => t.Key, t => t.Value);
+
+        lock (Mutex)
+        {
+            // Trigger handler based on serverState
+
+            if (serverState.ObjectId != null)
             {
-                // Trigger handler based on serverState
+                // If the objectId is being merged in, consider this object to be fetched.
 
-                if (serverState.ObjectId != null)
-                {
-                    // If the objectId is being merged in, consider this object to be fetched.
+                Fetched = true;
+                OnPropertyChanged(nameof(IsDataAvailable));
+            }
 
-                    Fetched = true;
-                    OnPropertyChanged(nameof(IsDataAvailable));
-                }
+            if (serverState.UpdatedAt != null)
+            {
+                OnPropertyChanged(nameof(UpdatedAt));
+            }
 
-                if (serverState.UpdatedAt != null)
-                {
-                    OnPropertyChanged(nameof(UpdatedAt));
-                }
+            if (serverState.CreatedAt != null)
+            {
+                OnPropertyChanged(nameof(CreatedAt));
+            }
 
-                if (serverState.CreatedAt != null)
-                {
-                    OnPropertyChanged(nameof(CreatedAt));
-                }
+            // We cache the fetched object because subsequent Save operation might flush the fetched objects into Pointers.
 
-                // We cache the fetched object because subsequent Save operation might flush the fetched objects into Pointers.
+            IDictionary fetchedObject = CollectFetchedObjects();
 
-                IDictionary fetchedObject = CollectFetchedObjects();
+            foreach (KeyValuePair pair in serverState)
+            {
+                object value = pair.Value;
 
-                foreach (KeyValuePair pair in serverState)
+                if (value is ParseObject)
                 {
-                    object value = pair.Value;
+                    // Resolve fetched object.
 
-                    if (value is ParseObject)
-                    {
-                        // Resolve fetched object.
+                    ParseObject entity = value as ParseObject;
 
-                        ParseObject entity = value as ParseObject;
-
-                        if (fetchedObject.ContainsKey(entity.ObjectId))
-                        {
-                            value = fetchedObject[entity.ObjectId];
-                        }
+                    if (fetchedObject.ContainsKey(entity.ObjectId))
+                    {
+                        value = fetchedObject[entity.ObjectId];
                     }
-                    newServerData[pair.Key] = value;
                 }
-
-                IsDirty = false;
-                MutateState(mutableClone => mutableClone.Apply(serverState.MutatedClone(mutableClone => mutableClone.ServerData = newServerData)));
+                newServerData[pair.Key] = value;
             }
+
+            IsDirty = false;
+            var s = serverState.MutatedClone(mutableClone => mutableClone.ServerData = newServerData);
+            MutateState(mutableClone => mutableClone.Apply(s));
         }
+    }
 
-        internal void MutateState(Action mutator)
+    internal void MutateState(Action mutator)
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
-            {
-                State = State.MutatedClone(mutator);
+            State = State.MutatedClone(mutator);
 
-                // Refresh the estimated data.
+            // Refresh the estimated data.
 
-                RebuildEstimatedData();
-            }
+            RebuildEstimatedData();
         }
+    }
 
-        /// 
-        /// Override to run validations on key/value pairs. Make sure to still
-        /// call the base version.
-        /// 
-        internal virtual void OnSettingValue(ref string key, ref object value)
+    /// 
+    /// Override to run validations on key/value pairs. Make sure to still
+    /// call the base version.
+    /// 
+    internal virtual void OnSettingValue(ref string key, ref object value)
+    {
+        if (key is null)
         {
-            if (key is null)
-            {
-                throw new ArgumentNullException(nameof(key));
-            }
+            throw new ArgumentNullException(nameof(key));
         }
+    }
 
-        /// 
-        /// PerformOperation is like setting a value at an index, but instead of
-        /// just taking a new value, it takes a ParseFieldOperation that modifies the value.
-        /// 
-        internal void PerformOperation(string key, IParseFieldOperation operation)
+    /// 
+    /// PerformOperation is like setting a value at an index, but instead of
+    /// just taking a new value, it takes a ParseFieldOperation that modifies the value.
+    /// 
+    internal void PerformOperation(string key, IParseFieldOperation operation)
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
-            {
-                EstimatedData.TryGetValue(key, out object oldValue);
-                object newValue = operation.Apply(oldValue, key);
-
-                if (newValue != ParseDeleteOperation.Token)
-                {
-                    EstimatedData[key] = newValue;
-                }
-                else
-                {
-                    EstimatedData.Remove(key);
-                }
+            EstimatedData.TryGetValue(key, out object oldValue);
+            object newValue = operation.Apply(oldValue, key);
 
-                bool wasDirty = CurrentOperations.Count > 0;
-                CurrentOperations.TryGetValue(key, out IParseFieldOperation oldOperation);
-                IParseFieldOperation newOperation = operation.MergeWithPrevious(oldOperation);
-                CurrentOperations[key] = newOperation;
+            if (newValue != ParseDeleteOperation.Token)
+            {
+                EstimatedData[key] = newValue;
+            }
+            else
+            {
+                EstimatedData.Remove(key);
+            }
 
-                if (!wasDirty)
-                {
-                    OnPropertyChanged(nameof(IsDirty));
-                }
+            bool wasDirty = CurrentOperations.Count > 0;
+            CurrentOperations.TryGetValue(key, out IParseFieldOperation oldOperation);
+            IParseFieldOperation newOperation = operation.MergeWithPrevious(oldOperation);
+            CurrentOperations[key] = newOperation;
 
-                OnFieldsChanged(new[] { key });
+            if (!wasDirty)
+            {
+                OnPropertyChanged(nameof(IsDirty));
             }
+
+            OnFieldsChanged(new[] { key });
         }
+    }
 
-        /// 
-        /// Regenerates the estimatedData map from the serverData and operations.
-        /// 
-        internal void RebuildEstimatedData()
+    /// 
+    /// Regenerates the estimatedData map from the serverData and operations.
+    /// 
+    internal void RebuildEstimatedData()
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
-            {
-                EstimatedData.Clear();
+            EstimatedData.Clear();
 
-                foreach (KeyValuePair item in State)
-                {
-                    EstimatedData.Add(item);
-                }
-                foreach (IDictionary operations in OperationSetQueue)
-                {
-                    ApplyOperations(operations, EstimatedData);
-                }
+            foreach (KeyValuePair item in State)
+            {
+                EstimatedData.Add(item);
+            }
+            foreach (IDictionary operations in OperationSetQueue)
+            {
+                ApplyOperations(operations, EstimatedData);
+            }
 
-                // We've just applied a bunch of operations to estimatedData which
-                // may have changed all of its keys. Notify of all keys and properties
-                // mapped to keys being changed.
+            // We've just applied a bunch of operations to estimatedData which
+            // may have changed all of its keys. Notify of all keys and properties
+            // mapped to keys being changed.
 
-                OnFieldsChanged(default);
-            }
+            OnFieldsChanged(default);
         }
+    }
 
-        public IDictionary ServerDataToJSONObjectForSerialization() => PointerOrLocalIdEncoder.Instance.Encode(State.ToDictionary(pair => pair.Key, pair => pair.Value), Services) as IDictionary;
+    public IDictionary ServerDataToJSONObjectForSerialization()
+    {
+        return PointerOrLocalIdEncoder.Instance.Encode(State.ToDictionary(pair => pair.Key, pair => pair.Value), Services) as IDictionary;
+    }
 
-        /// 
-        /// Perform Set internally which is not gated by mutability check.
-        /// 
-        /// key for the object.
-        /// the value for the key.
-        public void Set(string key, object value)
+    /// 
+    /// Perform Set internally which is not gated by mutability check.
+    /// 
+    /// key for the object.
+    /// the value for the key.
+    public void Set(string key, object value)
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
+            OnSettingValue(ref key, ref value);
+
+            if (!ParseDataEncoder.Validate(value))
             {
-                OnSettingValue(ref key, ref value);
+                throw new ArgumentException("Invalid type for value: " + value.GetType().ToString());
+            }
 
-                if (!ParseDataEncoder.Validate(value))
-                {
-                    throw new ArgumentException("Invalid type for value: " + value.GetType().ToString());
-                }
+            PerformOperation(key, new ParseSetOperation(value));
+            OnPropertyChanged(key);
 
-                PerformOperation(key, new ParseSetOperation(value));
-            }
         }
+    }
 
-        /// 
-        /// Allows subclasses to set values for non-pointer construction.
-        /// 
-        internal virtual void SetDefaultValues() { }
+    /// 
+    /// Allows subclasses to set values for non-pointer construction.
+    /// 
+    internal virtual void SetDefaultValues() { }
 
-        public void SetIfDifferent(string key, T value)
-        {
-            bool hasCurrent = TryGetValue(key, out T current);
+    public void SetIfDifferent(string key, T value)
+    {
+        bool hasCurrent = TryGetValue(key, out T current);
 
-            if (value == null)
+        if (value == null)
+        {
+            if (hasCurrent)
             {
-                if (hasCurrent)
-                {
-                    PerformOperation(key, ParseDeleteOperation.Instance);
-                }
-                return;
+                PerformOperation(key, ParseDeleteOperation.Instance);
             }
+            return;
+        }
 
-            if (!hasCurrent || !value.Equals(current))
-            {
-                Set(key, value);
-            }
+        if (!hasCurrent || !value.Equals(current))
+        {
+            Set(key, value);
         }
+    }
 
-        #endregion
+    #endregion
 
-        #region Save Object(s)
+    #region Save Object(s)
 
-        /// 
-        /// Pushes new operations onto the queue and returns the current set of operations.
-        /// 
-        internal IDictionary StartSave()
+    /// 
+    /// Pushes new operations onto the queue and returns the current set of operations.
+    /// 
+    internal IDictionary StartSave()
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
-            {
-                IDictionary currentOperations = CurrentOperations;
-                OperationSetQueue.AddLast(new Dictionary());
-                OnPropertyChanged(nameof(IsDirty));
-                return currentOperations;
-            }
+            IDictionary currentOperations = CurrentOperations;
+            OperationSetQueue.AddLast(new Dictionary());
+            OnPropertyChanged(nameof(IsDirty));
+            return currentOperations;
         }
+    }
 
-        #endregion
-
-        /// 
-        /// Gets the value of a property based upon its associated ParseFieldName attribute.
-        /// 
-        /// The value of the property.
-        /// The name of the property.
-        /// The return type of the property.
-        protected T GetProperty([CallerMemberName] string propertyName = null) => GetProperty(default(T), propertyName);
-
-        /// 
-        /// Gets the value of a property based upon its associated ParseFieldName attribute.
-        /// 
-        /// The value of the property.
-        /// The value to return if the property is not present on the ParseObject.
-        /// The name of the property.
-        /// The return type of the property.
-        protected T GetProperty(T defaultValue, [CallerMemberName] string propertyName = null) => TryGetValue(Services.GetFieldForPropertyName(ClassName, propertyName), out T result) ? result : defaultValue;
-
-        /// 
-        /// Gets a relation for a property based upon its associated ParseFieldName attribute.
-        /// 
-        /// The ParseRelation for the property.
-        /// The name of the property.
-        /// The ParseObject subclass type of the ParseRelation.
-        protected ParseRelation GetRelationProperty([CallerMemberName] string propertyName = null) where T : ParseObject => GetRelation(Services.GetFieldForPropertyName(ClassName, propertyName));
-
-        protected virtual bool CheckKeyMutable(string key) => true;
-
-        /// 
-        /// Raises change notifications for all properties associated with the given
-        /// field names. If fieldNames is null, this will notify for all known field-linked
-        /// properties (e.g. this happens when we recalculate all estimated data from scratch)
-        /// 
-        protected void OnFieldsChanged(IEnumerable fields)
-        {
-            IDictionary mappings = Services.ClassController.GetPropertyMappings(ClassName);
-
-            foreach (string property in mappings is { } ? fields is { } ? from mapping in mappings join field in fields on mapping.Value equals field select mapping.Key : mappings.Keys : Enumerable.Empty())
-            {
-                OnPropertyChanged(property);
-            }
+    #endregion
 
-            OnPropertyChanged("Item[]");
-        }
+    /// 
+    /// Gets the value of a property based upon its associated ParseFieldName attribute.
+    /// 
+    /// The value of the property.
+    /// The name of the property.
+    /// The return type of the property.
+    protected T GetProperty([CallerMemberName] string propertyName = null)
+    {
+        return GetProperty(default(T), propertyName);
+    }
 
-        /// 
-        /// Raises change notifications for a property. Passing null or the empty string
-        /// notifies the binding framework that all properties/indexes have changed.
-        /// Passing "Item[]" tells the binding framework that all indexed values
-        /// have changed (but not all properties)
-        /// 
-        protected void OnPropertyChanged([CallerMemberName] string propertyName = null) => PropertyChangedHandler.Invoke(this, new PropertyChangedEventArgs(propertyName));
+    /// 
+    /// Gets the value of a property based upon its associated ParseFieldName attribute.
+    /// 
+    /// The value of the property.
+    /// The value to return if the property is not present on the ParseObject.
+    /// The name of the property.
+    /// The return type of the property.
+    protected T GetProperty(T defaultValue, [CallerMemberName] string propertyName = null)
+    {
+        return TryGetValue(Services.GetFieldForPropertyName(ClassName, propertyName), out T result) ? result : defaultValue;
+    }
 
-        protected virtual Task SaveAsync(Task toAwait, CancellationToken cancellationToken)
-        {
-            IDictionary currentOperations = null;
+    /// 
+    /// Gets a relation for a property based upon its associated ParseFieldName attribute.
+    /// 
+    /// The ParseRelation for the property.
+    /// The name of the property.
+    /// The ParseObject subclass type of the ParseRelation.
+    protected ParseRelation GetRelationProperty([CallerMemberName] string propertyName = null) where T : ParseObject
+    {
+        return GetRelation(Services.GetFieldForPropertyName(ClassName, propertyName));
+    }
 
-            if (!IsDirty)
-            {
-                return Task.CompletedTask;
-            }
+    /// 
+    /// Parse Objects are mutable by default.
+    ///   
+    protected virtual bool CheckKeyMutable(string key)
+    {        
+        return true;
+    }
 
-            Task deepSaveTask;
-            string sessionToken;
+    /// 
+    /// Raises change notifications for all properties associated with the given
+    /// field names. If fieldNames is null, this will notify for all known field-linked
+    /// properties (e.g. this happens when we recalculate all estimated data from scratch)
+    /// 
+    protected void OnFieldsChanged(IEnumerable fields)
+    {
+        IDictionary mappings = Services.ClassController.GetPropertyMappings(ClassName);
 
-            lock (Mutex)
-            {
-                // Get the JSON representation of the object.
+        foreach (string property in mappings is { } ? fields is { } ? from mapping in mappings join field in fields on mapping.Value equals field select mapping.Key : mappings.Keys : Enumerable.Empty())
+        {
+            OnPropertyChanged(property);
+        }
 
-                currentOperations = StartSave();
-                sessionToken = Services.GetCurrentSessionToken();
-                deepSaveTask = Services.DeepSaveAsync(EstimatedData, sessionToken, cancellationToken);
-            }
+        OnPropertyChanged("Item[]");
+    }
 
-            return deepSaveTask.OnSuccess(_ => toAwait).Unwrap().OnSuccess(_ => Services.ObjectController.SaveAsync(State, currentOperations, sessionToken, Services, cancellationToken)).Unwrap().ContinueWith(task =>
-            {
-                if (task.IsFaulted || task.IsCanceled)
-                {
-                    HandleFailedSave(currentOperations);
-                }
-                else
-                {
-                    HandleSave(task.Result);
-                }
+    /// 
+    /// Raises change notifications for a property. Passing null or the empty string
+    /// notifies the binding framework that all properties/indexes have changed.
+    /// Passing "Item[]" tells the binding framework that all indexed values
+    /// have changed (but not all properties)
+    /// 
+    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
+    {
+        PropertyChangedHandler.Invoke(this, new PropertyChangedEventArgs(propertyName));
+    }
 
-                return task;
-            }).Unwrap();
+    protected virtual async Task SaveAsync(Task toAwait, CancellationToken cancellationToken)
+    {
+        if (!IsDirty)
+        {
+            // No need to save if the object is not dirty
+            return;
         }
 
-        /// 
-        /// Sets the value of a property based upon its associated ParseFieldName attribute.
-        /// 
-        /// The new value.
-        /// The name of the property.
-        /// The type for the property.
-        protected void SetProperty(T value, [CallerMemberName] string propertyName = null) => this[Services.GetFieldForPropertyName(ClassName, propertyName)] = value;
+        // Get the session token and prepare the save operation
+        var currentOperations = StartSave();
+        var sessionToken = await Services.GetCurrentSessionToken();
 
-        void ApplyOperations(IDictionary operations, IDictionary map)
+        // Perform the deep save asynchronously
+        try
         {
-            lock (Mutex)
-            {
-                foreach (KeyValuePair pair in operations)
-                {
-                    map.TryGetValue(pair.Key, out object oldValue);
-                    object newValue = pair.Value.Apply(oldValue, pair.Key);
+            // Await the deep save operation
+            await Services.DeepSaveAsync(EstimatedData, sessionToken, cancellationToken).ConfigureAwait(false);
 
-                    if (newValue != ParseDeleteOperation.Token)
-                    {
-                        map[pair.Key] = newValue;
-                    }
-                    else
-                    {
-                        map.Remove(pair.Key);
-                    }
-                }
+            // Proceed with the object save and update the state with the result
+            var newState = await Services.ObjectController.SaveAsync(State, currentOperations, sessionToken, Services, cancellationToken).ConfigureAwait(false);
+            if (newState == null)
+            {
+                throw new InvalidOperationException("SaveAsync returned a null state.");
             }
+            // Handle successful save with the updated state
+            HandleSave(newState);
+        }
+        catch (OperationCanceledException)
+        {
+            // Handle the cancellation case
+            HandleFailedSave(currentOperations);
         }
+        catch (Exception ex)
+        {
+            // Log or handle unexpected errors
+            HandleFailedSave(currentOperations);
+            Console.WriteLine($"Error during save: {ex.Message}");
+        }
+    }
+
 
-        void CheckGetAccess(string key)
+    /// 
+    /// Sets the value of a property based upon its associated ParseFieldName attribute.
+    /// 
+    /// The new value.
+    /// The name of the property.
+    /// The type for the property.
+    protected void SetProperty(T value, [CallerMemberName] string propertyName = null)
+    {
+        this[Services.GetFieldForPropertyName(ClassName, propertyName)] = value;
+    }
+
+    void ApplyOperations(IDictionary operations, IDictionary map)
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
+            foreach (KeyValuePair pair in operations)
             {
-                if (!CheckIsDataAvailable(key))
+                map.TryGetValue(pair.Key, out object oldValue);
+                object newValue = pair.Value.Apply(oldValue, pair.Key);
+
+                if (newValue != ParseDeleteOperation.Token)
                 {
-                    throw new InvalidOperationException("ParseObject has no data for this key. Call FetchIfNeededAsync() to get the data.");
+                    map[pair.Key] = newValue;
+                }
+                else
+                {
+                    map.Remove(pair.Key);
                 }
             }
         }
-
-        bool CheckIsDataAvailable(string key)
+    }
+    void CheckGetAccess(string key)
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
+            if (!CheckIsDataAvailable(key))
             {
-                return IsDataAvailable || EstimatedData.ContainsKey(key);
+                Debug.WriteLine($"Warning: ParseObject has no data for key '{key}'. Ensure FetchIfNeededAsync() is called before accessing data.");
+                Console.WriteLine($"Warning: ParseObject has no data for key '{key}'. Ensure FetchIfNeededAsync() is called before accessing data.");
+                
+                // Optionally, set a flag or return early to signal the issue.
+                return;
             }
         }
+    }
+
+
+    bool CheckIsDataAvailable(string key)
+    {
+        lock (Mutex)
+        {
+            return IsDataAvailable || EstimatedData.ContainsKey(key);
+        }
+    }
 
-        internal bool CheckIsDirty(bool considerChildren)
+    internal bool CheckIsDirty(bool considerChildren)
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
-            {
-                return Dirty || CurrentOperations.Count > 0 || considerChildren && HasDirtyChildren;
-            }
+            return Dirty || CurrentOperations.Count > 0 || considerChildren && HasDirtyChildren;
         }
+    }
 
-        void CheckKeyIsMutable(string key)
+    void CheckKeyIsMutable(string key)
+    {
+        if (!CheckKeyMutable(key))
         {
-            if (!CheckKeyMutable(key))
-            {
-                throw new InvalidOperationException($@"Cannot change the ""{key}"" property of a ""{ClassName}"" object.");
-            }
+            throw new InvalidOperationException($@"Cannot change the ""{key}"" property of a ""{ClassName}"" object.");
         }
+    }
 
-        /// 
-        /// Deep traversal of this object to grab a copy of any object referenced by this object.
-        /// These instances may have already been fetched, and we don't want to lose their data when
-        /// refreshing or saving.
-        /// 
-        /// Map of objectId to ParseObject which have been fetched.
-        IDictionary CollectFetchedObjects() => Services.TraverseObjectDeep(EstimatedData).OfType().Where(o => o.ObjectId != null && o.IsDataAvailable).GroupBy(o => o.ObjectId).ToDictionary(group => group.Key, group => group.Last());
+    /// 
+    /// Deep traversal of this object to grab a copy of any object referenced by this object.
+    /// These instances may have already been fetched, and we don't want to lose their data when
+    /// refreshing or saving.
+    /// 
+    /// Map of objectId to ParseObject which have been fetched.
+    IDictionary CollectFetchedObjects()
+    {
+        return Services.TraverseObjectDeep(EstimatedData).OfType().Where(o => o.ObjectId != null && o.IsDataAvailable).GroupBy(o => o.ObjectId).ToDictionary(group => group.Key, group => group.Last());
+    }
 
-        IEnumerable FindUnsavedChildren() => Services.TraverseObjectDeep(EstimatedData).OfType().Where(o => o.IsDirty);
+    IEnumerable FindUnsavedChildren()
+    {
+        return Services.TraverseObjectDeep(EstimatedData).OfType().Where(o => o.IsDirty);
+    }
 
-        IEnumerator> IEnumerable>.GetEnumerator()
+    IEnumerator> IEnumerable>.GetEnumerator()
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
-            {
-                return EstimatedData.GetEnumerator();
-            }
+            return EstimatedData.GetEnumerator();
         }
+    }
 
-        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
-            {
-                return ((IEnumerable>) this).GetEnumerator();
-            }
+            return ((IEnumerable>) this).GetEnumerator();
         }
-        /// 
-        /// Sets the objectId without marking dirty.
-        /// 
-        /// The new objectId
-        void SetObjectIdInternal(string objectId)
+    }
+    /// 
+    /// Sets the objectId without marking dirty.
+    /// 
+    /// The new objectId
+    void SetObjectIdInternal(string objectId)
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
-            {
-                MutateState(mutableClone => mutableClone.ObjectId = objectId);
-                OnPropertyChanged(nameof(ObjectId));
-            }
+            MutateState(mutableClone => mutableClone.ObjectId = objectId);
+            OnPropertyChanged(nameof(ObjectId));
         }
     }
 }
diff --git a/Parse/Platform/Objects/ParseObjectClass.cs b/Parse/Platform/Objects/ParseObjectClass.cs
index c7137fa0..c269271b 100644
--- a/Parse/Platform/Objects/ParseObjectClass.cs
+++ b/Parse/Platform/Objects/ParseObjectClass.cs
@@ -1,30 +1,57 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
 using System.Reflection;
 using Parse.Abstractions.Internal;
 using Parse.Infrastructure.Utilities;
 
-namespace Parse.Platform.Objects
+namespace Parse.Platform.Objects;
+
+internal class ParseObjectClass
 {
-    internal class ParseObjectClass
+    public ParseObjectClass(Type type, ConstructorInfo constructor)
     {
-        public ParseObjectClass(Type type, ConstructorInfo constructor)
-        {
-            TypeInfo = type.GetTypeInfo();
-            DeclaredName = TypeInfo.GetParseClassName();
-            Constructor = Constructor = constructor;
-            PropertyMappings = type.GetProperties().Select(property => (Property: property, FieldNameAttribute: property.GetCustomAttribute(true))).Where(set => set.FieldNameAttribute is { }).ToDictionary(set => set.Property.Name, set => set.FieldNameAttribute.FieldName);
-        }
+        TypeInfo = type.GetTypeInfo();
+        DeclaredName = TypeInfo.GetParseClassName();
+        Constructor = constructor;
+        PropertyMappings = type.GetProperties()
+            .Select(property => (Property: property, FieldNameAttribute: property.GetCustomAttribute(true)))
+            .Where(set => set.FieldNameAttribute is { })
+            .ToDictionary(set => set.Property.Name, set => set.FieldNameAttribute.FieldName);
+    }
 
-        public TypeInfo TypeInfo { get; }
+    public TypeInfo TypeInfo { get; }
 
-        public string DeclaredName { get; }
+    public string DeclaredName { get; }
 
-        public IDictionary PropertyMappings { get; }
+    public IDictionary PropertyMappings { get; }
 
-        public ParseObject Instantiate() => Constructor.Invoke(default) as ParseObject;
+    public ParseObject Instantiate()
+    {
+        var parameters = Constructor.GetParameters();
+        
+        if (parameters.Length == 0)
+        {
+            
+            // Parameterless constructor
+            return Constructor.Invoke(null) as ParseObject;
+        }
+        else if (parameters.Length == 2 &&
+                 parameters[0].ParameterType == typeof(string) &&
+                 parameters[1].ParameterType == typeof(Parse.Abstractions.Infrastructure.IServiceHub))
+        {
+         
 
-        ConstructorInfo Constructor { get; }
+            // Two-parameter constructor
+            string className = Constructor.DeclaringType?.Name ?? "_User"; //Still Unsure about this default value, maybe User is not the best choice, but what else?
+            var serviceHub = Parse.ParseClient.Instance.Services;
+            return Constructor.Invoke(new object[] { className, serviceHub }) as ParseObject;
+        }
+        
+
+        throw new InvalidOperationException("Unsupported constructor signature.");
     }
+
+    ConstructorInfo Constructor { get; }
 }
diff --git a/Parse/Platform/Objects/ParseObjectClassController.cs b/Parse/Platform/Objects/ParseObjectClassController.cs
index 13cf1b15..44522a30 100644
--- a/Parse/Platform/Objects/ParseObjectClassController.cs
+++ b/Parse/Platform/Objects/ParseObjectClassController.cs
@@ -1,145 +1,163 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Reflection;
 using System.Threading;
 using Parse.Abstractions.Infrastructure;
 using Parse.Abstractions.Platform.Objects;
 using Parse.Infrastructure.Utilities;
 
-namespace Parse.Platform.Objects
+namespace Parse.Platform.Objects;
+
+internal class ParseObjectClassController : IParseObjectClassController
 {
-    internal class ParseObjectClassController : IParseObjectClassController
-    {
-        // Class names starting with _ are documented to be reserved. Use this one here to allow us to "inherit" certain properties.
-        static string ReservedParseObjectClassName { get; } = "_ParseObject";
+    // Class names starting with _ are documented to be reserved. Use this one here to allow us to "inherit" certain properties.
+    static string ReservedParseObjectClassName { get; } = "_ParseObject";
 
-        ReaderWriterLockSlim Mutex { get; } = new ReaderWriterLockSlim { };
+    ReaderWriterLockSlim Mutex { get; } = new ReaderWriterLockSlim { };
 
-        IDictionary Classes { get; } = new Dictionary { };
+    IDictionary Classes { get; } = new Dictionary { };
 
-        Dictionary RegisterActions { get; set; } = new Dictionary { };
+    Dictionary RegisterActions { get; set; } = new Dictionary { };
 
-        public ParseObjectClassController() => AddValid(typeof(ParseObject));
+    public ParseObjectClassController() => AddValid(typeof(ParseObject));
 
-        public string GetClassName(Type type) => type == typeof(ParseObject) ? ReservedParseObjectClassName : type.GetParseClassName();
+    public string GetClassName(Type type)
+    {
+        return type == typeof(ParseObject) ? ReservedParseObjectClassName : type.GetParseClassName();
+    }
 
-        public Type GetType(string className)
-        {
-            Mutex.EnterReadLock();
-            Classes.TryGetValue(className, out ParseObjectClass info);
-            Mutex.ExitReadLock();
+    public Type GetType(string className)
+    {
+        Mutex.EnterReadLock();
+        Classes.TryGetValue(className, out ParseObjectClass info);
+        Mutex.ExitReadLock();
 
-            return info?.TypeInfo.AsType();
-        }
+        return info?.TypeInfo.AsType();
+    }
 
-        public bool GetClassMatch(string className, Type type)
-        {
-            Mutex.EnterReadLock();
-            Classes.TryGetValue(className, out ParseObjectClass subclassInfo);
-            Mutex.ExitReadLock();
+    public bool GetClassMatch(string className, Type type)
+    {
+        Mutex.EnterReadLock();
+        Classes.TryGetValue(className, out ParseObjectClass subclassInfo);
+        Mutex.ExitReadLock();
 
-            return subclassInfo is { } ? subclassInfo.TypeInfo == type.GetTypeInfo() : type == typeof(ParseObject);
-        }
+        return subclassInfo is { } ? subclassInfo.TypeInfo == type.GetTypeInfo() : type == typeof(ParseObject);
+    }
 
-        public void AddValid(Type type)
-        {
-            TypeInfo typeInfo = type.GetTypeInfo();
+    public void AddValid(Type type)
+    {
+        TypeInfo typeInfo = type.GetTypeInfo();
 
-            if (!typeof(ParseObject).GetTypeInfo().IsAssignableFrom(typeInfo))
-                throw new ArgumentException("Cannot register a type that is not a subclass of ParseObject");
+        if (!typeof(ParseObject).GetTypeInfo().IsAssignableFrom(typeInfo))
+            throw new ArgumentException("Cannot register a type that is not a subclass of ParseObject");
 
-            string className = GetClassName(type);
+        string className = GetClassName(type);
 
-            try
-            {
-                // Perform this as a single independent transaction, so we can never get into an
-                // intermediate state where we *theoretically* register the wrong class due to a
-                // TOCTTOU bug.
+        try
+        {
+            // Perform this as a single independent transaction, so we can never get into an
+            // intermediate state where we *theoretically* register the wrong class due to a
+            // TOCTTOU bug.
 
-                Mutex.EnterWriteLock();
+            Mutex.EnterWriteLock();
 
-                if (Classes.TryGetValue(className, out ParseObjectClass previousInfo))
-                    if (typeInfo.IsAssignableFrom(previousInfo.TypeInfo))
-                        // Previous subclass is more specific or equal to the current type, do nothing.
+            if (Classes.TryGetValue(className, out ParseObjectClass previousInfo))
+                if (typeInfo.IsAssignableFrom(previousInfo.TypeInfo))
+                    // Previous subclass is more specific or equal to the current type, do nothing.
 
-                        return;
-                    else if (previousInfo.TypeInfo.IsAssignableFrom(typeInfo))
-                    {
-                        // Previous subclass is parent of new child, fallthrough and actually register this class.
-                        /* Do nothing */
-                    }
-                    else
-                        throw new ArgumentException($"Tried to register both {previousInfo.TypeInfo.FullName} and {typeInfo.FullName} as the ParseObject subclass of {className}. Cannot determine the right class to use because neither inherits from the other.");
+                    return;
+                else if (previousInfo.TypeInfo.IsAssignableFrom(typeInfo))
+                {
+                    // Previous subclass is parent of new child, fallthrough and actually register this class.
+                    /* Do nothing */
+                }
+                else
+                    throw new ArgumentException($"Tried to register both {previousInfo.TypeInfo.FullName} and {typeInfo.FullName} as the ParseObject subclass of {className}. Cannot determine the right class to use because neither inherits from the other.");
 
+#pragma warning disable CS1030 // #warning directive
 #warning Constructor detection may erroneously find a constructor which should not be used.
 
-                ConstructorInfo constructor = type.FindConstructor() ?? type.FindConstructor(typeof(string), typeof(IServiceHub));
-
-                if (constructor is null)
-                    throw new ArgumentException("Cannot register a type that does not implement the default constructor!");
-
-                Classes[className] = new ParseObjectClass(type, constructor);
-            }
-            finally
-            {
-                Mutex.ExitWriteLock();
-            }
+            ConstructorInfo constructor = type.FindConstructor() ?? type.FindConstructor(typeof(string), typeof(IServiceHub));
+#pragma warning restore CS1030 // #warning directive
 
-            Mutex.EnterReadLock();
-            RegisterActions.TryGetValue(className, out Action toPerform);
-            Mutex.ExitReadLock();
+            if (constructor is null)
+                throw new ArgumentException("Cannot register a type that does not implement the default constructor!");
 
-            toPerform?.Invoke();
+            Classes[className] = new ParseObjectClass(type, constructor);
         }
-
-        public void RemoveClass(Type type)
+        finally
         {
-            Mutex.EnterWriteLock();
-            Classes.Remove(GetClassName(type));
             Mutex.ExitWriteLock();
         }
 
-        public void AddRegisterHook(Type type, Action action)
-        {
-            Mutex.EnterWriteLock();
-            RegisterActions.Add(GetClassName(type), action);
-            Mutex.ExitWriteLock();
-        }
+        Mutex.EnterReadLock();
+        RegisterActions.TryGetValue(className, out Action toPerform);
+        Mutex.ExitReadLock();
 
-        public ParseObject Instantiate(string className, IServiceHub serviceHub)
-        {
-            Mutex.EnterReadLock();
-            Classes.TryGetValue(className, out ParseObjectClass info);
-            Mutex.ExitReadLock();
+        toPerform?.Invoke();
+    }
 
-            return info is { } ? info.Instantiate().Bind(serviceHub) : new ParseObject(className, serviceHub);
-        }
+    public void RemoveClass(Type type)
+    {
+        Mutex.EnterWriteLock();
+        Classes.Remove(GetClassName(type));
+        Mutex.ExitWriteLock();
+    }
 
-        public IDictionary GetPropertyMappings(string className)
-        {
-            Mutex.EnterReadLock();
-            Classes.TryGetValue(className, out ParseObjectClass info);
+    public void AddRegisterHook(Type type, Action action)
+    {
+        Mutex.EnterWriteLock();
+        RegisterActions.Add(GetClassName(type), action);
+        Mutex.ExitWriteLock();
+    }
 
-            if (info is null)
-                Classes.TryGetValue(ReservedParseObjectClassName, out info);
+    public ParseObject Instantiate(string className, IServiceHub serviceHub)
+    {
+        
+        Mutex.EnterReadLock();
+        
+        Classes.TryGetValue(className, out ParseObjectClass info);
+        
+        Mutex.ExitReadLock();
+        
+        if (info is { })
+        {
+        
+            var obj = info.Instantiate().Bind(serviceHub);
+        
+            return obj;
+        }
+        else
+        {
+            
+            return  new ParseObject(className, serviceHub);
 
-            Mutex.ExitReadLock();
-            return info.PropertyMappings;
         }
+    }
 
-        bool SDKClassesAdded { get; set; }
+    public IDictionary GetPropertyMappings(string className)
+    {        
+        Mutex.EnterReadLock();
+        Classes.TryGetValue(className, out ParseObjectClass info);        
+        if (info is null)
+            Classes.TryGetValue(ReservedParseObjectClassName, out info);        
+        Mutex.ExitReadLock();        
+        return info.PropertyMappings;
+    }
 
-        // ALTERNATE NAME: AddObject, AddType, AcknowledgeType, CatalogType
+    bool SDKClassesAdded { get; set; }
 
-        public void AddIntrinsic()
+    // ALTERNATE NAME: AddObject, AddType, AcknowledgeType, CatalogType
+
+    public void AddIntrinsic()
+    {
+        if (!(SDKClassesAdded, SDKClassesAdded = true).SDKClassesAdded)
         {
-            if (!(SDKClassesAdded, SDKClassesAdded = true).SDKClassesAdded)
-            {
-                AddValid(typeof(ParseUser));
-                AddValid(typeof(ParseRole));
-                AddValid(typeof(ParseSession));
-                AddValid(typeof(ParseInstallation));
-            }
+            AddValid(typeof(ParseUser));
+            AddValid(typeof(ParseRole));
+            AddValid(typeof(ParseSession));
+            AddValid(typeof(ParseInstallation));
         }
     }
 }
diff --git a/Parse/Platform/Objects/ParseObjectController.cs b/Parse/Platform/Objects/ParseObjectController.cs
index 681f0956..8c5c1904 100644
--- a/Parse/Platform/Objects/ParseObjectController.cs
+++ b/Parse/Platform/Objects/ParseObjectController.cs
@@ -14,133 +14,181 @@
 using Parse.Infrastructure.Execution;
 using Parse.Infrastructure.Data;
 
-namespace Parse.Platform.Objects
+namespace Parse.Platform.Objects;
+
+public class ParseObjectController : IParseObjectController
 {
-    public class ParseObjectController : IParseObjectController
-    {
-        IParseCommandRunner CommandRunner { get; }
+    IParseCommandRunner CommandRunner { get; }
+
+    IParseDataDecoder Decoder { get; }
+
+    IServerConnectionData ServerConnectionData { get; }
 
-        IParseDataDecoder Decoder { get; }
+    public ParseObjectController(IParseCommandRunner commandRunner, IParseDataDecoder decoder, IServerConnectionData serverConnectionData) => (CommandRunner, Decoder, ServerConnectionData) = (commandRunner, decoder, serverConnectionData);
 
-        IServerConnectionData ServerConnectionData { get; }
+    public async Task FetchAsync(IObjectState state, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default)
+    {
+        var command = new ParseCommand($"classes/{Uri.EscapeDataString(state.ClassName)}/{Uri.EscapeDataString(state.ObjectId)}", method: "GET", sessionToken: sessionToken, data: null);
+
+        var result = await CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).ConfigureAwait(false);
+        return ParseObjectCoder.Instance.Decode(result.Item2, Decoder, serviceHub);
+    }
 
-        public ParseObjectController(IParseCommandRunner commandRunner, IParseDataDecoder decoder, IServerConnectionData serverConnectionData) => (CommandRunner, Decoder, ServerConnectionData) = (commandRunner, decoder, serverConnectionData);
 
-        public Task FetchAsync(IObjectState state, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default)
+    public async Task SaveAsync(IObjectState state, IDictionary operations, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default)
+    {
+        ParseCommand command;
+        if (state.ObjectId == null)
         {
-            ParseCommand command = new ParseCommand($"classes/{Uri.EscapeDataString(state.ClassName)}/{Uri.EscapeDataString(state.ObjectId)}", method: "GET", sessionToken: sessionToken, data: default);
-            return CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(task => ParseObjectCoder.Instance.Decode(task.Result.Item2, Decoder, serviceHub));
+            var method = "POST";
+            var relURI = $"classes/{Uri.EscapeDataString(state.ClassName)}";
+            var dataa = serviceHub.GenerateJSONObjectForSaving(operations);
+            command = new ParseCommand(relURI, method, sessionToken: sessionToken, data: dataa);
         }
-
-        public Task SaveAsync(IObjectState state, IDictionary operations, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default)
+        else
         {
-            ParseCommand command = new ParseCommand(state.ObjectId == null ? $"classes/{Uri.EscapeDataString(state.ClassName)}" : $"classes/{Uri.EscapeDataString(state.ClassName)}/{state.ObjectId}", method: state.ObjectId is null ? "POST" : "PUT", sessionToken: sessionToken, data: serviceHub.GenerateJSONObjectForSaving(operations));
-            return CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(task => ParseObjectCoder.Instance.Decode(task.Result.Item2, Decoder, serviceHub).MutatedClone(mutableClone => mutableClone.IsNew = task.Result.Item1 == System.Net.HttpStatusCode.Created));
+            var method = "PUT";
+            var relURI = $"classes/{Uri.EscapeDataString(state.ClassName)}/{state.ObjectId}";
+            var dataa = serviceHub.GenerateJSONObjectForSaving(operations);
+            command = new ParseCommand(relURI, method, sessionToken: sessionToken, data: dataa);
         }
+        var result = await CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).ConfigureAwait(false);
+        var decodedState = ParseObjectCoder.Instance.Decode(result.Item2, Decoder, serviceHub);
 
-        public IList> SaveAllAsync(IList states, IList> operationsList, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default) => ExecuteBatchRequests(states.Zip(operationsList, (item, operations) => new ParseCommand(item.ObjectId is null ? $"classes/{Uri.EscapeDataString(item.ClassName)}" : $"classes/{Uri.EscapeDataString(item.ClassName)}/{Uri.EscapeDataString(item.ObjectId)}", method: item.ObjectId is null ? "POST" : "PUT", data: serviceHub.GenerateJSONObjectForSaving(operations))).ToList(), sessionToken, cancellationToken).Select(task => task.OnSuccess(task => ParseObjectCoder.Instance.Decode(task.Result, Decoder, serviceHub))).ToList();
+        // Mutating the state and marking it as new if the status code is Created
+        decodedState.MutatedClone(mutableClone => mutableClone.IsNew = result.Item1 == System.Net.HttpStatusCode.Created);
 
-        public Task DeleteAsync(IObjectState state, string sessionToken, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand($"classes/{state.ClassName}/{state.ObjectId}", method: "DELETE", sessionToken: sessionToken, data: null), cancellationToken: cancellationToken);
+        return decodedState;
+    }
 
-        public IList DeleteAllAsync(IList states, string sessionToken, CancellationToken cancellationToken = default) => ExecuteBatchRequests(states.Where(item => item.ObjectId is { }).Select(item => new ParseCommand($"classes/{Uri.EscapeDataString(item.ClassName)}/{Uri.EscapeDataString(item.ObjectId)}", method: "DELETE", data: default)).ToList(), sessionToken, cancellationToken).Cast().ToList();
 
-        int MaximumBatchSize { get; } = 50;
+    public async Task>> SaveAllAsync(IEnumerable states,IEnumerable> operationsList,string sessionToken,IServiceHub serviceHub,CancellationToken cancellationToken = default)
+    {
+        // Create a list of tasks where each task represents a command to be executed
+        var tasks =
+            states.Zip(operationsList, (state, operations) => new ParseCommand(state.ObjectId == null? $"classes/{Uri.EscapeDataString(state.ClassName)}": $"classes/{Uri.EscapeDataString(state.ClassName)}/{Uri.EscapeDataString(state.ObjectId)}",
+            method: state.ObjectId == null ? "POST" : "PUT",sessionToken: sessionToken,data: serviceHub.GenerateJSONObjectForSaving(operations)))
+        .Select(command => CommandRunner.RunCommandAsync(command,null,null, cancellationToken)) // Run commands asynchronously
+        .ToList();
+
+        // Wait for all tasks to complete
+        var results = await Task.WhenAll(tasks).ConfigureAwait(false);
+        var decodedStates = results.Select(result => ParseObjectCoder.Instance.Decode(result.Item2, Decoder, serviceHub)).ToList();
+        // Decode results and return a list of tasks that resolve to IObjectState
+        return results.Select(result =>
+            Task.FromResult(ParseObjectCoder.Instance.Decode(result.Item2, Decoder, serviceHub)) // Return a task that resolves to IObjectState
+        ).ToList();
+    }
 
-        // TODO (hallucinogen): move this out to a class to be used by Analytics
 
-        internal IList>> ExecuteBatchRequests(IList requests, string sessionToken, CancellationToken cancellationToken = default)
-        {
-            List>> tasks = new List>>();
-            int batchSize = requests.Count;
 
-            IEnumerable remaining = requests;
 
-            while (batchSize > MaximumBatchSize)
-            {
-                List process = remaining.Take(MaximumBatchSize).ToList();
+    public Task DeleteAsync(IObjectState state, string sessionToken, CancellationToken cancellationToken = default)
+    {
+        return CommandRunner.RunCommandAsync(new ParseCommand($"classes/{state.ClassName}/{state.ObjectId}", method: "DELETE", sessionToken: sessionToken, data: null), cancellationToken: cancellationToken);
+    }
 
-                remaining = remaining.Skip(MaximumBatchSize);
-                tasks.AddRange(ExecuteBatchRequest(process, sessionToken, cancellationToken));
-                batchSize = remaining.Count();
-            }
+    public IEnumerable DeleteAllAsync(IEnumerable states, string sessionToken, CancellationToken cancellationToken = default)
+    {
+        return ExecuteBatchRequests(states.Where(item => item.ObjectId is { }).Select(item => new ParseCommand($"classes/{Uri.EscapeDataString(item.ClassName)}/{Uri.EscapeDataString(item.ObjectId)}", method: "DELETE", data: default)).ToList(), sessionToken, cancellationToken).Cast().ToList();
+    }
 
-            tasks.AddRange(ExecuteBatchRequest(remaining.ToList(), sessionToken, cancellationToken));
-            return tasks;
-        }
+    int MaximumBatchSize { get; } = 50;
+
+    // TODO (hallucinogen): move this out to a class to be used by Analytics
+
+    internal IList>> ExecuteBatchRequests(IList requests, string sessionToken, CancellationToken cancellationToken = default)
+    {
+        List>> tasks = new List>>();
+        int batchSize = requests.Count;
+
+        IEnumerable remaining = requests;
 
-        IList>> ExecuteBatchRequest(IList requests, string sessionToken, CancellationToken cancellationToken = default)
+        while (batchSize > MaximumBatchSize)
         {
-            int batchSize = requests.Count;
+            List process = remaining.Take(MaximumBatchSize).ToList();
+
+            remaining = remaining.Skip(MaximumBatchSize);
+            tasks.AddRange(ExecuteBatchRequest(process, sessionToken, cancellationToken));
+            batchSize = remaining.Count();
+        }
 
-            List>> tasks = new List>> { };
-            List>> completionSources = new List>> { };
+        tasks.AddRange(ExecuteBatchRequest(remaining.ToList(), sessionToken, cancellationToken));
+        return tasks;
+    }
 
-            for (int i = 0; i < batchSize; ++i)
-            {
-                TaskCompletionSource> tcs = new TaskCompletionSource>();
+    IList>> ExecuteBatchRequest(IList requests, string sessionToken, CancellationToken cancellationToken = default)
+    {
+        int batchSize = requests.Count;
 
-                completionSources.Add(tcs);
-                tasks.Add(tcs.Task);
-            }
+        List>> tasks = new List>> { };
+        List>> completionSources = new List>> { };
 
-            List encodedRequests = requests.Select(request =>
+        for (int i = 0; i < batchSize; ++i)
+        {
+            TaskCompletionSource> tcs = new TaskCompletionSource>();
+
+            completionSources.Add(tcs);
+            tasks.Add(tcs.Task);
+        }
+
+        List encodedRequests = requests.Select(request =>
+        {
+            Dictionary results = new Dictionary
             {
-                Dictionary results = new Dictionary
-                {
-                    ["method"] = request.Method,
-                    ["path"] = request is { Path: { }, Resource: { } } ? request.Target.AbsolutePath : new Uri(new Uri(ServerConnectionData.ServerURI), request.Path).AbsolutePath,
-                };
+                ["method"] = request.Method,
+                ["path"] = request is { Path: { }, Resource: { } } ? request.Target.AbsolutePath : new Uri(new Uri(ServerConnectionData.ServerURI), request.Path).AbsolutePath,
+            };
 
-                if (request.DataObject != null)
-                    results["body"] = request.DataObject;
+            if (request.DataObject != null)
+                results["body"] = request.DataObject;
 
-                return results;
-            }).Cast().ToList();
+            return results;
+        }).Cast().ToList();
 
-            ParseCommand command = new ParseCommand("batch", method: "POST", sessionToken: sessionToken, data: new Dictionary { [nameof(requests)] = encodedRequests });
+        ParseCommand command = new ParseCommand("batch", method: "POST", sessionToken: sessionToken, data: new Dictionary { [nameof(requests)] = encodedRequests });
 
-            CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).ContinueWith(task =>
+        CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).ContinueWith(task =>
+        {
+            if (task.IsFaulted || task.IsCanceled)
             {
-                if (task.IsFaulted || task.IsCanceled)
-                {
-                    foreach (TaskCompletionSource> tcs in completionSources)
-                        if (task.IsFaulted)
-                            tcs.TrySetException(task.Exception);
-                        else if (task.IsCanceled)
-                            tcs.TrySetCanceled();
+                foreach (TaskCompletionSource> tcs in completionSources)
+                    if (task.IsFaulted)
+                        tcs.TrySetException(task.Exception);
+                    else if (task.IsCanceled)
+                        tcs.TrySetCanceled();
 
-                    return;
-                }
+                return;
+            }
 
-                IList resultsArray = Conversion.As>(task.Result.Item2["results"]);
-                int resultLength = resultsArray.Count;
+            IList resultsArray = Conversion.As>(task.Result.Item2["results"]);
+            int resultLength = resultsArray.Count;
 
-                if (resultLength != batchSize)
-                {
-                    foreach (TaskCompletionSource> completionSource in completionSources)
-                        completionSource.TrySetException(new InvalidOperationException($"Batch command result count expected: {batchSize} but was: {resultLength}."));
+            if (resultLength != batchSize)
+            {
+                foreach (TaskCompletionSource> completionSource in completionSources)
+                    completionSource.TrySetException(new InvalidOperationException($"Batch command result count expected: {batchSize} but was: {resultLength}."));
 
-                    return;
-                }
+                return;
+            }
+
+            for (int i = 0; i < batchSize; ++i)
+            {
+                Dictionary result = resultsArray[i] as Dictionary;
+                TaskCompletionSource> target = completionSources[i];
 
-                for (int i = 0; i < batchSize; ++i)
+                if (result.ContainsKey("success"))
+                    target.TrySetResult(result["success"] as IDictionary);
+                else if (result.ContainsKey("error"))
                 {
-                    Dictionary result = resultsArray[i] as Dictionary;
-                    TaskCompletionSource> target = completionSources[i];
-
-                    if (result.ContainsKey("success"))
-                        target.TrySetResult(result["success"] as IDictionary);
-                    else if (result.ContainsKey("error"))
-                    {
-                        IDictionary error = result["error"] as IDictionary;
-                        target.TrySetException(new ParseFailureException((ParseFailureException.ErrorCode) (long) error["code"], error[nameof(error)] as string));
-                    }
-                    else
-                        target.TrySetException(new InvalidOperationException("Invalid batch command response."));
+                    IDictionary error = result["error"] as IDictionary;
+                    target.TrySetException(new ParseFailureException((ParseFailureException.ErrorCode) (long) error["code"], error[nameof(error)] as string));
                 }
-            });
+                else
+                    target.TrySetException(new InvalidOperationException("Invalid batch command response."));
+            }
+        });
 
-            return tasks;
-        }
+        return tasks;
     }
 }
diff --git a/Parse/Platform/ParseClient.cs b/Parse/Platform/ParseClient.cs
index 31121bd6..4e3b4a8b 100644
--- a/Parse/Platform/ParseClient.cs
+++ b/Parse/Platform/ParseClient.cs
@@ -10,142 +10,150 @@
 [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Parse.Tests")]
 #endif
 
-namespace Parse
+namespace Parse;
+
+/// 
+/// ParseClient contains static functions that handle global
+/// configuration for the Parse library.
+/// 
+public class ParseClient : CustomServiceHub, IServiceHubComposer
 {
     /// 
-    /// ParseClient contains static functions that handle global
-    /// configuration for the Parse library.
+    /// Contains, in order, the official ISO date and time format strings, and two modified versions that account for the possibility that the server-side string processing mechanism removed trailing zeroes.
     /// 
-    public class ParseClient : CustomServiceHub, IServiceHubComposer
+    internal static string[] DateFormatStrings { get; } =
     {
-        /// 
-        /// Contains, in order, the official ISO date and time format strings, and two modified versions that account for the possibility that the server-side string processing mechanism removed trailing zeroes.
-        /// 
-        internal static string[] DateFormatStrings { get; } =
+        "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'",
+        "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'ff'Z'",
+        "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'f'Z'",
+    };
+
+    /// 
+    /// Gets whether or not the assembly using the Parse SDK was compiled by IL2CPP.
+    /// 
+    public static bool IL2CPPCompiled { get; set; } = AppDomain.CurrentDomain?.FriendlyName?.Equals("IL2CPP Root Domain") == true;
+
+    /// 
+    /// The configured default instance of  to use.
+    /// 
+    public static ParseClient Instance { get; private set; }
+
+    internal static string Version => typeof(ParseClient)?.Assembly?.GetCustomAttribute()?.InformationalVersion ?? typeof(ParseClient)?.Assembly?.GetName()?.Version?.ToString();
+
+    /// 
+    /// Services that provide essential functionality.
+    /// 
+    public override IServiceHub Services { get; internal set; }
+
+    // TODO: Implement IServiceHubMutator in all IServiceHub-implementing classes in Parse.Library and possibly require all implementations to do so as an efficiency improvement over instantiating an OrchestrationServiceHub, only for another one to be possibly instantiated when configurators are specified.
+
+    /// 
+    /// Creates a new  and authenticates it as belonging to your application. This class is a hub for interacting with the SDK. The recommended way to use this class on client applications is to instantiate it, then call  on it in your application entry point. This allows you to access .
+    /// 
+    /// The Application ID provided in the Parse dashboard.
+    /// The server URI provided in the Parse dashboard.
+    /// The .NET Key provided in the Parse dashboard.
+    /// A service hub to override internal services and thereby make the Parse SDK operate in a custom manner.
+    /// A set of  implementation instances to tweak the behaviour of the SDK.
+    public ParseClient(string applicationID, string serverURI, string key, IServiceHub serviceHub = default, params IServiceHubMutator[] configurators) : this(new ServerConnectionData { ApplicationID = applicationID, ServerURI = serverURI, Key = key }, serviceHub, configurators) { }
+
+    /// 
+    /// Creates a new  and authenticates it as belonging to your application. This class is a hub for interacting with the SDK. The recommended way to use this class on client applications is to instantiate it, then call  on it in your application entry point. This allows you to access .
+    /// 
+    /// The configuration to initialize Parse with.
+    /// A service hub to override internal services and thereby make the Parse SDK operate in a custom manner.
+    /// A set of  implementation instances to tweak the behaviour of the SDK.
+    public ParseClient(IServerConnectionData configuration, IServiceHub serviceHub = default, params IServiceHubMutator[] configurators)
+    {
+        Services = serviceHub is { } ? new OrchestrationServiceHub { Custom = serviceHub, Default = new ServiceHub { ServerConnectionData = GenerateServerConnectionData() } } : new ServiceHub { ServerConnectionData = GenerateServerConnectionData() } as IServiceHub;
+
+        IServerConnectionData GenerateServerConnectionData() => configuration switch
         {
-            "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'",
-            "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'ff'Z'",
-            "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'f'Z'",
+            null => throw new ArgumentNullException(nameof(configuration)),
+            ServerConnectionData { Test: true, ServerURI: { } } data => data,
+            ServerConnectionData { Test: true } data => new ServerConnectionData
+            {
+                ApplicationID = data.ApplicationID,
+                Headers = data.Headers,
+                MasterKey = data.MasterKey,
+                Test = data.Test,
+                Key = data.Key,
+                ServerURI = "https://api.parse.com/1/"
+            },
+            { ServerURI: "https://api.parse.com/1/" } => throw new InvalidOperationException("Since the official parse server has shut down, you must specify a URI that points to a hosted instance."),
+            { ApplicationID: { }, ServerURI: { }, Key: { } } data => data,
+            _ => throw new InvalidOperationException("The IServerConnectionData implementation instance provided to the ParseClient constructor must be populated with the information needed to connect to a Parse server instance.")
         };
 
-        /// 
-        /// Gets whether or not the assembly using the Parse SDK was compiled by IL2CPP.
-        /// 
-        public static bool IL2CPPCompiled { get; set; } = AppDomain.CurrentDomain?.FriendlyName?.Equals("IL2CPP Root Domain") == true;
-
-        /// 
-        /// The configured default instance of  to use.
-        /// 
-        public static ParseClient Instance { get; private set; }
-
-        internal static string Version => typeof(ParseClient)?.Assembly?.GetCustomAttribute()?.InformationalVersion ?? typeof(ParseClient)?.Assembly?.GetName()?.Version?.ToString();
-
-        /// 
-        /// Services that provide essential functionality.
-        /// 
-        public override IServiceHub Services { get; internal set; }
-
-        // TODO: Implement IServiceHubMutator in all IServiceHub-implementing classes in Parse.Library and possibly require all implementations to do so as an efficiency improvement over instantiating an OrchestrationServiceHub, only for another one to be possibly instantiated when configurators are specified.
-
-        /// 
-        /// Creates a new  and authenticates it as belonging to your application. This class is a hub for interacting with the SDK. The recommended way to use this class on client applications is to instantiate it, then call  on it in your application entry point. This allows you to access .
-        /// 
-        /// The Application ID provided in the Parse dashboard.
-        /// The server URI provided in the Parse dashboard.
-        /// The .NET Key provided in the Parse dashboard.
-        /// A service hub to override internal services and thereby make the Parse SDK operate in a custom manner.
-        /// A set of  implementation instances to tweak the behaviour of the SDK.
-        public ParseClient(string applicationID, string serverURI, string key, IServiceHub serviceHub = default, params IServiceHubMutator[] configurators) : this(new ServerConnectionData { ApplicationID = applicationID, ServerURI = serverURI, Key = key }, serviceHub, configurators) { }
-
-        /// 
-        /// Creates a new  and authenticates it as belonging to your application. This class is a hub for interacting with the SDK. The recommended way to use this class on client applications is to instantiate it, then call  on it in your application entry point. This allows you to access .
-        /// 
-        /// The configuration to initialize Parse with.
-        /// A service hub to override internal services and thereby make the Parse SDK operate in a custom manner.
-        /// A set of  implementation instances to tweak the behaviour of the SDK.
-        public ParseClient(IServerConnectionData configuration, IServiceHub serviceHub = default, params IServiceHubMutator[] configurators)
+        if (configurators is { Length: int length } && length > 0)
         {
-            Services = serviceHub is { } ? new OrchestrationServiceHub { Custom = serviceHub, Default = new ServiceHub { ServerConnectionData = GenerateServerConnectionData() } } : new ServiceHub { ServerConnectionData = GenerateServerConnectionData() } as IServiceHub;
-
-            IServerConnectionData GenerateServerConnectionData() => configuration switch
+            Services = serviceHub switch
             {
-                null => throw new ArgumentNullException(nameof(configuration)),
-                ServerConnectionData { Test: true, ServerURI: { } } data => data,
-                ServerConnectionData { Test: true } data => new ServerConnectionData
-                {
-                    ApplicationID = data.ApplicationID,
-                    Headers = data.Headers,
-                    MasterKey = data.MasterKey,
-                    Test = data.Test,
-                    Key = data.Key,
-                    ServerURI = "https://api.parse.com/1/"
-                },
-                { ServerURI: "https://api.parse.com/1/" } => throw new InvalidOperationException("Since the official parse server has shut down, you must specify a URI that points to a hosted instance."),
-                { ApplicationID: { }, ServerURI: { }, Key: { } } data => data,
-                _ => throw new InvalidOperationException("The IServerConnectionData implementation instance provided to the ParseClient constructor must be populated with the information needed to connect to a Parse server instance.")
+                IMutableServiceHub { } mutableServiceHub => BuildHub((Hub: mutableServiceHub, mutableServiceHub.ServerConnectionData = serviceHub.ServerConnectionData ?? Services.ServerConnectionData).Hub, Services, configurators),
+                { } => BuildHub(default, Services, configurators)
             };
-
-            if (configurators is { Length: int length } && length > 0)
-            {
-                Services = serviceHub switch
-                {
-                    IMutableServiceHub { } mutableServiceHub => BuildHub((Hub: mutableServiceHub, mutableServiceHub.ServerConnectionData = serviceHub.ServerConnectionData ?? Services.ServerConnectionData).Hub, Services, configurators),
-                    { } => BuildHub(default, Services, configurators)
-                };
-            }
-
-            Services.ClassController.AddIntrinsic();
         }
 
-        /// 
-        /// Initializes a  instance using the  set on the 's   implementation instance.
-        /// 
-        public ParseClient() => Services = (Instance ?? throw new InvalidOperationException("A ParseClient instance with an initializer service must first be publicized in order for the default constructor to be used.")).Services.Cloner.BuildHub(Instance.Services, this);
+        Services.ClassController.AddIntrinsic();
+    }
+
+    /// 
+    /// Initializes a  instance using the  set on the 's   implementation instance.
+    /// 
+    public ParseClient() => Services = (Instance ?? throw new InvalidOperationException("A ParseClient instance with an initializer service must first be publicized in order for the default constructor to be used.")).Services.Cloner.BuildHub(Instance.Services, this);
 
-        /// 
-        /// Sets this  instance as the template to create new instances from.
-        /// 
-        ///// Declares that the current  instance should be the publicly-accesible .
-        public void Publicize()
+    /// 
+    /// Sets this  instance as the template to create new instances from.
+    /// 
+    ///// Declares that the current  instance should be the publicly-accesible .
+    public void Publicize()
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
-            {
-                Instance = this;
-            }
+            Instance = this;
         }
+    }
 
-        static object Mutex { get; } = new object { };
-
-        internal static string BuildQueryString(IDictionary parameters) => String.Join("&", (from pair in parameters let valueString = pair.Value as string select $"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(String.IsNullOrEmpty(valueString) ? JsonUtilities.Encode(pair.Value) : valueString)}").ToArray());
+    static object Mutex { get; } = new object { };
 
-        internal static IDictionary DecodeQueryString(string queryString)
-        {
-            Dictionary query = new Dictionary { };
+    internal static string BuildQueryString(IDictionary parameters)
+    {
+        return String.Join("&", (from pair in parameters let valueString = pair.Value as string select $"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(String.IsNullOrEmpty(valueString) ? JsonUtilities.Encode(pair.Value) : valueString)}").ToArray());
+    }
 
-            foreach (string pair in queryString.Split('&'))
-            {
-                string[] parts = pair.Split(new char[] { '=' }, 2);
-                query[parts[0]] = parts.Length == 2 ? Uri.UnescapeDataString(parts[1].Replace("+", " ")) : null;
-            }
+    internal static IDictionary DecodeQueryString(string queryString)
+    {
+        Dictionary query = new Dictionary { };
 
-            return query;
+        foreach (string pair in queryString.Split('&'))
+        {
+            string[] parts = pair.Split(new char[] { '=' }, 2);
+            query[parts[0]] = parts.Length == 2 ? Uri.UnescapeDataString(parts[1].Replace("+", " ")) : null;
         }
 
-        internal static IDictionary DeserializeJsonString(string jsonData) => JsonUtilities.Parse(jsonData) as IDictionary;
+        return query;
+    }
 
-        internal static string SerializeJsonString(IDictionary jsonData) => JsonUtilities.Encode(jsonData);
+    internal static IDictionary DeserializeJsonString(string jsonData)
+    {
+        return JsonUtilities.Parse(jsonData) as IDictionary;
+    }
 
-        public IServiceHub BuildHub(IMutableServiceHub target = default, IServiceHub extension = default, params IServiceHubMutator[] configurators)
-        {
-            OrchestrationServiceHub orchestrationServiceHub = new OrchestrationServiceHub { Custom = target ??= new MutableServiceHub { }, Default = extension ?? new ServiceHub { } };
+    internal static string SerializeJsonString(IDictionary jsonData)
+    {
+        return JsonUtilities.Encode(jsonData);
+    }
 
-            foreach (IServiceHubMutator mutator in configurators.Where(configurator => configurator.Valid))
-            {
-                mutator.Mutate(ref target, orchestrationServiceHub);
-                orchestrationServiceHub.Custom = target;
-            }
+    public IServiceHub BuildHub(IMutableServiceHub target = default, IServiceHub extension = default, params IServiceHubMutator[] configurators)
+    {
+        OrchestrationServiceHub orchestrationServiceHub = new OrchestrationServiceHub { Custom = target ??= new MutableServiceHub { }, Default = extension ?? new ServiceHub { } };
 
-            return orchestrationServiceHub;
+        foreach (IServiceHubMutator mutator in configurators.Where(configurator => configurator.Valid))
+        {
+            mutator.Mutate(ref target, orchestrationServiceHub);
+            orchestrationServiceHub.Custom = target;
         }
+
+        return orchestrationServiceHub;
     }
 }
diff --git a/Parse/Platform/Push/MutablePushState.cs b/Parse/Platform/Push/MutablePushState.cs
index 1f00cfda..61f78069 100644
--- a/Parse/Platform/Push/MutablePushState.cs
+++ b/Parse/Platform/Push/MutablePushState.cs
@@ -3,26 +3,28 @@
 using Parse.Abstractions.Platform.Push;
 using Parse.Infrastructure.Utilities;
 
-namespace Parse.Platform.Push
+namespace Parse.Platform.Push;
+
+public class MutablePushState : IPushState
 {
-    public class MutablePushState : IPushState
+    public ParseQuery Query { get; set; }
+    public IEnumerable Channels { get; set; }
+    public DateTime? Expiration { get; set; }
+    public TimeSpan? ExpirationInterval { get; set; }
+    public DateTime? PushTime { get; set; }
+    public IDictionary Data { get; set; }
+    public string Alert { get; set; }
+
+    public IPushState MutatedClone(Action func)
     {
-        public ParseQuery Query { get; set; }
-        public IEnumerable Channels { get; set; }
-        public DateTime? Expiration { get; set; }
-        public TimeSpan? ExpirationInterval { get; set; }
-        public DateTime? PushTime { get; set; }
-        public IDictionary Data { get; set; }
-        public string Alert { get; set; }
-
-        public IPushState MutatedClone(Action func)
-        {
-            MutablePushState clone = MutableClone();
-            func(clone);
-            return clone;
-        }
+        MutablePushState clone = MutableClone();
+        func(clone);
+        return clone;
+    }
 
-        protected virtual MutablePushState MutableClone() => new MutablePushState
+    protected virtual MutablePushState MutableClone()
+    {
+        return new MutablePushState
         {
             Query = Query,
             Channels = Channels == null ? null : new List(Channels),
@@ -32,24 +34,53 @@ public IPushState MutatedClone(Action func)
             Data = Data == null ? null : new Dictionary(Data),
             Alert = Alert
         };
+    }
+
+    public override bool Equals(object obj)
+    {
+        if (obj == null || !(obj is MutablePushState))
+            return false;
 
-        public override bool Equals(object obj)
+        MutablePushState other = obj as MutablePushState;
+        return Equals(Query, other.Query) &&
+               Channels.CollectionsEqual(other.Channels) &&
+               Equals(Expiration, other.Expiration) &&
+               Equals(ExpirationInterval, other.ExpirationInterval) &&
+               Equals(PushTime, other.PushTime) &&
+               Data.CollectionsEqual(other.Data) &&
+               Equals(Alert, other.Alert);
+    }
+
+    public override int GetHashCode()
+    {
+        HashCode hash = new HashCode();
+
+        // Add primitive and simple values
+        hash.Add(Query);
+        hash.Add(Expiration);
+        hash.Add(ExpirationInterval);
+        hash.Add(PushTime);
+        hash.Add(Alert);
+
+        // Add collections (order-independent where necessary)
+        if (Channels != null)
         {
-            if (obj == null || !(obj is MutablePushState))
-                return false;
-
-            MutablePushState other = obj as MutablePushState;
-            return Equals(Query, other.Query) &&
-                   Channels.CollectionsEqual(other.Channels) &&
-                   Equals(Expiration, other.Expiration) &&
-                   Equals(ExpirationInterval, other.ExpirationInterval) &&
-                   Equals(PushTime, other.PushTime) &&
-                   Data.CollectionsEqual(other.Data) &&
-                   Equals(Alert, other.Alert);
+            foreach (string channel in Channels)
+            {
+                hash.Add(channel);
+            }
         }
 
-        public override int GetHashCode() =>
-            // TODO (richardross): Implement this.
-            0;
+        if (Data != null)
+        {
+            foreach (var kvp in Data)
+            {
+                hash.Add(kvp.Key);
+                hash.Add(kvp.Value);
+            }
+        }
+
+        return hash.ToHashCode();
     }
+
 }
diff --git a/Parse/Platform/Push/ParsePush.cs b/Parse/Platform/Push/ParsePush.cs
index fa1baa78..6d4218a2 100644
--- a/Parse/Platform/Push/ParsePush.cs
+++ b/Parse/Platform/Push/ParsePush.cs
@@ -6,209 +6,219 @@
 using Parse.Abstractions.Platform.Push;
 using Parse.Platform.Push;
 
-namespace Parse
+namespace Parse;
+
+/// 
+///  A utility class for sending and receiving push notifications.
+/// 
+public partial class ParsePush
 {
-    /// 
-    ///  A utility class for sending and receiving push notifications.
-    /// 
-    public partial class ParsePush
-    {
-        object Mutex { get; } = new object { };
+    object Mutex { get; } = new object { };
 
-        IPushState State { get; set; }
+    IPushState State { get; set; }
 
-        IServiceHub Services { get; }
+    IServiceHub Services { get; }
 
+#pragma warning disable CS1030 // #warning directive
 #warning Make default(IServiceHub) the default value of serviceHub once all dependents properly inject it.
 
-        /// 
-        /// Creates a push which will target every device. The Data field must be set before calling SendAsync.
-        /// 
-        public ParsePush(IServiceHub serviceHub)
-        {
-            Services = serviceHub ?? ParseClient.Instance;
-            State = new MutablePushState { Query = Services.GetInstallationQuery() };
-        }
+    /// 
+    /// Creates a push which will target every device. The Data field must be set before calling SendAsync.
+    /// 
+    public ParsePush(IServiceHub serviceHub)
+#pragma warning restore CS1030 // #warning directive
+    {
+        Services = serviceHub ?? ParseClient.Instance;
+        State = new MutablePushState { Query = Services.GetInstallationQuery() };
+    }
 
-        #region Properties
+    #region Properties
 
-        /// 
-        /// An installation query that specifies which installations should receive
-        /// this push.
-        /// This should not be used in tandem with Channels.
-        /// 
-        public ParseQuery Query
+    /// 
+    /// An installation query that specifies which installations should receive
+    /// this push.
+    /// This should not be used in tandem with Channels.
+    /// 
+    public ParseQuery Query
+    {
+        get => State.Query;
+        set => MutateState(state =>
         {
-            get => State.Query;
-            set => MutateState(state =>
+            if (state.Channels is { } && value is { } && value.GetConstraint("channels") is { })
             {
-                if (state.Channels is { } && value is { } && value.GetConstraint("channels") is { })
-                {
-                    throw new InvalidOperationException("A push may not have both Channels and a Query with a channels constraint.");
-                }
+                throw new InvalidOperationException("A push may not have both Channels and a Query with a channels constraint.");
+            }
 
-                state.Query = value;
-            });
-        }
+            state.Query = value;
+        });
+    }
 
-        /// 
-        /// A short-hand to set a query which only discriminates on the channels to which a device is subscribed.
-        /// This is shorthand for:
-        ///
-        /// 
-        /// var push = new Push();
-        /// push.Query = ParseInstallation.Query.WhereKeyContainedIn("channels", channels);
-        /// 
-        ///
-        /// This cannot be used in tandem with Query.
-        /// 
-        public IEnumerable Channels
+    /// 
+    /// A short-hand to set a query which only discriminates on the channels to which a device is subscribed.
+    /// This is shorthand for:
+    ///
+    /// 
+    /// var push = new Push();
+    /// push.Query = ParseInstallation.Query.WhereKeyContainedIn("channels", channels);
+    /// 
+    ///
+    /// This cannot be used in tandem with Query.
+    /// 
+    public IEnumerable Channels
+    {
+        get => State.Channels;
+        set => MutateState(state =>
         {
-            get => State.Channels;
-            set => MutateState(state =>
+            if (value is { } && state.Query is { } && state.Query.GetConstraint("channels") is { })
             {
-                if (value is { } && state.Query is { } && state.Query.GetConstraint("channels") is { })
-                {
-                    throw new InvalidOperationException("A push may not have both Channels and a Query with a channels constraint.");
-                }
+                throw new InvalidOperationException("A push may not have both Channels and a Query with a channels constraint.");
+            }
 
-                state.Channels = value;
-            });
-        }
+            state.Channels = value;
+        });
+    }
 
-        /// 
-        /// The time at which this push will expire. This should not be used in tandem with ExpirationInterval.
-        /// 
-        public DateTime? Expiration
+    /// 
+    /// The time at which this push will expire. This should not be used in tandem with ExpirationInterval.
+    /// 
+    public DateTime? Expiration
+    {
+        get => State.Expiration;
+        set => MutateState(state =>
         {
-            get => State.Expiration;
-            set => MutateState(state =>
+            if (state.ExpirationInterval is { })
             {
-                if (state.ExpirationInterval is { })
-                {
-                    throw new InvalidOperationException("Cannot set Expiration after setting ExpirationInterval.");
-                }
+                throw new InvalidOperationException("Cannot set Expiration after setting ExpirationInterval.");
+            }
 
-                state.Expiration = value;
-            });
-        }
+            state.Expiration = value;
+        });
+    }
 
-        /// 
-        /// The time at which this push will be sent.
-        /// 
-        public DateTime? PushTime
+    /// 
+    /// The time at which this push will be sent.
+    /// 
+    public DateTime? PushTime
+    {
+        get => State.PushTime;
+        set => MutateState(state =>
         {
-            get => State.PushTime;
-            set => MutateState(state =>
-            {
-                DateTime now = DateTime.Now;
+            DateTime now = DateTime.Now;
 
-                if (value < now || value > now.AddDays(14))
-                {
-                    throw new InvalidOperationException("Cannot set PushTime in the past or more than two weeks later than now.");
-                }
+            if (value < now || value > now.AddDays(14))
+            {
+                throw new InvalidOperationException("Cannot set PushTime in the past or more than two weeks later than now.");
+            }
 
-                state.PushTime = value;
-            });
-        }
+            state.PushTime = value;
+        });
+    }
 
-        /// 
-        /// The time from initial schedul when this push will expire. This should not be used in tandem with Expiration.
-        /// 
-        public TimeSpan? ExpirationInterval
+    /// 
+    /// The time from initial schedul when this push will expire. This should not be used in tandem with Expiration.
+    /// 
+    public TimeSpan? ExpirationInterval
+    {
+        get => State.ExpirationInterval;
+        set => MutateState(state =>
         {
-            get => State.ExpirationInterval;
-            set => MutateState(state =>
+            if (state.Expiration is { })
             {
-                if (state.Expiration is { })
-                {
-                    throw new InvalidOperationException("Cannot set ExpirationInterval after setting Expiration.");
-                }
+                throw new InvalidOperationException("Cannot set ExpirationInterval after setting Expiration.");
+            }
 
-                state.ExpirationInterval = value;
-            });
-        }
+            state.ExpirationInterval = value;
+        });
+    }
 
-        /// 
-        /// The contents of this push. Some keys have special meaning. A full list of pre-defined
-        /// keys can be found in the Parse Push Guide. The following keys affect WinRT devices.
-        /// Keys which do not start with x-winrt- can be prefixed with x-winrt- to specify an
-        /// override only sent to winrt devices.
-        /// alert: the body of the alert text.
-        /// title: The title of the text.
-        /// x-winrt-payload: A full XML payload to be sent to WinRT installations instead of
-        ///      the auto-layout.
-        /// This should not be used in tandem with Alert.
-        /// 
-        public IDictionary Data
+    /// 
+    /// The contents of this push. Some keys have special meaning. A full list of pre-defined
+    /// keys can be found in the Parse Push Guide. The following keys affect WinRT devices.
+    /// Keys which do not start with x-winrt- can be prefixed with x-winrt- to specify an
+    /// override only sent to winrt devices.
+    /// alert: the body of the alert text.
+    /// title: The title of the text.
+    /// x-winrt-payload: A full XML payload to be sent to WinRT installations instead of
+    ///      the auto-layout.
+    /// This should not be used in tandem with Alert.
+    /// 
+    public IDictionary Data
+    {
+        get => State.Data;
+        set => MutateState(state =>
         {
-            get => State.Data;
-            set => MutateState(state =>
+            if (state.Alert is { } && value is { })
             {
-                if (state.Alert is { } && value is { })
-                {
-                    throw new InvalidOperationException("A push may not have both an Alert and Data.");
-                }
+                throw new InvalidOperationException("A push may not have both an Alert and Data.");
+            }
 
-                state.Data = value;
-            });
-        }
+            state.Data = value;
+        });
+    }
 
-        /// 
-        /// A convenience method which sets Data to a dictionary with alert as its only field. Equivalent to
-        ///
-        /// 
-        /// Data = new Dictionary<string, object> {{"alert", alert}};
-        /// 
-        ///
-        /// This should not be used in tandem with Data.
-        /// 
-        public string Alert
+    /// 
+    /// A convenience method which sets Data to a dictionary with alert as its only field. Equivalent to
+    ///
+    /// 
+    /// Data = new Dictionary<string, object> {{"alert", alert}};
+    /// 
+    ///
+    /// This should not be used in tandem with Data.
+    /// 
+    public string Alert
+    {
+        get => State.Alert;
+        set => MutateState(state =>
         {
-            get => State.Alert;
-            set => MutateState(state =>
+            if (state.Data is { } && value is { })
             {
-                if (state.Data is { } && value is { })
-                {
-                    throw new InvalidOperationException("A push may not have both an Alert and Data.");
-                }
+                throw new InvalidOperationException("A push may not have both an Alert and Data.");
+            }
 
-                state.Alert = value;
-            });
-        }
+            state.Alert = value;
+        });
+    }
 
-        #endregion
+    #endregion
 
-        internal IDictionary Encode() => ParsePushEncoder.Instance.Encode(State);
+    internal IDictionary Encode()
+    {
+        return ParsePushEncoder.Instance.Encode(State);
+    }
 
-        void MutateState(Action func)
+    void MutateState(Action func)
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
-            {
-                State = State.MutatedClone(func);
-            }
+            State = State.MutatedClone(func);
         }
+    }
 
-        #region Sending Push
-
-        /// 
-        /// Request a push to be sent. When this task completes, Parse has successfully acknowledged a request
-        /// to send push notifications but has not necessarily finished sending all notifications
-        /// requested. The current status of recent push notifications can be seen in your Push Notifications
-        /// console.
-        /// 
-        /// A Task for continuation.
-        public Task SendAsync() => SendAsync(CancellationToken.None);
-
-        /// 
-        /// Request a push to be sent. When this task completes, Parse has successfully acknowledged a request
-        /// to send push notifications but has not necessarily finished sending all notifications
-        /// requested. The current status of recent push notifications can be seen in your Push Notifications
-        /// console.
-        /// 
-        /// CancellationToken to cancel the current operation.
-        public Task SendAsync(CancellationToken cancellationToken) => Services.PushController.SendPushNotificationAsync(State, Services, cancellationToken);
-
-        #endregion
+    #region Sending Push
+
+    /// 
+    /// Request a push to be sent. When this task completes, Parse has successfully acknowledged a request
+    /// to send push notifications but has not necessarily finished sending all notifications
+    /// requested. The current status of recent push notifications can be seen in your Push Notifications
+    /// console.
+    /// 
+    /// A Task for continuation.
+    public Task SendAsync()
+    {
+        return SendAsync(CancellationToken.None);
+    }
+
+    /// 
+    /// Request a push to be sent. When this task completes, Parse has successfully acknowledged a request
+    /// to send push notifications but has not necessarily finished sending all notifications
+    /// requested. The current status of recent push notifications can be seen in your Push Notifications
+    /// console.
+    /// 
+    /// CancellationToken to cancel the current operation.
+    public Task SendAsync(CancellationToken cancellationToken)
+    {
+        return Services.PushController.SendPushNotificationAsync(State, Services, cancellationToken);
     }
+
+    #endregion
 }
diff --git a/Parse/Platform/Push/ParsePushChannelsController.cs b/Parse/Platform/Push/ParsePushChannelsController.cs
index 3cd6f11c..249e6183 100644
--- a/Parse/Platform/Push/ParsePushChannelsController.cs
+++ b/Parse/Platform/Push/ParsePushChannelsController.cs
@@ -1,30 +1,32 @@
 using System.Collections.Generic;
 using System.Threading;
 using System.Threading.Tasks;
-using Parse;
 using Parse.Abstractions.Infrastructure;
 using Parse.Abstractions.Platform.Installations;
 using Parse.Abstractions.Platform.Push;
-using Parse.Infrastructure.Utilities;
 
-namespace Parse.Platform.Push
+namespace Parse.Platform.Push;
+internal class ParsePushChannelsController : IParsePushChannelsController
 {
-    internal class ParsePushChannelsController : IParsePushChannelsController
-    {
-        IParseCurrentInstallationController CurrentInstallationController { get; }
+    private IParseCurrentInstallationController CurrentInstallationController { get; }
 
-        public ParsePushChannelsController(IParseCurrentInstallationController currentInstallationController) => CurrentInstallationController = currentInstallationController;
+    public ParsePushChannelsController(IParseCurrentInstallationController currentInstallationController)
+    {
+        CurrentInstallationController = currentInstallationController;
+    }
 
-        public Task SubscribeAsync(IEnumerable channels, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CurrentInstallationController.GetAsync(serviceHub, cancellationToken).OnSuccess(task =>
-        {
-            task.Result.AddRangeUniqueToList(nameof(channels), channels);
-            return task.Result.SaveAsync(cancellationToken);
-        }).Unwrap();
+    public async Task SubscribeAsync(IEnumerable channels, IServiceHub serviceHub, CancellationToken cancellationToken = default)
+    {
+        var installation = await CurrentInstallationController.GetAsync(serviceHub, cancellationToken).ConfigureAwait(false);
+        installation.AddRangeUniqueToList(nameof(channels), channels);
+        await installation.SaveAsync(cancellationToken).ConfigureAwait(false);
+    }
 
-        public Task UnsubscribeAsync(IEnumerable channels, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CurrentInstallationController.GetAsync(serviceHub, cancellationToken).OnSuccess(task =>
-        {
-            task.Result.RemoveAllFromList(nameof(channels), channels);
-            return task.Result.SaveAsync(cancellationToken);
-        }).Unwrap();
+    public async Task UnsubscribeAsync(IEnumerable channels, IServiceHub serviceHub, CancellationToken cancellationToken = default)
+    {
+        var installation = await CurrentInstallationController.GetAsync(serviceHub, cancellationToken).ConfigureAwait(false);
+        installation.RemoveAllFromList(nameof(channels), channels);
+        await installation.SaveAsync(cancellationToken).ConfigureAwait(false);
     }
 }
+
diff --git a/Parse/Platform/Push/ParsePushController.cs b/Parse/Platform/Push/ParsePushController.cs
index ab407447..1ea0b3af 100644
--- a/Parse/Platform/Push/ParsePushController.cs
+++ b/Parse/Platform/Push/ParsePushController.cs
@@ -5,22 +5,31 @@
 using Parse.Abstractions.Platform.Push;
 using Parse.Abstractions.Platform.Users;
 using Parse.Infrastructure.Execution;
-using Parse.Infrastructure.Utilities;
 
-namespace Parse.Platform.Push
+namespace Parse.Platform.Push;
+internal class ParsePushController : IParsePushController
 {
-    internal class ParsePushController : IParsePushController
+    private IParseCommandRunner CommandRunner { get; }
+    private IParseCurrentUserController CurrentUserController { get; }
+
+    public ParsePushController(IParseCommandRunner commandRunner, IParseCurrentUserController currentUserController)
     {
-        IParseCommandRunner CommandRunner { get; }
+        CommandRunner = commandRunner;
+        CurrentUserController = currentUserController;
+    }
 
-        IParseCurrentUserController CurrentUserController { get; }
+    public async Task SendPushNotificationAsync(IPushState state, IServiceHub serviceHub, CancellationToken cancellationToken = default)
+    {
+        // Fetch the current session token
+        var sessionToken = await CurrentUserController.GetCurrentSessionTokenAsync(serviceHub, cancellationToken).ConfigureAwait(false);
 
-        public ParsePushController(IParseCommandRunner commandRunner, IParseCurrentUserController currentUserController)
-        {
-            CommandRunner = commandRunner;
-            CurrentUserController = currentUserController;
-        }
+        // Create the push command and execute it
+        var pushCommand = new ParseCommand(
+            "push",
+            method: "POST",
+            sessionToken: sessionToken,
+            data: ParsePushEncoder.Instance.Encode(state));
 
-        public Task SendPushNotificationAsync(IPushState state, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CurrentUserController.GetCurrentSessionTokenAsync(serviceHub, cancellationToken).OnSuccess(sessionTokenTask => CommandRunner.RunCommandAsync(new ParseCommand("push", method: "POST", sessionToken: sessionTokenTask.Result, data: ParsePushEncoder.Instance.Encode(state)), cancellationToken: cancellationToken)).Unwrap();
+        await CommandRunner.RunCommandAsync(pushCommand, cancellationToken: cancellationToken).ConfigureAwait(false);
     }
 }
diff --git a/Parse/Platform/Push/ParsePushEncoder.cs b/Parse/Platform/Push/ParsePushEncoder.cs
index 17fdb335..1463ee82 100644
--- a/Parse/Platform/Push/ParsePushEncoder.cs
+++ b/Parse/Platform/Push/ParsePushEncoder.cs
@@ -3,49 +3,50 @@
 using Parse.Abstractions.Platform.Push;
 using Parse.Infrastructure.Utilities;
 
-namespace Parse.Platform.Push
+namespace Parse.Platform.Push;
+
+public class ParsePushEncoder
 {
-    public class ParsePushEncoder
-    {
-        public static ParsePushEncoder Instance { get; } = new ParsePushEncoder { };
+    public static ParsePushEncoder Instance { get; } = new ParsePushEncoder { };
 
-        private ParsePushEncoder() { }
+    private ParsePushEncoder() { }
 
-        public IDictionary Encode(IPushState state)
-        {
-            if (state.Alert is null && state.Data is null)
-                throw new InvalidOperationException("A push must have either an Alert or Data");
+    public IDictionary Encode(IPushState state)
+    {
+        if (state.Alert is null && state.Data is null)
+            throw new InvalidOperationException("A push must have either an Alert or Data");
 
-            if (state.Channels is null && state.Query is null)
-                throw new InvalidOperationException("A push must have either Channels or a Query");
+        if (state.Channels is null && state.Query is null)
+            throw new InvalidOperationException("A push must have either Channels or a Query");
 
-            IDictionary data = state.Data ?? new Dictionary
-            {
-                ["alert"] = state.Alert
-            };
+        IDictionary data = state.Data ?? new Dictionary
+        {
+            ["alert"] = state.Alert
+        };
 
+#pragma warning disable CS1030 // #warning directive
 #warning Verify that it is fine to instantiate a ParseQuery here with a default(IServiceHub).
 
-            ParseQuery query = state.Query ?? new ParseQuery(default, "_Installation") { };
+        ParseQuery query = state.Query ?? new ParseQuery(default, "_Installation") { };
+#pragma warning restore CS1030 // #warning directive
 
-            if (state.Channels != null)
-                query = query.WhereContainedIn("channels", state.Channels);
+        if (state.Channels != null)
+            query = query.WhereContainedIn("channels", state.Channels);
 
-            Dictionary payload = new Dictionary
-            {
-                [nameof(data)] = data,
-                ["where"] = query.BuildParameters().GetOrDefault("where", new Dictionary { }),
-            };
+        Dictionary payload = new Dictionary
+        {
+            [nameof(data)] = data,
+            ["where"] = query.BuildParameters().GetOrDefault("where", new Dictionary { }),
+        };
 
-            if (state.Expiration.HasValue)
-                payload["expiration_time"] = state.Expiration.Value.ToString("yyyy-MM-ddTHH:mm:ssZ");
-            else if (state.ExpirationInterval.HasValue)
-                payload["expiration_interval"] = state.ExpirationInterval.Value.TotalSeconds;
+        if (state.Expiration.HasValue)
+            payload["expiration_time"] = state.Expiration.Value.ToString("yyyy-MM-ddTHH:mm:ssZ");
+        else if (state.ExpirationInterval.HasValue)
+            payload["expiration_interval"] = state.ExpirationInterval.Value.TotalSeconds;
 
-            if (state.PushTime.HasValue)
-                payload["push_time"] = state.PushTime.Value.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ");
+        if (state.PushTime.HasValue)
+            payload["push_time"] = state.PushTime.Value.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ");
 
-            return payload;
-        }
+        return payload;
     }
 }
diff --git a/Parse/Platform/Push/ParsePushNotificationEvent.cs b/Parse/Platform/Push/ParsePushNotificationEvent.cs
index a295779e..27583ce1 100644
--- a/Parse/Platform/Push/ParsePushNotificationEvent.cs
+++ b/Parse/Platform/Push/ParsePushNotificationEvent.cs
@@ -2,37 +2,36 @@
 using System.Collections.Generic;
 using Parse.Infrastructure.Utilities;
 
-namespace Parse.Platform.Push
+namespace Parse.Platform.Push;
+
+/// 
+/// A wrapper around Parse push notification payload.
+/// 
+public class ParsePushNotificationEvent : EventArgs
 {
-    /// 
-    /// A wrapper around Parse push notification payload.
-    /// 
-    public class ParsePushNotificationEvent : EventArgs
+    internal ParsePushNotificationEvent(IDictionary content)
     {
-        internal ParsePushNotificationEvent(IDictionary content)
-        {
-            Content = content;
-            TextContent = JsonUtilities.Encode(content);
-        }
+        Content = content;
+        TextContent = JsonUtilities.Encode(content);
+    }
 
-        // TODO: (richardross) investigate this.
-        // Obj-C type -> .NET type is impossible to do flawlessly (especially
-        // on NSNumber). We can't transform NSDictionary into string because of this reason.
+    // TODO: (richardross) investigate this.
+    // Obj-C type -> .NET type is impossible to do flawlessly (especially
+    // on NSNumber). We can't transform NSDictionary into string because of this reason.
 
-        internal ParsePushNotificationEvent(string stringPayload)
-        {
-            TextContent = stringPayload;
-            Content = JsonUtilities.Parse(stringPayload) as IDictionary;
-        }
+    internal ParsePushNotificationEvent(string stringPayload)
+    {
+        TextContent = stringPayload;
+        Content = JsonUtilities.Parse(stringPayload) as IDictionary;
+    }
 
-        /// 
-        /// The payload of the push notification as IDictionary.
-        /// 
-        public IDictionary Content { get; internal set; }
+    /// 
+    /// The payload of the push notification as IDictionary.
+    /// 
+    public IDictionary Content { get; internal set; }
 
-        /// 
-        /// The payload of the push notification as string.
-        /// 
-        public string TextContent { get; internal set; }
-    }
+    /// 
+    /// The payload of the push notification as string.
+    /// 
+    public string TextContent { get; internal set; }
 }
diff --git a/Parse/Platform/Queries/ParseQuery.cs b/Parse/Platform/Queries/ParseQuery.cs
index 77f6f659..4587c85b 100644
--- a/Parse/Platform/Queries/ParseQuery.cs
+++ b/Parse/Platform/Queries/ParseQuery.cs
@@ -6,443 +6,522 @@
 using System.Threading;
 using System.Threading.Tasks;
 using Parse.Abstractions.Infrastructure;
-using Parse.Abstractions.Platform.Objects;
 using Parse.Infrastructure;
 using Parse.Infrastructure.Data;
 using Parse.Infrastructure.Utilities;
 
-namespace Parse
+namespace Parse;
+
+/// 
+/// The ParseQuery class defines a query that is used to fetch ParseObjects. The
+/// most common use case is finding all objects that match a query through the
+///  method.
+/// 
+/// 
+/// This sample code fetches all objects of
+/// class "MyClass":
+///
+/// 
+/// ParseQuery query = new ParseQuery("MyClass");
+/// IEnumerable<ParseObject> result = await query.FindAsync();
+/// 
+///
+/// A ParseQuery can also be used to retrieve a single object whose id is known,
+/// through the  method. For example, this sample code
+/// fetches an object of class "MyClass" and id myId.
+///
+/// 
+/// ParseQuery query = new ParseQuery("MyClass");
+/// ParseObject result = await query.GetAsync(myId);
+/// 
+///
+/// A ParseQuery can also be used to count the number of objects that match the
+/// query without retrieving all of those objects. For example, this sample code
+/// counts the number of objects of the class "MyClass".
+///
+/// 
+/// ParseQuery query = new ParseQuery("MyClass");
+/// int count = await query.CountAsync();
+/// 
+/// 
+public class ParseQuery where T : ParseObject
 {
     /// 
-    /// The ParseQuery class defines a query that is used to fetch ParseObjects. The
-    /// most common use case is finding all objects that match a query through the
-    ///  method.
+    /// Serialized  clauses.
     /// 
-    /// 
-    /// This sample code fetches all objects of
-    /// class "MyClass":
-    ///
-    /// 
-    /// ParseQuery query = new ParseQuery("MyClass");
-    /// IEnumerable<ParseObject> result = await query.FindAsync();
-    /// 
-    ///
-    /// A ParseQuery can also be used to retrieve a single object whose id is known,
-    /// through the  method. For example, this sample code
-    /// fetches an object of class "MyClass" and id myId.
-    ///
-    /// 
-    /// ParseQuery query = new ParseQuery("MyClass");
-    /// ParseObject result = await query.GetAsync(myId);
-    /// 
-    ///
-    /// A ParseQuery can also be used to count the number of objects that match the
-    /// query without retrieving all of those objects. For example, this sample code
-    /// counts the number of objects of the class "MyClass".
-    ///
-    /// 
-    /// ParseQuery query = new ParseQuery("MyClass");
-    /// int count = await query.CountAsync();
-    /// 
-    /// 
-    public class ParseQuery where T : ParseObject
-    {
-        /// 
-        /// Serialized  clauses.
-        /// 
-        Dictionary Filters { get; }
+    Dictionary Filters { get; }
 
-        /// 
-        /// Serialized  clauses.
-        /// 
-        ReadOnlyCollection Orderings { get; }
+    /// 
+    /// Serialized  clauses.
+    /// 
+    ReadOnlyCollection Orderings { get; }
 
-        /// 
-        /// Serialized related data query merging request (data inclusion) clauses.
-        /// 
-        ReadOnlyCollection Includes { get; }
+    /// 
+    /// Serialized related data query merging request (data inclusion) clauses.
+    /// 
+    ReadOnlyCollection Includes { get; }
 
-        /// 
-        /// Serialized key selections.
-        /// 
-        ReadOnlyCollection KeySelections { get; }
+    /// 
+    /// Serialized key selections.
+    /// 
+    ReadOnlyCollection KeySelections { get; }
 
-        string RedirectClassNameForKey { get; }
+    string RedirectClassNameForKey { get; }
 
-        int? SkipAmount { get; }
+    int? SkipAmount { get; }
 
-        int? LimitAmount { get; }
+    int? LimitAmount { get; }
 
-        internal string ClassName { get; }
+    internal string ClassName { get; }
 
-        internal IServiceHub Services { get; }
+    internal IServiceHub Services { get; }
 
-        /// 
-        /// Private constructor for composition of queries. A source query is required,
-        /// but the remaining values can be null if they won't be changed in this
-        /// composition.
-        /// 
-        internal ParseQuery(ParseQuery source, IDictionary where = null, IEnumerable replacementOrderBy = null, IEnumerable thenBy = null, int? skip = null, int? limit = null, IEnumerable includes = null, IEnumerable selectedKeys = null, string redirectClassNameForKey = null)
+    /// 
+    /// Private constructor for composition of queries. A source query is required,
+    /// but the remaining values can be null if they won't be changed in this
+    /// composition.
+    /// 
+    internal ParseQuery(ParseQuery source, IDictionary where = null, IEnumerable replacementOrderBy = null, IEnumerable thenBy = null, int? skip = null, int? limit = null, IEnumerable includes = null, IEnumerable selectedKeys = null, string redirectClassNameForKey = null)
+    {
+        if (source == null)
         {
-            if (source == null)
-            {
-                throw new ArgumentNullException(nameof(source));
-            }
+            throw new ArgumentNullException(nameof(source));
+        }
 
-            Services = source.Services;
-            ClassName = source.ClassName;
-            Filters = source.Filters;
-            Orderings = replacementOrderBy is null ? source.Orderings : new ReadOnlyCollection(replacementOrderBy.ToList());
+        Services = source.Services;
+        ClassName = source.ClassName;
+        Filters = source.Filters;
+        Orderings = replacementOrderBy is null ? source.Orderings : new ReadOnlyCollection(replacementOrderBy.ToList());
 
-            // 0 could be handled differently from null.
+        // 0 could be handled differently from null.
 
-            SkipAmount = skip is null ? source.SkipAmount : (source.SkipAmount ?? 0) + skip;
-            LimitAmount = limit ?? source.LimitAmount;
-            Includes = source.Includes;
-            KeySelections = source.KeySelections;
-            RedirectClassNameForKey = redirectClassNameForKey ?? source.RedirectClassNameForKey;
+        SkipAmount = skip is null ? source.SkipAmount : (source.SkipAmount ?? 0) + skip;
+        LimitAmount = limit ?? source.LimitAmount;
+        Includes = source.Includes;
+        KeySelections = source.KeySelections;
+        RedirectClassNameForKey = redirectClassNameForKey ?? source.RedirectClassNameForKey;
 
-            if (thenBy is { })
-            {
-                List newOrderBy = new List(Orderings ?? throw new ArgumentException("You must call OrderBy before calling ThenBy."));
-
-                newOrderBy.AddRange(thenBy);
-                Orderings = new ReadOnlyCollection(newOrderBy);
-            }
+        if (thenBy is { })
+        {
+            List newOrderBy = new List(Orderings ?? throw new ArgumentException("You must call OrderBy before calling ThenBy."));
 
-            // Remove duplicates.
+            newOrderBy.AddRange(thenBy);
+            Orderings = new ReadOnlyCollection(newOrderBy);
+        }
 
-            if (Orderings is { })
-            {
-                Orderings = new ReadOnlyCollection(new HashSet(Orderings).ToList());
-            }
+        // Remove duplicates.
 
-            if (where is { })
-            {
-                Filters = new Dictionary(MergeWhereClauses(where));
-            }
+        if (Orderings is { })
+        {
+            Orderings = new ReadOnlyCollection(new HashSet(Orderings).ToList());
+        }
 
-            if (includes is { })
-            {
-                Includes = new ReadOnlyCollection(MergeIncludes(includes).ToList());
-            }
+        if (where is { })
+        {
+            Filters = new Dictionary(MergeWhereClauses(where));
+        }
 
-            if (selectedKeys is { })
-            {
-                KeySelections = new ReadOnlyCollection(MergeSelectedKeys(selectedKeys).ToList());
-            }
+        if (includes is { })
+        {
+            Includes = new ReadOnlyCollection(MergeIncludes(includes).ToList());
         }
 
-        HashSet MergeIncludes(IEnumerable includes)
+        if (selectedKeys is { })
         {
-            if (Includes is null)
-            {
-                return new HashSet(includes);
-            }
+            KeySelections = new ReadOnlyCollection(MergeSelectedKeys(selectedKeys).ToList());
+        }
+    }
 
-            HashSet newIncludes = new HashSet(Includes);
+    HashSet MergeIncludes(IEnumerable includes)
+    {
+        if (Includes is null)
+        {
+            return new HashSet(includes);
+        }
 
-            foreach (string item in includes)
-            {
-                newIncludes.Add(item);
-            }
+        HashSet newIncludes = new HashSet(Includes);
 
-            return newIncludes;
+        foreach (string item in includes)
+        {
+            newIncludes.Add(item);
         }
 
-        HashSet MergeSelectedKeys(IEnumerable selectedKeys) => new HashSet((KeySelections ?? Enumerable.Empty()).Concat(selectedKeys));
+        return newIncludes;
+    }
 
-        IDictionary MergeWhereClauses(IDictionary where)
+    HashSet MergeSelectedKeys(IEnumerable selectedKeys)
+    {
+        return new HashSet((KeySelections ?? Enumerable.Empty()).Concat(selectedKeys));
+    }
+
+    IDictionary MergeWhereClauses(IDictionary where)
+    {
+        if (Filters is null)
         {
-            if (Filters is null)
-            {
-                return where;
-            }
+            return where;
+        }
 
-            Dictionary newWhere = new Dictionary(Filters);
-            foreach (KeyValuePair pair in where)
+        Dictionary newWhere = new Dictionary(Filters);
+        foreach (KeyValuePair pair in where)
+        {
+            if (newWhere.ContainsKey(pair.Key))
             {
-                if (newWhere.ContainsKey(pair.Key))
+                if (!(newWhere[pair.Key] is IDictionary oldCondition) || !(pair.Value is IDictionary condition))
                 {
-                    if (!(newWhere[pair.Key] is IDictionary oldCondition) || !(pair.Value is IDictionary condition))
-                    {
-                        throw new ArgumentException("More than one where clause for the given key provided.");
-                    }
+                    throw new ArgumentException("More than one where clause for the given key provided.");
+                }
 
-                    Dictionary newCondition = new Dictionary(oldCondition);
-                    foreach (KeyValuePair conditionPair in condition)
+                Dictionary newCondition = new Dictionary(oldCondition);
+                foreach (KeyValuePair conditionPair in condition)
+                {
+                    if (newCondition.ContainsKey(conditionPair.Key))
                     {
-                        if (newCondition.ContainsKey(conditionPair.Key))
-                        {
-                            throw new ArgumentException("More than one condition for the given key provided.");
-                        }
-
-                        newCondition[conditionPair.Key] = conditionPair.Value;
+                        throw new ArgumentException("More than one condition for the given key provided.");
                     }
 
-                    newWhere[pair.Key] = newCondition;
-                }
-                else
-                {
-                    newWhere[pair.Key] = pair.Value;
+                    newCondition[conditionPair.Key] = conditionPair.Value;
                 }
+
+                newWhere[pair.Key] = newCondition;
+            }
+            else
+            {
+                newWhere[pair.Key] = pair.Value;
             }
-            return newWhere;
         }
+        return newWhere;
+    }
+
+    /// 
+    /// Constructs a query based upon the ParseObject subclass used as the generic parameter for the ParseQuery.
+    /// 
+    public ParseQuery(IServiceHub serviceHub) : this(serviceHub, serviceHub.ClassController.GetClassName(typeof(T))) { }
+
+    /// 
+    /// Constructs a query. A default query with no further parameters will retrieve
+    /// all s of the provided class.
+    /// 
+    /// The name of the class to retrieve ParseObjects for.
+    public ParseQuery(IServiceHub serviceHub, string className) => (ClassName, Services) = (className ?? throw new ArgumentNullException(nameof(className), "Must specify a ParseObject class name when creating a ParseQuery."), serviceHub);
+
+    #region Order By
+
+    /// 
+    /// Sorts the results in ascending order by the given key.
+    /// This will override any existing ordering for the query.
+    /// 
+    /// The key to order by.
+    /// A new query with the additional constraint.
+    public ParseQuery OrderBy(string key)
+    {
+        return new ParseQuery(this, replacementOrderBy: new List { key });
+    }
+
+    /// 
+    /// Sorts the results in descending order by the given key.
+    /// This will override any existing ordering for the query.
+    /// 
+    /// The key to order by.
+    /// A new query with the additional constraint.
+    public ParseQuery OrderByDescending(string key)
+    {
+        return new ParseQuery(this, replacementOrderBy: new List { "-" + key });
+    }
+
+    /// 
+    /// Sorts the results in ascending order by the given key, after previous
+    /// ordering has been applied.
+    ///
+    /// This method can only be called if there is already an 
+    /// or 
+    /// on this query.
+    /// 
+    /// The key to order by.
+    /// A new query with the additional constraint.
+    public ParseQuery ThenBy(string key)
+    {
+        return new ParseQuery(this, thenBy: new List { key });
+    }
+
+    /// 
+    /// Sorts the results in descending order by the given key, after previous
+    /// ordering has been applied.
+    ///
+    /// This method can only be called if there is already an 
+    /// or  on this query.
+    /// 
+    /// The key to order by.
+    /// A new query with the additional constraint.
+    public ParseQuery ThenByDescending(string key)
+    {
+        return new ParseQuery(this, thenBy: new List { "-" + key });
+    }
+
+    #endregion
+
+    /// 
+    /// Include nested ParseObjects for the provided key. You can use dot notation
+    /// to specify which fields in the included objects should also be fetched.
+    /// 
+    /// The key that should be included.
+    /// A new query with the additional constraint.
+    public ParseQuery Include(string key)
+    {
+        return new ParseQuery(this, includes: new List { key });
+    }
+
+    /// 
+    /// Restrict the fields of returned ParseObjects to only include the provided key.
+    /// If this is called multiple times, then all of the keys specified in each of
+    /// the calls will be included.
+    /// 
+    /// The key that should be included.
+    /// A new query with the additional constraint.
+    public ParseQuery Select(string key)
+    {
+        return new ParseQuery(this, selectedKeys: new List { key });
+    }
+
+    /// 
+    /// Skips a number of results before returning. This is useful for pagination
+    /// of large queries. Chaining multiple skips together will cause more results
+    /// to be skipped.
+    /// 
+    /// The number of results to skip.
+    /// A new query with the additional constraint.
+    public ParseQuery Skip(int count)
+    {
+        return new ParseQuery(this, skip: count);
+    }
+
+    /// 
+    /// Controls the maximum number of results that are returned. Setting a negative
+    /// limit denotes retrieval without a limit. Chaining multiple limits
+    /// results in the last limit specified being used. The default limit is
+    /// 100, with a maximum of 1000 results being returned at a time.
+    /// 
+    /// The maximum number of results to return.
+    /// A new query with the additional constraint.
+    public ParseQuery Limit(int count)
+    {
+        return new ParseQuery(this, limit: count);
+    }
+
+    internal ParseQuery RedirectClassName(string key)
+    {
+        return new ParseQuery(this, redirectClassNameForKey: key);
+    }
+
+    #region Where
+
+    /// 
+    /// Adds a constraint to the query that requires a particular key's value to be
+    /// contained in the provided list of values.
+    /// 
+    /// The key to check.
+    /// The values that will match.
+    /// A new query with the additional constraint.
+    public ParseQuery WhereContainedIn(string key, IEnumerable values)
+    {
+        return new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$in", values.ToList() } } } });
+    }
 
-        /// 
-        /// Constructs a query based upon the ParseObject subclass used as the generic parameter for the ParseQuery.
-        /// 
-        public ParseQuery(IServiceHub serviceHub) : this(serviceHub, serviceHub.ClassController.GetClassName(typeof(T))) { }
-
-        /// 
-        /// Constructs a query. A default query with no further parameters will retrieve
-        /// all s of the provided class.
-        /// 
-        /// The name of the class to retrieve ParseObjects for.
-        public ParseQuery(IServiceHub serviceHub, string className) => (ClassName, Services) = (className ?? throw new ArgumentNullException(nameof(className), "Must specify a ParseObject class name when creating a ParseQuery."), serviceHub);
-
-        #region Order By
-
-        /// 
-        /// Sorts the results in ascending order by the given key.
-        /// This will override any existing ordering for the query.
-        /// 
-        /// The key to order by.
-        /// A new query with the additional constraint.
-        public ParseQuery OrderBy(string key) => new ParseQuery(this, replacementOrderBy: new List { key });
-
-        /// 
-        /// Sorts the results in descending order by the given key.
-        /// This will override any existing ordering for the query.
-        /// 
-        /// The key to order by.
-        /// A new query with the additional constraint.
-        public ParseQuery OrderByDescending(string key) => new ParseQuery(this, replacementOrderBy: new List { "-" + key });
-
-        /// 
-        /// Sorts the results in ascending order by the given key, after previous
-        /// ordering has been applied.
-        ///
-        /// This method can only be called if there is already an 
-        /// or 
-        /// on this query.
-        /// 
-        /// The key to order by.
-        /// A new query with the additional constraint.
-        public ParseQuery ThenBy(string key) => new ParseQuery(this, thenBy: new List { key });
-
-        /// 
-        /// Sorts the results in descending order by the given key, after previous
-        /// ordering has been applied.
-        ///
-        /// This method can only be called if there is already an 
-        /// or  on this query.
-        /// 
-        /// The key to order by.
-        /// A new query with the additional constraint.
-        public ParseQuery ThenByDescending(string key) => new ParseQuery(this, thenBy: new List { "-" + key });
-
-        #endregion
-
-        /// 
-        /// Include nested ParseObjects for the provided key. You can use dot notation
-        /// to specify which fields in the included objects should also be fetched.
-        /// 
-        /// The key that should be included.
-        /// A new query with the additional constraint.
-        public ParseQuery Include(string key) => new ParseQuery(this, includes: new List { key });
-
-        /// 
-        /// Restrict the fields of returned ParseObjects to only include the provided key.
-        /// If this is called multiple times, then all of the keys specified in each of
-        /// the calls will be included.
-        /// 
-        /// The key that should be included.
-        /// A new query with the additional constraint.
-        public ParseQuery Select(string key) => new ParseQuery(this, selectedKeys: new List { key });
-
-        /// 
-        /// Skips a number of results before returning. This is useful for pagination
-        /// of large queries. Chaining multiple skips together will cause more results
-        /// to be skipped.
-        /// 
-        /// The number of results to skip.
-        /// A new query with the additional constraint.
-        public ParseQuery Skip(int count) => new ParseQuery(this, skip: count);
-
-        /// 
-        /// Controls the maximum number of results that are returned. Setting a negative
-        /// limit denotes retrieval without a limit. Chaining multiple limits
-        /// results in the last limit specified being used. The default limit is
-        /// 100, with a maximum of 1000 results being returned at a time.
-        /// 
-        /// The maximum number of results to return.
-        /// A new query with the additional constraint.
-        public ParseQuery Limit(int count) => new ParseQuery(this, limit: count);
-
-        internal ParseQuery RedirectClassName(string key) => new ParseQuery(this, redirectClassNameForKey: key);
-
-        #region Where
-
-        /// 
-        /// Adds a constraint to the query that requires a particular key's value to be
-        /// contained in the provided list of values.
-        /// 
-        /// The key to check.
-        /// The values that will match.
-        /// A new query with the additional constraint.
-        public ParseQuery WhereContainedIn(string key, IEnumerable values) => new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$in", values.ToList() } } } });
-
-        /// 
-        /// Add a constraint to the querey that requires a particular key's value to be
-        /// a list containing all of the elements in the provided list of values.
-        /// 
-        /// The key to check.
-        /// The values that will match.
-        /// A new query with the additional constraint.
-        public ParseQuery WhereContainsAll(string key, IEnumerable values) => new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$all", values.ToList() } } } });
-
-        /// 
-        /// Adds a constraint for finding string values that contain a provided string.
-        /// This will be slow for large data sets.
-        /// 
-        /// The key that the string to match is stored in.
-        /// The substring that the value must contain.
-        /// A new query with the additional constraint.
-        public ParseQuery WhereContains(string key, string substring) => new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$regex", RegexQuote(substring) } } } });
-
-        /// 
-        /// Adds a constraint for finding objects that do not contain a given key.
-        /// 
-        /// The key that should not exist.
-        /// A new query with the additional constraint.
-        public ParseQuery WhereDoesNotExist(string key) => new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$exists", false } } } });
-
-        /// 
-        /// Adds a constraint to the query that requires that a particular key's value
-        /// does not match another ParseQuery. This only works on keys whose values are
-        /// ParseObjects or lists of ParseObjects.
-        /// 
-        /// The key to check.
-        /// The query that the value should not match.
-        /// A new query with the additional constraint.
-        public ParseQuery WhereDoesNotMatchQuery(string key, ParseQuery query) where TOther : ParseObject => new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$notInQuery", query.BuildParameters(true) } } } });
-
-        /// 
-        /// Adds a constraint for finding string values that end with a provided string.
-        /// This will be slow for large data sets.
-        /// 
-        /// The key that the string to match is stored in.
-        /// The substring that the value must end with.
-        /// A new query with the additional constraint.
-        public ParseQuery WhereEndsWith(string key, string suffix) => new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$regex", RegexQuote(suffix) + "$" } } } });
-
-        /// 
-        /// Adds a constraint to the query that requires a particular key's value to be
-        /// equal to the provided value.
-        /// 
-        /// The key to check.
-        /// The value that the ParseObject must contain.
-        /// A new query with the additional constraint.
-        public ParseQuery WhereEqualTo(string key, object value) => new ParseQuery(this, where: new Dictionary { { key, value } });
-
-        /// 
-        /// Adds a constraint for finding objects that contain a given key.
-        /// 
-        /// The key that should exist.
-        /// A new query with the additional constraint.
-        public ParseQuery WhereExists(string key) => new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$exists", true } } } });
-
-        /// 
-        /// Adds a constraint to the query that requires a particular key's value to be
-        /// greater than the provided value.
-        /// 
-        /// The key to check.
-        /// The value that provides a lower bound.
-        /// A new query with the additional constraint.
-        public ParseQuery WhereGreaterThan(string key, object value) => new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$gt", value } } } });
-
-        /// 
-        /// Adds a constraint to the query that requires a particular key's value to be
-        /// greater or equal to than the provided value.
-        /// 
-        /// The key to check.
-        /// The value that provides a lower bound.
-        /// A new query with the additional constraint.
-        public ParseQuery WhereGreaterThanOrEqualTo(string key, object value) => new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$gte", value } } } });
-
-        /// 
-        /// Adds a constraint to the query that requires a particular key's value to be
-        /// less than the provided value.
-        /// 
-        /// The key to check.
-        /// The value that provides an upper bound.
-        /// A new query with the additional constraint.
-        public ParseQuery WhereLessThan(string key, object value) => new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$lt", value } } } });
-
-        /// 
-        /// Adds a constraint to the query that requires a particular key's value to be
-        /// less than or equal to the provided value.
-        /// 
-        /// The key to check.
-        /// The value that provides a lower bound.
-        /// A new query with the additional constraint.
-        public ParseQuery WhereLessThanOrEqualTo(string key, object value) => new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$lte", value } } } });
-
-        /// 
-        /// Adds a regular expression constraint for finding string values that match the provided
-        /// regular expression. This may be slow for large data sets.
-        /// 
-        /// The key that the string to match is stored in.
-        /// The regular expression pattern to match. The Regex must
-        /// have the  options flag set.
-        /// Any of the following supported PCRE modifiers:
-        /// i - Case insensitive search
-        /// m Search across multiple lines of input
-        /// A new query with the additional constraint.
-        public ParseQuery WhereMatches(string key, Regex regex, string modifiers) => !regex.Options.HasFlag(RegexOptions.ECMAScript) ? throw new ArgumentException("Only ECMAScript-compatible regexes are supported. Please use the ECMAScript RegexOptions flag when creating your regex.") : new ParseQuery(this, where: new Dictionary { { key, EncodeRegex(regex, modifiers) } });
-
-        /// 
-        /// Adds a regular expression constraint for finding string values that match the provided
-        /// regular expression. This may be slow for large data sets.
-        /// 
-        /// The key that the string to match is stored in.
-        /// The regular expression pattern to match. The Regex must
-        /// have the  options flag set.
-        /// A new query with the additional constraint.
-        public ParseQuery WhereMatches(string key, Regex regex) => WhereMatches(key, regex, null);
-
-        /// 
-        /// Adds a regular expression constraint for finding string values that match the provided
-        /// regular expression. This may be slow for large data sets.
-        /// 
-        /// The key that the string to match is stored in.
-        /// The PCRE regular expression pattern to match.
-        /// Any of the following supported PCRE modifiers:
-        /// i - Case insensitive search
-        /// m Search across multiple lines of input
-        /// A new query with the additional constraint.
-        public ParseQuery WhereMatches(string key, string pattern, string modifiers = null) => WhereMatches(key, new Regex(pattern, RegexOptions.ECMAScript), modifiers);
-
-        /// 
-        /// Adds a regular expression constraint for finding string values that match the provided
-        /// regular expression. This may be slow for large data sets.
-        /// 
-        /// The key that the string to match is stored in.
-        /// The PCRE regular expression pattern to match.
-        /// A new query with the additional constraint.
-        public ParseQuery WhereMatches(string key, string pattern) => WhereMatches(key, pattern, null);
-
-        /// 
-        /// Adds a constraint to the query that requires a particular key's value
-        /// to match a value for a key in the results of another ParseQuery.
-        /// 
-        /// The key whose value is being checked.
-        /// The key in the objects from the subquery to look in.
-        /// The subquery to run
-        /// A new query with the additional constraint.
-        public ParseQuery WhereMatchesKeyInQuery(string key, string keyInQuery, ParseQuery query) where TOther : ParseObject => new ParseQuery(this, where: new Dictionary
+    /// 
+    /// Add a constraint to the querey that requires a particular key's value to be
+    /// a list containing all of the elements in the provided list of values.
+    /// 
+    /// The key to check.
+    /// The values that will match.
+    /// A new query with the additional constraint.
+    public ParseQuery WhereContainsAll(string key, IEnumerable values)
+    {
+        return new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$all", values.ToList() } } } });
+    }
+
+    /// 
+    /// Adds a constraint for finding string values that contain a provided string.
+    /// This will be slow for large data sets.
+    /// 
+    /// The key that the string to match is stored in.
+    /// The substring that the value must contain.
+    /// A new query with the additional constraint.
+    public ParseQuery WhereContains(string key, string substring)
+    {
+        return new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$regex", RegexQuote(substring) } } } });
+    }
+
+    /// 
+    /// Adds a constraint for finding objects that do not contain a given key.
+    /// 
+    /// The key that should not exist.
+    /// A new query with the additional constraint.
+    public ParseQuery WhereDoesNotExist(string key)
+    {
+        return new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$exists", false } } } });
+    }
+
+    /// 
+    /// Adds a constraint to the query that requires that a particular key's value
+    /// does not match another ParseQuery. This only works on keys whose values are
+    /// ParseObjects or lists of ParseObjects.
+    /// 
+    /// The key to check.
+    /// The query that the value should not match.
+    /// A new query with the additional constraint.
+    public ParseQuery WhereDoesNotMatchQuery(string key, ParseQuery query) where TOther : ParseObject
+    {
+        return new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$notInQuery", query.BuildParameters(true) } } } });
+    }
+
+    /// 
+    /// Adds a constraint for finding string values that end with a provided string.
+    /// This will be slow for large data sets.
+    /// 
+    /// The key that the string to match is stored in.
+    /// The substring that the value must end with.
+    /// A new query with the additional constraint.
+    public ParseQuery WhereEndsWith(string key, string suffix)
+    {
+        return new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$regex", RegexQuote(suffix) + "$" } } } });
+    }
+
+    /// 
+    /// Adds a constraint to the query that requires a particular key's value to be
+    /// equal to the provided value.
+    /// 
+    /// The key to check.
+    /// The value that the ParseObject must contain.
+    /// A new query with the additional constraint.
+    public ParseQuery WhereEqualTo(string key, object value)
+    {
+        return new ParseQuery(this, where: new Dictionary { { key, value } });
+    }
+
+    /// 
+    /// Adds a constraint for finding objects that contain a given key.
+    /// 
+    /// The key that should exist.
+    /// A new query with the additional constraint.
+    public ParseQuery WhereExists(string key)
+    {
+        return new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$exists", true } } } });
+    }
+
+    /// 
+    /// Adds a constraint to the query that requires a particular key's value to be
+    /// greater than the provided value.
+    /// 
+    /// The key to check.
+    /// The value that provides a lower bound.
+    /// A new query with the additional constraint.
+    public ParseQuery WhereGreaterThan(string key, object value)
+    {
+        return new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$gt", value } } } });
+    }
+
+    /// 
+    /// Adds a constraint to the query that requires a particular key's value to be
+    /// greater or equal to than the provided value.
+    /// 
+    /// The key to check.
+    /// The value that provides a lower bound.
+    /// A new query with the additional constraint.
+    public ParseQuery WhereGreaterThanOrEqualTo(string key, object value)
+    {
+        return new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$gte", value } } } });
+    }
+
+    /// 
+    /// Adds a constraint to the query that requires a particular key's value to be
+    /// less than the provided value.
+    /// 
+    /// The key to check.
+    /// The value that provides an upper bound.
+    /// A new query with the additional constraint.
+    public ParseQuery WhereLessThan(string key, object value)
+    {
+        return new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$lt", value } } } });
+    }
+
+    /// 
+    /// Adds a constraint to the query that requires a particular key's value to be
+    /// less than or equal to the provided value.
+    /// 
+    /// The key to check.
+    /// The value that provides a lower bound.
+    /// A new query with the additional constraint.
+    public ParseQuery WhereLessThanOrEqualTo(string key, object value)
+    {
+        return new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$lte", value } } } });
+    }
+
+    /// 
+    /// Adds a regular expression constraint for finding string values that match the provided
+    /// regular expression. This may be slow for large data sets.
+    /// 
+    /// The key that the string to match is stored in.
+    /// The regular expression pattern to match. The Regex must
+    /// have the  options flag set.
+    /// Any of the following supported PCRE modifiers:
+    /// i - Case insensitive search
+    /// m Search across multiple lines of input
+    /// A new query with the additional constraint.
+    public ParseQuery WhereMatches(string key, Regex regex, string modifiers)
+    {
+        return !regex.Options.HasFlag(RegexOptions.ECMAScript) ? throw new ArgumentException("Only ECMAScript-compatible regexes are supported. Please use the ECMAScript RegexOptions flag when creating your regex.") : new ParseQuery(this, where: new Dictionary { { key, EncodeRegex(regex, modifiers) } });
+    }
+
+    /// 
+    /// Adds a regular expression constraint for finding string values that match the provided
+    /// regular expression. This may be slow for large data sets.
+    /// 
+    /// The key that the string to match is stored in.
+    /// The regular expression pattern to match. The Regex must
+    /// have the  options flag set.
+    /// A new query with the additional constraint.
+    public ParseQuery WhereMatches(string key, Regex regex)
+    {
+        return WhereMatches(key, regex, null);
+    }
+
+    /// 
+    /// Adds a regular expression constraint for finding string values that match the provided
+    /// regular expression. This may be slow for large data sets.
+    /// 
+    /// The key that the string to match is stored in.
+    /// The PCRE regular expression pattern to match.
+    /// Any of the following supported PCRE modifiers:
+    /// i - Case insensitive search
+    /// m Search across multiple lines of input
+    /// A new query with the additional constraint.
+    public ParseQuery WhereMatches(string key, string pattern, string modifiers = null)
+    {
+        return WhereMatches(key, new Regex(pattern, RegexOptions.ECMAScript), modifiers);
+    }
+
+    /// 
+    /// Adds a regular expression constraint for finding string values that match the provided
+    /// regular expression. This may be slow for large data sets.
+    /// 
+    /// The key that the string to match is stored in.
+    /// The PCRE regular expression pattern to match.
+    /// A new query with the additional constraint.
+    public ParseQuery WhereMatches(string key, string pattern)
+    {
+        return WhereMatches(key, pattern, null);
+    }
+
+    /// 
+    /// Adds a constraint to the query that requires a particular key's value
+    /// to match a value for a key in the results of another ParseQuery.
+    /// 
+    /// The key whose value is being checked.
+    /// The key in the objects from the subquery to look in.
+    /// The subquery to run
+    /// A new query with the additional constraint.
+    public ParseQuery WhereMatchesKeyInQuery(string key, string keyInQuery, ParseQuery query) where TOther : ParseObject
+    {
+        return new ParseQuery(this, where: new Dictionary
         {
             [key] = new Dictionary
             {
@@ -453,16 +532,19 @@ public ParseQuery(IServiceHub serviceHub) : this(serviceHub, serviceHub.ClassCon
                 }
             }
         });
+    }
 
-        /// 
-        /// Adds a constraint to the query that requires a particular key's value
-        /// does not match any value for a key in the results of another ParseQuery.
-        /// 
-        /// The key whose value is being checked.
-        /// The key in the objects from the subquery to look in.
-        /// The subquery to run
-        /// A new query with the additional constraint.
-        public ParseQuery WhereDoesNotMatchesKeyInQuery(string key, string keyInQuery, ParseQuery query) where TOther : ParseObject => new ParseQuery(this, where: new Dictionary
+    /// 
+    /// Adds a constraint to the query that requires a particular key's value
+    /// does not match any value for a key in the results of another ParseQuery.
+    /// 
+    /// The key whose value is being checked.
+    /// The key in the objects from the subquery to look in.
+    /// The subquery to run
+    /// A new query with the additional constraint.
+    public ParseQuery WhereDoesNotMatchesKeyInQuery(string key, string keyInQuery, ParseQuery query) where TOther : ParseObject
+    {
+        return new ParseQuery(this, where: new Dictionary
         {
             [key] = new Dictionary
             {
@@ -473,123 +555,147 @@ public ParseQuery(IServiceHub serviceHub) : this(serviceHub, serviceHub.ClassCon
                 }
             }
         });
+    }
 
-        /// 
-        /// Adds a constraint to the query that requires that a particular key's value
-        /// matches another ParseQuery. This only works on keys whose values are
-        /// ParseObjects or lists of ParseObjects.
-        /// 
-        /// The key to check.
-        /// The query that the value should match.
-        /// A new query with the additional constraint.
-        public ParseQuery WhereMatchesQuery(string key, ParseQuery query) where TOther : ParseObject => new ParseQuery(this, where: new Dictionary
+    /// 
+    /// Adds a constraint to the query that requires that a particular key's value
+    /// matches another ParseQuery. This only works on keys whose values are
+    /// ParseObjects or lists of ParseObjects.
+    /// 
+    /// The key to check.
+    /// The query that the value should match.
+    /// A new query with the additional constraint.
+    public ParseQuery WhereMatchesQuery(string key, ParseQuery query) where TOther : ParseObject
+    {
+        return new ParseQuery(this, where: new Dictionary
         {
             [key] = new Dictionary
             {
                 ["$inQuery"] = query.BuildParameters(true)
             }
         });
+    }
 
-        /// 
-        /// Adds a proximity-based constraint for finding objects with keys whose GeoPoint
-        /// values are near the given point.
-        /// 
-        /// The key that the ParseGeoPoint is stored in.
-        /// The reference ParseGeoPoint.
-        /// A new query with the additional constraint.
-        public ParseQuery WhereNear(string key, ParseGeoPoint point) => new ParseQuery(this, where: new Dictionary
+    /// 
+    /// Adds a proximity-based constraint for finding objects with keys whose GeoPoint
+    /// values are near the given point.
+    /// 
+    /// The key that the ParseGeoPoint is stored in.
+    /// The reference ParseGeoPoint.
+    /// A new query with the additional constraint.
+    public ParseQuery WhereNear(string key, ParseGeoPoint point)
+    {
+        return new ParseQuery(this, where: new Dictionary
         {
             [key] = new Dictionary
             {
                 ["$nearSphere"] = point
             }
         });
+    }
 
-        /// 
-        /// Adds a constraint to the query that requires a particular key's value to be
-        /// contained in the provided list of values.
-        /// 
-        /// The key to check.
-        /// The values that will match.
-        /// A new query with the additional constraint.
-        public ParseQuery WhereNotContainedIn(string key, IEnumerable values) => new ParseQuery(this, where: new Dictionary
+    /// 
+    /// Adds a constraint to the query that requires a particular key's value to be
+    /// contained in the provided list of values.
+    /// 
+    /// The key to check.
+    /// The values that will match.
+    /// A new query with the additional constraint.
+    public ParseQuery WhereNotContainedIn(string key, IEnumerable values)
+    {
+        return new ParseQuery(this, where: new Dictionary
         {
             [key] = new Dictionary
             {
                 ["$nin"] = values.ToList()
             }
         });
+    }
 
-        /// 
-        /// Adds a constraint to the query that requires a particular key's value not
-        /// to be equal to the provided value.
-        /// 
-        /// The key to check.
-        /// The value that that must not be equalled.
-        /// A new query with the additional constraint.
-        public ParseQuery WhereNotEqualTo(string key, object value) => new ParseQuery(this, where: new Dictionary
+    /// 
+    /// Adds a constraint to the query that requires a particular key's value not
+    /// to be equal to the provided value.
+    /// 
+    /// The key to check.
+    /// The value that that must not be equalled.
+    /// A new query with the additional constraint.
+    public ParseQuery WhereNotEqualTo(string key, object value)
+    {
+        return new ParseQuery(this, where: new Dictionary
         {
             [key] = new Dictionary
             {
                 ["$ne"] = value
             }
         });
+    }
 
-        /// 
-        /// Adds a constraint for finding string values that start with the provided string.
-        /// This query will use the backend index, so it will be fast even with large data sets.
-        /// 
-        /// The key that the string to match is stored in.
-        /// The substring that the value must start with.
-        /// A new query with the additional constraint.
-        public ParseQuery WhereStartsWith(string key, string suffix) => new ParseQuery(this, where: new Dictionary
+    /// 
+    /// Adds a constraint for finding string values that start with the provided string.
+    /// This query will use the backend index, so it will be fast even with large data sets.
+    /// 
+    /// The key that the string to match is stored in.
+    /// The substring that the value must start with.
+    /// A new query with the additional constraint.
+    public ParseQuery WhereStartsWith(string key, string suffix)
+    {
+        return new ParseQuery(this, where: new Dictionary
         {
             [key] = new Dictionary
             {
                 ["$regex"] = $"^{RegexQuote(suffix)}"
             }
         });
+    }
 
-        /// 
-        /// Add a constraint to the query that requires a particular key's coordinates to be
-        /// contained within a given rectangular geographic bounding box.
-        /// 
-        /// The key to be constrained.
-        /// The lower-left inclusive corner of the box.
-        /// The upper-right inclusive corner of the box.
-        /// A new query with the additional constraint.
-        public ParseQuery WhereWithinGeoBox(string key, ParseGeoPoint southwest, ParseGeoPoint northeast) => new ParseQuery(this, where: new Dictionary
+    /// 
+    /// Add a constraint to the query that requires a particular key's coordinates to be
+    /// contained within a given rectangular geographic bounding box.
+    /// 
+    /// The key to be constrained.
+    /// The lower-left inclusive corner of the box.
+    /// The upper-right inclusive corner of the box.
+    /// A new query with the additional constraint.
+    public ParseQuery WhereWithinGeoBox(string key, ParseGeoPoint southwest, ParseGeoPoint northeast)
+    {
+        return new ParseQuery(this, where: new Dictionary
         {
             [key] = new Dictionary
             {
                 ["$within"] = new Dictionary
                 {
                     ["$box"] = new[]
-                    {
-                        southwest,
-                        northeast
-                    }
+                {
+                    southwest,
+                    northeast
+                }
                 }
             }
         });
+    }
 
-        /// 
-        /// Adds a proximity-based constraint for finding objects with keys whose GeoPoint
-        /// values are near the given point and within the maximum distance given.
-        /// 
-        /// The key that the ParseGeoPoint is stored in.
-        /// The reference ParseGeoPoint.
-        /// The maximum distance (in radians) of results to return.
-        /// A new query with the additional constraint.
-        public ParseQuery WhereWithinDistance(string key, ParseGeoPoint point, ParseGeoDistance maxDistance) => new ParseQuery(WhereNear(key, point), where: new Dictionary
+    /// 
+    /// Adds a proximity-based constraint for finding objects with keys whose GeoPoint
+    /// values are near the given point and within the maximum distance given.
+    /// 
+    /// The key that the ParseGeoPoint is stored in.
+    /// The reference ParseGeoPoint.
+    /// The maximum distance (in radians) of results to return.
+    /// A new query with the additional constraint.
+    public ParseQuery WhereWithinDistance(string key, ParseGeoPoint point, ParseGeoDistance maxDistance)
+    {
+        return new ParseQuery(WhereNear(key, point), where: new Dictionary
         {
             [key] = new Dictionary
             {
                 ["$maxDistance"] = maxDistance.Radians
             }
         });
+    }
 
-        internal ParseQuery WhereRelatedTo(ParseObject parent, string key) => new ParseQuery(this, where: new Dictionary
+    internal ParseQuery WhereRelatedTo(ParseObject parent, string key)
+    {
+        return new ParseQuery(this, where: new Dictionary
         {
             ["$relatedTo"] = new Dictionary
             {
@@ -597,169 +703,212 @@ public ParseQuery(IServiceHub serviceHub) : this(serviceHub, serviceHub.ClassCon
                 [nameof(key)] = key
             }
         });
+    }
 
-        #endregion
+    #endregion
 
-        /// 
-        /// Retrieves a list of ParseObjects that satisfy this query from Parse.
-        /// 
-        /// The list of ParseObjects that match this query.
-        public Task> FindAsync() => FindAsync(CancellationToken.None);
+    /// 
+    /// Retrieves a list of ParseObjects that satisfy this query from Parse.
+    /// 
+    /// The list of ParseObjects that match this query.
+    public Task> FindAsync()
+    {
+        return FindAsync(CancellationToken.None);
+    }
+    /// 
+    /// Retrieves a list of ParseObjects that satisfy this query from Parse.
+    /// 
+    /// The cancellation token.
+    /// The list of ParseObjects that match this query.
+    public async Task> FindAsync(CancellationToken cancellationToken)
+    {
+        EnsureNotInstallationQuery();
+        var result = await Services.QueryController.FindAsync(this, await Services.GetCurrentUser(), cancellationToken).ConfigureAwait(false);
+        return result.Select(state => Services.GenerateObjectFromState(state, ClassName));
+    }
 
-        /// 
-        /// Retrieves a list of ParseObjects that satisfy this query from Parse.
-        /// 
-        /// The cancellation token.
-        /// The list of ParseObjects that match this query.
-        public Task> FindAsync(CancellationToken cancellationToken)
-        {
-            EnsureNotInstallationQuery();
-            return Services.QueryController.FindAsync(this, Services.GetCurrentUser(), cancellationToken).OnSuccess(task => from state in task.Result select Services.GenerateObjectFromState(state, ClassName));
-        }
+    /// 
+    /// Retrieves at most one ParseObject that satisfies this query.
+    /// 
+    /// A single ParseObject that satisfies this query, or else null.
+    public Task FirstOrDefaultAsync()
+    {
+        return FirstOrDefaultAsync(CancellationToken.None);
+    }
 
-        /// 
-        /// Retrieves at most one ParseObject that satisfies this query.
-        /// 
-        /// A single ParseObject that satisfies this query, or else null.
-        public Task FirstOrDefaultAsync() => FirstOrDefaultAsync(CancellationToken.None);
-
-        /// 
-        /// Retrieves at most one ParseObject that satisfies this query.
-        /// 
-        /// The cancellation token.
-        /// A single ParseObject that satisfies this query, or else null.
-        public Task FirstOrDefaultAsync(CancellationToken cancellationToken)
-        {
-            EnsureNotInstallationQuery();
-            return Services.QueryController.FirstAsync(this, Services.GetCurrentUser(), cancellationToken).OnSuccess(task => task.Result is IObjectState state && state is { } ? Services.GenerateObjectFromState(state, ClassName) : default);
-        }
+    /// 
+    /// Retrieves at most one ParseObject that satisfies this query.
+    /// 
+    /// The cancellation token.
+    /// A single ParseObject that satisfies this query, or else null.
+    public async Task FirstOrDefaultAsync(CancellationToken cancellationToken)
+    {
+        EnsureNotInstallationQuery();
+        var result = await Services.QueryController.FirstAsync(this, await Services.GetCurrentUser(), cancellationToken).ConfigureAwait(false);
 
-        /// 
-        /// Retrieves at most one ParseObject that satisfies this query.
-        /// 
-        /// A single ParseObject that satisfies this query.
-        /// If no results match the query.
-        public Task FirstAsync() => FirstAsync(CancellationToken.None);
-
-        /// 
-        /// Retrieves at most one ParseObject that satisfies this query.
-        /// 
-        /// The cancellation token.
-        /// A single ParseObject that satisfies this query.
-        /// If no results match the query.
-        public Task FirstAsync(CancellationToken cancellationToken) => FirstOrDefaultAsync(cancellationToken).OnSuccess(task => task.Result ?? throw new ParseFailureException(ParseFailureException.ErrorCode.ObjectNotFound, "No results matched the query."));
-
-        /// 
-        /// Counts the number of objects that match this query.
-        /// 
-        /// The number of objects that match this query.
-        public Task CountAsync() => CountAsync(CancellationToken.None);
-
-        /// 
-        /// Counts the number of objects that match this query.
-        /// 
-        /// The cancellation token.
-        /// The number of objects that match this query.
-        public Task CountAsync(CancellationToken cancellationToken)
-        {
-            EnsureNotInstallationQuery();
-            return Services.QueryController.CountAsync(this, Services.GetCurrentUser(), cancellationToken);
-        }
+        return result != null
+            ? Services.GenerateObjectFromState(result, ClassName)
+            : default; // Return default value (null for reference types) if result is null
+    }
 
-        /// 
-        /// Constructs a ParseObject whose id is already known by fetching data
-        /// from the server.
-        /// 
-        /// ObjectId of the ParseObject to fetch.
-        /// The ParseObject for the given objectId.
-        public Task GetAsync(string objectId) => GetAsync(objectId, CancellationToken.None);
-
-        /// 
-        /// Constructs a ParseObject whose id is already known by fetching data
-        /// from the server.
-        /// 
-        /// ObjectId of the ParseObject to fetch.
-        /// The cancellation token.
-        /// The ParseObject for the given objectId.
-        public Task GetAsync(string objectId, CancellationToken cancellationToken)
-        {
-            ParseQuery singleItemQuery = new ParseQuery(Services, ClassName).WhereEqualTo(nameof(objectId), objectId);
-            singleItemQuery = new ParseQuery(singleItemQuery, includes: Includes, selectedKeys: KeySelections, limit: 1);
-            return singleItemQuery.FindAsync(cancellationToken).OnSuccess(t => t.Result.FirstOrDefault() ?? throw new ParseFailureException(ParseFailureException.ErrorCode.ObjectNotFound, "Object with the given objectId not found."));
-        }
 
-        internal object GetConstraint(string key) => Filters?.GetOrDefault(key, null);
+    /// 
+    /// Retrieves at most one ParseObject that satisfies this query.
+    /// 
+    /// A single ParseObject that satisfies this query.
+    /// If no results match the query.
+    public Task FirstAsync()
+    {
+        return FirstAsync(CancellationToken.None);
+    }
 
-        internal IDictionary BuildParameters(bool includeClassName = false)
+    /// 
+    /// Retrieves at most one ParseObject that satisfies this query.
+    /// 
+    /// The cancellation token.
+    /// A single ParseObject that satisfies this query.
+    /// If no results match the query.
+    public async Task FirstAsync(CancellationToken cancellationToken)
+    {
+        var result = await FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
+        if (result == null)
         {
-            Dictionary result = new Dictionary();
-            if (Filters != null)
-                result["where"] = PointerOrLocalIdEncoder.Instance.Encode(Filters, Services);
-            if (Orderings != null)
-                result["order"] = String.Join(",", Orderings.ToArray());
-            if (SkipAmount != null)
-                result["skip"] = SkipAmount.Value;
-            if (LimitAmount != null)
-                result["limit"] = LimitAmount.Value;
-            if (Includes != null)
-                result["include"] = String.Join(",", Includes.ToArray());
-            if (KeySelections != null)
-                result["keys"] = String.Join(",", KeySelections.ToArray());
-            if (includeClassName)
-                result["className"] = ClassName;
-            if (RedirectClassNameForKey != null)
-                result["redirectClassNameForKey"] = RedirectClassNameForKey;
-            return result;
+            throw new ParseFailureException(ParseFailureException.ErrorCode.ObjectNotFound, "No results matched the query.");
         }
+        return result;
+    }
 
-        string RegexQuote(string input) => "\\Q" + input.Replace("\\E", "\\E\\\\E\\Q") + "\\E";
+    /// 
+    /// Counts the number of objects that match this query.
+    /// 
+    /// The number of objects that match this query.
+    public Task CountAsync()
+    {
+        return CountAsync(CancellationToken.None);
+    }
 
-        string GetRegexOptions(Regex regex, string modifiers)
-        {
-            string result = modifiers ?? "";
-            if (regex.Options.HasFlag(RegexOptions.IgnoreCase) && !modifiers.Contains("i"))
-                result += "i";
-            if (regex.Options.HasFlag(RegexOptions.Multiline) && !modifiers.Contains("m"))
-                result += "m";
-            return result;
-        }
+    /// 
+    /// Counts the number of objects that match this query.
+    /// 
+    /// The cancellation token.
+    /// The number of objects that match this query.
+    public async Task CountAsync(CancellationToken cancellationToken)
+    {
+        EnsureNotInstallationQuery();
+        var val = await Services.QueryController.CountAsync(this, await Services.GetCurrentUser(), cancellationToken);
+        return val;
+    }
 
-        IDictionary EncodeRegex(Regex regex, string modifiers)
-        {
-            string options = GetRegexOptions(regex, modifiers);
-            Dictionary dict = new Dictionary { ["$regex"] = regex.ToString() };
+    /// 
+    /// Constructs a ParseObject whose id is already known by fetching data
+    /// from the server.
+    /// 
+    /// ObjectId of the ParseObject to fetch.
+    /// The ParseObject for the given objectId.
+    public Task GetAsync(string objectId)
+    {
+        return GetAsync(objectId, CancellationToken.None);
+    }
 
-            if (!String.IsNullOrEmpty(options))
-            {
-                dict["$options"] = options;
-            }
+    /// 
+    /// Constructs a ParseObject whose id is already known by fetching data
+    /// from the server.
+    /// 
+    /// ObjectId of the ParseObject to fetch.
+    /// The cancellation token.
+    /// The ParseObject for the given objectId.
+    public async Task GetAsync(string objectId, CancellationToken cancellationToken)
+    {
+        var query = new ParseQuery(Services, ClassName)
+            .WhereEqualTo(nameof(objectId), objectId)
+            .Limit(1);
 
-            return dict;
-        }
+        var result = await query.FindAsync(cancellationToken).ConfigureAwait(false);
+        return result.FirstOrDefault() ?? throw new ParseFailureException(ParseFailureException.ErrorCode.ObjectNotFound, "Object with the given objectId not found.");
+    }
+
+    internal object GetConstraint(string key)
+    {
+        return Filters?.GetOrDefault(key, null);
+    }
+
+    internal IDictionary BuildParameters(bool includeClassName = false)
+    {
+        Dictionary result = new Dictionary();
+        if (Filters != null)
+            result["where"] = PointerOrLocalIdEncoder.Instance.Encode(Filters, Services);
+        if (Orderings != null)
+            result["order"] = String.Join(",", Orderings.ToArray());
+        if (SkipAmount != null)
+            result["skip"] = SkipAmount.Value;
+        if (LimitAmount != null)
+            result["limit"] = LimitAmount.Value;
+        if (Includes != null)
+            result["include"] = String.Join(",", Includes.ToArray());
+        if (KeySelections != null)
+            result["keys"] = String.Join(",", KeySelections.ToArray());
+        if (includeClassName)
+            result["className"] = ClassName;
+        if (RedirectClassNameForKey != null)
+            result["redirectClassNameForKey"] = RedirectClassNameForKey;
+        return result;
+    }
+
+    string RegexQuote(string input)
+    {
+        return "\\Q" + input.Replace("\\E", "\\E\\\\E\\Q") + "\\E";
+    }
+
+    string GetRegexOptions(Regex regex, string modifiers)
+    {
+        string result = modifiers ?? "";
+        if (regex.Options.HasFlag(RegexOptions.IgnoreCase) && !modifiers.Contains("i"))
+            result += "i";
+        if (regex.Options.HasFlag(RegexOptions.Multiline) && !modifiers.Contains("m"))
+            result += "m";
+        return result;
+    }
+
+    IDictionary EncodeRegex(Regex regex, string modifiers)
+    {
+        string options = GetRegexOptions(regex, modifiers);
+        Dictionary dict = new Dictionary { ["$regex"] = regex.ToString() };
 
-        void EnsureNotInstallationQuery()
+        if (!String.IsNullOrEmpty(options))
         {
-            // The ParseInstallation class is not accessible from this project; using string literal.
+            dict["$options"] = options;
+        }
 
-            if (ClassName.Equals("_Installation"))
-            {
-                throw new InvalidOperationException("Cannot directly query the Installation class.");
-            }
+        return dict;
+    }
+
+    void EnsureNotInstallationQuery()
+    {
+        // The ParseInstallation class is not accessible from this project; using string literal.
+
+        if (ClassName.Equals("_Installation"))
+        {
+            throw new InvalidOperationException("Cannot directly query the Installation class.");
         }
+    }
+
+    /// 
+    /// Determines whether the specified object is equal to the current object.
+    /// 
+    /// The object to compare with the current object.
+    /// true if the specified object is equal to the current object; otherwise, false
+    public override bool Equals(object obj)
+    {
+        return obj == null || !(obj is ParseQuery other) ? false : Equals(ClassName, other.ClassName) && Filters.CollectionsEqual(other.Filters) && Orderings.CollectionsEqual(other.Orderings) && Includes.CollectionsEqual(other.Includes) && KeySelections.CollectionsEqual(other.KeySelections) && Equals(SkipAmount, other.SkipAmount) && Equals(LimitAmount, other.LimitAmount);
+    }
 
-        /// 
-        /// Determines whether the specified object is equal to the current object.
-        /// 
-        /// The object to compare with the current object.
-        /// true if the specified object is equal to the current object; otherwise, false
-        public override bool Equals(object obj) => obj == null || !(obj is ParseQuery other) ? false : Equals(ClassName, other.ClassName) && Filters.CollectionsEqual(other.Filters) && Orderings.CollectionsEqual(other.Orderings) && Includes.CollectionsEqual(other.Includes) && KeySelections.CollectionsEqual(other.KeySelections) && Equals(SkipAmount, other.SkipAmount) && Equals(LimitAmount, other.LimitAmount);
-
-        /// 
-        /// Serves as the default hash function.
-        /// 
-        /// A hash code for the current object.
-        public override int GetHashCode() =>
-            // TODO (richardross): Implement this.
-            0;
+    /// 
+    /// Serves as the default hash function.
+    /// 
+    /// A hash code for the current object.
+    public override int GetHashCode()
+    {
+        // TODO (richardross): Implement this.
+        return 0;
     }
 }
diff --git a/Parse/Platform/Queries/ParseQueryController.cs b/Parse/Platform/Queries/ParseQueryController.cs
index e56c0107..1793fbd0 100644
--- a/Parse/Platform/Queries/ParseQueryController.cs
+++ b/Parse/Platform/Queries/ParseQueryController.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
@@ -9,40 +10,83 @@
 using Parse.Abstractions.Platform.Queries;
 using Parse.Infrastructure.Data;
 using Parse.Infrastructure.Execution;
-using Parse.Infrastructure.Utilities;
 
-namespace Parse.Platform.Queries
-{
-    /// 
-    /// A straightforward implementation of  that uses  to decode raw server data when needed.
-    /// 
-    internal class ParseQueryController : IParseQueryController
-    {
-        IParseCommandRunner CommandRunner { get; }
+namespace Parse.Platform.Queries;
 
-        IParseDataDecoder Decoder { get; }
+/// 
+/// A straightforward implementation of  that uses  to decode raw server data when needed.
+/// 
 
-        public ParseQueryController(IParseCommandRunner commandRunner, IParseDataDecoder decoder) => (CommandRunner, Decoder) = (commandRunner, decoder);
+internal class ParseQueryController : IParseQueryController
+{
+    private IParseCommandRunner CommandRunner { get; }
+    private IParseDataDecoder Decoder { get; }
 
-        public Task> FindAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken = default) where T : ParseObject => FindAsync(query.ClassName, query.BuildParameters(), user?.SessionToken, cancellationToken).OnSuccess(t => (from item in t.Result["results"] as IList select ParseObjectCoder.Instance.Decode(item as IDictionary, Decoder, user?.Services)));
+    public ParseQueryController(IParseCommandRunner commandRunner, IParseDataDecoder decoder)
+    {
+        CommandRunner = commandRunner;
+        Decoder = decoder;
+    }
 
-        public Task CountAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken = default) where T : ParseObject
+    public async Task> FindAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken = default) where T : ParseObject
+    {
+        var result = await FindAsync(query.ClassName, query.BuildParameters(), user?.SessionToken, cancellationToken).ConfigureAwait(false);
+
+        // Check if the result contains an error code
+        if (result.TryGetValue("code", out object codeValue) && codeValue is long errorCode)
         {
-            IDictionary parameters = query.BuildParameters();
-            parameters["limit"] = 0;
-            parameters["count"] = 1;
+            if (errorCode == 102) // Specific handling for "Cannot query on ACL"
+            {
+                throw new InvalidOperationException("Cannot query on ACL. Ensure your query does not filter by ACL.");
+            }
 
-            return FindAsync(query.ClassName, parameters, user?.SessionToken, cancellationToken).OnSuccess(task => Convert.ToInt32(task.Result["count"]));
+            // Handle other error codes here if needed
         }
 
-        public Task FirstAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken = default) where T : ParseObject
+        // Process raw results
+        var rawResults = result.TryGetValue("results", out object results) ? results as IList : new List();
+        if (rawResults is null || rawResults.Count == 0)
         {
-            IDictionary parameters = query.BuildParameters();
-            parameters["limit"] = 1;
-
-            return FindAsync(query.ClassName, parameters, user?.SessionToken, cancellationToken).OnSuccess(task => (task.Result["results"] as IList).FirstOrDefault() as IDictionary is Dictionary item && item != null ? ParseObjectCoder.Instance.Decode(item, Decoder, user.Services) : null);
+            return Enumerable.Empty();
         }
 
-        Task> FindAsync(string className, IDictionary parameters, string sessionToken, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand($"classes/{Uri.EscapeDataString(className)}?{ParseClient.BuildQueryString(parameters)}", method: "GET", sessionToken: sessionToken, data: null), cancellationToken: cancellationToken).OnSuccess(t => t.Result.Item2);
+        return rawResults
+            .Select(item => ParseObjectCoder.Instance.Decode(item as IDictionary, Decoder, user?.Services));
+    }
+
+
+    public async Task CountAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken = default) where T : ParseObject
+    {
+        var parameters = query.BuildParameters();
+        parameters["limit"] = 0;
+        parameters["count"] = 1;
+
+        var result = await FindAsync(query.ClassName, parameters, user?.SessionToken, cancellationToken).ConfigureAwait(false);
+        return Convert.ToInt32(result["count"]);
+    }
+
+    public async Task FirstAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken = default) where T : ParseObject
+    {
+        var parameters = query.BuildParameters();
+        parameters["limit"] = 1;
+
+        var result = await FindAsync(query.ClassName, parameters, user?.SessionToken, cancellationToken).ConfigureAwait(false);
+        var rawResults = result["results"] as IList ?? new List();
+
+        var firstItem = rawResults.FirstOrDefault() as IDictionary;
+        return firstItem != null ? ParseObjectCoder.Instance.Decode(firstItem, Decoder, user?.Services) : null;
+    }
+
+    private async Task> FindAsync(string className, IDictionary parameters, string sessionToken, CancellationToken cancellationToken = default)
+    {
+        var command = new ParseCommand(
+            $"classes/{Uri.EscapeDataString(className)}?{ParseClient.BuildQueryString(parameters)}",
+            method: "GET",
+            sessionToken: sessionToken,
+            data: null
+        );
+
+        var response = await CommandRunner.RunCommandAsync(command, null,null,cancellationToken).ConfigureAwait(false);
+        return response.Item2;
     }
-}
+}
\ No newline at end of file
diff --git a/Parse/Platform/Relations/ParseRelation.cs b/Parse/Platform/Relations/ParseRelation.cs
index ff977ae3..b0d65ee2 100644
--- a/Parse/Platform/Relations/ParseRelation.cs
+++ b/Parse/Platform/Relations/ParseRelation.cs
@@ -7,100 +7,118 @@
 using Parse.Abstractions.Platform.Objects;
 using Parse.Infrastructure.Control;
 
-namespace Parse
+namespace Parse;
+
+public static class RelationServiceExtensions
 {
-    public static class RelationServiceExtensions
+    /// 
+    /// Produces the proper ParseRelation<T> instance for the given classname.
+    /// 
+    internal static ParseRelationBase CreateRelation(this IServiceHub serviceHub, ParseObject parent, string key, string targetClassName)
     {
-        /// 
-        /// Produces the proper ParseRelation<T> instance for the given classname.
-        /// 
-        internal static ParseRelationBase CreateRelation(this IServiceHub serviceHub, ParseObject parent, string key, string targetClassName) => serviceHub.ClassController.CreateRelation(parent, key, targetClassName);
-
-        internal static ParseRelationBase CreateRelation(this IParseObjectClassController classController, ParseObject parent, string key, string targetClassName)
-        {
-            Expression>> createRelationExpr = () => CreateRelation(parent, key, targetClassName);
-            return (createRelationExpr.Body as MethodCallExpression).Method.GetGenericMethodDefinition().MakeGenericMethod(classController.GetType(targetClassName) ?? typeof(ParseObject)).Invoke(default, new object[] { parent, key, targetClassName }) as ParseRelationBase;
-        }
+        return serviceHub.ClassController.CreateRelation(parent, key, targetClassName);
+    }
 
-        static ParseRelation CreateRelation(ParseObject parent, string key, string targetClassName) where T : ParseObject => new ParseRelation(parent, key, targetClassName);
+    internal static ParseRelationBase CreateRelation(this IParseObjectClassController classController, ParseObject parent, string key, string targetClassName)
+    {
+        Expression>> createRelationExpr = () => CreateRelation(parent, key, targetClassName);
+        return (createRelationExpr.Body as MethodCallExpression).Method.GetGenericMethodDefinition().MakeGenericMethod(classController.GetType(targetClassName) ?? typeof(ParseObject)).Invoke(default, new object[] { parent, key, targetClassName }) as ParseRelationBase;
     }
 
-    /// 
-    /// A common base class for ParseRelations.
-    /// 
-    [EditorBrowsable(EditorBrowsableState.Never)]
-    public abstract class ParseRelationBase : IJsonConvertible
+    static ParseRelation CreateRelation(ParseObject parent, string key, string targetClassName) where T : ParseObject
     {
-        ParseObject Parent { get; set; }
+        return new ParseRelation(parent, key, targetClassName);
+    }
+}
 
-        string Key { get; set; }
+/// 
+/// A common base class for ParseRelations.
+/// 
+[EditorBrowsable(EditorBrowsableState.Never)]
+public abstract class ParseRelationBase : IJsonConvertible
+{
+    public ParseObject Parent { get; set; } 
 
-        internal ParseRelationBase(ParseObject parent, string key) => EnsureParentAndKey(parent, key);
+    public string Key { get; set; }
 
-        internal ParseRelationBase(ParseObject parent, string key, string targetClassName) : this(parent, key) => TargetClassName = targetClassName;
+    internal ParseRelationBase(ParseObject parent, string key) => EnsureParentAndKey(parent, key);
 
-        internal void EnsureParentAndKey(ParseObject parent, string key)
-        {
-            Parent ??= parent;
-            Key ??= key;
+    internal ParseRelationBase(ParseObject parent, string key, string targetClassName) : this(parent, key) => TargetClassName = targetClassName;
 
-            Debug.Assert(Parent == parent, "Relation retrieved from two different objects");
-            Debug.Assert(Key == key, "Relation retrieved from two different keys");
-        }
+    internal void EnsureParentAndKey(ParseObject parent, string key)
+    {
+        Parent ??= parent;
+        Key ??= key;
 
-        internal void Add(ParseObject entity)
-        {
-            ParseRelationOperation change = new ParseRelationOperation(Parent.Services.ClassController, new[] { entity }, default);
+        Debug.Assert(Parent == parent, "Relation retrieved from two different objects");
+        Debug.Assert(Key == key, "Relation retrieved from two different keys");
+    }
 
-            Parent.PerformOperation(Key, change);
-            TargetClassName = change.TargetClassName;
-        }
+    internal void Add(ParseObject entity)
+    {
+        ParseRelationOperation change = new ParseRelationOperation(Parent.Services.ClassController, new[] { entity }, default);
 
-        internal void Remove(ParseObject entity)
-        {
-            ParseRelationOperation change = new ParseRelationOperation(Parent.Services.ClassController, default, new[] { entity });
+        Parent.PerformOperation(Key, change);
+        TargetClassName = change.TargetClassName;
+    }
 
-            Parent.PerformOperation(Key, change);
-            TargetClassName = change.TargetClassName;
-        }
+    internal void Remove(ParseObject entity)
+    {
+        ParseRelationOperation change = new ParseRelationOperation(Parent.Services.ClassController, default, new[] { entity });
+
+        Parent.PerformOperation(Key, change);
+        TargetClassName = change.TargetClassName;
+    }
 
-        IDictionary IJsonConvertible.ConvertToJSON() => new Dictionary
+    public IDictionary ConvertToJSON(IServiceHub serviceHub = default)
+    {
+        return new Dictionary
         {
             ["__type"] = "Relation",
             ["className"] = TargetClassName
         };
+    }
+
+    internal ParseQuery GetQuery() where T : ParseObject
+    {
+        return TargetClassName is { } ? new ParseQuery(Parent.Services, TargetClassName).WhereRelatedTo(Parent, Key) : new ParseQuery(Parent.Services, Parent.ClassName).RedirectClassName(Key).WhereRelatedTo(Parent, Key);
+    }
+
+    internal string TargetClassName { get; set; }
+}
+
+/// 
+/// Provides access to all of the children of a many-to-many relationship. Each instance of
+/// ParseRelation is associated with a particular parent and key.
+/// 
+/// The type of the child objects.
+public sealed class ParseRelation : ParseRelationBase where T : ParseObject
+{
+    
+    internal ParseRelation(ParseObject parent, string key) : base(parent, key) { }
 
-        internal ParseQuery GetQuery() where T : ParseObject => TargetClassName is { } ? new ParseQuery(Parent.Services, TargetClassName).WhereRelatedTo(Parent, Key) : new ParseQuery(Parent.Services, Parent.ClassName).RedirectClassName(Key).WhereRelatedTo(Parent, Key);
+    internal ParseRelation(ParseObject parent, string key, string targetClassName) : base(parent, key, targetClassName) { }
 
-        internal string TargetClassName { get; set; }
+    /// 
+    /// Adds an object to this relation. The object must already have been saved.
+    /// 
+    /// The object to add.
+    public void Add(T obj)
+    {
+        base.Add(obj);
     }
 
     /// 
-    /// Provides access to all of the children of a many-to-many relationship. Each instance of
-    /// ParseRelation is associated with a particular parent and key.
+    /// Removes an object from this relation. The object must already have been saved.
     /// 
-    /// The type of the child objects.
-    public sealed class ParseRelation : ParseRelationBase where T : ParseObject
+    /// The object to remove.
+    public void Remove(T obj)
     {
-        internal ParseRelation(ParseObject parent, string key) : base(parent, key) { }
-
-        internal ParseRelation(ParseObject parent, string key, string targetClassName) : base(parent, key, targetClassName) { }
-
-        /// 
-        /// Adds an object to this relation. The object must already have been saved.
-        /// 
-        /// The object to add.
-        public void Add(T obj) => base.Add(obj);
-
-        /// 
-        /// Removes an object from this relation. The object must already have been saved.
-        /// 
-        /// The object to remove.
-        public void Remove(T obj) => base.Remove(obj);
-
-        /// 
-        /// Gets a query that can be used to query the objects in this relation.
-        /// 
-        public ParseQuery Query => GetQuery();
+        base.Remove(obj);
     }
+
+    /// 
+    /// Gets a query that can be used to query the objects in this relation.
+    /// 
+    public ParseQuery Query => GetQuery();
 }
diff --git a/Parse/Platform/Roles/ParseRole.cs b/Parse/Platform/Roles/ParseRole.cs
index 1d91429e..8eb66bf2 100644
--- a/Parse/Platform/Roles/ParseRole.cs
+++ b/Parse/Platform/Roles/ParseRole.cs
@@ -1,85 +1,84 @@
 using System;
 using System.Text.RegularExpressions;
 
-namespace Parse
+namespace Parse;
+
+/// 
+/// Represents a Role on the Parse server. ParseRoles represent groupings
+/// of s for the purposes of granting permissions (e.g.
+/// specifying a  for a . Roles
+/// are specified by their sets of child users and child roles, all of which are granted
+/// any permissions that the parent role has.
+///
+/// Roles must have a name (that cannot be changed after creation of the role),
+/// and must specify an ACL.
+/// 
+[ParseClassName("_Role")]
+public class ParseRole : ParseObject
 {
+    private static readonly Regex namePattern = new Regex("^[0-9a-zA-Z_\\- ]+$");
+
     /// 
-    /// Represents a Role on the Parse server. ParseRoles represent groupings
-    /// of s for the purposes of granting permissions (e.g.
-    /// specifying a  for a . Roles
-    /// are specified by their sets of child users and child roles, all of which are granted
-    /// any permissions that the parent role has.
-    ///
-    /// Roles must have a name (that cannot be changed after creation of the role),
-    /// and must specify an ACL.
+    /// Constructs a new ParseRole. You must assign a name and ACL to the role.
     /// 
-    [ParseClassName("_Role")]
-    public class ParseRole : ParseObject
-    {
-        private static readonly Regex namePattern = new Regex("^[0-9a-zA-Z_\\- ]+$");
-
-        /// 
-        /// Constructs a new ParseRole. You must assign a name and ACL to the role.
-        /// 
-        public ParseRole() : base() { }
+    public ParseRole() : base() { }
 
-        /// 
-        /// Constructs a new ParseRole with the given name.
-        /// 
-        /// The name of the role to create.
-        /// The ACL for this role. Roles must have an ACL.
-        public ParseRole(string name, ParseACL acl) : this()
-        {
-            Name = name;
-            ACL = acl;
-        }
+    /// 
+    /// Constructs a new ParseRole with the given name.
+    /// 
+    /// The name of the role to create.
+    /// The ACL for this role. Roles must have an ACL.
+    public ParseRole(string name, ParseACL acl) : this()
+    {
+        Name = name;
+        ACL = acl;
+    }
 
-        /// 
-        /// Gets the name of the role.
-        /// 
-        [ParseFieldName("name")]
-        public string Name
-        {
-            get => GetProperty(nameof(Name));
-            set => SetProperty(value, nameof(Name));
-        }
+    /// 
+    /// Gets the name of the role.
+    /// 
+    [ParseFieldName("name")]
+    public string Name
+    {
+        get => GetProperty(nameof(Name));
+        set => SetProperty(value, nameof(Name));
+    }
 
-        /// 
-        /// Gets the  for the s that are
-        /// direct children of this role. These users are granted any privileges that
-        /// this role has been granted (e.g. read or write access through ACLs). You can
-        /// add or remove child users from the role through this relation.
-        /// 
-        [ParseFieldName("users")]
-        public ParseRelation Users => GetRelationProperty("Users");
+    /// 
+    /// Gets the  for the s that are
+    /// direct children of this role. These users are granted any privileges that
+    /// this role has been granted (e.g. read or write access through ACLs). You can
+    /// add or remove child users from the role through this relation.
+    /// 
+    [ParseFieldName("users")]
+    public ParseRelation Users => GetRelationProperty("Users");
 
-        /// 
-        /// Gets the  for the s that are
-        /// direct children of this role. These roles' users are granted any privileges that
-        /// this role has been granted (e.g. read or write access through ACLs). You can
-        /// add or remove child roles from the role through this relation.
-        /// 
-        [ParseFieldName("roles")]
-        public ParseRelation Roles => GetRelationProperty("Roles");
+    /// 
+    /// Gets the  for the s that are
+    /// direct children of this role. These roles' users are granted any privileges that
+    /// this role has been granted (e.g. read or write access through ACLs). You can
+    /// add or remove child roles from the role through this relation.
+    /// 
+    [ParseFieldName("roles")]
+    public ParseRelation Roles => GetRelationProperty("Roles");
 
-        internal override void OnSettingValue(ref string key, ref object value)
+    internal override void OnSettingValue(ref string key, ref object value)
+    {
+        base.OnSettingValue(ref key, ref value);
+        if (key == "name")
         {
-            base.OnSettingValue(ref key, ref value);
-            if (key == "name")
+            if (ObjectId != null)
+            {
+                throw new InvalidOperationException(
+                    "A role's name can only be set before it has been saved.");
+            }
+            if (!(value is string))
+            {
+                throw new ArgumentException("A role's name must be a string.", nameof(value));
+            }
+            if (!namePattern.IsMatch((string) value))
             {
-                if (ObjectId != null)
-                {
-                    throw new InvalidOperationException(
-                        "A role's name can only be set before it has been saved.");
-                }
-                if (!(value is string))
-                {
-                    throw new ArgumentException("A role's name must be a string.", nameof(value));
-                }
-                if (!namePattern.IsMatch((string) value))
-                {
-                    throw new ArgumentException("A role's name can only contain alphanumeric characters, _, -, and spaces.", nameof(value));
-                }
+                throw new ArgumentException("A role's name can only contain alphanumeric characters, _, -, and spaces.", nameof(value));
             }
         }
     }
diff --git a/Parse/Platform/Security/ParseACL.cs b/Parse/Platform/Security/ParseACL.cs
index 4258c5fc..30d3376e 100644
--- a/Parse/Platform/Security/ParseACL.cs
+++ b/Parse/Platform/Security/ParseACL.cs
@@ -3,264 +3,365 @@
 using System.Linq;
 using Parse.Abstractions.Internal;
 using Parse.Abstractions.Infrastructure;
+using System.Diagnostics;
+using System.Xml.Linq;
 
-namespace Parse
+namespace Parse;
+
+/// 
+/// A ParseACL is used to control which users and roles can access or modify a particular object. Each
+///  can have its own ParseACL. You can grant read and write permissions
+/// separately to specific users, to groups of users that belong to roles, or you can grant permissions
+/// to "the public" so that, for example, any user could read a particular object but only a particular
+/// set of users could write to that object.
+/// 
+public class ParseACL : IJsonConvertible
 {
-    /// 
-    /// A ParseACL is used to control which users and roles can access or modify a particular object. Each
-    ///  can have its own ParseACL. You can grant read and write permissions
-    /// separately to specific users, to groups of users that belong to roles, or you can grant permissions
-    /// to "the public" so that, for example, any user could read a particular object but only a particular
-    /// set of users could write to that object.
-    /// 
-    public class ParseACL : IJsonConvertible
+    private enum AccessKind
     {
-        private enum AccessKind
-        {
-            Read,
-            Write
-        }
-        private const string publicName = "*";
-        private readonly ICollection readers = new HashSet();
-        private readonly ICollection writers = new HashSet();
-
-        internal ParseACL(IDictionary jsonObject)
-        {
-            readers = new HashSet(from pair in jsonObject
-                                          where ((IDictionary) pair.Value).ContainsKey("read")
-                                          select pair.Key);
-            writers = new HashSet(from pair in jsonObject
-                                          where ((IDictionary) pair.Value).ContainsKey("write")
-                                          select pair.Key);
-        }
-
-        /// 
-        /// Creates an ACL with no permissions granted.
-        /// 
-        public ParseACL()
-        {
-        }
-
-        /// 
-        /// Creates an ACL where only the provided user has access.
-        /// 
-        /// The only user that can read or write objects governed by this ACL.
-        public ParseACL(ParseUser owner)
-        {
-            SetReadAccess(owner, true);
-            SetWriteAccess(owner, true);
-        }
+        Read,
+        Write
+    }
+    private const string publicName = "*";
+    private readonly ICollection readers = new HashSet();
+    private readonly ICollection writers = new HashSet();
 
-        IDictionary IJsonConvertible.ConvertToJSON()
+    internal ParseACL(IDictionary jsonObject)
+    {
+        // Recursive function to process ACL data
+        void ProcessAclData(IDictionary aclData)
         {
-            Dictionary result = new Dictionary();
-            foreach (string user in readers.Union(writers))
+            foreach (var pair in aclData)
             {
-                Dictionary userPermissions = new Dictionary();
-                if (readers.Contains(user))
+                if (pair.Key == publicName) // Special handling for public access
                 {
-                    userPermissions["read"] = true;
+                    if (pair.Value is IDictionary permissions)
+                    {
+                        if (permissions.ContainsKey("read"))
+                        {
+                            readers.Add(publicName); // Grant read access to the public
+                        }
+                        if (permissions.ContainsKey("write"))
+                        {
+                            writers.Add(publicName); // Grant write access to the public
+                        }
+                    }
+                    else
+                    {
+                        Debug.WriteLine($"Public access (ACL key: {publicName}) is not in the expected format.");
+                    }
                 }
-                if (writers.Contains(user))
+                else if (pair.Value is IDictionary permissions)
                 {
-                    userPermissions["write"] = true;
+                    // Process user/role ACLs
+                    if (permissions.ContainsKey("read"))
+                    {
+                        readers.Add(pair.Key); // Add read access for the user/role
+                    }
+                    if (permissions.ContainsKey("write"))
+                    {
+                        writers.Add(pair.Key); // Add write access for the user/role
+                    }
+                }
+                else
+                {
+                    Debug.WriteLine($"ACL entry for key '{pair.Key}' is not a valid permissions dictionary.");
                 }
-                result[user] = userPermissions;
             }
-            return result;
         }
 
-        private void SetAccess(AccessKind kind, string userId, bool allowed)
+        // Check if the input is nested
+        if (jsonObject.ContainsKey("ACL") && jsonObject["ACL"] is IDictionary nestedAcl)
+        {            
+            ProcessAclData(nestedAcl); // Process the nested ACL
+        }
+        else
+        {            
+            ProcessAclData(jsonObject); // Process the flat ACL
+        }
+    }
+
+
+
+    /// 
+    /// Creates an ACL with no permissions granted.
+    /// 
+    public ParseACL()
+    {
+        
+    }
+
+    /// 
+    /// Creates an ACL where only the provided user has access.
+    /// 
+    /// The only user that can read or write objects governed by this ACL.
+    public ParseACL(ParseUser owner)
+    {
+        if (owner?.ObjectId == null)
         {
-            if (userId == null)
-            {
-                throw new ArgumentException("Cannot set access for an unsaved user or role.");
-            }
-            ICollection target = null;
-            switch (kind)
-            {
-                case AccessKind.Read:
-                    target = readers;
-                    break;
-                case AccessKind.Write:
-                    target = writers;
-                    break;
-                default:
-                    throw new NotImplementedException("Unknown AccessKind");
-            }
-            if (allowed)
-            {
-                target.Add(userId);
-            }
-            else
-            {
-                target.Remove(userId);
-            }
+            throw new ArgumentException("ParseUser must have a valid ObjectId.");
         }
+        SetReadAccess(owner, true);
+        SetWriteAccess(owner, true);
+    }
 
-        private bool GetAccess(AccessKind kind, string userId)
+    public IDictionary ConvertToJSON(IServiceHub serviceHub = default)
+    {
+        Dictionary result = new Dictionary();
+        foreach (string user in readers.Union(writers))
         {
-            if (userId == null)
+            Dictionary userPermissions = new Dictionary();
+            if (readers.Contains(user))
             {
-                throw new ArgumentException("Cannot get access for an unsaved user or role.");
+                userPermissions["read"] = true;
             }
-            switch (kind)
+            if (writers.Contains(user))
             {
-                case AccessKind.Read:
-                    return readers.Contains(userId);
-                case AccessKind.Write:
-                    return writers.Contains(userId);
-                default:
-                    throw new NotImplementedException("Unknown AccessKind");
+                userPermissions["write"] = true;
             }
+            result[user] = userPermissions;
         }
+        return result;
+    }
 
-        /// 
-        /// Gets or sets whether the public is allowed to read this object.
-        /// 
-        public bool PublicReadAccess
+    private void SetAccess(AccessKind kind, string userId, bool allowed)
+    {
+        if (userId == null)
+        {
+            throw new ArgumentException("Cannot set access for an unsaved user or role.");
+        }
+        ICollection target = null;
+        switch (kind)
+        {
+            case AccessKind.Read:
+                target = readers;
+                break;
+            case AccessKind.Write:
+                target = writers;
+                break;
+            default:
+                throw new NotImplementedException("Unknown AccessKind");
+        }
+        if (allowed)
         {
-            get => GetAccess(AccessKind.Read, publicName);
-            set => SetAccess(AccessKind.Read, publicName, value);
+            target.Add(userId);
         }
+        else
+        {
+            target.Remove(userId);
+        }
+    }
 
-        /// 
-        /// Gets or sets whether the public is allowed to write this object.
-        /// 
-        public bool PublicWriteAccess
+    private bool GetAccess(AccessKind kind, string userId)
+    {
+        if (userId == null)
         {
-            get => GetAccess(AccessKind.Write, publicName);
-            set => SetAccess(AccessKind.Write, publicName, value);
+            throw new ArgumentException("Cannot get access for an unsaved user or role.");
         }
+        switch (kind)
+        {
+            case AccessKind.Read:
+                return readers.Contains(userId);
+            case AccessKind.Write:
+                return writers.Contains(userId);
+            default:
+                throw new NotImplementedException("Unknown AccessKind");
+        }
+    }
+
+    /// 
+    /// Gets or sets whether the public is allowed to read this object.
+    /// 
+    public bool PublicReadAccess
+    {
+        get => GetAccess(AccessKind.Read, publicName);
+        set => SetAccess(AccessKind.Read, publicName, value);
+    }
+
+    /// 
+    /// Gets or sets whether the public is allowed to write this object.
+    /// 
+    public bool PublicWriteAccess
+    {
+        get => GetAccess(AccessKind.Write, publicName);
+        set => SetAccess(AccessKind.Write, publicName, value);
+    }
 
-        /// 
-        /// Sets whether the given user id is allowed to read this object.
-        /// 
-        /// The objectId of the user.
-        /// Whether the user has permission.
-        public void SetReadAccess(string userId, bool allowed) => SetAccess(AccessKind.Read, userId, allowed);
+    /// 
+    /// Sets whether the given user id is allowed to read this object.
+    /// 
+    /// The objectId of the user.
+    /// Whether the user has permission.
+    public void SetReadAccess(string userId, bool allowed)
+    {
+        SetAccess(AccessKind.Read, userId, allowed);
+    }
 
-        /// 
-        /// Sets whether the given user is allowed to read this object.
-        /// 
-        /// The user.
-        /// Whether the user has permission.
-        public void SetReadAccess(ParseUser user, bool allowed) => SetReadAccess(user.ObjectId, allowed);
+    /// 
+    /// Sets whether the given user is allowed to read this object.
+    /// 
+    /// The user.
+    /// Whether the user has permission.
+    public void SetReadAccess(ParseUser user, bool allowed)
+    {
+        SetReadAccess(user.ObjectId, allowed);
+    }
 
-        /// 
-        /// Sets whether the given user id is allowed to write this object.
-        /// 
-        /// The objectId of the user.
-        /// Whether the user has permission.
-        public void SetWriteAccess(string userId, bool allowed) => SetAccess(AccessKind.Write, userId, allowed);
+    /// 
+    /// Sets whether the given user id is allowed to write this object.
+    /// 
+    /// The objectId of the user.
+    /// Whether the user has permission.
+    public void SetWriteAccess(string userId, bool allowed)
+    {
+        SetAccess(AccessKind.Write, userId, allowed);
+    }
 
-        /// 
-        /// Sets whether the given user is allowed to write this object.
-        /// 
-        /// The user.
-        /// Whether the user has permission.
-        public void SetWriteAccess(ParseUser user, bool allowed) => SetWriteAccess(user.ObjectId, allowed);
+    /// 
+    /// Sets whether the given user is allowed to write this object.
+    /// 
+    /// The user.
+    /// Whether the user has permission.
+    public void SetWriteAccess(ParseUser user, bool allowed)
+    {
+        SetWriteAccess(user.ObjectId, allowed);
+    }
 
-        /// 
-        /// Gets whether the given user id is *explicitly* allowed to read this object.
-        /// Even if this returns false, the user may still be able to read it if
-        /// PublicReadAccess is true or a role that the user belongs to has read access.
-        /// 
-        /// The user objectId to check.
-        /// Whether the user has access.
-        public bool GetReadAccess(string userId) => GetAccess(AccessKind.Read, userId);
+    /// 
+    /// Gets whether the given user id is *explicitly* allowed to read this object.
+    /// Even if this returns false, the user may still be able to read it if
+    /// PublicReadAccess is true or a role that the user belongs to has read access.
+    /// 
+    /// The user objectId to check.
+    /// Whether the user has access.
+    public bool GetReadAccess(string userId)
+    {
+        return GetAccess(AccessKind.Read, userId);
+    }
 
-        /// 
-        /// Gets whether the given user is *explicitly* allowed to read this object.
-        /// Even if this returns false, the user may still be able to read it if
-        /// PublicReadAccess is true or a role that the user belongs to has read access.
-        /// 
-        /// The user to check.
-        /// Whether the user has access.
-        public bool GetReadAccess(ParseUser user) => GetReadAccess(user.ObjectId);
+    /// 
+    /// Gets whether the given user is *explicitly* allowed to read this object.
+    /// Even if this returns false, the user may still be able to read it if
+    /// PublicReadAccess is true or a role that the user belongs to has read access.
+    /// 
+    /// The user to check.
+    /// Whether the user has access.
+    public bool GetReadAccess(ParseUser user)
+    {
+        return GetReadAccess(user.ObjectId);
+    }
 
-        /// 
-        /// Gets whether the given user id is *explicitly* allowed to write this object.
-        /// Even if this returns false, the user may still be able to write it if
-        /// PublicReadAccess is true or a role that the user belongs to has write access.
-        /// 
-        /// The user objectId to check.
-        /// Whether the user has access.
-        public bool GetWriteAccess(string userId) => GetAccess(AccessKind.Write, userId);
+    /// 
+    /// Gets whether the given user id is *explicitly* allowed to write this object.
+    /// Even if this returns false, the user may still be able to write it if
+    /// PublicReadAccess is true or a role that the user belongs to has write access.
+    /// 
+    /// The user objectId to check.
+    /// Whether the user has access.
+    public bool GetWriteAccess(string userId)
+    {
+        return GetAccess(AccessKind.Write, userId);
+    }
 
-        /// 
-        /// Gets whether the given user is *explicitly* allowed to write this object.
-        /// Even if this returns false, the user may still be able to write it if
-        /// PublicReadAccess is true or a role that the user belongs to has write access.
-        /// 
-        /// The user to check.
-        /// Whether the user has access.
-        public bool GetWriteAccess(ParseUser user) => GetWriteAccess(user.ObjectId);
+    /// 
+    /// Gets whether the given user is *explicitly* allowed to write this object.
+    /// Even if this returns false, the user may still be able to write it if
+    /// PublicReadAccess is true or a role that the user belongs to has write access.
+    /// 
+    /// The user to check.
+    /// Whether the user has access.
+    public bool GetWriteAccess(ParseUser user)
+    {
+        return GetWriteAccess(user.ObjectId);
+    }
 
-        /// 
-        /// Sets whether users belonging to the role with the given 
-        /// are allowed to read this object.
-        /// 
-        /// The name of the role.
-        /// Whether the role has access.
-        public void SetRoleReadAccess(string roleName, bool allowed) => SetAccess(AccessKind.Read, "role:" + roleName, allowed);
+    /// 
+    /// Sets whether users belonging to the role with the given 
+    /// are allowed to read this object.
+    /// 
+    /// The name of the role.
+    /// Whether the role has access.
+    public void SetRoleReadAccess(string roleName, bool allowed)
+    {
+        SetAccess(AccessKind.Read, "role:" + roleName, allowed);
+    }
 
-        /// 
-        /// Sets whether users belonging to the given role are allowed to read this object.
-        /// 
-        /// The role.
-        /// Whether the role has access.
-        public void SetRoleReadAccess(ParseRole role, bool allowed) => SetRoleReadAccess(role.Name, allowed);
+    /// 
+    /// Sets whether users belonging to the given role are allowed to read this object.
+    /// 
+    /// The role.
+    /// Whether the role has access.
+    public void SetRoleReadAccess(ParseRole role, bool allowed)
+    {
+        SetRoleReadAccess(role.Name, allowed);
+    }
 
-        /// 
-        /// Gets whether users belonging to the role with the given 
-        /// are allowed to read this object. Even if this returns false, the role may still be
-        /// able to read it if a parent role has read access.
-        /// 
-        /// The name of the role.
-        /// Whether the role has access.
-        public bool GetRoleReadAccess(string roleName) => GetAccess(AccessKind.Read, "role:" + roleName);
+    /// 
+    /// Gets whether users belonging to the role with the given 
+    /// are allowed to read this object. Even if this returns false, the role may still be
+    /// able to read it if a parent role has read access.
+    /// 
+    /// The name of the role.
+    /// Whether the role has access.
+    public bool GetRoleReadAccess(string roleName)
+    {
+        return GetAccess(AccessKind.Read, "role:" + roleName);
+    }
 
-        /// 
-        /// Gets whether users belonging to the role are allowed to read this object.
-        /// Even if this returns false, the role may still be able to read it if a
-        /// parent role has read access.
-        /// 
-        /// The name of the role.
-        /// Whether the role has access.
-        public bool GetRoleReadAccess(ParseRole role) => GetRoleReadAccess(role.Name);
+    /// 
+    /// Gets whether users belonging to the role are allowed to read this object.
+    /// Even if this returns false, the role may still be able to read it if a
+    /// parent role has read access.
+    /// 
+    /// The name of the role.
+    /// Whether the role has access.
+    public bool GetRoleReadAccess(ParseRole role)
+    {
+        return GetRoleReadAccess(role.Name);
+    }
 
-        /// 
-        /// Sets whether users belonging to the role with the given 
-        /// are allowed to write this object.
-        /// 
-        /// The name of the role.
-        /// Whether the role has access.
-        public void SetRoleWriteAccess(string roleName, bool allowed) => SetAccess(AccessKind.Write, "role:" + roleName, allowed);
+    /// 
+    /// Sets whether users belonging to the role with the given 
+    /// are allowed to write this object.
+    /// 
+    /// The name of the role.
+    /// Whether the role has access.
+    public void SetRoleWriteAccess(string roleName, bool allowed)
+    {
+        SetAccess(AccessKind.Write, "role:" + roleName, allowed);
+    }
 
-        /// 
-        /// Sets whether users belonging to the given role are allowed to write this object.
-        /// 
-        /// The role.
-        /// Whether the role has access.
-        public void SetRoleWriteAccess(ParseRole role, bool allowed) => SetRoleWriteAccess(role.Name, allowed);
+    /// 
+    /// Sets whether users belonging to the given role are allowed to write this object.
+    /// 
+    /// The role.
+    /// Whether the role has access.
+    public void SetRoleWriteAccess(ParseRole role, bool allowed)
+    {
+        SetRoleWriteAccess(role.Name, allowed);
+    }
 
-        /// 
-        /// Gets whether users belonging to the role with the given 
-        /// are allowed to write this object. Even if this returns false, the role may still be
-        /// able to write it if a parent role has write access.
-        /// 
-        /// The name of the role.
-        /// Whether the role has access.
-        public bool GetRoleWriteAccess(string roleName) => GetAccess(AccessKind.Write, "role:" + roleName);
+    /// 
+    /// Gets whether users belonging to the role with the given 
+    /// are allowed to write this object. Even if this returns false, the role may still be
+    /// able to write it if a parent role has write access.
+    /// 
+    /// The name of the role.
+    /// Whether the role has access.
+    public bool GetRoleWriteAccess(string roleName)
+    {
+        return GetAccess(AccessKind.Write, "role:" + roleName);
+    }
 
-        /// 
-        /// Gets whether users belonging to the role are allowed to write this object.
-        /// Even if this returns false, the role may still be able to write it if a
-        /// parent role has write access.
-        /// 
-        /// The name of the role.
-        /// Whether the role has access.
-        public bool GetRoleWriteAccess(ParseRole role) => GetRoleWriteAccess(role.Name);
+    /// 
+    /// Gets whether users belonging to the role are allowed to write this object.
+    /// Even if this returns false, the role may still be able to write it if a
+    /// parent role has write access.
+    /// 
+    /// The name of the role.
+    /// Whether the role has access.
+    public bool GetRoleWriteAccess(ParseRole role)
+    {
+        return GetRoleWriteAccess(role.Name);
     }
 }
diff --git a/Parse/Platform/Sessions/ParseSession.cs b/Parse/Platform/Sessions/ParseSession.cs
index dc6d5f10..3bfd17e9 100644
--- a/Parse/Platform/Sessions/ParseSession.cs
+++ b/Parse/Platform/Sessions/ParseSession.cs
@@ -1,21 +1,23 @@
 using System.Collections.Generic;
 
-namespace Parse
-{
-    /// 
-    /// Represents a session of a user for a Parse application.
-    /// 
-    [ParseClassName("_Session")]
-    public class ParseSession : ParseObject
-    {
-        static HashSet ImmutableKeys { get; } = new HashSet { "sessionToken", "createdWith", "restricted", "user", "expiresAt", "installationId" };
+namespace Parse;
 
-        protected override bool CheckKeyMutable(string key) => !ImmutableKeys.Contains(key);
+/// 
+/// Represents a session of a user for a Parse application.
+/// 
+[ParseClassName("_Session")]
+public class ParseSession : ParseObject
+{
+    static HashSet ImmutableKeys { get; } = new HashSet { "sessionToken", "createdWith", "restricted", "user", "expiresAt", "installationId" };
 
-        /// 
-        /// Gets the session token for a user, if they are logged in.
-        /// 
-        [ParseFieldName("sessionToken")]
-        public string SessionToken => GetProperty(default, "SessionToken");
+    protected override bool CheckKeyMutable(string key)
+    {
+        return !ImmutableKeys.Contains(key);
     }
+
+    /// 
+    /// Gets the session token for a user, if they are logged in.
+    /// 
+    [ParseFieldName("sessionToken")]
+    public string SessionToken => GetProperty(default, "SessionToken");
 }
diff --git a/Parse/Platform/Sessions/ParseSessionController.cs b/Parse/Platform/Sessions/ParseSessionController.cs
index 5e448fa6..fd69829f 100644
--- a/Parse/Platform/Sessions/ParseSessionController.cs
+++ b/Parse/Platform/Sessions/ParseSessionController.cs
@@ -5,27 +5,58 @@
 using Parse.Abstractions.Infrastructure.Execution;
 using Parse.Abstractions.Infrastructure;
 using Parse.Abstractions.Platform.Sessions;
-using Parse.Infrastructure.Utilities;
 using Parse.Abstractions.Platform.Objects;
 using Parse.Infrastructure.Execution;
 using Parse.Infrastructure.Data;
 
-namespace Parse.Platform.Sessions
+namespace Parse.Platform.Sessions;
+
+public class ParseSessionController : IParseSessionController
 {
-    public class ParseSessionController : IParseSessionController
+    IParseCommandRunner CommandRunner { get; }
+
+    IParseDataDecoder Decoder { get; }
+
+    public ParseSessionController(IParseCommandRunner commandRunner, IParseDataDecoder decoder) => (CommandRunner, Decoder) = (commandRunner, decoder);
+
+    public async Task GetSessionAsync(string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default)
     {
-        IParseCommandRunner CommandRunner { get; }
+        var result = await CommandRunner.RunCommandAsync(
+            new ParseCommand("sessions/me", method: "GET", sessionToken: sessionToken, data: null),
+            cancellationToken: cancellationToken
+        );
+
+        return ParseObjectCoder.Instance.Decode(result.Item2, Decoder, serviceHub);
+    }
 
-        IParseDataDecoder Decoder { get; }
 
-        public ParseSessionController(IParseCommandRunner commandRunner, IParseDataDecoder decoder) => (CommandRunner, Decoder) = (commandRunner, decoder);
+    public Task RevokeAsync(string sessionToken, CancellationToken cancellationToken = default)
+    {
+        return CommandRunner
+            .RunCommandAsync(new ParseCommand("logout", method: "POST", sessionToken: sessionToken, data: new Dictionary { }), cancellationToken: cancellationToken);
+    }
+
+    public async Task UpgradeToRevocableSessionAsync(
+       string sessionToken,
+       IServiceHub serviceHub,
+       CancellationToken cancellationToken = default)
+    {
+        var command = new ParseCommand(
+            "upgradeToRevocableSession",
+            method: "POST",
+            sessionToken: sessionToken,
+            data: new Dictionary()
+        );
 
-        public Task GetSessionAsync(string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand("sessions/me", method: "GET", sessionToken: sessionToken, data: null), cancellationToken: cancellationToken).OnSuccess(task => ParseObjectCoder.Instance.Decode(task.Result.Item2, Decoder, serviceHub));
+        var response = await CommandRunner.RunCommandAsync(command,null,null, cancellationToken).ConfigureAwait(false);
+        var decoded = ParseObjectCoder.Instance.Decode(response.Item2, Decoder, serviceHub);
 
-        public Task RevokeAsync(string sessionToken, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand("logout", method: "POST", sessionToken: sessionToken, data: new Dictionary { }), cancellationToken: cancellationToken);
+        return decoded;
+    }
 
-        public Task UpgradeToRevocableSessionAsync(string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand("upgradeToRevocableSession", method: "POST", sessionToken: sessionToken, data: new Dictionary()), cancellationToken: cancellationToken).OnSuccess(task => ParseObjectCoder.Instance.Decode(task.Result.Item2, Decoder, serviceHub));
 
-        public bool IsRevocableSessionToken(string sessionToken) => sessionToken.Contains("r:");
+    public bool IsRevocableSessionToken(string sessionToken)
+    {
+        return sessionToken.Contains("r:");
     }
 }
diff --git a/Parse/Platform/Users/ParseCurrentUserController.cs b/Parse/Platform/Users/ParseCurrentUserController.cs
index dfda99fc..65cc03bd 100644
--- a/Parse/Platform/Users/ParseCurrentUserController.cs
+++ b/Parse/Platform/Users/ParseCurrentUserController.cs
@@ -9,106 +9,146 @@
 using Parse.Abstractions.Platform.Users;
 using Parse.Infrastructure.Utilities;
 using Parse.Infrastructure.Data;
+using System;
+using System.Diagnostics;
 
-namespace Parse.Platform.Users
-{
-#warning This class needs to be rewritten (PCuUsC).
+namespace Parse.Platform.Users;
 
-    public class ParseCurrentUserController : IParseCurrentUserController
-    {
-        object Mutex { get; } = new object { };
+#pragma warning disable CS1030 // #warning directive
+#warning This class needs to be rewritten (PCuUsC).
 
-        TaskQueue TaskQueue { get; } = new TaskQueue { };
 
-        ICacheController StorageController { get; }
+public class ParseCurrentUserController : IParseCurrentUserController
+{
+    private readonly ICacheController StorageController;
+    private readonly IParseObjectClassController ClassController;
+    private readonly IParseDataDecoder Decoder;
 
-        IParseObjectClassController ClassController { get; }
+    private readonly TaskQueue TaskQueue = new();
+    private ParseUser? currentUser; // Nullable to explicitly handle absence of a user
 
-        IParseDataDecoder Decoder { get; }
+    public ParseCurrentUserController(ICacheController storageController, IParseObjectClassController classController, IParseDataDecoder decoder)
+    {
+        StorageController = storageController ?? throw new ArgumentNullException(nameof(storageController));
+        ClassController = classController ?? throw new ArgumentNullException(nameof(classController));
+        Decoder = decoder ?? throw new ArgumentNullException(nameof(decoder));
+    }
 
-        public ParseCurrentUserController(ICacheController storageController, IParseObjectClassController classController, IParseDataDecoder decoder) => (StorageController, ClassController, Decoder) = (storageController, classController, decoder);
+    public ParseUser CurrentUser
+    {
+        get => currentUser;
+        private set => currentUser = value; // Setter is private to ensure controlled modification
+    }
+    private static string GenerateParseObjectId()
+    {
+        const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+        var random = new Random();
+        return new string(Enumerable.Repeat(chars, 10)
+            .Select(s => s[random.Next(s.Length)]).ToArray());
+    }
 
-        ParseUser currentUser;
-        public ParseUser CurrentUser
+    public async Task SetAsync(ParseUser user, CancellationToken cancellationToken)
+    {
+        var usr = await TaskQueue.Enqueue>(async _ =>
         {
-            get
-            {
-                lock (Mutex)
-                    return currentUser;
-            }
-            set
+            if (user == null)
             {
-                lock (Mutex)
-                    currentUser = value;
+                var storage = await StorageController.LoadAsync().ConfigureAwait(false);
+                await storage.RemoveAsync(nameof(CurrentUser)).ConfigureAwait(false);
             }
-        }
-
-        public Task SetAsync(ParseUser user, CancellationToken cancellationToken) => TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ =>
-        {
-            Task saveTask = default;
-
-            if (user is null)
-                saveTask = StorageController.LoadAsync().OnSuccess(task => task.Result.RemoveAsync(nameof(CurrentUser))).Unwrap();
             else
             {
-                // TODO (hallucinogen): we need to use ParseCurrentCoder instead of this janky encoding
+                // Use ParseCurrentCoder for encoding if available
+                var data = new Dictionary
+                {
+                    ["objectId"] = user.ObjectId ?? GenerateParseObjectId()
+                };
+
+                // Additional properties can be added to the dictionary as needed
 
-                IDictionary data = user.ServerDataToJSONObjectForSerialization();
-                data["objectId"] = user.ObjectId;
 
                 if (user.CreatedAt != null)
                     data["createdAt"] = user.CreatedAt.Value.ToString(ParseClient.DateFormatStrings.First(), CultureInfo.InvariantCulture);
+
                 if (user.UpdatedAt != null)
                     data["updatedAt"] = user.UpdatedAt.Value.ToString(ParseClient.DateFormatStrings.First(), CultureInfo.InvariantCulture);
 
-                saveTask = StorageController.LoadAsync().OnSuccess(task => task.Result.AddAsync(nameof(CurrentUser), JsonUtilities.Encode(data))).Unwrap();
+                var storage = await StorageController.LoadAsync().ConfigureAwait(false);
+                await storage.AddAsync(nameof(CurrentUser), JsonUtilities.Encode(data)).ConfigureAwait(false);
+                
+                CurrentUser = user;
             }
 
-            CurrentUser = user;
-            return saveTask;
-        }).Unwrap(), cancellationToken);
+            return user; // Enforce return type as `Task`
+        }, cancellationToken).ConfigureAwait(false);
+        
+        return usr;
+    }
 
-        public Task GetAsync(IServiceHub serviceHub, CancellationToken cancellationToken = default)
-        {
-            ParseUser cachedCurrent;
 
-            lock (Mutex)
-                cachedCurrent = CurrentUser;
+    public async Task GetAsync(IServiceHub serviceHub, CancellationToken cancellationToken = default)
+    {
+        
+        if (CurrentUser is { ObjectId: { } })
+            return CurrentUser;
 
-            return cachedCurrent is { } ? Task.FromResult(cachedCurrent) : TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => StorageController.LoadAsync().OnSuccess(task =>
+        var usr = await TaskQueue.Enqueue>(async _ =>
+        {
+            var storage = await StorageController.LoadAsync().ConfigureAwait(false);
+            if (storage.TryGetValue(nameof(CurrentUser), out var serializedData) && serializedData is string serialization)
+            {
+                var state = ParseObjectCoder.Instance.Decode(JsonUtilities.Parse(serialization) as IDictionary, Decoder, serviceHub);
+                
+                CurrentUser = ClassController.GenerateObjectFromState(state, "_User", serviceHub);
+            }
+            else
             {
-                task.Result.TryGetValue(nameof(CurrentUser), out object data);
-                ParseUser user = default;
+                CurrentUser = null;
+            }
 
-                if (data is string { } serialization)
-                    user = ClassController.GenerateObjectFromState(ParseObjectCoder.Instance.Decode(JsonUtilities.Parse(serialization) as IDictionary, Decoder, serviceHub), "_User", serviceHub);
+            return CurrentUser; // Explicitly return the current user (or null)
+        }, cancellationToken).ConfigureAwait(false);
 
-                return CurrentUser = user;
-            })).Unwrap(), cancellationToken);
-        }
+        return usr;
+    }
 
-        public Task ExistsAsync(CancellationToken cancellationToken) => CurrentUser is { } ? Task.FromResult(true) : TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => StorageController.LoadAsync().OnSuccess(t => t.Result.ContainsKey(nameof(CurrentUser)))).Unwrap(), cancellationToken);
 
-        public bool IsCurrent(ParseUser user)
+    public async Task ExistsAsync(CancellationToken cancellationToken = default)
+    {
+        return CurrentUser != null || await TaskQueue.Enqueue(async _ =>
         {
-            lock (Mutex)
-                return CurrentUser == user;
-        }
+            var storage = await StorageController.LoadAsync().ConfigureAwait(false);
+            return storage.ContainsKey(nameof(CurrentUser));
+        }, cancellationToken).ConfigureAwait(false);
+    }
 
-        public void ClearFromMemory() => CurrentUser = default;
+    public bool IsCurrent(ParseUser user) => CurrentUser == user;
 
-        public void ClearFromDisk()
-        {
-            lock (Mutex)
-            {
-                ClearFromMemory();
+    public void ClearFromMemory() => CurrentUser = null;
 
-                TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => StorageController.LoadAsync().OnSuccess(t => t.Result.RemoveAsync(nameof(CurrentUser)))).Unwrap().Unwrap(), CancellationToken.None);
-            }
-        }
+    public async Task ClearFromDiskAsync()
+    {
+        ClearFromMemory();
+        await TaskQueue.Enqueue(async _ =>
+        {
+            var storage = await StorageController.LoadAsync().ConfigureAwait(false);
+            await storage.RemoveAsync(nameof(CurrentUser)).ConfigureAwait(false);
+        }, CancellationToken.None).ConfigureAwait(false);
+    }
 
-        public Task GetCurrentSessionTokenAsync(IServiceHub serviceHub, CancellationToken cancellationToken = default) => GetAsync(serviceHub, cancellationToken).OnSuccess(task => task.Result?.SessionToken);
+    public async Task GetCurrentSessionTokenAsync(IServiceHub serviceHub, CancellationToken cancellationToken = default)
+    {
+        var user = await GetAsync(serviceHub, cancellationToken).ConfigureAwait(false);
+        return user?.SessionToken;
+    }
 
-        public Task LogOutAsync(IServiceHub serviceHub, CancellationToken cancellationToken = default) => TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => GetAsync(serviceHub, cancellationToken)).Unwrap().OnSuccess(task => ClearFromDisk()), cancellationToken);
+    public async Task LogOutAsync(IServiceHub serviceHub, CancellationToken cancellationToken = default)
+    {
+        await TaskQueue.Enqueue(async _ =>
+        {
+            await GetAsync(serviceHub, cancellationToken).ConfigureAwait(false);
+            await ClearFromDiskAsync();
+        }, cancellationToken).ConfigureAwait(false);
     }
-}
+
+}
\ No newline at end of file
diff --git a/Parse/Platform/Users/ParseUser.cs b/Parse/Platform/Users/ParseUser.cs
index 74f7c914..1a3832ed 100644
--- a/Parse/Platform/Users/ParseUser.cs
+++ b/Parse/Platform/Users/ParseUser.cs
@@ -1,327 +1,270 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Threading;
 using System.Threading.Tasks;
-using Parse.Abstractions.Infrastructure.Control;
 using Parse.Abstractions.Platform.Authentication;
 using Parse.Abstractions.Platform.Objects;
-using Parse.Infrastructure.Utilities;
 
-namespace Parse
+namespace Parse;
+
+[ParseClassName("_User")]
+public class ParseUser : ParseObject
 {
-    /// 
-    /// Represents a user for a Parse application.
-    /// 
-    [ParseClassName("_User")]
-    public class ParseUser : ParseObject
+    public async Task IsAuthenticatedAsync()
     {
-        /// 
-        /// Whether the ParseUser has been authenticated on this device. Only an authenticated
-        /// ParseUser can be saved and deleted.
-        /// 
-        public bool IsAuthenticated
+        // Early exit if SessionToken is null
+        lock (Mutex)
         {
-            get
-            {
-                lock (Mutex)
-                {
-                    return SessionToken is { } && Services.GetCurrentUser() is { } user && user.ObjectId == ObjectId;
-                }
-            }
+            if (SessionToken == null)
+                return false;
         }
 
-        /// 
-        /// Removes a key from the object's data if it exists.
-        /// 
-        /// The key to remove.
-        /// Cannot remove the username key.
-        public override void Remove(string key)
+        try
         {
-            if (key == "username")
-            {
-                throw new InvalidOperationException("Cannot remove the username key.");
-            }
+            // Await the asynchronous GetCurrentUserAsync method
+            var currentUser = await Services.GetCurrentUserAsync();
 
-            base.Remove(key);
+            // Check if the current user's ObjectId matches
+            return currentUser?.ObjectId == ObjectId;
         }
-
-        protected override bool CheckKeyMutable(string key) => !ImmutableKeys.Contains(key);
-
-        internal override void HandleSave(IObjectState serverState)
+        catch (Exception ex)
         {
-            base.HandleSave(serverState);
+            
+            return false;
+        }
+    }
 
-            SynchronizeAllAuthData();
-            CleanupAuthData();
+    public override void Remove(string key)
+    {
+        if (key == "username")
+            throw new InvalidOperationException("Cannot remove the username key.");
 
-            MutateState(mutableClone => mutableClone.ServerData.Remove("password"));
-        }
+        base.Remove(key);
+    }
 
-        public string SessionToken => State.ContainsKey("sessionToken") ? State["sessionToken"] as string : null;
+    protected override bool CheckKeyMutable(string key) => !ImmutableKeys.Contains(key);
 
-        internal Task SetSessionTokenAsync(string newSessionToken) => SetSessionTokenAsync(newSessionToken, CancellationToken.None);
+    internal override void HandleSave(IObjectState serverState)
+    {
+        base.HandleSave(serverState);
+        SynchronizeAllAuthData();
+        CleanupAuthData();
+        MutateState(mutableClone => mutableClone.ServerData.Remove("password"));
+    }
 
-        internal Task SetSessionTokenAsync(string newSessionToken, CancellationToken cancellationToken)
-        {
-            MutateState(mutableClone => mutableClone.ServerData["sessionToken"] = newSessionToken);
-            return Services.SaveCurrentUserAsync(this);
-        }
+    public string SessionToken => State.ContainsKey("sessionToken") ? State["sessionToken"] as string : null;
 
-        /// 
-        /// Gets or sets the username.
-        /// 
-        [ParseFieldName("username")]
-        public string Username
-        {
-            get => GetProperty(null, nameof(Username));
-            set => SetProperty(value, nameof(Username));
-        }
 
-        /// 
-        /// Sets the password.
-        /// 
-        [ParseFieldName("password")]
-        public string Password
-        {
-            get => GetProperty(null, nameof(Password));
-            set => SetProperty(value, nameof(Password));
-        }
+    internal async Task SetSessionTokenAsync(string newSessionToken, CancellationToken cancellationToken = default)
+    {
+        MutateState(mutableClone => mutableClone.ServerData["sessionToken"] = newSessionToken);
+        await Services.SaveCurrentUserAsync(this, cancellationToken).ConfigureAwait(false);
+    }
 
-        /// 
-        /// Sets the email address.
-        /// 
-        [ParseFieldName("email")]
-        public string Email
-        {
-            get => GetProperty(null, nameof(Email));
-            set => SetProperty(value, nameof(Email));
-        }
+    [ParseFieldName("username")]
+    public string Username
+    {
+        get => GetProperty(null, nameof(Username));
+        set => SetProperty(value, nameof(Username));
+    }
 
-        internal Task SignUpAsync(Task toAwait, CancellationToken cancellationToken)
-        {
-            if (AuthData == null)
-            {
-                // TODO (hallucinogen): make an Extension of Task to create Task with exception/canceled.
-                if (String.IsNullOrEmpty(Username))
-                {
-                    TaskCompletionSource tcs = new TaskCompletionSource();
-                    tcs.TrySetException(new InvalidOperationException("Cannot sign up user with an empty name."));
-                    return tcs.Task;
-                }
-                if (String.IsNullOrEmpty(Password))
-                {
-                    TaskCompletionSource tcs = new TaskCompletionSource();
-                    tcs.TrySetException(new InvalidOperationException("Cannot sign up user with an empty password."));
-                    return tcs.Task;
-                }
-            }
-            if (!String.IsNullOrEmpty(ObjectId))
-            {
-                TaskCompletionSource tcs = new TaskCompletionSource();
-                tcs.TrySetException(new InvalidOperationException("Cannot sign up a user that already exists."));
-                return tcs.Task;
-            }
+    [ParseFieldName("password")]
+    public string Password
+    {
+        get => GetProperty(null, nameof(Password));
+        set => SetProperty(value, nameof(Password));
+    }
 
-            IDictionary currentOperations = StartSave();
+    [ParseFieldName("email")]
+    public string Email
+    {
+        get => GetProperty(null, nameof(Email));
+        set => SetProperty(value, nameof(Email));
+    }
 
-            return toAwait.OnSuccess(_ => Services.UserController.SignUpAsync(State, currentOperations, Services, cancellationToken)).Unwrap().ContinueWith(t =>
-            {
-                if (t.IsFaulted || t.IsCanceled)
-                {
-                    HandleFailedSave(currentOperations);
-                }
-                else
-                {
-                    HandleSave(t.Result);
-                }
-                return t;
-            }).Unwrap().OnSuccess(_ => Services.SaveCurrentUserAsync(this)).Unwrap();
-        }
+    internal async Task SignUpAsync(CancellationToken cancellationToken = default)
+    {
+        
 
-        /// 
-        /// Signs up a new user. This will create a new ParseUser on the server and will also persist the
-        /// session on disk so that you can access the user using . A username and
-        /// password must be set before calling SignUpAsync.
-        /// 
-        public Task SignUpAsync() => SignUpAsync(CancellationToken.None);
-
-        /// 
-        /// Signs up a new user. This will create a new ParseUser on the server and will also persist the
-        /// session on disk so that you can access the user using . A username and
-        /// password must be set before calling SignUpAsync.
-        /// 
-        /// The cancellation token.
-        public Task SignUpAsync(CancellationToken cancellationToken) => TaskQueue.Enqueue(toAwait => SignUpAsync(toAwait, cancellationToken), cancellationToken);
-
-        protected override Task SaveAsync(Task toAwait, CancellationToken cancellationToken)
-        {
-            lock (Mutex)
-            {
-                if (ObjectId is null)
-                {
-                    throw new InvalidOperationException("You must call SignUpAsync before calling SaveAsync.");
-                }
+        if (string.IsNullOrWhiteSpace(Username))
+            throw new InvalidOperationException("Cannot sign up user with an empty name.");
 
-                return base.SaveAsync(toAwait, cancellationToken).OnSuccess(_ => Services.CurrentUserController.IsCurrent(this) ? Services.SaveCurrentUserAsync(this) : Task.CompletedTask).Unwrap();
-            }
-        }
+        if (string.IsNullOrWhiteSpace(Password))
+            throw new InvalidOperationException("Cannot sign up user with an empty password.");
 
-        // If this is already the current user, refresh its state on disk.
-        internal override Task FetchAsyncInternal(Task toAwait, CancellationToken cancellationToken) => base.FetchAsyncInternal(toAwait, cancellationToken).OnSuccess(t => !Services.CurrentUserController.IsCurrent(this) ? Task.FromResult(t.Result) : Services.SaveCurrentUserAsync(this).OnSuccess(_ => t.Result)).Unwrap();
+        if (!string.IsNullOrWhiteSpace(ObjectId))
+            throw new InvalidOperationException("Cannot sign up a user that already exists.");
 
-        internal Task LogOutAsync(Task toAwait, CancellationToken cancellationToken)
-        {
-            string oldSessionToken = SessionToken;
-            if (oldSessionToken == null)
-            {
-                return Task.FromResult(0);
-            }
+        
 
-            // Cleanup in-memory session.
+        var currentOperations = StartSave();
 
-            MutateState(mutableClone => mutableClone.ServerData.Remove("sessionToken"));
-            Task revokeSessionTask = Services.RevokeSessionAsync(oldSessionToken, cancellationToken);
-            return Task.WhenAll(revokeSessionTask, Services.CurrentUserController.LogOutAsync(Services, cancellationToken));
+        try
+        {
+            var result = await Services.UserController.SignUpAsync(State, currentOperations, Services, cancellationToken).ConfigureAwait(false);
+            Debug.WriteLine($"SignUpAsync on UserController completed. ObjectId: {result.ObjectId}");
+            HandleSave(result);
+            var usr= await Services.SaveAndReturnCurrentUserAsync(this).ConfigureAwait(false);
+            
+            return usr;
+        }
+        catch (Exception ex)
+        {
+            Debug.WriteLine($"SignUpAsync failed: {ex.Message}");
+            HandleFailedSave(currentOperations);
+            throw;
         }
+    }
 
-        internal Task UpgradeToRevocableSessionAsync() => UpgradeToRevocableSessionAsync(CancellationToken.None);
 
-        internal Task UpgradeToRevocableSessionAsync(CancellationToken cancellationToken) => TaskQueue.Enqueue(toAwait => UpgradeToRevocableSessionAsync(toAwait, cancellationToken), cancellationToken);
+    protected override async Task SaveAsync(Task toAwait, CancellationToken cancellationToken)
+    {
+        await toAwait.ConfigureAwait(false);
 
-        internal Task UpgradeToRevocableSessionAsync(Task toAwait, CancellationToken cancellationToken)
-        {
-            string sessionToken = SessionToken;
+        if (ObjectId is null)
+            throw new InvalidOperationException("You must call SignUpAsync before calling SaveAsync.");
 
-            return toAwait.OnSuccess(_ => Services.UpgradeToRevocableSessionAsync(sessionToken, cancellationToken)).Unwrap().OnSuccess(task => SetSessionTokenAsync(task.Result)).Unwrap();
-        }
+        await base.SaveAsync(toAwait, cancellationToken).ConfigureAwait(false);
 
-        /// 
-        /// Gets the authData for this user.
-        /// 
-        public IDictionary> AuthData
+        if (Services.CurrentUserController.IsCurrent(this))
         {
-            get => TryGetValue("authData", out IDictionary> authData) ? authData : null;
-            set => this["authData"] = value;
+            await Services.SaveCurrentUserAsync(this, cancellationToken).ConfigureAwait(false);
         }
+    }
+
+    internal override async Task FetchAsyncInternal(CancellationToken cancellationToken)
+    {
+        //await toAwait.ConfigureAwait(false);
+
+        var result = await base.FetchAsyncInternal(cancellationToken).ConfigureAwait(false);
 
-        /// 
-        /// Removes null values from authData (which exist temporarily for unlinking)
-        /// 
-        void CleanupAuthData()
+        if (Services.CurrentUserController.IsCurrent(this))
         {
-            lock (Mutex)
-            {
-                if (!Services.CurrentUserController.IsCurrent(this))
-                {
-                    return;
-                }
+            await Services.SaveCurrentUserAsync(this, cancellationToken).ConfigureAwait(false);
+        }
 
-                IDictionary> authData = AuthData;
+        return result;
+    }
 
-                if (authData == null)
-                {
-                    return;
-                }
+    internal async Task LogOutAsync(CancellationToken cancellationToken)
+    {
+        var oldSessionToken = SessionToken;
+        if (oldSessionToken == null)
+            return;
 
-                foreach (KeyValuePair> pair in new Dictionary>(authData))
-                {
-                    if (pair.Value == null)
-                    {
-                        authData.Remove(pair.Key);
-                    }
-                }
-            }
-        }
+        MutateState(mutableClone => mutableClone.ServerData.Remove("sessionToken"));
 
-#warning Check if the following properties should be injected via IServiceHub.UserController (except for ImmutableKeys).
+        await Task.WhenAll(
+            Services.RevokeSessionAsync(oldSessionToken, cancellationToken),
+            Services.CurrentUserController.LogOutAsync(Services, cancellationToken)
+        ).ConfigureAwait(false);
+    }
 
-        internal static IParseAuthenticationProvider GetProvider(string providerName) => Authenticators.TryGetValue(providerName, out IParseAuthenticationProvider provider) ? provider : null;
+    internal async Task UpgradeToRevocableSessionAsync(CancellationToken cancellationToken = default)
+    {
+        var sessionToken = SessionToken;
+        var newSessionToken = await Services.UpgradeToRevocableSessionAsync(sessionToken, cancellationToken).ConfigureAwait(false);
+        await SetSessionTokenAsync(newSessionToken, cancellationToken).ConfigureAwait(false);
+    }
+    //public string SessionToken => State.ContainsKey("sessionToken") ? State["sessionToken"] as string : null;
 
-        internal static IDictionary Authenticators { get; } = new Dictionary { };
+    public IDictionary> AuthData
+    {
 
-        internal static HashSet ImmutableKeys { get; } = new HashSet { "sessionToken", "isNew" };
+        get => ContainsKey("authData") ? AuthData["authData"] as IDictionary> : null;
+        set => this["authData"] = value;
+    }
 
-        /// 
-        /// Synchronizes authData for all providers.
-        /// 
-        internal void SynchronizeAllAuthData()
+    void CleanupAuthData()
+    {
+        lock (Mutex)
         {
-            lock (Mutex)
-            {
-                IDictionary> authData = AuthData;
+            if (!Services.CurrentUserController.IsCurrent(this))
+                return;
 
-                if (authData == null)
-                {
-                    return;
-                }
+            var authData = AuthData;
+            if (authData == null)
+                return;
 
-                foreach (KeyValuePair> pair in authData)
+            foreach (var key in new List(authData.Keys))
+            {
+                if (authData[key] == null)
                 {
-                    SynchronizeAuthData(GetProvider(pair.Key));
+                    authData.Remove(key);
                 }
             }
         }
+    }
 
-        internal void SynchronizeAuthData(IParseAuthenticationProvider provider)
+    internal async Task LinkWithAsync(string authType, IDictionary data, CancellationToken cancellationToken)
+    {
+        lock (Mutex)
         {
-            bool restorationSuccess = false;
+            AuthData ??= new Dictionary>();
+            AuthData[authType] = data;
+        }
 
-            lock (Mutex)
-            {
-                IDictionary> authData = AuthData;
+        await SaveAsync(cancellationToken).ConfigureAwait(false);
+    }
 
-                if (authData == null || provider == null)
-                {
-                    return;
-                }
+    internal async Task LinkWithAsync(string authType, CancellationToken cancellationToken)
+    {
+        var provider = GetProvider(authType);
+        if (provider != null)
+        {
+            var authData = await provider.AuthenticateAsync(cancellationToken).ConfigureAwait(false);
+            await LinkWithAsync(authType, authData, cancellationToken).ConfigureAwait(false);
+        }
+    }
 
-                if (authData.TryGetValue(provider.AuthType, out IDictionary data))
-                {
-                    restorationSuccess = provider.RestoreAuthentication(data);
-                }
-            }
+    internal Task UnlinkFromAsync(string authType, CancellationToken cancellationToken)
+    {
+        return LinkWithAsync(authType, null, cancellationToken);
+    }
 
-            if (!restorationSuccess)
-            {
-                UnlinkFromAsync(provider.AuthType, CancellationToken.None);
-            }
+    internal bool IsLinked(string authType)
+    {
+        lock (Mutex)
+        {
+            return AuthData != null && AuthData.TryGetValue(authType, out var data) && data != null;
         }
+    }
 
-        internal Task LinkWithAsync(string authType, IDictionary data, CancellationToken cancellationToken) => TaskQueue.Enqueue(toAwait =>
-        {
-            IDictionary> authData = AuthData;
+    internal static IParseAuthenticationProvider GetProvider(string providerName)
+    {
+        return Authenticators.TryGetValue(providerName, out var provider) ? provider : null;
+    }
+
+    internal static IDictionary Authenticators { get; } = new Dictionary();
+    internal static HashSet ImmutableKeys { get; } = new() { "sessionToken", "isNew" };
 
+    internal void SynchronizeAllAuthData()
+    {
+        lock (Mutex)
+        {
+            var authData = AuthData;
             if (authData == null)
+                return;
+
+            foreach (var provider in authData.Keys)
             {
-                authData = AuthData = new Dictionary>();
+                SynchronizeAuthData(GetProvider(provider));
             }
-
-            authData[authType] = data;
-            AuthData = authData;
-
-            return SaveAsync(cancellationToken);
-        }, cancellationToken);
-
-        internal Task LinkWithAsync(string authType, CancellationToken cancellationToken)
-        {
-            IParseAuthenticationProvider provider = GetProvider(authType);
-            return provider.AuthenticateAsync(cancellationToken).OnSuccess(t => LinkWithAsync(authType, t.Result, cancellationToken)).Unwrap();
         }
+    }
 
-        /// 
-        /// Unlinks a user from a service.
-        /// 
-        internal Task UnlinkFromAsync(string authType, CancellationToken cancellationToken) => LinkWithAsync(authType, null, cancellationToken);
+    internal void SynchronizeAuthData(IParseAuthenticationProvider provider)
+    {
+        if (provider == null || AuthData == null)
+            return;
+
+        if (!AuthData.TryGetValue(provider.AuthType, out var data))
+            return;
 
-        /// 
-        /// Checks whether a user is linked to a service.
-        /// 
-        internal bool IsLinked(string authType)
+        if (!provider.RestoreAuthentication(data))
         {
-            lock (Mutex)
-            {
-                return AuthData != null && AuthData.ContainsKey(authType) && AuthData[authType] != null;
-            }
+            UnlinkFromAsync(provider.AuthType, CancellationToken.None);
         }
     }
 }
diff --git a/Parse/Platform/Users/ParseUserController.cs b/Parse/Platform/Users/ParseUserController.cs
index 0d2097e0..20add59b 100644
--- a/Parse/Platform/Users/ParseUserController.cs
+++ b/Parse/Platform/Users/ParseUserController.cs
@@ -6,41 +6,125 @@
 using Parse.Abstractions.Infrastructure.Execution;
 using Parse.Abstractions.Infrastructure;
 using Parse.Abstractions.Platform.Users;
-using Parse.Infrastructure.Utilities;
 using Parse.Abstractions.Platform.Objects;
 using Parse.Infrastructure.Execution;
 using Parse.Infrastructure.Data;
+using System.Net.Http;
+using System;
 
-namespace Parse.Platform.Users
+namespace Parse.Platform.Users;
+
+
+public class ParseUserController : IParseUserController
 {
-    public class ParseUserController : IParseUserController
+    private IParseCommandRunner CommandRunner { get; }
+    private IParseDataDecoder Decoder { get; }
+
+    public bool RevocableSessionEnabled { get; set; } = false; // Use a simple property
+
+    public ParseUserController(IParseCommandRunner commandRunner, IParseDataDecoder decoder)
     {
-        IParseCommandRunner CommandRunner { get; }
+        CommandRunner = commandRunner ?? throw new ArgumentNullException(nameof(commandRunner));
+        Decoder = decoder ?? throw new ArgumentNullException(nameof(decoder));
+    }
 
-        IParseDataDecoder Decoder { get; }
+    public async Task SignUpAsync(
+        IObjectState state,
+        IDictionary operations,
+        IServiceHub serviceHub,
+        CancellationToken cancellationToken = default)
+    {
+        if (state == null)
+            throw new ArgumentNullException(nameof(state));
+        if (operations == null)
+            throw new ArgumentNullException(nameof(operations));
+        if (serviceHub == null)
+            throw new ArgumentNullException(nameof(serviceHub));
 
-        public bool RevocableSessionEnabled { get; set; }
+        var command = new ParseCommand(
+            "classes/_User",
+            HttpMethod.Post.ToString(),
+            data: serviceHub.GenerateJSONObjectForSaving(operations));
 
-        public object RevocableSessionEnabledMutex { get; } = new object { };
+        var result = await CommandRunner.RunCommandAsync(command).ConfigureAwait(false);
+        return ParseObjectCoder.Instance
+            .Decode(result.Item2, Decoder, serviceHub)
+            .MutatedClone(mutableClone => mutableClone.IsNew = true);
+    }
 
-        public ParseUserController(IParseCommandRunner commandRunner, IParseDataDecoder decoder) => (CommandRunner, Decoder) = (commandRunner, decoder);
+    public async Task LogInAsync(
+        string username,
+        string password,
+        IServiceHub serviceHub,
+        CancellationToken cancellationToken = default)
+    {
+        if (string.IsNullOrWhiteSpace(username))
+            throw new ArgumentException("Username cannot be null or empty.", nameof(username));
+        if (string.IsNullOrWhiteSpace(password))
+            throw new ArgumentException("Password cannot be null or empty.", nameof(password));
+        if (serviceHub == null)
+            throw new ArgumentNullException(nameof(serviceHub));
 
-        public Task SignUpAsync(IObjectState state, IDictionary operations, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand("classes/_User", method: "POST", data: serviceHub.GenerateJSONObjectForSaving(operations)), cancellationToken: cancellationToken).OnSuccess(task => ParseObjectCoder.Instance.Decode(task.Result.Item2, Decoder, serviceHub).MutatedClone(mutableClone => mutableClone.IsNew = true));
+        // Use POST for login with credentials in the body to improve security
+        var command = new ParseCommand(
+            "login",
+            HttpMethod.Post.ToString(),
+            data: new Dictionary { ["username"] = username, ["password"] = password });
 
-        public Task LogInAsync(string username, string password, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand($"login?{ParseClient.BuildQueryString(new Dictionary { [nameof(username)] = username, [nameof(password)] = password })}", method: "GET", data: null), cancellationToken: cancellationToken).OnSuccess(task => ParseObjectCoder.Instance.Decode(task.Result.Item2, Decoder, serviceHub).MutatedClone(mutableClone => mutableClone.IsNew = task.Result.Item1 == System.Net.HttpStatusCode.Created));
+        var result = await CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).ConfigureAwait(false);
+        return ParseObjectCoder.Instance
+            .Decode(result.Item2, Decoder, serviceHub)
+            .MutatedClone(mutableClone => mutableClone.IsNew = result.Item1 == System.Net.HttpStatusCode.Created);
+    }
 
-        public Task LogInAsync(string authType, IDictionary data, IServiceHub serviceHub, CancellationToken cancellationToken = default)
-        {
-            Dictionary authData = new Dictionary
-            {
-                [authType] = data
-            };
+    public async Task LogInAsync(
+        string authType,
+        IDictionary data,
+        IServiceHub serviceHub,
+        CancellationToken cancellationToken = default)
+    {
+        if (string.IsNullOrWhiteSpace(authType))
+            throw new ArgumentException("AuthType cannot be null or empty.", nameof(authType));
+        if (data == null)
+            throw new ArgumentNullException(nameof(data));
+        if (serviceHub == null)
+            throw new ArgumentNullException(nameof(serviceHub));
 
-            return CommandRunner.RunCommandAsync(new ParseCommand("users", method: "POST", data: new Dictionary { [nameof(authData)] = authData }), cancellationToken: cancellationToken).OnSuccess(task => ParseObjectCoder.Instance.Decode(task.Result.Item2, Decoder, serviceHub).MutatedClone(mutableClone => mutableClone.IsNew = task.Result.Item1 == System.Net.HttpStatusCode.Created));
-        }
+        var authData = new Dictionary { [authType] = data };
+
+        var command = new ParseCommand("users",HttpMethod.Post.ToString(),data: new Dictionary { ["authData"] = authData });
+
+        var result = await CommandRunner.RunCommandAsync(command).ConfigureAwait(false);
+        return ParseObjectCoder.Instance
+            .Decode(result.Item2, Decoder, serviceHub)
+            .MutatedClone(mutableClone => mutableClone.IsNew = result.Item1 == System.Net.HttpStatusCode.Created);
+    }
+
+    public async Task GetUserAsync(
+        string sessionToken,
+        IServiceHub serviceHub,
+        CancellationToken cancellationToken = default)
+    {
+        if (string.IsNullOrWhiteSpace(sessionToken))
+            throw new ArgumentException("Session token cannot be null or empty.", nameof(sessionToken));
+        if (serviceHub == null)
+            throw new ArgumentNullException(nameof(serviceHub));
+
+        var command = new ParseCommand("users/me",HttpMethod.Get.ToString(),sessionToken: sessionToken, null, null);
+        var result = await CommandRunner.RunCommandAsync(command).ConfigureAwait(false);
+        return ParseObjectCoder.Instance.Decode(result.Item2, Decoder, serviceHub);
+    }
+
+    public Task RequestPasswordResetAsync(string email, CancellationToken cancellationToken = default)
+    {
+        if (string.IsNullOrWhiteSpace(email))
+            throw new ArgumentException("Email cannot be null or empty.", nameof(email));
 
-        public Task GetUserAsync(string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand("users/me", method: "GET", sessionToken: sessionToken, data: default), cancellationToken: cancellationToken).OnSuccess(task => ParseObjectCoder.Instance.Decode(task.Result.Item2, Decoder, serviceHub));
+        var command = new ParseCommand(
+            "requestPasswordReset",
+            HttpMethod.Post.ToString(),
+            data: new Dictionary { ["email"] = email });
 
-        public Task RequestPasswordResetAsync(string email, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand("requestPasswordReset", method: "POST", data: new Dictionary { [nameof(email)] = email }), cancellationToken: cancellationToken);
+        return CommandRunner.RunCommandAsync(command);
     }
-}
+}
\ No newline at end of file
diff --git a/Parse/Properties/PublishProfiles/FolderProfile.pubxml b/Parse/Properties/PublishProfiles/FolderProfile.pubxml
new file mode 100644
index 00000000..3edd9b8c
--- /dev/null
+++ b/Parse/Properties/PublishProfiles/FolderProfile.pubxml
@@ -0,0 +1,19 @@
+
+
+
+  
+    Release
+    Any CPU
+    bin\Release\netstandard2.0\publish\win-x64\
+    FileSystem
+    <_TargetId>Folder
+    net9.0
+    win-x64
+    true
+    false
+    false
+    false
+  
+
\ No newline at end of file
diff --git a/Parse/Resources.Designer.cs b/Parse/Resources.Designer.cs
index 3b6e39ff..3a868035 100644
--- a/Parse/Resources.Designer.cs
+++ b/Parse/Resources.Designer.cs
@@ -19,7 +19,7 @@ namespace Parse {
     // class via a tool like ResGen or Visual Studio.
     // To add or remove a member, edit your .ResX file then rerun ResGen
     // with the /str option, or rebuild your VS project.
-    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
     [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
     [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
     internal class Resources {
diff --git a/Parse/Utilities/AnalyticsServiceExtensions.cs b/Parse/Utilities/AnalyticsServiceExtensions.cs
index 423d3481..ddb4f538 100644
--- a/Parse/Utilities/AnalyticsServiceExtensions.cs
+++ b/Parse/Utilities/AnalyticsServiceExtensions.cs
@@ -2,93 +2,103 @@
 using System.Collections.Generic;
 using System.Threading.Tasks;
 using Parse.Abstractions.Infrastructure;
-using Parse.Infrastructure.Utilities;
 
-namespace Parse
+namespace Parse;
+
+/// 
+/// Provides an interface to Parse's logging and analytics backend.
+///
+/// Methods will return immediately and cache requests (along with timestamps)
+/// to be handled in the background.
+/// 
+public static class AnalyticsServiceExtensions
 {
     /// 
-    /// Provides an interface to Parse's logging and analytics backend.
-    ///
-    /// Methods will return immediately and cache requests (along with timestamps)
-    /// to be handled in the background.
+    /// Tracks this application being launched.
     /// 
-    public static class AnalyticsServiceExtensions
+    /// An Async Task that can be waited on or ignored.
+    public static Task TrackLaunchAsync(this IServiceHub serviceHub)
     {
-        /// 
-        /// Tracks this application being launched.
-        /// 
-        /// An Async Task that can be waited on or ignored.
-        public static Task TrackLaunchAsync(this IServiceHub serviceHub) => TrackLaunchWithPushHashAsync(serviceHub);
+        return TrackLaunchWithPushHashAsync(serviceHub);
+    }
 
-        /// 
-        /// Tracks the occurrence of a custom event with additional dimensions.
-        /// Parse will store a data point at the time of invocation with the
-        /// given event name.
-        ///
-        /// Dimensions will allow segmentation of the occurrences of this
-        /// custom event.
-        ///
-        /// To track a user signup along with additional metadata, consider the
-        /// following:
-        /// 
-        /// IDictionary<string, string> dims = new Dictionary<string, string> {
-        ///   { "gender", "m" },
-        ///   { "source", "web" },
-        ///   { "dayType", "weekend" }
-        /// };
-        /// ParseAnalytics.TrackEventAsync("signup", dims);
-        /// 
-        ///
-        /// There is a default limit of 8 dimensions per event tracked.
-        /// 
-        /// The name of the custom event to report to ParseClient
-        /// as having happened.
-        /// An Async Task that can be waited on or ignored.
-        public static Task TrackAnalyticsEventAsync(this IServiceHub serviceHub, string name) => TrackAnalyticsEventAsync(serviceHub, name, default);
+    /// 
+    /// Tracks the occurrence of a custom event with additional dimensions.
+    /// Parse will store a data point at the time of invocation with the
+    /// given event name.
+    ///
+    /// Dimensions will allow segmentation of the occurrences of this
+    /// custom event.
+    ///
+    /// To track a user signup along with additional metadata, consider the
+    /// following:
+    /// 
+    /// IDictionary<string, string> dims = new Dictionary<string, string> {
+    ///   { "gender", "m" },
+    ///   { "source", "web" },
+    ///   { "dayType", "weekend" }
+    /// };
+    /// ParseAnalytics.TrackEventAsync("signup", dims);
+    /// 
+    ///
+    /// There is a default limit of 8 dimensions per event tracked.
+    /// 
+    /// The name of the custom event to report to ParseClient
+    /// as having happened.
+    /// An Async Task that can be waited on or ignored.
+    public static Task TrackAnalyticsEventAsync(this IServiceHub serviceHub, string name)
+    {
+        return TrackAnalyticsEventAsync(serviceHub, name, default);
+    }
 
-        /// 
-        /// Tracks the occurrence of a custom event with additional dimensions.
-        /// Parse will store a data point at the time of invocation with the
-        /// given event name.
-        ///
-        /// Dimensions will allow segmentation of the occurrences of this
-        /// custom event.
-        ///
-        /// To track a user signup along with additional metadata, consider the
-        /// following:
-        /// 
-        /// IDictionary<string, string> dims = new Dictionary<string, string> {
-        ///   { "gender", "m" },
-        ///   { "source", "web" },
-        ///   { "dayType", "weekend" }
-        /// };
-        /// ParseAnalytics.TrackEventAsync("signup", dims);
-        /// 
-        ///
-        /// There is a default limit of 8 dimensions per event tracked.
-        /// 
-        /// The name of the custom event to report to ParseClient
-        /// as having happened.
-        /// The dictionary of information by which to
-        /// segment this event.
-        /// An Async Task that can be waited on or ignored.
-        public static Task TrackAnalyticsEventAsync(this IServiceHub serviceHub, string name, IDictionary dimensions)
+    /// 
+    /// Tracks the occurrence of a custom event with additional dimensions.
+    /// Parse will store a data point at the time of invocation with the
+    /// given event name.
+    ///
+    /// Dimensions will allow segmentation of the occurrences of this
+    /// custom event.
+    ///
+    /// To track a user signup along with additional metadata, consider the
+    /// following:
+    /// 
+    /// IDictionary<string, string> dims = new Dictionary<string, string> {
+    ///   { "gender", "m" },
+    ///   { "source", "web" },
+    ///   { "dayType", "weekend" }
+    /// };
+    /// ParseAnalytics.TrackEventAsync("signup", dims);
+    /// 
+    ///
+    /// There is a default limit of 8 dimensions per event tracked.
+    /// 
+    /// The name of the custom event to report to ParseClient
+    /// as having happened.
+    /// The dictionary of information by which to
+    /// segment this event.
+    /// An Async Task that can be awaited on or ignored.
+    public static async Task TrackAnalyticsEventAsync(this IServiceHub serviceHub, string name, IDictionary dimensions)
+    {
+        if (string.IsNullOrWhiteSpace(name))
         {
-            if (name is null || name.Trim().Length == 0)
-            {
-                throw new ArgumentException("A name for the custom event must be provided.");
-            }
-
-            return serviceHub.CurrentUserController.GetCurrentSessionTokenAsync(serviceHub).OnSuccess(task => serviceHub.AnalyticsController.TrackEventAsync(name, dimensions, task.Result, serviceHub)).Unwrap();
+            throw new ArgumentException("A name for the custom event must be provided.", nameof(name));
         }
 
-        /// 
-        /// Private method, used by platform-specific extensions to report an app-open
-        /// to the server.
-        /// 
-        /// An identifying hash for a given push notification,
-        /// passed down from the server.
-        /// An Async Task that can be waited on or ignored.
-        static Task TrackLaunchWithPushHashAsync(this IServiceHub serviceHub, string pushHash = null) => serviceHub.CurrentUserController.GetCurrentSessionTokenAsync(serviceHub).OnSuccess(task => serviceHub.AnalyticsController.TrackAppOpenedAsync(pushHash, task.Result, serviceHub)).Unwrap();
+        var sessionToken = await serviceHub.CurrentUserController.GetCurrentSessionTokenAsync(serviceHub).ConfigureAwait(false);
+        await serviceHub.AnalyticsController.TrackEventAsync(name, dimensions, sessionToken, serviceHub).ConfigureAwait(false);
     }
+
+    /// 
+    /// Private method, used by platform-specific extensions to report an app-open
+    /// to the server.
+    /// 
+    /// An identifying hash for a given push notification,
+    /// passed down from the server.
+    /// An Async Task that can be waited on or ignored.
+    static async Task TrackLaunchWithPushHashAsync(this IServiceHub serviceHub, string pushHash = null)
+    {
+        var sessionToken = await serviceHub.CurrentUserController.GetCurrentSessionTokenAsync(serviceHub).ConfigureAwait(false);
+        await serviceHub.AnalyticsController.TrackAppOpenedAsync(pushHash, sessionToken, serviceHub).ConfigureAwait(false);
+    }
+
 }
diff --git a/Parse/Utilities/CloudCodeServiceExtensions.cs b/Parse/Utilities/CloudCodeServiceExtensions.cs
index cb0136e9..9c94e317 100644
--- a/Parse/Utilities/CloudCodeServiceExtensions.cs
+++ b/Parse/Utilities/CloudCodeServiceExtensions.cs
@@ -3,48 +3,53 @@
 using System.Threading.Tasks;
 using Parse.Abstractions.Infrastructure;
 
-namespace Parse
+namespace Parse;
+
+/// 
+/// The ParseCloud class provides methods for interacting with Parse Cloud Functions.
+/// 
+/// 
+/// For example, this sample code calls the
+/// "validateGame" Cloud Function and calls processResponse if the call succeeded
+/// and handleError if it failed.
+///
+/// 
+/// var result =
+///     await ParseCloud.CallFunctionAsync<IDictionary<string, object>>("validateGame", parameters);
+/// 
+/// 
+public static class CloudCodeServiceExtensions
 {
     /// 
-    /// The ParseCloud class provides methods for interacting with Parse Cloud Functions.
+    /// Calls a cloud function.
     /// 
-    /// 
-    /// For example, this sample code calls the
-    /// "validateGame" Cloud Function and calls processResponse if the call succeeded
-    /// and handleError if it failed.
-    ///
-    /// 
-    /// var result =
-    ///     await ParseCloud.CallFunctionAsync<IDictionary<string, object>>("validateGame", parameters);
-    /// 
-    /// 
-    public static class CloudCodeServiceExtensions
+    /// The type of data you will receive from the cloud function. This
+    /// can be an IDictionary, string, IList, ParseObject, or any other type supported by
+    /// ParseObject.
+    /// The cloud function to call.
+    /// The parameters to send to the cloud function. This
+    /// dictionary can contain anything that could be passed into a ParseObject except for
+    /// ParseObjects themselves.
+    /// The result of the cloud call.
+    public static Task CallCloudCodeFunctionAsync(this IServiceHub serviceHub, string name, IDictionary parameters)
     {
-        /// 
-        /// Calls a cloud function.
-        /// 
-        /// The type of data you will receive from the cloud function. This
-        /// can be an IDictionary, string, IList, ParseObject, or any other type supported by
-        /// ParseObject.
-        /// The cloud function to call.
-        /// The parameters to send to the cloud function. This
-        /// dictionary can contain anything that could be passed into a ParseObject except for
-        /// ParseObjects themselves.
-        /// The result of the cloud call.
-        public static Task CallCloudCodeFunctionAsync(this IServiceHub serviceHub, string name, IDictionary parameters) => CallCloudCodeFunctionAsync(serviceHub, name, parameters, CancellationToken.None);
+        return CallCloudCodeFunctionAsync(serviceHub, name, parameters, CancellationToken.None);
+    }
 
-        /// 
-        /// Calls a cloud function.
-        /// 
-        /// The type of data you will receive from the cloud function. This
-        /// can be an IDictionary, string, IList, ParseObject, or any other type supported by
-        /// ParseObject.
-        /// The cloud function to call.
-        /// The parameters to send to the cloud function. This
-        /// dictionary can contain anything that could be passed into a ParseObject except for
-        /// ParseObjects themselves.
-        /// The cancellation token.
-        /// The result of the cloud call.
-        public static Task CallCloudCodeFunctionAsync(this IServiceHub serviceHub, string name, IDictionary parameters, CancellationToken cancellationToken) => serviceHub.CloudCodeController.CallFunctionAsync(name, parameters, serviceHub.GetCurrentSessionToken(), serviceHub, cancellationToken);
+    /// 
+    /// Calls a cloud function.
+    /// 
+    /// The type of data you will receive from the cloud function. This
+    /// can be an IDictionary, string, IList, ParseObject, or any other type supported by
+    /// ParseObject.
+    /// The cloud function to call.
+    /// The parameters to send to the cloud function. This
+    /// dictionary can contain anything that could be passed into a ParseObject except for
+    /// ParseObjects themselves.
+    /// The cancellation token.
+    /// The result of the cloud call.
+    public static async Task CallCloudCodeFunctionAsync(this IServiceHub serviceHub, string name, IDictionary parameters, CancellationToken cancellationToken)
+    {
+        return await serviceHub.CloudCodeController.CallFunctionAsync(name, parameters, await serviceHub.GetCurrentSessionToken(), serviceHub, cancellationToken);
     }
 }
diff --git a/Parse/Utilities/ConfigurationServiceExtensions.cs b/Parse/Utilities/ConfigurationServiceExtensions.cs
index 1378e72a..50726009 100644
--- a/Parse/Utilities/ConfigurationServiceExtensions.cs
+++ b/Parse/Utilities/ConfigurationServiceExtensions.cs
@@ -5,37 +5,52 @@
 using Parse.Abstractions.Infrastructure.Data;
 using Parse.Platform.Configuration;
 
-namespace Parse
+namespace Parse;
+
+public static class ConfigurationServiceExtensions
 {
-    public static class ConfigurationServiceExtensions
+    public static ParseConfiguration BuildConfiguration(this IServiceHub serviceHub, IDictionary configurationData)
     {
-        public static ParseConfiguration BuildConfiguration(this IServiceHub serviceHub, IDictionary configurationData) => ParseConfiguration.Create(configurationData, serviceHub.Decoder, serviceHub);
+        return ParseConfiguration.Create(configurationData, serviceHub.Decoder, serviceHub);
+    }
 
-        public static ParseConfiguration BuildConfiguration(this IParseDataDecoder dataDecoder, IDictionary configurationData, IServiceHub serviceHub) => ParseConfiguration.Create(configurationData, dataDecoder, serviceHub);
+    public static ParseConfiguration BuildConfiguration(this IParseDataDecoder dataDecoder, IDictionary configurationData, IServiceHub serviceHub)
+    {
+        return ParseConfiguration.Create(configurationData, dataDecoder, serviceHub);
+    }
 
+#pragma warning disable CS1030 // #warning directive
 #warning Investigate if these methods which simply block a thread waiting for an asynchronous process to complete should be eliminated.
 
-        /// 
-        /// Gets the latest fetched ParseConfig.
-        /// 
-        /// ParseConfig object
-        public static ParseConfiguration GetCurrentConfiguration(this IServiceHub serviceHub)
-        {
-            Task task = serviceHub.ConfigurationController.CurrentConfigurationController.GetCurrentConfigAsync(serviceHub);
+    /// 
+    /// Gets the latest fetched ParseConfig.
+    /// 
+    /// ParseConfig object
+    public static async Task GetCurrentConfiguration(this IServiceHub serviceHub)
+#pragma warning restore CS1030 // #warning directive
+    {
+        ParseConfiguration parseConfig = await serviceHub.ConfigurationController.CurrentConfigurationController.GetCurrentConfigAsync(serviceHub);
 
-            task.Wait();
-            return task.Result;
-        }
+        return parseConfig;
+    }
 
-        internal static void ClearCurrentConfig(this IServiceHub serviceHub) => serviceHub.ConfigurationController.CurrentConfigurationController.ClearCurrentConfigAsync().Wait();
+    internal static void ClearCurrentConfig(this IServiceHub serviceHub)
+    {
+        _ = serviceHub.ConfigurationController.CurrentConfigurationController.ClearCurrentConfigAsync();
+    }
 
-        internal static void ClearCurrentConfigInMemory(this IServiceHub serviceHub) => serviceHub.ConfigurationController.CurrentConfigurationController.ClearCurrentConfigInMemoryAsync().Wait();
+    internal static void ClearCurrentConfigInMemory(this IServiceHub serviceHub)
+    {
+        _ =  serviceHub.ConfigurationController.CurrentConfigurationController.ClearCurrentConfigInMemoryAsync();
+    }
 
-        /// 
-        /// Retrieves the ParseConfig asynchronously from the server.
-        /// 
-        /// The cancellation token.
-        /// ParseConfig object that was fetched
-        public static Task GetConfigurationAsync(this IServiceHub serviceHub, CancellationToken cancellationToken = default) => serviceHub.ConfigurationController.FetchConfigAsync(serviceHub.GetCurrentSessionToken(), serviceHub, cancellationToken);
+    /// 
+    /// Retrieves the ParseConfig asynchronously from the server.
+    /// 
+    /// The cancellation token.
+    /// ParseConfig object that was fetched
+    public static async Task GetConfigurationAsync(this IServiceHub serviceHub, CancellationToken cancellationToken = default)
+    {
+        return await serviceHub.ConfigurationController.FetchConfigAsync(await serviceHub.GetCurrentSessionToken(), serviceHub, cancellationToken);
     }
 }
diff --git a/Parse/Utilities/InstallationServiceExtensions.cs b/Parse/Utilities/InstallationServiceExtensions.cs
index 2c28cb5c..c46f59d4 100644
--- a/Parse/Utilities/InstallationServiceExtensions.cs
+++ b/Parse/Utilities/InstallationServiceExtensions.cs
@@ -1,41 +1,48 @@
 using System.Threading.Tasks;
 using Parse.Abstractions.Infrastructure;
 
-namespace Parse
+namespace Parse;
+
+public static class InstallationServiceExtensions
 {
-    public static class InstallationServiceExtensions
+    /// 
+    /// Constructs a  for ParseInstallations.
+    /// 
+    /// 
+    /// Only the following types of queries are allowed for installations:
+    ///
+    /// 
+    /// query.GetAsync(objectId)
+    /// query.WhereEqualTo(key, value)
+    /// query.WhereMatchesKeyInQuery<TOther>(key, keyInQuery, otherQuery)
+    /// 
+    ///
+    /// You can add additional query conditions, but one of the above must appear as a top-level AND
+    /// clause in the query.
+    /// 
+    public static ParseQuery GetInstallationQuery(this IServiceHub serviceHub)
     {
-        /// 
-        /// Constructs a  for ParseInstallations.
-        /// 
-        /// 
-        /// Only the following types of queries are allowed for installations:
-        ///
-        /// 
-        /// query.GetAsync(objectId)
-        /// query.WhereEqualTo(key, value)
-        /// query.WhereMatchesKeyInQuery<TOther>(key, keyInQuery, otherQuery)
-        /// 
-        ///
-        /// You can add additional query conditions, but one of the above must appear as a top-level AND
-        /// clause in the query.
-        /// 
-        public static ParseQuery GetInstallationQuery(this IServiceHub serviceHub) => new ParseQuery(serviceHub);
+        return new ParseQuery(serviceHub);
+    }
 
+#pragma warning disable CS1030 // #warning directive
 #warning Consider making the following method asynchronous.
 
-        /// 
-        /// Gets the ParseInstallation representing this app on this device.
-        /// 
-        public static ParseInstallation GetCurrentInstallation(this IServiceHub serviceHub)
-        {
-            Task task = serviceHub.CurrentInstallationController.GetAsync(serviceHub);
+    /// 
+    /// Gets the ParseInstallation representing this app on this device.
+    /// 
+    public static async Task GetCurrentInstallation(this IServiceHub serviceHub)
+#pragma warning restore CS1030 // #warning directive
+    {
+        ParseInstallation parseInstallation = await serviceHub.CurrentInstallationController.GetAsync(serviceHub);
 
-            // TODO (hallucinogen): this will absolutely break on Unity, but how should we resolve this?
-            task.Wait();
-            return task.Result;
-        }
+        // TODO (hallucinogen): this will absolutely break on Unity, but how should we resolve this?
+        
+        return parseInstallation;
+    }
 
-        internal static void ClearInMemoryInstallation(this IServiceHub serviceHub) => serviceHub.CurrentInstallationController.ClearFromMemory();
+    internal static void ClearInMemoryInstallation(this IServiceHub serviceHub)
+    {
+        serviceHub.CurrentInstallationController.ClearFromMemory();
     }
 }
diff --git a/Parse/Utilities/ObjectServiceExtensions.cs b/Parse/Utilities/ObjectServiceExtensions.cs
index 009a8f51..4f7a8320 100644
--- a/Parse/Utilities/ObjectServiceExtensions.cs
+++ b/Parse/Utilities/ObjectServiceExtensions.cs
@@ -9,504 +9,685 @@
 using Parse.Abstractions.Platform.Objects;
 using Parse.Infrastructure.Utilities;
 using Parse.Infrastructure.Data;
+using System.Diagnostics;
 
-namespace Parse
+namespace Parse;
+
+public static class ObjectServiceExtensions
 {
-    public static class ObjectServiceExtensions
-    {
-        /// 
-        /// Registers a custom subclass type with the Parse SDK, enabling strong-typing of those ParseObjects whenever
-        /// they appear. Subclasses must specify the ParseClassName attribute, have a default constructor, and properties
-        /// backed by ParseObject fields should have ParseFieldName attributes supplied.
-        /// 
-        /// The target  instance.
-        /// The ParseObject subclass type to register.
-        public static void AddValidClass(this IServiceHub serviceHub) where T : ParseObject, new() => serviceHub.ClassController.AddValid(typeof(T));
-
-        /// 
-        /// Registers a custom subclass type with the Parse SDK, enabling strong-typing of those ParseObjects whenever
-        /// they appear. Subclasses must specify the ParseClassName attribute, have a default constructor, and properties
-        /// backed by ParseObject fields should have ParseFieldName attributes supplied.
-        /// 
-        /// The ParseObject subclass type to register.
-        /// The target  instance.
-        public static void RegisterSubclass(this IServiceHub serviceHub, Type type)
+    /// 
+    /// Registers a custom subclass type with the Parse SDK, enabling strong-typing of those ParseObjects whenever
+    /// they appear. Subclasses must specify the ParseClassName attribute, have a default constructor, and properties
+    /// backed by ParseObject fields should have ParseFieldName attributes supplied.
+    /// 
+    /// The target  instance.
+    /// The ParseObject subclass type to register.
+    public static void AddValidClass(this IServiceHub serviceHub) where T : ParseObject, new()
+    {
+        serviceHub.ClassController.AddValid(typeof(T));
+    }
+
+    /// 
+    /// Registers a custom subclass type with the Parse SDK, enabling strong-typing of those ParseObjects whenever
+    /// they appear. Subclasses must specify the ParseClassName attribute, have a default constructor, and properties
+    /// backed by ParseObject fields should have ParseFieldName attributes supplied.
+    /// 
+    /// The ParseObject subclass type to register.
+    /// The target  instance.
+    public static void RegisterSubclass(this IServiceHub serviceHub, Type type)
+    {
+        if (typeof(ParseObject).IsAssignableFrom(type))
         {
-            if (typeof(ParseObject).IsAssignableFrom(type))
-            {
-                serviceHub.ClassController.AddValid(type);
-            }
+            serviceHub.ClassController.AddValid(type);
         }
+    }
+
+    /// 
+    /// Unregisters a previously-registered sub-class of  with the subclassing controller.
+    /// 
+    /// 
+    /// 
+    public static void RemoveClass(this IServiceHub serviceHub) where T : ParseObject, new()
+    {
+        serviceHub.ClassController.RemoveClass(typeof(T));
+    }
 
-        /// 
-        /// Unregisters a previously-registered sub-class of  with the subclassing controller.
-        /// 
-        /// 
-        /// 
-        public static void RemoveClass(this IServiceHub serviceHub) where T : ParseObject, new() => serviceHub.ClassController.RemoveClass(typeof(T));
-
-        /// 
-        /// Unregisters a previously-registered sub-class of  with the subclassing controller.
-        /// 
-        /// 
-        /// 
-        public static void RemoveClass(this IParseObjectClassController subclassingController, Type type)
+    /// 
+    /// Unregisters a previously-registered sub-class of  with the subclassing controller.
+    /// 
+    /// 
+    /// 
+    public static void RemoveClass(this IParseObjectClassController subclassingController, Type type)
+    {
+        if (typeof(ParseObject).IsAssignableFrom(type))
         {
-            if (typeof(ParseObject).IsAssignableFrom(type))
-            {
-                subclassingController.RemoveClass(type);
-            }
+            subclassingController.RemoveClass(type);
         }
+    }
 
-        /// 
-        /// Creates a new ParseObject based upon a class name. If the class name is a special type (e.g.
-        /// for ), then the appropriate type of ParseObject is returned.
-        /// 
-        /// The class of object to create.
-        /// A new ParseObject for the given class name.
-        public static ParseObject CreateObject(this IServiceHub serviceHub, string className) => serviceHub.ClassController.Instantiate(className, serviceHub);
-
-        /// 
-        /// Creates a new ParseObject based upon a given subclass type.
-        /// 
-        /// A new ParseObject for the given class name.
-        public static T CreateObject(this IServiceHub serviceHub) where T : ParseObject => (T) serviceHub.ClassController.CreateObject(serviceHub);
-
-        /// 
-        /// Creates a new ParseObject based upon a given subclass type.
-        /// 
-        /// A new ParseObject for the given class name.
-        public static T CreateObject(this IParseObjectClassController classController, IServiceHub serviceHub) where T : ParseObject => (T) classController.Instantiate(classController.GetClassName(typeof(T)), serviceHub);
-
-        /// 
-        /// Creates a reference to an existing ParseObject for use in creating associations between
-        /// ParseObjects. Calling  on this object will return
-        /// false until  has been called.
-        /// No network request will be made.
-        /// 
-        /// The object's class.
-        /// The object id for the referenced object.
-        /// A ParseObject without data.
-        public static ParseObject CreateObjectWithoutData(this IServiceHub serviceHub, string className, string objectId) => serviceHub.ClassController.CreateObjectWithoutData(className, objectId, serviceHub);
-
-        /// 
-        /// Creates a reference to an existing ParseObject for use in creating associations between
-        /// ParseObjects. Calling  on this object will return
-        /// false until  has been called.
-        /// No network request will be made.
-        /// 
-        /// The object's class.
-        /// The object id for the referenced object.
-        /// A ParseObject without data.
-        public static ParseObject CreateObjectWithoutData(this IParseObjectClassController classController, string className, string objectId, IServiceHub serviceHub)
-        {
-            ParseObject.CreatingPointer.Value = true;
-            try
-            {
-                ParseObject result = classController.Instantiate(className, serviceHub);
-                result.ObjectId = objectId;
+    /// 
+    /// Creates a new ParseObject based upon a class name. If the class name is a special type (e.g.
+    /// for ), then the appropriate type of ParseObject is returned.
+    /// 
+    /// The class of object to create.
+    /// A new ParseObject for the given class name.
+    public static ParseObject CreateObject(this IServiceHub serviceHub, string className)
+    {
+        return serviceHub.ClassController.Instantiate(className, serviceHub);
+    }
 
-                // Left in because the property setter might be doing something funky.
+    /// 
+    /// Creates a new ParseObject based upon a given subclass type.
+    /// 
+    /// A new ParseObject for the given class name.
+    public static T CreateObject(this IServiceHub serviceHub) where T : ParseObject
+    {
+        return (T) serviceHub.ClassController.CreateObject(serviceHub);
+    }
 
-                result.IsDirty = false;
-                return result.IsDirty ? throw new InvalidOperationException("A ParseObject subclass default constructor must not make changes to the object that cause it to be dirty.") : result;
-            }
-            finally { ParseObject.CreatingPointer.Value = false; }
-        }
+    /// 
+    /// Creates a new ParseObject based upon a given subclass type.
+    /// 
+    /// A new ParseObject for the given class name.
+    public static T CreateObject(this IParseObjectClassController classController, IServiceHub serviceHub) where T : ParseObject
+    {
+        
+        return (T) classController.Instantiate(classController.GetClassName(typeof(T)), serviceHub);
+    }
 
-        /// 
-        /// Creates a reference to an existing ParseObject for use in creating associations between
-        /// ParseObjects. Calling  on this object will return
-        /// false until  has been called.
-        /// No network request will be made.
-        /// 
-        /// The object id for the referenced object.
-        /// A ParseObject without data.
-        public static T CreateObjectWithoutData(this IServiceHub serviceHub, string objectId) where T : ParseObject => (T) serviceHub.CreateObjectWithoutData(serviceHub.ClassController.GetClassName(typeof(T)), objectId);
-
-        /// 
-        /// Deletes each object in the provided list.
-        /// 
-        /// The objects to delete.
-        public static Task DeleteObjectsAsync(this IServiceHub serviceHub, IEnumerable objects) where T : ParseObject => DeleteObjectsAsync(serviceHub, objects, CancellationToken.None);
-
-        /// 
-        /// Deletes each object in the provided list.
-        /// 
-        /// The objects to delete.
-        /// The cancellation token.
-        public static Task DeleteObjectsAsync(this IServiceHub serviceHub, IEnumerable objects, CancellationToken cancellationToken) where T : ParseObject
-        {
-            HashSet unique = new HashSet(objects.OfType().ToList(), new IdentityEqualityComparer { });
+    /// 
+    /// Creates a reference to an existing ParseObject for use in creating associations between
+    /// ParseObjects. Calling  on this object will return
+    /// false until  has been called.
+    /// No network request will be made.
+    /// 
+    /// The object's class.
+    /// The object id for the referenced object.
+    /// A ParseObject without data.
+    public static ParseObject CreateObjectWithoutData(this IServiceHub serviceHub, string className, string objectId)
+    {
+        return serviceHub.ClassController.CreateObjectWithoutData(className, objectId, serviceHub);
+    }
 
-            return EnqueueForAll(unique, toAwait => toAwait.OnSuccess(_ => Task.WhenAll(serviceHub.ObjectController.DeleteAllAsync(unique.Select(task => task.State).ToList(), serviceHub.GetCurrentSessionToken(), cancellationToken))).Unwrap().OnSuccess(task =>
-            {
-                // Dirty all objects in memory.
+    /// 
+    /// Creates a reference to an existing ParseObject for use in creating associations between
+    /// ParseObjects. Calling  on this object will return
+    /// false until  has been called.
+    /// No network request will be made.
+    /// 
+    /// The object's class.
+    /// The object id for the referenced object.
+    /// A ParseObject without data.
+    public static ParseObject CreateObjectWithoutData(this IParseObjectClassController classController, string className, string objectId, IServiceHub serviceHub)
+    {
+        ParseObject.CreatingPointer.Value = true;
+        try
+        {
+            ParseObject result = classController.Instantiate(className, serviceHub);
+            result.ObjectId = objectId;
 
-                foreach (ParseObject obj in unique)
-                {
-                    obj.IsDirty = true;
-                }
+            // Left in because the property setter might be doing something funky.
 
-                return default(object);
-            }), cancellationToken);
+            result.IsDirty = false;
+            if (result.IsDirty)
+                throw new InvalidOperationException("A ParseObject subclass default constructor must not make changes to the object that cause it to be dirty.");
+            else
+                return  result;
         }
-
-        /// 
-        /// Fetches all of the objects in the provided list.
-        /// 
-        /// The objects to fetch.
-        /// The list passed in for convenience.
-        public static Task> FetchObjectsAsync(this IServiceHub serviceHub, IEnumerable objects) where T : ParseObject => FetchObjectsAsync(serviceHub, objects, CancellationToken.None);
-
-        /// 
-        /// Fetches all of the objects in the provided list.
-        /// 
-        /// The objects to fetch.
-        /// The cancellation token.
-        /// The list passed in for convenience.
-        public static Task> FetchObjectsAsync(this IServiceHub serviceHub, IEnumerable objects, CancellationToken cancellationToken) where T : ParseObject => EnqueueForAll(objects.Cast(), (Task toAwait) => serviceHub.FetchAllInternalAsync(objects, true, toAwait, cancellationToken), cancellationToken);
-
-        /// 
-        /// Fetches all of the objects that don't have data in the provided list.
-        /// 
-        /// todo: describe objects parameter on FetchAllIfNeededAsync
-        /// The list passed in for convenience.
-        public static Task> FetchObjectsIfNeededAsync(this IServiceHub serviceHub, IEnumerable objects) where T : ParseObject => FetchObjectsIfNeededAsync(serviceHub, objects, CancellationToken.None);
-
-        /// 
-        /// Fetches all of the objects that don't have data in the provided list.
-        /// 
-        /// The objects to fetch.
-        /// The cancellation token.
-        /// The list passed in for convenience.
-        public static Task> FetchObjectsIfNeededAsync(this IServiceHub serviceHub, IEnumerable objects, CancellationToken cancellationToken) where T : ParseObject => EnqueueForAll(objects.Cast(), (Task toAwait) => serviceHub.FetchAllInternalAsync(objects, false, toAwait, cancellationToken), cancellationToken);
-
-        /// 
-        /// Gets a  for the type of object specified by
-        /// 
-        /// 
-        /// The class name of the object.
-        /// A new .
-        public static ParseQuery GetQuery(this IServiceHub serviceHub, string className)
+        finally
         {
-            // Since we can't return a ParseQuery (due to strong-typing with
-            // generics), we'll require you to go through subclasses. This is a better
-            // experience anyway, especially with LINQ integration, since you'll get
-            // strongly-typed queries and compile-time checking of property names and
-            // types.
-
-            if (serviceHub.ClassController.GetType(className) is { })
-            {
-                throw new ArgumentException($"Use the class-specific query properties for class {className}", nameof(className));
-            }
-            return new ParseQuery(serviceHub, className);
+            ParseObject.CreatingPointer.Value = false;
         }
+    }
 
-        /// 
-        /// Saves each object in the provided list.
-        /// 
-        /// The objects to save.
-        public static Task SaveObjectsAsync(this IServiceHub serviceHub, IEnumerable objects) where T : ParseObject => SaveObjectsAsync(serviceHub, objects, CancellationToken.None);
-
-        /// 
-        /// Saves each object in the provided list.
-        /// 
-        /// The objects to save.
-        /// The cancellation token.
-        public static Task SaveObjectsAsync(this IServiceHub serviceHub, IEnumerable objects, CancellationToken cancellationToken) where T : ParseObject => DeepSaveAsync(serviceHub, objects.ToList(), serviceHub.GetCurrentSessionToken(), cancellationToken);
-
-        /// 
-        /// Flattens dictionaries and lists into a single enumerable of all contained objects
-        /// that can then be queried over.
-        /// 
-        /// The root of the traversal
-        /// Whether to traverse into ParseObjects' children
-        /// Whether to include the root in the result
-        /// 
-        internal static IEnumerable TraverseObjectDeep(this IServiceHub serviceHub, object root, bool traverseParseObjects = false, bool yieldRoot = false)
+    /// 
+    /// Creates a reference to an existing ParseObject for use in creating associations between
+    /// ParseObjects. Calling  on this object will return
+    /// false until  has been called.
+    /// No network request will be made.
+    /// 
+    /// The object id for the referenced object.
+    /// A ParseObject without data.
+    public static T CreateObjectWithoutData(this IServiceHub serviceHub, string objectId) where T : ParseObject
+    {
+        return (T) serviceHub.CreateObjectWithoutData(serviceHub.ClassController.GetClassName(typeof(T)), objectId);
+    }
+    /// 
+    /// Creates a reference to a new ParseObject with the specified initial data.
+    /// This can be used for creating new objects with predefined values.
+    /// No network request will be made until the object is saved.
+    /// 
+    /// A dictionary containing the initial key-value pairs for the object.
+    /// A new ParseObject with the specified initial data.
+    public static T CreateObjectWithData(this IServiceHub serviceHub, IDictionary initialData) where T : ParseObject
+    {
+        if (initialData == null)
         {
-            IEnumerable items = DeepTraversalInternal(serviceHub, root, traverseParseObjects, new HashSet(new IdentityEqualityComparer()));
-            return yieldRoot ? new[] { root }.Concat(items) : items;
+            throw new ArgumentNullException(nameof(initialData), "Initial data cannot be null.");
         }
 
-        // TODO (hallucinogen): add unit test
-        internal static T GenerateObjectFromState(this IServiceHub serviceHub, IObjectState state, string defaultClassName) where T : ParseObject => serviceHub.ClassController.GenerateObjectFromState(state, defaultClassName, serviceHub);
+        // Create a new instance of the specified ParseObject type
+        var parseObject = (T) serviceHub.CreateObject(serviceHub.ClassController.GetClassName(typeof(T)));
 
-        internal static T GenerateObjectFromState(this IParseObjectClassController classController, IObjectState state, string defaultClassName, IServiceHub serviceHub) where T : ParseObject
+        // Set initial data properties
+        foreach (var kvp in initialData)
         {
-            T obj = (T) classController.CreateObjectWithoutData(state.ClassName ?? defaultClassName, state.ObjectId, serviceHub);
-            obj.HandleFetchResult(state);
-
-            return obj;
+            parseObject[kvp.Key] = kvp.Value;
         }
 
-        internal static IDictionary GenerateJSONObjectForSaving(this IServiceHub serviceHub, IDictionary operations)
-        {
-            Dictionary result = new Dictionary();
-
-            foreach (KeyValuePair pair in operations)
-            {
-                result[pair.Key] = PointerOrLocalIdEncoder.Instance.Encode(pair.Value, serviceHub);
-            }
+        return parseObject;
+    }
 
-            return result;
-        }
+    /// 
+    /// Deletes each object in the provided list.
+    /// 
+    /// The objects to delete.
+    public static Task DeleteObjectsAsync(this IServiceHub serviceHub, IEnumerable objects) where T : ParseObject
+    {
+        return DeleteObjectsAsync(serviceHub, objects, CancellationToken.None);
+    }
 
-        /// 
-        /// Returns true if the given object can be serialized for saving as a value
-        /// that is pointed to by a ParseObject.
-        /// 
-        internal static bool CanBeSerializedAsValue(this IServiceHub serviceHub, object value) => TraverseObjectDeep(serviceHub, value, yieldRoot: true).OfType().All(entity => entity.ObjectId is { });
+    /// 
+    /// Deletes each object in the provided list.
+    /// 
+    /// The objects to delete.
+    /// The cancellation token.
+    public static async Task DeleteObjectsAsync(this IServiceHub serviceHub, IEnumerable objects, CancellationToken cancellationToken) where T : ParseObject
+    {
+        // Get a unique set of ParseObjects
+        var uniqueObjects = new HashSet(objects.OfType(), new IdentityEqualityComparer());
 
-        static void CollectDirtyChildren(this IServiceHub serviceHub, object node, IList dirtyChildren, ICollection seen, ICollection seenNew)
+        await EnqueueForAll(uniqueObjects, async toAwait =>
         {
-            foreach (ParseObject target in TraverseObjectDeep(serviceHub, node).OfType())
+            // Wait for the preceding task (toAwait) to complete
+            await toAwait.ConfigureAwait(false);
+
+            // Perform the delete operation for all objects
+            await Task.WhenAll(
+                serviceHub.ObjectController.DeleteAllAsync(
+                    uniqueObjects.Select(obj => obj.State).ToList(),
+                    await serviceHub.GetCurrentSessionToken(),
+                    cancellationToken)
+            ).ConfigureAwait(false);
+
+            // Mark all objects as dirty
+            foreach (var obj in uniqueObjects)
             {
-                ICollection scopedSeenNew;
+                obj.IsDirty = true;
+            }
 
-                // Check for cycles of new objects. Any such cycle means it will be impossible to save
-                // this collection of objects, so throw an exception.
+            return true; // Return a meaningful result if needed
+        }, cancellationToken).ConfigureAwait(false);
+    }
 
-                if (target.ObjectId != null)
-                {
-                    scopedSeenNew = new HashSet(new IdentityEqualityComparer());
-                }
-                else
-                {
-                    if (seenNew.Contains(target))
-                    {
-                        throw new InvalidOperationException("Found a circular dependency while saving");
-                    }
 
-                    scopedSeenNew = new HashSet(seenNew, new IdentityEqualityComparer()) { target };
-                }
 
-                // Check for cycles of any object. If this occurs, then there's no problem, but
-                // we shouldn't recurse any deeper, because it would be an infinite recursion.
 
-                if (seen.Contains(target))
-                {
-                    return;
-                }
 
-                seen.Add(target);
+    /// 
+    /// Fetches all of the objects in the provided list.
+    /// 
+    /// The objects to fetch.
+    /// The list passed in for convenience.
+    public static Task> FetchObjectsAsync(this IServiceHub serviceHub, IEnumerable objects) where T : ParseObject
+    {
+        return FetchObjectsAsync(serviceHub, objects, CancellationToken.None);
+    }
 
-                // Recurse into this object's children looking for dirty children.
-                // We only need to look at the child object's current estimated data,
-                // because that's the only data that might need to be saved now.
+    /// 
+    /// Fetches all of the objects in the provided list.
+    /// 
+    /// The objects to fetch.
+    /// The cancellation token.
+    /// The list passed in for convenience.
+    public static Task> FetchObjectsAsync(this IServiceHub serviceHub, IEnumerable objects, CancellationToken cancellationToken) where T : ParseObject
+    {
+        return EnqueueForAll(objects.Cast(), (Task toAwait) => serviceHub.FetchAllInternalAsync(objects, true, toAwait, cancellationToken), cancellationToken);
+    }
 
-                CollectDirtyChildren(serviceHub, target.EstimatedData, dirtyChildren, seen, scopedSeenNew);
+    /// 
+    /// Fetches all of the objects that don't have data in the provided list.
+    /// 
+    /// todo: describe objects parameter on FetchAllIfNeededAsync
+    /// The list passed in for convenience.
+    public static Task> FetchObjectsIfNeededAsync(this IServiceHub serviceHub, IEnumerable objects) where T : ParseObject
+    {
+        return FetchObjectsIfNeededAsync(serviceHub, objects, CancellationToken.None);
+    }
 
-                if (target.CheckIsDirty(false))
-                {
-                    dirtyChildren.Add(target);
-                }
-            }
-        }
+    /// 
+    /// Fetches all of the objects that don't have data in the provided list.
+    /// 
+    /// The objects to fetch.
+    /// The cancellation token.
+    /// The list passed in for convenience.
+    public static Task> FetchObjectsIfNeededAsync(this IServiceHub serviceHub, IEnumerable objects, CancellationToken cancellationToken) where T : ParseObject
+    {
+        return EnqueueForAll(objects.Cast(), (Task toAwait) => serviceHub.FetchAllInternalAsync(objects, false, toAwait, cancellationToken), cancellationToken);
+    }
 
-        /// 
-        /// Helper version of CollectDirtyChildren so that callers don't have to add the internally
-        /// used parameters.
-        /// 
-        static void CollectDirtyChildren(this IServiceHub serviceHub, object node, IList dirtyChildren) => CollectDirtyChildren(serviceHub, node, dirtyChildren, new HashSet(new IdentityEqualityComparer()), new HashSet(new IdentityEqualityComparer()));
+    /// 
+    /// Gets a  for the type of object specified by
+    /// 
+    /// 
+    /// The class name of the object.
+    /// A new .
+    public static ParseQuery GetQuery(this IServiceHub serviceHub, string className)
+    {
+        // Since we can't return a ParseQuery (due to strong-typing with
+        // generics), we'll require you to go through subclasses. This is a better
+        // experience anyway, especially with LINQ integration, since you'll get
+        // strongly-typed queries and compile-time checking of property names and
+        // types.
 
-        internal static Task DeepSaveAsync(this IServiceHub serviceHub, object target, string sessionToken, CancellationToken cancellationToken)
+        if (serviceHub.ClassController.GetType(className) is { })
         {
-            List objects = new List();
-            CollectDirtyChildren(serviceHub, target, objects);
+            throw new ArgumentException($"Use the class-specific query properties for class {className}", nameof(className));
+        }
+        return new ParseQuery(serviceHub, className);
+    }
 
-            HashSet uniqueObjects = new HashSet(objects, new IdentityEqualityComparer());
-            List saveDirtyFileTasks = TraverseObjectDeep(serviceHub, target, true).OfType().Where(file => file.IsDirty).Select(file => file.SaveAsync(serviceHub, cancellationToken)).ToList();
+    /// 
+    /// Saves each object in the provided list.
+    /// 
+    /// The objects to save.
+    public static Task SaveObjectsAsync(this IServiceHub serviceHub, IEnumerable objects) where T : ParseObject
+    {
+        return SaveObjectsAsync(serviceHub, objects, CancellationToken.None);
+    }
 
-            return Task.WhenAll(saveDirtyFileTasks).OnSuccess(_ =>
-            {
-                IEnumerable remaining = new List(uniqueObjects);
-                return InternalExtensions.WhileAsync(() => Task.FromResult(remaining.Any()), () =>
-                {
-                    // Partition the objects into two sets: those that can be saved immediately,
-                    // and those that rely on other objects to be created first.
+    /// 
+    /// Saves each object in the provided list.
+    /// 
+    /// The objects to save.
+    /// The cancellation token.
+    public static async Task SaveObjectsAsync(this IServiceHub serviceHub, IEnumerable objects, CancellationToken cancellationToken) where T : ParseObject
+    {
+        _ = DeepSaveAsync(serviceHub, objects.ToList(),await serviceHub.GetCurrentSessionToken(), cancellationToken);
+    }
 
-                    List current = (from item in remaining where item.CanBeSerialized select item).ToList(), nextBatch = (from item in remaining where !item.CanBeSerialized select item).ToList();
-                    remaining = nextBatch;
+    /// 
+    /// Flattens dictionaries and lists into a single enumerable of all contained objects
+    /// that can then be queried over.
+    /// 
+    /// The root of the traversal
+    /// Whether to traverse into ParseObjects' children
+    /// Whether to include the root in the result
+    /// 
+    internal static IEnumerable TraverseObjectDeep(this IServiceHub serviceHub, object root, bool traverseParseObjects = false, bool yieldRoot = false)
+    {
+        IEnumerable items = DeepTraversalInternal(serviceHub, root, traverseParseObjects, new HashSet(new IdentityEqualityComparer()));
+        return yieldRoot ? new[] { root }.Concat(items) : items;
+    }
 
-                    if (current.Count == 0)
-                    {
-                        // We do cycle-detection when building the list of objects passed to this
-                        // function, so this should never get called. But we should check for it
-                        // anyway, so that we get an exception instead of an infinite loop.
+    // TODO (hallucinogen): add unit test
+    internal static T GenerateObjectFromState(this IServiceHub serviceHub, IObjectState state, string defaultClassName) where T : ParseObject
+    {
+        var obj = serviceHub.ClassController.GenerateObjectFromState(state, defaultClassName, serviceHub);
+        return obj;
+    }
 
-                        throw new InvalidOperationException("Unable to save a ParseObject with a relation to a cycle.");
-                    }
+    internal static T GenerateObjectFromState(
+     this IParseObjectClassController classController,
+     IObjectState state,
+     string defaultClassName,
+     IServiceHub serviceHub) where T : ParseObject
+    {
+        if (state == null)
+        {
+            throw new ArgumentNullException(nameof(state), "The state cannot be null.");
+        }
+        
+        // Ensure the class name is determined or throw an exception
+        string className = state.ClassName ?? defaultClassName;
+        if (string.IsNullOrEmpty(className))
+        {
+        
+            throw new InvalidOperationException("Both state.ClassName and defaultClassName are null or empty. Unable to determine class name.");
+        }
+        
+        // Create the object using the class controller
+        T obj = classController.Instantiate(className, serviceHub) as T;
+        
+        if (obj == null)
+        {
+        
+            throw new InvalidOperationException($"Failed to instantiate object of type {typeof(T).Name} for class {className}.");
+        }
 
-                    // Save all of the objects in current.
+        // Apply the state to the object
+        obj.HandleFetchResult(state);
 
-                    return EnqueueForAll(current, toAwait => toAwait.OnSuccess(__ =>
-                    {
-                        List states = (from item in current select item.State).ToList();
-                        List> operationsList = (from item in current select item.StartSave()).ToList();
-
-                        IList> saveTasks = serviceHub.ObjectController.SaveAllAsync(states, operationsList, sessionToken, serviceHub, cancellationToken);
-
-                        return Task.WhenAll(saveTasks).ContinueWith(task =>
-                        {
-                            if (task.IsFaulted || task.IsCanceled)
-                            {
-                                foreach ((ParseObject item, IDictionary ops) pair in current.Zip(operationsList, (item, ops) => (item, ops)))
-                                {
-                                    pair.item.HandleFailedSave(pair.ops);
-                                }
-                            }
-                            else
-                            {
-                                foreach ((ParseObject item, IObjectState state) pair in current.Zip(task.Result, (item, state) => (item, state)))
-                                {
-                                    pair.item.HandleSave(pair.state);
-                                }
-                            }
-
-                            cancellationToken.ThrowIfCancellationRequested();
-                            return task;
-                        }).Unwrap();
-                    }).Unwrap().OnSuccess(t => (object) null), cancellationToken);
-                });
-            }).Unwrap();
+        return obj;
+    }
+
+
+    internal static IDictionary GenerateJSONObjectForSaving(this IServiceHub serviceHub, IDictionary operations)
+    {
+        Dictionary result = new Dictionary();
+
+        foreach (KeyValuePair pair in operations)
+        {
+            //result[pair.Key] = PointerOrLocalIdEncoder.Instance.Encode(pair.Value, serviceHub);
+            result[pair.Key] = PointerOrLocalIdEncoder.Instance.Encode(pair.Value.Value, serviceHub);
         }
 
-        static IEnumerable DeepTraversalInternal(this IServiceHub serviceHub, object root, bool traverseParseObjects, ICollection seen)
+        return result;
+    }
+
+    /// 
+    /// Returns true if the given object can be serialized for saving as a value
+    /// that is pointed to by a ParseObject.
+    /// 
+    internal static bool CanBeSerializedAsValue(this IServiceHub serviceHub, object value)
+    {
+        return TraverseObjectDeep(serviceHub, value, yieldRoot: true).OfType().All(entity => entity.ObjectId is { });
+    }
+
+    static void CollectDirtyChildren(this IServiceHub serviceHub, object node, IList dirtyChildren, ICollection seen, ICollection seenNew)
+    {
+        foreach (ParseObject target in TraverseObjectDeep(serviceHub, node).OfType())
         {
-            seen.Add(root);
-            System.Collections.IEnumerable targets = ParseClient.IL2CPPCompiled ? null : null as IEnumerable;
+            ICollection scopedSeenNew;
 
-            if (Conversion.As>(root) is { } rootDictionary)
+            // Check for cycles of new objects. Any such cycle means it will be impossible to save
+            // this collection of objects, so throw an exception.
+
+            if (target.ObjectId != null)
             {
-                targets = rootDictionary.Values;
+                scopedSeenNew = new HashSet(new IdentityEqualityComparer());
             }
             else
             {
-                if (Conversion.As>(root) is { } rootList)
-                {
-                    targets = rootList;
-                }
-                else if (traverseParseObjects)
+                if (seenNew.Contains(target))
                 {
-                    if (root is ParseObject entity)
-                    {
-                        targets = entity.Keys.ToList().Select(key => entity[key]);
-                    }
+                    throw new InvalidOperationException("Found a circular dependency while saving");
                 }
+
+                scopedSeenNew = new HashSet(seenNew, new IdentityEqualityComparer()) { target };
             }
 
-            if (targets is { })
+            // Check for cycles of any object. If this occurs, then there's no problem, but
+            // we shouldn't recurse any deeper, because it would be an infinite recursion.
+
+            if (seen.Contains(target))
             {
-                foreach (object item in targets)
-                {
-                    if (!seen.Contains(item))
-                    {
-                        yield return item;
+                return;
+            }
 
-                        foreach (object child in DeepTraversalInternal(serviceHub, item, traverseParseObjects, seen))
-                        {
-                            yield return child;
-                        }
-                    }
-                }
+            seen.Add(target);
+
+            // Recurse into this object's children looking for dirty children.
+            // We only need to look at the child object's current estimated data,
+            // because that's the only data that might need to be saved now.
+
+            CollectDirtyChildren(serviceHub, target.EstimatedData, dirtyChildren, seen, scopedSeenNew);
+
+            if (target.CheckIsDirty(false))
+            {
+                dirtyChildren.Add(target);
             }
         }
+    }
 
-        /// 
-        /// Adds a task to the queue for all of the given objects.
-        /// 
-        static Task EnqueueForAll(IEnumerable objects, Func> taskStart, CancellationToken cancellationToken)
-        {
-            // The task that will be complete when all of the child queues indicate they're ready to start.
+    /// 
+    /// Helper version of CollectDirtyChildren so that callers don't have to add the internally
+    /// used parameters.
+    /// 
+    static void CollectDirtyChildren(this IServiceHub serviceHub, object node, IList dirtyChildren)
+    {
+        CollectDirtyChildren(serviceHub, node, dirtyChildren, new HashSet(new IdentityEqualityComparer()), new HashSet(new IdentityEqualityComparer()));
+    }
+    
+    internal static async Task DeepSaveAsync(this IServiceHub serviceHub, object target, string sessionToken, CancellationToken cancellationToken)
+    {
+        // Collect dirty objects
+        var objects = new List();
+        CollectDirtyChildren(serviceHub, target, objects);
 
-            TaskCompletionSource readyToStart = new TaskCompletionSource();
+        var uniqueObjects = new HashSet(objects, new IdentityEqualityComparer());
 
-            // First, we need to lock the mutex for the queue for every object. We have to hold this
-            // from at least when taskStart() is called to when obj.taskQueue enqueue is called, so
-            // that saves actually get executed in the order they were setup by taskStart().
-            // The locks have to be sorted so that we always acquire them in the same order.
-            // Otherwise, there's some risk of deadlock.
+        // Save all dirty files
+        var saveDirtyFileTasks = TraverseObjectDeep(serviceHub, target, true)
+            .OfType()
+            .Where(file => file.IsDirty)
+            .Select(file => file.SaveAsync(serviceHub, cancellationToken))
+            .ToList();
 
-            LockSet lockSet = new LockSet(objects.Select(o => o.TaskQueue.Mutex));
+        await Task.WhenAll(saveDirtyFileTasks).ConfigureAwait(false);
 
-            lockSet.Enter();
-            try
+        // Save remaining objects in batches
+        var remaining = new List(uniqueObjects);
+        while (remaining.Count>0)
+        {
+            // Partition objects into those that can be saved immediately and those that cannot
+            var current = remaining.Where(item => item.CanBeSerialized).ToList();
+            var nextBatch = remaining.Where(item => !item.CanBeSerialized).ToList();
+            remaining = nextBatch;
+
+            if (current.Count<1)
             {
-                // The task produced by taskStart. By running this immediately, we allow everything prior
-                // to toAwait to run before waiting for all of the queues on all of the objects.
+                throw new InvalidOperationException("Unable to save a ParseObject with a relation to a cycle.");
+            }
 
-                Task fullTask = taskStart(readyToStart.Task);
+            // Save all objects in the current batch
+            var states = current.Select(item => item.State).ToList();
+            var operationsList = current.Select(item => item.StartSave()).ToList();
 
-                // Add fullTask to each of the objects' queues.
+            try
+            {
+                // Await SaveAllAsync to get the collection of Task
+                var saveTasks = await serviceHub.ObjectController.SaveAllAsync(states, operationsList, sessionToken, serviceHub, cancellationToken).ConfigureAwait(false);
 
-                List childTasks = new List();
-                foreach (ParseObject obj in objects)
+                // Await individual tasks in the result
+                foreach (var (item, stateTask) in current.Zip(saveTasks, (item, stateTask) => (item, stateTask)))
                 {
-                    obj.TaskQueue.Enqueue((Task task) =>
-                    {
-                        childTasks.Add(task);
-                        return fullTask;
-                    }, cancellationToken);
+                    var state = await stateTask.ConfigureAwait(false); // Await the Task
+                    item.HandleSave(state); // Now state is IObjectState
                 }
+            }
+            catch (Exception ex) when (ex is TaskCanceledException || ex is OperationCanceledException)
+            {
+                foreach (var (item, ops) in current.Zip(operationsList, (item, ops) => (item, ops)))
+                {
+                    item.HandleFailedSave(ops);
+                }
+
+                throw; // Re-throw cancellation exceptions
+            }
+        }
+    }
 
-                // When all of the objects' queues are ready, signal fullTask that it's ready to go on.
-                Task.WhenAll(childTasks.ToArray()).ContinueWith((Task task) => readyToStart.SetResult(default));
-                return fullTask;
+    static IEnumerable DeepTraversalInternal(this IServiceHub serviceHub, object root, bool traverseParseObjects, ICollection seen)
+    {
+        seen.Add(root);
+        System.Collections.IEnumerable targets = ParseClient.IL2CPPCompiled ? null : null as IEnumerable;
+
+        if (Conversion.As>(root) is { } rootDictionary)
+        {
+            targets = rootDictionary.Values;
+        }
+        else
+        {
+            if (Conversion.As>(root) is { } rootList)
+            {
+                targets = rootList;
             }
-            finally
+            else if (traverseParseObjects)
             {
-                lockSet.Exit();
+                if (root is ParseObject entity)
+                {
+                    targets = entity.Keys.ToList().Select(key => entity[key]);
+                }
             }
         }
 
-        /// 
-        /// Fetches all of the objects in the list.
-        /// 
-        /// The objects to fetch.
-        /// If false, only objects without data will be fetched.
-        /// A task to await before starting.
-        /// The cancellation token.
-        /// The list passed in for convenience.
-        static Task> FetchAllInternalAsync(this IServiceHub serviceHub, IEnumerable objects, bool force, Task toAwait, CancellationToken cancellationToken) where T : ParseObject => toAwait.OnSuccess(_ =>
+        if (targets is { })
         {
-            if (objects.Any(obj => obj.State.ObjectId == null))
+            foreach (object item in targets)
             {
-                throw new InvalidOperationException("You cannot fetch objects that haven't already been saved.");
+                if (!seen.Contains(item))
+                {
+                    yield return item;
+
+                    foreach (object child in DeepTraversalInternal(serviceHub, item, traverseParseObjects, seen))
+                    {
+                        yield return child;
+                    }
+                }
             }
+        }
+    }
+
+    /// 
+    /// Adds a task to the queue for all of the given objects.
+    /// 
+    static Task EnqueueForAll(IEnumerable objects, Func> taskStart, CancellationToken cancellationToken)
+    {
+        // The task that will be complete when all of the child queues indicate they're ready to start.
 
-            List objectsToFetch = (from obj in objects where force || !obj.IsDataAvailable select obj).ToList();
+        TaskCompletionSource readyToStart = new TaskCompletionSource();
 
-            if (objectsToFetch.Count == 0)
+        // First, we need to lock the mutex for the queue for every object. We have to hold this
+        // from at least when taskStart() is called to when obj.taskQueue enqueue is called, so
+        // that saves actually get executed in the order they were setup by taskStart().
+        // The locks have to be sorted so that we always acquire them in the same order.
+        // Otherwise, there's some risk of deadlock.
+
+        LockSet lockSet = new LockSet(objects.Select(o => o.TaskQueue.Mutex));
+
+        lockSet.Enter();
+        try
+        {
+            // The task produced by taskStart. By running this immediately, we allow everything prior
+            // to toAwait to run before waiting for all of the queues on all of the objects.
+
+            Task fullTask = taskStart(readyToStart.Task);
+
+            // Add fullTask to each of the objects' queues.
+
+            List childTasks = new List();
+            foreach (ParseObject obj in objects)
             {
-                return Task.FromResult(objects);
+                obj.TaskQueue.Enqueue((Task task) =>
+                {
+                    childTasks.Add(task);
+                    return fullTask;
+                }, cancellationToken);
             }
 
-            // Do one Find for each class.
+            // When all of the objects' queues are ready, signal fullTask that it's ready to go on.
+            Task.WhenAll(childTasks.ToArray()).ContinueWith((Task task) => readyToStart.SetResult(default));
+            return fullTask;
+        }
+        finally
+        {
+            lockSet.Exit();
+        }
+    }
+
+    /// 
+    /// Fetches all of the objects in the list.
+    /// 
+    /// The objects to fetch.
+    /// If false, only objects without data will be fetched.
+    /// A task to await before starting.
+    /// The cancellation token.
+    /// The list passed in for convenience.
+    static async Task> FetchAllInternalAsync(this IServiceHub serviceHub, IEnumerable objects, bool force, Task toAwait, CancellationToken cancellationToken) where T : ParseObject
+    {
+        // Wait for the preceding task (toAwait) to complete
+        await toAwait.ConfigureAwait(false);
 
-            Dictionary>> findsByClass = (from obj in objectsToFetch group obj.ObjectId by obj.ClassName into classGroup where classGroup.Count() > 0 select (ClassName: classGroup.Key, FindTask: new ParseQuery(serviceHub, classGroup.Key).WhereContainedIn("objectId", classGroup).FindAsync(cancellationToken))).ToDictionary(pair => pair.ClassName, pair => pair.FindTask);
+        // Ensure all objects have an ObjectId
+        if (objects.Any(obj => obj.State.ObjectId == null))
+        {
+            throw new InvalidOperationException("You cannot fetch objects that haven't already been saved.");
+        }
 
-            // Wait for all the Finds to complete.
+        // Filter objects to fetch based on the force flag and data availability
+        var objectsToFetch = objects.Where(obj => force || !obj.IsDataAvailable).ToList();
 
-            return Task.WhenAll(findsByClass.Values.ToList()).OnSuccess(__ =>
+        if (objectsToFetch.Count == 0)
+        {
+            return objects; // No objects need to be fetched
+        }
+
+        // Group objects by ClassName and prepare queries
+        var findsByClass = objectsToFetch
+            .GroupBy(obj => obj.ClassName)
+            .Where(group => group.Any())
+            .ToDictionary(
+                group => group.Key,
+                group => new ParseQuery(serviceHub, group.Key)
+                            .WhereContainedIn("objectId", group.Select(obj => obj.State.ObjectId))
+                            .FindAsync(cancellationToken)
+            );
+
+        // Execute all queries in parallel
+        var findResults = await Task.WhenAll(findsByClass.Values).ConfigureAwait(false);
+
+        // If the operation was canceled, return the original list
+        if (cancellationToken.IsCancellationRequested)
+        {
+            return objects;
+        }
+
+        // Merge fetched data into the original objects
+        foreach (var obj in objectsToFetch)
+        {
+            if (findsByClass.TryGetValue(obj.ClassName, out var resultsTask))
             {
-                if (cancellationToken.IsCancellationRequested)
-                {
-                    return objects;
-                }
+                var results = await resultsTask.ConfigureAwait(false);
+                var match = results.FirstOrDefault(result => result.ObjectId == obj.ObjectId);
 
-                // Merge the data from the Finds into the input objects.
-                foreach ((T obj, ParseObject result) in from obj in objectsToFetch from result in findsByClass[obj.ClassName].Result where result.ObjectId == obj.ObjectId select (obj, result))
+                if (match != null)
                 {
-                    obj.MergeFromObject(result);
+                    obj.MergeFromObject(match);
                     obj.Fetched = true;
                 }
+            }
+        }
+
+        return objects;
+    }
+
+
+    internal static string GetFieldForPropertyName(this IServiceHub serviceHub, string className, string propertyName)
+    {
+        if (serviceHub == null)
+        {
+            return null;
+        }
+
+        if (string.IsNullOrEmpty(className))
+        {
+            throw new ArgumentException("ClassName cannot be null or empty.", nameof(className));
+        }
 
-                return objects;
-            });
-        }).Unwrap();
+        if (string.IsNullOrEmpty(propertyName))
+        {
+            throw new ArgumentException("PropertyName cannot be null or empty.", nameof(propertyName));
+        }
 
-        internal static string GetFieldForPropertyName(this IServiceHub serviceHub, string className, string propertyName) => serviceHub.ClassController.GetPropertyMappings(className).TryGetValue(propertyName, out string fieldName) ? fieldName : fieldName;
+        var classController = serviceHub.ClassController;
+        if (classController == null)
+        {
+            throw new InvalidOperationException("ClassController is null.");
+        }
+
+        var propertyMappings = classController.GetPropertyMappings(className);
+        if (propertyMappings == null)
+        {
+            throw new InvalidOperationException($"Property mappings for class '{className}' are null."); //throws here
+        }
+
+        if (!propertyMappings.TryGetValue(propertyName, out string fieldName))
+        {
+            throw new KeyNotFoundException($"Property '{propertyName}' not found in class '{className}'.");
+        }
+
+        return fieldName;
     }
+
 }
diff --git a/Parse/Utilities/ParseExtensions.cs b/Parse/Utilities/ParseExtensions.cs
index 8bf33c26..46092d56 100644
--- a/Parse/Utilities/ParseExtensions.cs
+++ b/Parse/Utilities/ParseExtensions.cs
@@ -1,40 +1,63 @@
+using System.Collections;
+using System.Collections.Generic;
 using System.Threading;
 using System.Threading.Tasks;
-using Parse.Infrastructure.Utilities;
+using System.Linq;
+namespace Parse;
 
-namespace Parse
+/// 
+/// Provides convenience extension methods for working with collections
+/// of ParseObjects so that you can easily save and fetch them in batches.
+/// 
+/// 
+/// Provides convenience extension methods for working with collections
+/// of ParseObjects so that you can easily save and fetch them in batches.
+/// 
+public static class ParseExtensions
 {
     /// 
-    /// Provides convenience extension methods for working with collections
-    /// of ParseObjects so that you can easily save and fetch them in batches.
+    /// Fetches this object with the data from the server.
     /// 
-    public static class ParseExtensions
+    /// The ParseObject to fetch.
+    /// The cancellation token (optional).
+    public static async Task FetchAsync(this T obj, CancellationToken cancellationToken = default) where T : ParseObject
     {
-        /// 
-        /// Fetches this object with the data from the server.
-        /// 
-        public static Task FetchAsync(this T obj) where T : ParseObject => obj.FetchAsyncInternal(CancellationToken.None).OnSuccess(t => (T) t.Result);
+        var result = await obj.FetchAsyncInternal(cancellationToken).ConfigureAwait(false);
+        return (T) result;
+    }
+    /// 
+    /// Fetches all objects in the collection from the server.
+    /// 
+    public static async Task> FetchAllAsync(this IEnumerable objects, CancellationToken cancellationToken = default) where T : ParseObject
+    {
+        if (objects == null || !objects.Any()) return objects;
 
-        /// 
-        /// Fetches this object with the data from the server.
-        /// 
-        /// The ParseObject to fetch.
-        /// The cancellation token.
-        public static Task FetchAsync(this T target, CancellationToken cancellationToken) where T : ParseObject => target.FetchAsyncInternal(cancellationToken).OnSuccess(task => (T) task.Result);
+        var result = await Task.WhenAll(objects.Select(obj => obj.FetchAsyncInternal(cancellationToken))).ConfigureAwait(false);
+        return result.Cast();
+    }
+
+    /// 
+    /// If this ParseObject has not been fetched (i.e.  returns
+    /// false), fetches this object with the data from the server.
+    /// 
+    /// The ParseObject to fetch.
+    /// The cancellation token (optional).
+    public static async Task FetchIfNeededAsync(this T obj, CancellationToken cancellationToken = default) where T : ParseObject
+    {
+        var result = await obj.FetchIfNeededAsyncInternal(cancellationToken).ConfigureAwait(false);
+        return (T) result;
+    }
 
-        /// 
-        /// If this ParseObject has not been fetched (i.e.  returns
-        /// false), fetches this object with the data from the server.
-        /// 
-        /// The ParseObject to fetch.
-        public static Task FetchIfNeededAsync(this T obj) where T : ParseObject => obj.FetchIfNeededAsyncInternal(CancellationToken.None).OnSuccess(t => (T) t.Result);
+    /// 
+    /// Fetches all objects in the collection from the server only if their data is not available.
+    /// 
+    public static async Task> FetchAllIfNeededAsync(this IEnumerable objects, CancellationToken cancellationToken = default) where T : ParseObject
+    {
+        if (objects == null || !objects.Any())
+            return objects;
 
-        /// 
-        /// If this ParseObject has not been fetched (i.e.  returns
-        /// false), fetches this object with the data from the server.
-        /// 
-        /// The ParseObject to fetch.
-        /// The cancellation token.
-        public static Task FetchIfNeededAsync(this T obj, CancellationToken cancellationToken) where T : ParseObject => obj.FetchIfNeededAsyncInternal(cancellationToken).OnSuccess(t => (T) t.Result);
+        var result = await Task.WhenAll(objects.Select(obj => obj.FetchIfNeededAsyncInternal(cancellationToken))).ConfigureAwait(false);
+        return result.Cast();
     }
+
 }
diff --git a/Parse/Utilities/ParseFileExtensions.cs b/Parse/Utilities/ParseFileExtensions.cs
index 7009bf71..fb8ed845 100644
--- a/Parse/Utilities/ParseFileExtensions.cs
+++ b/Parse/Utilities/ParseFileExtensions.cs
@@ -1,19 +1,21 @@
 using System;
 
-namespace Parse.Abstractions.Internal
+namespace Parse.Abstractions.Internal;
+
+/// 
+/// So here's the deal. We have a lot of internal APIs for ParseObject, ParseUser, etc.
+///
+/// These cannot be 'internal' anymore if we are fully modularizing things out, because
+/// they are no longer a part of the same library, especially as we create things like
+/// Installation inside push library.
+///
+/// So this class contains a bunch of extension methods that can live inside another
+/// namespace, which 'wrap' the intenral APIs that already exist.
+/// 
+public static class ParseFileExtensions
 {
-    /// 
-    /// So here's the deal. We have a lot of internal APIs for ParseObject, ParseUser, etc.
-    ///
-    /// These cannot be 'internal' anymore if we are fully modularizing things out, because
-    /// they are no longer a part of the same library, especially as we create things like
-    /// Installation inside push library.
-    ///
-    /// So this class contains a bunch of extension methods that can live inside another
-    /// namespace, which 'wrap' the intenral APIs that already exist.
-    /// 
-    public static class ParseFileExtensions
+    public static ParseFile Create(string name, Uri uri, string mimeType = null)
     {
-        public static ParseFile Create(string name, Uri uri, string mimeType = null) => new ParseFile(name, uri, mimeType);
+        return new ParseFile(name, uri, mimeType);
     }
 }
diff --git a/Parse/Utilities/ParseQueryExtensions.cs b/Parse/Utilities/ParseQueryExtensions.cs
index 10e65d88..7e34f29f 100644
--- a/Parse/Utilities/ParseQueryExtensions.cs
+++ b/Parse/Utilities/ParseQueryExtensions.cs
@@ -6,120 +6,187 @@
 using System.Reflection;
 using Parse.Infrastructure.Data;
 
-namespace Parse.Abstractions.Internal
-{
+namespace Parse.Abstractions.Internal;
+
+#pragma warning disable CS1030 // #warning directive
 #warning Fully refactor at some point.
 
-    /// 
-    /// So here's the deal. We have a lot of internal APIs for ParseObject, ParseUser, etc.
-    ///
-    /// These cannot be 'internal' anymore if we are fully modularizing things out, because
-    /// they are no longer a part of the same library, especially as we create things like
-    /// Installation inside push library.
-    ///
-    /// So this class contains a bunch of extension methods that can live inside another
-    /// namespace, which 'wrap' the intenral APIs that already exist.
-    /// 
-    public static class ParseQueryExtensions
-    {
-        static MethodInfo ParseObjectGetMethod { get; }
+/// 
+/// So here's the deal. We have a lot of internal APIs for ParseObject, ParseUser, etc.
+///
+/// These cannot be 'internal' anymore if we are fully modularizing things out, because
+/// they are no longer a part of the same library, especially as we create things like
+/// Installation inside push library.
+///
+/// So this class contains a bunch of extension methods that can live inside another
+/// namespace, which 'wrap' the intenral APIs that already exist.
+/// 
+public static class ParseQueryExtensions
+#pragma warning restore CS1030 // #warning directive
+{
+    static MethodInfo ParseObjectGetMethod { get; }
+
+    static MethodInfo StringContainsMethod { get; }
 
-        static MethodInfo StringContainsMethod { get; }
+    static MethodInfo StringStartsWithMethod { get; }
 
-        static MethodInfo StringStartsWithMethod { get; }
+    static MethodInfo StringEndsWithMethod { get; }
 
-        static MethodInfo StringEndsWithMethod { get; }
+    static MethodInfo ContainsMethod { get; }
 
-        static MethodInfo ContainsMethod { get; }
+    static MethodInfo NotContainsMethod { get; }
 
-        static MethodInfo NotContainsMethod { get; }
+    static MethodInfo ContainsKeyMethod { get; }
 
-        static MethodInfo ContainsKeyMethod { get; }
+    static MethodInfo NotContainsKeyMethod { get; }
 
-        static MethodInfo NotContainsKeyMethod { get; }
+    static Dictionary Mappings { get; }
 
-        static Dictionary Mappings { get; }
+    static ParseQueryExtensions()
+    {
+        ParseObjectGetMethod = GetMethod(target => target.Get(null)).GetGenericMethodDefinition();
+        StringContainsMethod = GetMethod(text => text.Contains(null));
+        StringStartsWithMethod = GetMethod(text => text.StartsWith(null));
+        StringEndsWithMethod = GetMethod(text => text.EndsWith(null));
 
-        static ParseQueryExtensions()
+        Mappings = new Dictionary
         {
-            ParseObjectGetMethod = GetMethod(target => target.Get(null)).GetGenericMethodDefinition();
-            StringContainsMethod = GetMethod(text => text.Contains(null));
-            StringStartsWithMethod = GetMethod(text => text.StartsWith(null));
-            StringEndsWithMethod = GetMethod(text => text.EndsWith(null));
+            [StringContainsMethod] = GetMethod>(query => query.WhereContains(null, null)),
+            [StringStartsWithMethod] = GetMethod>(query => query.WhereStartsWith(null, null)),
+            [StringEndsWithMethod] = GetMethod>(query => query.WhereEndsWith(null, null)),
+        };
 
-            Mappings = new Dictionary
-            {
-                [StringContainsMethod] = GetMethod>(query => query.WhereContains(null, null)),
-                [StringStartsWithMethod] = GetMethod>(query => query.WhereStartsWith(null, null)),
-                [StringEndsWithMethod] = GetMethod>(query => query.WhereEndsWith(null, null)),
-            };
+        ContainsMethod = GetMethod(o => ContainsStub(null, null)).GetGenericMethodDefinition();
+        NotContainsMethod = GetMethod(o => NotContainsStub(null, null)).GetGenericMethodDefinition();
 
-            ContainsMethod = GetMethod(o => ContainsStub(null, null)).GetGenericMethodDefinition();
-            NotContainsMethod = GetMethod(o => NotContainsStub(null, null)).GetGenericMethodDefinition();
+        ContainsKeyMethod = GetMethod(o => ContainsKeyStub(null, null));
+        NotContainsKeyMethod = GetMethod(o => NotContainsKeyStub(null, null));
+    }
 
-            ContainsKeyMethod = GetMethod(o => ContainsKeyStub(null, null));
-            NotContainsKeyMethod = GetMethod(o => NotContainsKeyStub(null, null));
-        }
+    /// 
+    /// Gets a MethodInfo for a top-level method call.
+    /// 
+    static MethodInfo GetMethod(Expression> expression)
+    {
+        return (expression.Body as MethodCallExpression).Method;
+    }
 
-        /// 
-        /// Gets a MethodInfo for a top-level method call.
-        /// 
-        static MethodInfo GetMethod(Expression> expression) => (expression.Body as MethodCallExpression).Method;
+    /// 
+    /// When a query is normalized, this is a placeholder to indicate we should
+    /// add a WhereContainedIn() clause.
+    /// 
+    static bool ContainsStub(object collection, T value)
+    {
+        throw new NotImplementedException("Exists only for expression translation as a placeholder.");
+    }
 
-        /// 
-        /// When a query is normalized, this is a placeholder to indicate we should
-        /// add a WhereContainedIn() clause.
-        /// 
-        static bool ContainsStub(object collection, T value) => throw new NotImplementedException("Exists only for expression translation as a placeholder.");
+    /// 
+    /// When a query is normalized, this is a placeholder to indicate we should
+    /// add a WhereNotContainedIn() clause.
+    /// 
+    static bool NotContainsStub(object collection, T value)
+    {
+        throw new NotImplementedException("Exists only for expression translation as a placeholder.");
+    }
 
-        /// 
-        /// When a query is normalized, this is a placeholder to indicate we should
-        /// add a WhereNotContainedIn() clause.
-        /// 
-        static bool NotContainsStub(object collection, T value) => throw new NotImplementedException("Exists only for expression translation as a placeholder.");
+    /// 
+    /// When a query is normalized, this is a placeholder to indicate that we should
+    /// add a WhereExists() clause.
+    /// 
+    static bool ContainsKeyStub(ParseObject obj, string key)
+    {
+        throw new NotImplementedException("Exists only for expression translation as a placeholder.");
+    }
 
-        /// 
-        /// When a query is normalized, this is a placeholder to indicate that we should
-        /// add a WhereExists() clause.
-        /// 
-        static bool ContainsKeyStub(ParseObject obj, string key) => throw new NotImplementedException("Exists only for expression translation as a placeholder.");
+    /// 
+    /// When a query is normalized, this is a placeholder to indicate that we should
+    /// add a WhereDoesNotExist() clause.
+    /// 
+    static bool NotContainsKeyStub(ParseObject obj, string key)
+    {
+        throw new NotImplementedException("Exists only for expression translation as a placeholder.");
+    }
 
-        /// 
-        /// When a query is normalized, this is a placeholder to indicate that we should
-        /// add a WhereDoesNotExist() clause.
-        /// 
-        static bool NotContainsKeyStub(ParseObject obj, string key) => throw new NotImplementedException("Exists only for expression translation as a placeholder.");
+    /// 
+    /// Evaluates an expression and throws if the expression has components that can't be
+    /// evaluated (e.g. uses the parameter that's only represented by an object on the server).
+    /// 
+    static object GetValue(Expression exp)
+    {
+        try
+        {
+            return Expression.Lambda(typeof(Func<>).MakeGenericType(exp.Type), exp).Compile().DynamicInvoke();
+        }
+        catch (Exception e)
+        {
+            throw new InvalidOperationException("Unable to evaluate expression: " + exp, e);
+        }
+    }
 
-        /// 
-        /// Evaluates an expression and throws if the expression has components that can't be
-        /// evaluated (e.g. uses the parameter that's only represented by an object on the server).
-        /// 
-        static object GetValue(Expression exp)
+    /// 
+    /// Checks whether the MethodCallExpression is a call to ParseObject.Get(),
+    /// which is the call we normalize all indexing into the ParseObject to.
+    /// 
+    static bool IsParseObjectGet(MethodCallExpression node)
+    {
+        return node is { Object: { } } && typeof(ParseObject).GetTypeInfo().IsAssignableFrom(node.Object.Type.GetTypeInfo()) && node.Method.IsGenericMethod && node.Method.GetGenericMethodDefinition() == ParseObjectGetMethod;
+    }
+
+    /// 
+    /// Visits an Expression, converting ParseObject.Get/ParseObject[]/ParseObject.Property,
+    /// and nested indices into a single call to ParseObject.Get() with a "field path" like
+    /// "foo.bar.baz"
+    /// 
+    class ObjectNormalizer : ExpressionVisitor
+    {
+        protected override Expression VisitIndex(IndexExpression node)
         {
-            try
-            {
-                return Expression.Lambda(typeof(Func<>).MakeGenericType(exp.Type), exp).Compile().DynamicInvoke();
-            }
-            catch (Exception e)
+            Expression visitedObject = Visit(node.Object);
+            MethodCallExpression indexer = visitedObject as MethodCallExpression;
+
+            if (IsParseObjectGet(indexer))
             {
-                throw new InvalidOperationException("Unable to evaluate expression: " + exp, e);
+                if (!(GetValue(node.Arguments[0]) is string indexValue))
+                {
+                    throw new InvalidOperationException("Index must be a string");
+                }
+
+                return Expression.Call(indexer.Object, ParseObjectGetMethod.MakeGenericMethod(node.Type), Expression.Constant($"{GetValue(indexer.Arguments[0])}.{indexValue}", typeof(string)));
             }
+
+            return base.VisitIndex(node);
         }
 
         /// 
-        /// Checks whether the MethodCallExpression is a call to ParseObject.Get(),
-        /// which is the call we normalize all indexing into the ParseObject to.
+        /// Check for a ParseFieldName attribute and use that as the path component, turning
+        /// properties like foo.ObjectId into foo.Get("objectId")
         /// 
-        static bool IsParseObjectGet(MethodCallExpression node) => node is { Object: { } } && typeof(ParseObject).GetTypeInfo().IsAssignableFrom(node.Object.Type.GetTypeInfo()) && node.Method.IsGenericMethod && node.Method.GetGenericMethodDefinition() == ParseObjectGetMethod;
+        protected override Expression VisitMember(MemberExpression node)
+        {
+            return node.Member.GetCustomAttribute() is { } fieldName && typeof(ParseObject).GetTypeInfo().IsAssignableFrom(node.Expression.Type.GetTypeInfo()) ? Expression.Call(node.Expression, ParseObjectGetMethod.MakeGenericMethod(node.Type), Expression.Constant(fieldName.FieldName, typeof(string))) : base.VisitMember(node);
+        }
 
         /// 
-        /// Visits an Expression, converting ParseObject.Get/ParseObject[]/ParseObject.Property,
-        /// and nested indices into a single call to ParseObject.Get() with a "field path" like
-        /// "foo.bar.baz"
+        /// If a ParseObject.Get() call has been cast, just change the generic parameter.
         /// 
-        class ObjectNormalizer : ExpressionVisitor
+        protected override Expression VisitUnary(UnaryExpression node)
+        {
+            MethodCallExpression methodCall = Visit(node.Operand) as MethodCallExpression;
+            return (node.NodeType == ExpressionType.Convert || node.NodeType == ExpressionType.ConvertChecked) && IsParseObjectGet(methodCall) ? Expression.Call(methodCall.Object, ParseObjectGetMethod.MakeGenericMethod(node.Type), methodCall.Arguments) : base.VisitUnary(node);
+        }
+
+        protected override Expression VisitMethodCall(MethodCallExpression node)
         {
-            protected override Expression VisitIndex(IndexExpression node)
+            // Turn parseObject["foo"] into parseObject.Get("foo")
+
+            if (node.Method.Name == "get_Item" && node.Object is ParameterExpression)
+            {
+                return Expression.Call(node.Object, ParseObjectGetMethod.MakeGenericMethod(typeof(object)), Expression.Constant(GetValue(node.Arguments[0]) as string, typeof(string)));
+            }
+
+            // Turn parseObject.Get("foo")["bar"] into parseObject.Get("foo.bar")
+
+            if (node.Method.Name == "get_Item" || IsParseObjectGet(node))
             {
                 Expression visitedObject = Visit(node.Object);
                 MethodCallExpression indexer = visitedObject as MethodCallExpression;
@@ -133,552 +200,530 @@ protected override Expression VisitIndex(IndexExpression node)
 
                     return Expression.Call(indexer.Object, ParseObjectGetMethod.MakeGenericMethod(node.Type), Expression.Constant($"{GetValue(indexer.Arguments[0])}.{indexValue}", typeof(string)));
                 }
-
-                return base.VisitIndex(node);
             }
 
-            /// 
-            /// Check for a ParseFieldName attribute and use that as the path component, turning
-            /// properties like foo.ObjectId into foo.Get("objectId")
-            /// 
-            protected override Expression VisitMember(MemberExpression node) => node.Member.GetCustomAttribute() is { } fieldName && typeof(ParseObject).GetTypeInfo().IsAssignableFrom(node.Expression.Type.GetTypeInfo()) ? Expression.Call(node.Expression, ParseObjectGetMethod.MakeGenericMethod(node.Type), Expression.Constant(fieldName.FieldName, typeof(string))) : base.VisitMember(node);
+            return base.VisitMethodCall(node);
+        }
+    }
+
+    /// 
+    /// Normalizes Where expressions.
+    /// 
+    class WhereNormalizer : ExpressionVisitor
+    {
 
-            /// 
-            /// If a ParseObject.Get() call has been cast, just change the generic parameter.
-            /// 
-            protected override Expression VisitUnary(UnaryExpression node)
+        /// 
+        /// Normalizes binary operators. <, >, <=, >= !=, and ==
+        /// This puts the ParseObject.Get() on the left side of the operation
+        /// (reversing it if necessary), and normalizes the ParseObject.Get()
+        /// 
+        protected override Expression VisitBinary(BinaryExpression node)
+        {
+            MethodCallExpression rightTransformed = new ObjectNormalizer().Visit(node.Right) as MethodCallExpression, objectExpression;
+            Expression filterExpression;
+            bool inverted;
+
+            if (new ObjectNormalizer().Visit(node.Left) is MethodCallExpression leftTransformed)
             {
-                MethodCallExpression methodCall = Visit(node.Operand) as MethodCallExpression;
-                return (node.NodeType == ExpressionType.Convert || node.NodeType == ExpressionType.ConvertChecked) && IsParseObjectGet(methodCall) ? Expression.Call(methodCall.Object, ParseObjectGetMethod.MakeGenericMethod(node.Type), methodCall.Arguments) : base.VisitUnary(node);
+                objectExpression = leftTransformed;
+                filterExpression = node.Right;
+                inverted = false;
             }
-
-            protected override Expression VisitMethodCall(MethodCallExpression node)
+            else
             {
-                // Turn parseObject["foo"] into parseObject.Get("foo")
-
-                if (node.Method.Name == "get_Item" && node.Object is ParameterExpression)
-                {
-                    return Expression.Call(node.Object, ParseObjectGetMethod.MakeGenericMethod(typeof(object)), Expression.Constant(GetValue(node.Arguments[0]) as string, typeof(string)));
-                }
-
-                // Turn parseObject.Get("foo")["bar"] into parseObject.Get("foo.bar")
+                objectExpression = rightTransformed;
+                filterExpression = node.Left;
+                inverted = true;
+            }
 
-                if (node.Method.Name == "get_Item" || IsParseObjectGet(node))
+            try
+            {
+                switch (node.NodeType)
                 {
-                    Expression visitedObject = Visit(node.Object);
-                    MethodCallExpression indexer = visitedObject as MethodCallExpression;
-
-                    if (IsParseObjectGet(indexer))
-                    {
-                        if (!(GetValue(node.Arguments[0]) is string indexValue))
-                        {
-                            throw new InvalidOperationException("Index must be a string");
-                        }
-
-                        return Expression.Call(indexer.Object, ParseObjectGetMethod.MakeGenericMethod(node.Type), Expression.Constant($"{GetValue(indexer.Arguments[0])}.{indexValue}", typeof(string)));
-                    }
+                    case ExpressionType.GreaterThan:
+                        return inverted ? Expression.LessThan(objectExpression, filterExpression) : Expression.GreaterThan(objectExpression, filterExpression);
+                    case ExpressionType.GreaterThanOrEqual:
+                        return inverted ? Expression.LessThanOrEqual(objectExpression, filterExpression) : Expression.GreaterThanOrEqual(objectExpression, filterExpression);
+                    case ExpressionType.LessThan:
+                        return inverted ? Expression.GreaterThan(objectExpression, filterExpression) : Expression.LessThan(objectExpression, filterExpression);
+                    case ExpressionType.LessThanOrEqual:
+                        return inverted ? Expression.GreaterThanOrEqual(objectExpression, filterExpression) : Expression.LessThanOrEqual(objectExpression, filterExpression);
+                    case ExpressionType.Equal:
+                        return Expression.Equal(objectExpression, filterExpression);
+                    case ExpressionType.NotEqual:
+                        return Expression.NotEqual(objectExpression, filterExpression);
                 }
-
-                return base.VisitMethodCall(node);
             }
+            catch (ArgumentException)
+            {
+                throw new InvalidOperationException("Operation not supported: " + node);
+            }
+
+            return base.VisitBinary(node);
         }
 
         /// 
-        /// Normalizes Where expressions.
+        /// If a ! operator is used, this removes the ! and instead calls the equivalent
+        /// function (so e.g. == becomes !=, < becomes >=, Contains becomes NotContains)
         /// 
-        class WhereNormalizer : ExpressionVisitor
+        protected override Expression VisitUnary(UnaryExpression node)
         {
-
-            /// 
-            /// Normalizes binary operators. <, >, <=, >= !=, and ==
-            /// This puts the ParseObject.Get() on the left side of the operation
-            /// (reversing it if necessary), and normalizes the ParseObject.Get()
-            /// 
-            protected override Expression VisitBinary(BinaryExpression node)
+            // This is incorrect because control is supposed to be able to flow out of the binaryOperand case if the value of NodeType is not matched against an ExpressionType value, which it will not do.
+            //
+            // return node switch
+            // {
+            //     { NodeType: ExpressionType.Not, Operand: var operand } => Visit(operand) switch
+            //     {
+            //         BinaryExpression { Left: var left, Right: var right, NodeType: var type } binaryOperand => type switch
+            //         {
+            //             ExpressionType.GreaterThan => Expression.LessThanOrEqual(left, right),
+            //             ExpressionType.GreaterThanOrEqual => Expression.LessThan(left, right),
+            //             ExpressionType.LessThan => Expression.GreaterThanOrEqual(left, right),
+            //             ExpressionType.LessThanOrEqual => Expression.GreaterThan(left, right),
+            //             ExpressionType.Equal => Expression.NotEqual(left, right),
+            //             ExpressionType.NotEqual => Expression.Equal(left, right),
+            //         },
+            //         _ => base.VisitUnary(node)
+            //     },
+            //     _ => base.VisitUnary(node)
+            // };
+
+            // Normalizes inversion
+
+            if (node.NodeType == ExpressionType.Not)
             {
-                MethodCallExpression rightTransformed = new ObjectNormalizer().Visit(node.Right) as MethodCallExpression, objectExpression;
-                Expression filterExpression;
-                bool inverted;
-
-                if (new ObjectNormalizer().Visit(node.Left) is MethodCallExpression leftTransformed)
-                {
-                    objectExpression = leftTransformed;
-                    filterExpression = node.Right;
-                    inverted = false;
-                }
-                else
+                Expression visitedOperand = Visit(node.Operand);
+                if (visitedOperand is BinaryExpression binaryOperand)
                 {
-                    objectExpression = rightTransformed;
-                    filterExpression = node.Left;
-                    inverted = true;
-                }
-
-                try
-                {
-                    switch (node.NodeType)
+                    switch (binaryOperand.NodeType)
                     {
                         case ExpressionType.GreaterThan:
-                            return inverted ? Expression.LessThan(objectExpression, filterExpression) : Expression.GreaterThan(objectExpression, filterExpression);
+                            return Expression.LessThanOrEqual(binaryOperand.Left, binaryOperand.Right);
                         case ExpressionType.GreaterThanOrEqual:
-                            return inverted ? Expression.LessThanOrEqual(objectExpression, filterExpression) : Expression.GreaterThanOrEqual(objectExpression, filterExpression);
+                            return Expression.LessThan(binaryOperand.Left, binaryOperand.Right);
                         case ExpressionType.LessThan:
-                            return inverted ? Expression.GreaterThan(objectExpression, filterExpression) : Expression.LessThan(objectExpression, filterExpression);
+                            return Expression.GreaterThanOrEqual(binaryOperand.Left, binaryOperand.Right);
                         case ExpressionType.LessThanOrEqual:
-                            return inverted ? Expression.GreaterThanOrEqual(objectExpression, filterExpression) : Expression.LessThanOrEqual(objectExpression, filterExpression);
+                            return Expression.GreaterThan(binaryOperand.Left, binaryOperand.Right);
                         case ExpressionType.Equal:
-                            return Expression.Equal(objectExpression, filterExpression);
+                            return Expression.NotEqual(binaryOperand.Left, binaryOperand.Right);
                         case ExpressionType.NotEqual:
-                            return Expression.NotEqual(objectExpression, filterExpression);
+                            return Expression.Equal(binaryOperand.Left, binaryOperand.Right);
                     }
                 }
-                catch (ArgumentException)
-                {
-                    throw new InvalidOperationException("Operation not supported: " + node);
-                }
-
-                return base.VisitBinary(node);
-            }
 
-            /// 
-            /// If a ! operator is used, this removes the ! and instead calls the equivalent
-            /// function (so e.g. == becomes !=, < becomes >=, Contains becomes NotContains)
-            /// 
-            protected override Expression VisitUnary(UnaryExpression node)
-            {
-                // This is incorrect because control is supposed to be able to flow out of the binaryOperand case if the value of NodeType is not matched against an ExpressionType value, which it will not do.
-                //
-                // return node switch
-                // {
-                //     { NodeType: ExpressionType.Not, Operand: var operand } => Visit(operand) switch
-                //     {
-                //         BinaryExpression { Left: var left, Right: var right, NodeType: var type } binaryOperand => type switch
-                //         {
-                //             ExpressionType.GreaterThan => Expression.LessThanOrEqual(left, right),
-                //             ExpressionType.GreaterThanOrEqual => Expression.LessThan(left, right),
-                //             ExpressionType.LessThan => Expression.GreaterThanOrEqual(left, right),
-                //             ExpressionType.LessThanOrEqual => Expression.GreaterThan(left, right),
-                //             ExpressionType.Equal => Expression.NotEqual(left, right),
-                //             ExpressionType.NotEqual => Expression.Equal(left, right),
-                //         },
-                //         _ => base.VisitUnary(node)
-                //     },
-                //     _ => base.VisitUnary(node)
-                // };
-
-                // Normalizes inversion
-
-                if (node.NodeType == ExpressionType.Not)
+                if (visitedOperand is MethodCallExpression methodCallOperand)
                 {
-                    Expression visitedOperand = Visit(node.Operand);
-                    if (visitedOperand is BinaryExpression binaryOperand)
-                    {
-                        switch (binaryOperand.NodeType)
-                        {
-                            case ExpressionType.GreaterThan:
-                                return Expression.LessThanOrEqual(binaryOperand.Left, binaryOperand.Right);
-                            case ExpressionType.GreaterThanOrEqual:
-                                return Expression.LessThan(binaryOperand.Left, binaryOperand.Right);
-                            case ExpressionType.LessThan:
-                                return Expression.GreaterThanOrEqual(binaryOperand.Left, binaryOperand.Right);
-                            case ExpressionType.LessThanOrEqual:
-                                return Expression.GreaterThan(binaryOperand.Left, binaryOperand.Right);
-                            case ExpressionType.Equal:
-                                return Expression.NotEqual(binaryOperand.Left, binaryOperand.Right);
-                            case ExpressionType.NotEqual:
-                                return Expression.Equal(binaryOperand.Left, binaryOperand.Right);
-                        }
-                    }
-
-                    if (visitedOperand is MethodCallExpression methodCallOperand)
+                    if (methodCallOperand.Method.IsGenericMethod)
                     {
-                        if (methodCallOperand.Method.IsGenericMethod)
+                        if (methodCallOperand.Method.GetGenericMethodDefinition() == ContainsMethod)
                         {
-                            if (methodCallOperand.Method.GetGenericMethodDefinition() == ContainsMethod)
-                            {
-                                return Expression.Call(NotContainsMethod.MakeGenericMethod(methodCallOperand.Method.GetGenericArguments()), methodCallOperand.Arguments.ToArray());
-                            }
-                            if (methodCallOperand.Method.GetGenericMethodDefinition() == NotContainsMethod)
-                            {
-                                return Expression.Call(ContainsMethod.MakeGenericMethod(methodCallOperand.Method.GetGenericArguments()), methodCallOperand.Arguments.ToArray());
-                            }
+                            return Expression.Call(NotContainsMethod.MakeGenericMethod(methodCallOperand.Method.GetGenericArguments()), methodCallOperand.Arguments.ToArray());
                         }
-                        if (methodCallOperand.Method == ContainsKeyMethod)
+                        if (methodCallOperand.Method.GetGenericMethodDefinition() == NotContainsMethod)
                         {
-                            return Expression.Call(NotContainsKeyMethod, methodCallOperand.Arguments.ToArray());
+                            return Expression.Call(ContainsMethod.MakeGenericMethod(methodCallOperand.Method.GetGenericArguments()), methodCallOperand.Arguments.ToArray());
                         }
-                        if (methodCallOperand.Method == NotContainsKeyMethod)
-                        {
-                            return Expression.Call(ContainsKeyMethod, methodCallOperand.Arguments.ToArray());
-                        }
-                    }
-                }
-                return base.VisitUnary(node);
-            }
-
-            /// 
-            /// Normalizes .Equals into == and Contains() into the appropriate stub.
-            /// 
-            protected override Expression VisitMethodCall(MethodCallExpression node)
-            {
-                // Convert .Equals() into ==
-
-                if (node.Method.Name == "Equals" && node.Method.ReturnType == typeof(bool) && node.Method.GetParameters().Length == 1)
-                {
-                    MethodCallExpression obj = new ObjectNormalizer().Visit(node.Object) as MethodCallExpression,  parameter = new ObjectNormalizer().Visit(node.Arguments[0]) as MethodCallExpression;
-
-                    if (IsParseObjectGet(obj) && obj.Object is ParameterExpression || IsParseObjectGet(parameter) && parameter.Object is ParameterExpression)
-                    {
-                        return Expression.Equal(node.Object, node.Arguments[0]);
-                    }
-                }
-
-                // Convert the .Contains() into a ContainsStub
-
-                if (node.Method != StringContainsMethod && node.Method.Name == "Contains" && node.Method.ReturnType == typeof(bool) && node.Method.GetParameters().Length <= 2)
-                {
-                    Expression collection = node.Method.GetParameters().Length == 1 ? node.Object : node.Arguments[0];
-                    int parameterIndex = node.Method.GetParameters().Length - 1;
-
-                    if (new ObjectNormalizer().Visit(node.Arguments[parameterIndex]) is MethodCallExpression { } parameter && IsParseObjectGet(parameter) && parameter.Object is ParameterExpression)
-                    {
-                        return Expression.Call(ContainsMethod.MakeGenericMethod(parameter.Type), collection, parameter);
                     }
-
-                    if (new ObjectNormalizer().Visit(collection) is MethodCallExpression { } target && IsParseObjectGet(target) && target.Object is ParameterExpression)
+                    if (methodCallOperand.Method == ContainsKeyMethod)
                     {
-                        Expression element = node.Arguments[parameterIndex];
-                        return Expression.Call(ContainsMethod.MakeGenericMethod(element.Type), target, element);
+                        return Expression.Call(NotContainsKeyMethod, methodCallOperand.Arguments.ToArray());
                     }
-                }
-
-                // Convert obj["foo.bar"].ContainsKey("baz") into obj.ContainsKey("foo.bar.baz").
-
-                if (node.Method.Name == "ContainsKey" && node.Method.ReturnType == typeof(bool) && node.Method.GetParameters().Length == 1)
-                {
-                    Expression target = null;
-                    string path = null;
-
-                    if (new ObjectNormalizer().Visit(node.Object) is MethodCallExpression { } getter && IsParseObjectGet(getter) && getter.Object is ParameterExpression)
-                    {
-                        return Expression.Call(ContainsKeyMethod, getter.Object, Expression.Constant($"{GetValue(getter.Arguments[0])}.{GetValue(node.Arguments[0])}"));
-                    }
-                    else if (node.Object is ParameterExpression)
-                    {
-                        target = node.Object;
-                        path = GetValue(node.Arguments[0]) as string;
-                    }
-
-                    if (target is { } && path is { })
+                    if (methodCallOperand.Method == NotContainsKeyMethod)
                     {
-                        return Expression.Call(ContainsKeyMethod, target, Expression.Constant(path));
+                        return Expression.Call(ContainsKeyMethod, methodCallOperand.Arguments.ToArray());
                     }
                 }
-                return base.VisitMethodCall(node);
             }
+            return base.VisitUnary(node);
         }
 
         /// 
-        /// Converts a normalized method call expression into the appropriate ParseQuery clause.
+        /// Normalizes .Equals into == and Contains() into the appropriate stub.
         /// 
-        static ParseQuery WhereMethodCall(this ParseQuery source, Expression> expression, MethodCallExpression node) where T : ParseObject
+        protected override Expression VisitMethodCall(MethodCallExpression node)
         {
-            if (IsParseObjectGet(node) && (node.Type == typeof(bool) || node.Type == typeof(bool?)))
-            {
-                // This is a raw boolean field access like 'where obj.Get("foo")'.
-
-                return source.WhereEqualTo(GetValue(node.Arguments[0]) as string, true);
-            }
+            // Convert .Equals() into ==
 
-            if (Mappings.TryGetValue(node.Method, out MethodInfo translatedMethod))
+            if (node.Method.Name == "Equals" && node.Method.ReturnType == typeof(bool) && node.Method.GetParameters().Length == 1)
             {
-                MethodCallExpression objTransformed = new ObjectNormalizer().Visit(node.Object) as MethodCallExpression;
+                MethodCallExpression obj = new ObjectNormalizer().Visit(node.Object) as MethodCallExpression, parameter = new ObjectNormalizer().Visit(node.Arguments[0]) as MethodCallExpression;
 
-                if (!(IsParseObjectGet(objTransformed) && objTransformed.Object == expression.Parameters[0]))
+                if (IsParseObjectGet(obj) && obj.Object is ParameterExpression || IsParseObjectGet(parameter) && parameter.Object is ParameterExpression)
                 {
-                    throw new InvalidOperationException("The left-hand side of a supported function call must be a ParseObject field access.");
+                    return Expression.Equal(node.Object, node.Arguments[0]);
                 }
-
-                return translatedMethod.DeclaringType.GetGenericTypeDefinition().MakeGenericType(typeof(T)).GetRuntimeMethod(translatedMethod.Name, translatedMethod.GetParameters().Select(parameter => parameter.ParameterType).ToArray()).Invoke(source, new[] { GetValue(objTransformed.Arguments[0]), GetValue(node.Arguments[0]) }) as ParseQuery;
             }
 
-            if (node.Arguments[0] == expression.Parameters[0])
+            // Convert the .Contains() into a ContainsStub
+
+            if (node.Method != StringContainsMethod && node.Method.Name == "Contains" && node.Method.ReturnType == typeof(bool) && node.Method.GetParameters().Length <= 2)
             {
-                // obj.ContainsKey("foo") --> query.WhereExists("foo")
+                Expression collection = node.Method.GetParameters().Length == 1 ? node.Object : node.Arguments[0];
+                int parameterIndex = node.Method.GetParameters().Length - 1;
 
-                if (node.Method == ContainsKeyMethod)
+                if (new ObjectNormalizer().Visit(node.Arguments[parameterIndex]) is MethodCallExpression { } parameter && IsParseObjectGet(parameter) && parameter.Object is ParameterExpression)
                 {
-                    return source.WhereExists(GetValue(node.Arguments[1]) as string);
+                    return Expression.Call(ContainsMethod.MakeGenericMethod(parameter.Type), collection, parameter);
                 }
 
-                // !obj.ContainsKey("foo") --> query.WhereDoesNotExist("foo")
-
-                if (node.Method == NotContainsKeyMethod)
+                if (new ObjectNormalizer().Visit(collection) is MethodCallExpression { } target && IsParseObjectGet(target) && target.Object is ParameterExpression)
                 {
-                    return source.WhereDoesNotExist(GetValue(node.Arguments[1]) as string);
+                    Expression element = node.Arguments[parameterIndex];
+                    return Expression.Call(ContainsMethod.MakeGenericMethod(element.Type), target, element);
                 }
             }
 
-            if (node.Method.IsGenericMethod)
-            {
-                if (node.Method.GetGenericMethodDefinition() == ContainsMethod)
-                {
-                    // obj.Get>("path").Contains(someValue)
+            // Convert obj["foo.bar"].ContainsKey("baz") into obj.ContainsKey("foo.bar.baz").
 
-                    if (IsParseObjectGet(node.Arguments[0] as MethodCallExpression))
-                    {
-                        return source.WhereEqualTo(GetValue(((MethodCallExpression) node.Arguments[0]).Arguments[0]) as string, GetValue(node.Arguments[1]));
-                    }
-
-                    // someList.Contains(obj.Get("path"))
+            if (node.Method.Name == "ContainsKey" && node.Method.ReturnType == typeof(bool) && node.Method.GetParameters().Length == 1)
+            {
+                Expression target = null;
+                string path = null;
 
-                    if (IsParseObjectGet(node.Arguments[1] as MethodCallExpression))
-                    {
-                        return source.WhereContainedIn(GetValue(((MethodCallExpression) node.Arguments[1]).Arguments[0]) as string, (GetValue(node.Arguments[0]) as IEnumerable).Cast());
-                    }
+                if (new ObjectNormalizer().Visit(node.Object) is MethodCallExpression { } getter && IsParseObjectGet(getter) && getter.Object is ParameterExpression)
+                {
+                    return Expression.Call(ContainsKeyMethod, getter.Object, Expression.Constant($"{GetValue(getter.Arguments[0])}.{GetValue(node.Arguments[0])}"));
                 }
-
-                if (node.Method.GetGenericMethodDefinition() == NotContainsMethod)
+                else if (node.Object is ParameterExpression)
                 {
-                    // !obj.Get>("path").Contains(someValue)
-
-                    if (IsParseObjectGet(node.Arguments[0] as MethodCallExpression))
-                    {
-                        return source.WhereNotEqualTo(GetValue(((MethodCallExpression) node.Arguments[0]).Arguments[0]) as string, GetValue(node.Arguments[1]));
-                    }
-
-                    // !someList.Contains(obj.Get("path"))
+                    target = node.Object;
+                    path = GetValue(node.Arguments[0]) as string;
+                }
 
-                    if (IsParseObjectGet(node.Arguments[1] as MethodCallExpression))
-                    {
-                        return source.WhereNotContainedIn(GetValue(((MethodCallExpression) node.Arguments[1]).Arguments[0]) as string, (GetValue(node.Arguments[0]) as IEnumerable).Cast());
-                    }
+                if (target is { } && path is { })
+                {
+                    return Expression.Call(ContainsKeyMethod, target, Expression.Constant(path));
                 }
             }
-            throw new InvalidOperationException(node.Method + " is not a supported method call in a where expression.");
+            return base.VisitMethodCall(node);
         }
+    }
 
-        /// 
-        /// Converts a normalized binary expression into the appropriate ParseQuery clause.
-        /// 
-        static ParseQuery WhereBinaryExpression(this ParseQuery source, Expression> expression, BinaryExpression node) where T : ParseObject
+    /// 
+    /// Converts a normalized method call expression into the appropriate ParseQuery clause.
+    /// 
+    static ParseQuery WhereMethodCall(this ParseQuery source, Expression> expression, MethodCallExpression node) where T : ParseObject
+    {
+        if (IsParseObjectGet(node) && (node.Type == typeof(bool) || node.Type == typeof(bool?)))
+        {
+            // This is a raw boolean field access like 'where obj.Get("foo")'.
+
+            return source.WhereEqualTo(GetValue(node.Arguments[0]) as string, true);
+        }
+
+        if (Mappings.TryGetValue(node.Method, out MethodInfo translatedMethod))
         {
-            MethodCallExpression leftTransformed = new ObjectNormalizer().Visit(node.Left) as MethodCallExpression;
+            MethodCallExpression objTransformed = new ObjectNormalizer().Visit(node.Object) as MethodCallExpression;
 
-            if (!(IsParseObjectGet(leftTransformed) && leftTransformed.Object == expression.Parameters[0]))
+            if (!(IsParseObjectGet(objTransformed) && objTransformed.Object == expression.Parameters[0]))
             {
-                throw new InvalidOperationException("Where expressions must have one side be a field operation on a ParseObject.");
+                throw new InvalidOperationException("The left-hand side of a supported function call must be a ParseObject field access.");
             }
 
-            string fieldPath = GetValue(leftTransformed.Arguments[0]) as string;
-            object filterValue = GetValue(node.Right);
+            return translatedMethod.DeclaringType.GetGenericTypeDefinition().MakeGenericType(typeof(T)).GetRuntimeMethod(translatedMethod.Name, translatedMethod.GetParameters().Select(parameter => parameter.ParameterType).ToArray()).Invoke(source, new[] { GetValue(objTransformed.Arguments[0]), GetValue(node.Arguments[0]) }) as ParseQuery;
+        }
+
+        if (node.Arguments[0] == expression.Parameters[0])
+        {
+            // obj.ContainsKey("foo") --> query.WhereExists("foo")
 
-            if (filterValue != null && !ParseDataEncoder.Validate(filterValue))
+            if (node.Method == ContainsKeyMethod)
             {
-                throw new InvalidOperationException("Where clauses must use types compatible with ParseObjects.");
+                return source.WhereExists(GetValue(node.Arguments[1]) as string);
             }
 
-            return node.NodeType switch
+            // !obj.ContainsKey("foo") --> query.WhereDoesNotExist("foo")
+
+            if (node.Method == NotContainsKeyMethod)
             {
-                ExpressionType.GreaterThan => source.WhereGreaterThan(fieldPath, filterValue),
-                ExpressionType.GreaterThanOrEqual => source.WhereGreaterThanOrEqualTo(fieldPath, filterValue),
-                ExpressionType.LessThan => source.WhereLessThan(fieldPath, filterValue),
-                ExpressionType.LessThanOrEqual => source.WhereLessThanOrEqualTo(fieldPath, filterValue),
-                ExpressionType.Equal => source.WhereEqualTo(fieldPath, filterValue),
-                ExpressionType.NotEqual => source.WhereNotEqualTo(fieldPath, filterValue),
-                _ => throw new InvalidOperationException("Where expressions do not support this operator."),
-            };
+                return source.WhereDoesNotExist(GetValue(node.Arguments[1]) as string);
+            }
         }
 
-        /// 
-        /// Filters a query based upon the predicate provided.
-        /// 
-        /// The type of ParseObject being queried for.
-        /// The base  to which
-        /// the predicate will be added.
-        /// A function to test each ParseObject for a condition.
-        /// The predicate must be able to be represented by one of the standard Where
-        /// functions on ParseQuery
-        /// A new ParseQuery whose results will match the given predicate as
-        /// well as the source's filters.
-        public static ParseQuery Where(this ParseQuery source, Expression> predicate) where TSource : ParseObject
+        if (node.Method.IsGenericMethod)
         {
-            // Handle top-level logic operators && and ||
-
-            if (predicate.Body is BinaryExpression binaryExpression)
+            if (node.Method.GetGenericMethodDefinition() == ContainsMethod)
             {
-                if (binaryExpression.NodeType == ExpressionType.AndAlso)
+                // obj.Get>("path").Contains(someValue)
+
+                if (IsParseObjectGet(node.Arguments[0] as MethodCallExpression))
                 {
-                    return source.Where(Expression.Lambda>(binaryExpression.Left, predicate.Parameters)).Where(Expression.Lambda>(binaryExpression.Right, predicate.Parameters));
+                    return source.WhereEqualTo(GetValue(((MethodCallExpression) node.Arguments[0]).Arguments[0]) as string, GetValue(node.Arguments[1]));
                 }
 
-                if (binaryExpression.NodeType == ExpressionType.OrElse)
+                // someList.Contains(obj.Get("path"))
+
+                if (IsParseObjectGet(node.Arguments[1] as MethodCallExpression))
                 {
-                    return source.Services.ConstructOrQuery(source.Where(Expression.Lambda>(binaryExpression.Left, predicate.Parameters)), (ParseQuery) source.Where(Expression.Lambda>(binaryExpression.Right, predicate.Parameters)));
+                    return source.WhereContainedIn(GetValue(((MethodCallExpression) node.Arguments[1]).Arguments[0]) as string, (GetValue(node.Arguments[0]) as IEnumerable).Cast());
                 }
             }
 
-            Expression normalized = new WhereNormalizer().Visit(predicate.Body);
-
-            if (normalized is MethodCallExpression methodCallExpr)
+            if (node.Method.GetGenericMethodDefinition() == NotContainsMethod)
             {
-                return source.WhereMethodCall(predicate, methodCallExpr);
-            }
+                // !obj.Get>("path").Contains(someValue)
 
-            if (normalized is BinaryExpression binaryExpr)
-            {
-                return source.WhereBinaryExpression(predicate, binaryExpr);
-            }
+                if (IsParseObjectGet(node.Arguments[0] as MethodCallExpression))
+                {
+                    return source.WhereNotEqualTo(GetValue(((MethodCallExpression) node.Arguments[0]).Arguments[0]) as string, GetValue(node.Arguments[1]));
+                }
 
-            if (normalized is UnaryExpression { NodeType: ExpressionType.Not, Operand: MethodCallExpression { } node, Type: var type } unaryExpr && IsParseObjectGet(node) && (type == typeof(bool) || type == typeof(bool?)))
-            {
-                // This is a raw boolean field access like 'where !obj.Get("foo")'.
+                // !someList.Contains(obj.Get("path"))
 
-                return source.WhereNotEqualTo(GetValue(node.Arguments[0]) as string, true);
+                if (IsParseObjectGet(node.Arguments[1] as MethodCallExpression))
+                {
+                    return source.WhereNotContainedIn(GetValue(((MethodCallExpression) node.Arguments[1]).Arguments[0]) as string, (GetValue(node.Arguments[0]) as IEnumerable).Cast());
+                }
             }
+        }
+        throw new InvalidOperationException(node.Method + " is not a supported method call in a where expression.");
+    }
+
+    /// 
+    /// Converts a normalized binary expression into the appropriate ParseQuery clause.
+    /// 
+    static ParseQuery WhereBinaryExpression(this ParseQuery source, Expression> expression, BinaryExpression node) where T : ParseObject
+    {
+        MethodCallExpression leftTransformed = new ObjectNormalizer().Visit(node.Left) as MethodCallExpression;
 
-            throw new InvalidOperationException("Encountered an unsupported expression for ParseQueries.");
+        if (!(IsParseObjectGet(leftTransformed) && leftTransformed.Object == expression.Parameters[0]))
+        {
+            throw new InvalidOperationException("Where expressions must have one side be a field operation on a ParseObject.");
         }
 
-        /// 
-        /// Normalizes an OrderBy's keySelector expression and then extracts the path
-        /// from the ParseObject.Get() call.
-        /// 
-        static string GetOrderByPath(Expression> keySelector)
+        string fieldPath = GetValue(leftTransformed.Arguments[0]) as string;
+        object filterValue = GetValue(node.Right);
+
+        if (filterValue != null && !ParseDataEncoder.Validate(filterValue))
         {
-            string result = null;
-            Expression normalized = new ObjectNormalizer().Visit(keySelector.Body);
-            MethodCallExpression callExpr = normalized as MethodCallExpression;
+            throw new InvalidOperationException("Where clauses must use types compatible with ParseObjects.");
+        }
 
-            if (IsParseObjectGet(callExpr) && callExpr.Object == keySelector.Parameters[0])
-            {
-                // We're operating on the parameter
+        return node.NodeType switch
+        {
+            ExpressionType.GreaterThan => source.WhereGreaterThan(fieldPath, filterValue),
+            ExpressionType.GreaterThanOrEqual => source.WhereGreaterThanOrEqualTo(fieldPath, filterValue),
+            ExpressionType.LessThan => source.WhereLessThan(fieldPath, filterValue),
+            ExpressionType.LessThanOrEqual => source.WhereLessThanOrEqualTo(fieldPath, filterValue),
+            ExpressionType.Equal => source.WhereEqualTo(fieldPath, filterValue),
+            ExpressionType.NotEqual => source.WhereNotEqualTo(fieldPath, filterValue),
+            _ => throw new InvalidOperationException("Where expressions do not support this operator."),
+        };
+    }
 
-                result = GetValue(callExpr.Arguments[0]) as string;
+    /// 
+    /// Filters a query based upon the predicate provided.
+    /// 
+    /// The type of ParseObject being queried for.
+    /// The base  to which
+    /// the predicate will be added.
+    /// A function to test each ParseObject for a condition.
+    /// The predicate must be able to be represented by one of the standard Where
+    /// functions on ParseQuery
+    /// A new ParseQuery whose results will match the given predicate as
+    /// well as the source's filters.
+    public static ParseQuery Where(this ParseQuery source, Expression> predicate) where TSource : ParseObject
+    {
+        // Handle top-level logic operators && and ||
+
+        if (predicate.Body is BinaryExpression binaryExpression)
+        {
+            if (binaryExpression.NodeType == ExpressionType.AndAlso)
+            {
+                return source.Where(Expression.Lambda>(binaryExpression.Left, predicate.Parameters)).Where(Expression.Lambda>(binaryExpression.Right, predicate.Parameters));
             }
 
-            if (result == null)
+            if (binaryExpression.NodeType == ExpressionType.OrElse)
             {
-                throw new InvalidOperationException("OrderBy expression must be a field access on a ParseObject.");
+                return source.Services.ConstructOrQuery(source.Where(Expression.Lambda>(binaryExpression.Left, predicate.Parameters)), (ParseQuery) source.Where(Expression.Lambda>(binaryExpression.Right, predicate.Parameters)));
             }
+        }
 
-            return result;
+        Expression normalized = new WhereNormalizer().Visit(predicate.Body);
+
+        if (normalized is MethodCallExpression methodCallExpr)
+        {
+            return source.WhereMethodCall(predicate, methodCallExpr);
         }
 
-        /// 
-        /// Orders a query based upon the key selector provided.
-        /// 
-        /// The type of ParseObject being queried for.
-        /// The type of key returned by keySelector.
-        /// The query to order.
-        /// A function to extract a key from the ParseObject.
-        /// A new ParseQuery based on source whose results will be ordered by
-        /// the key specified in the keySelector.
-        public static ParseQuery OrderBy(this ParseQuery source, Expression> keySelector) where TSource : ParseObject => source.OrderBy(GetOrderByPath(keySelector));
+        if (normalized is BinaryExpression binaryExpr)
+        {
+            return source.WhereBinaryExpression(predicate, binaryExpr);
+        }
 
-        /// 
-        /// Orders a query based upon the key selector provided.
-        /// 
-        /// The type of ParseObject being queried for.
-        /// The type of key returned by keySelector.
-        /// The query to order.
-        /// A function to extract a key from the ParseObject.
-        /// A new ParseQuery based on source whose results will be ordered by
-        /// the key specified in the keySelector.
-        public static ParseQuery OrderByDescending( this ParseQuery source, Expression> keySelector) where TSource : ParseObject => source.OrderByDescending(GetOrderByPath(keySelector));
+        if (normalized is UnaryExpression { NodeType: ExpressionType.Not, Operand: MethodCallExpression { } node, Type: var type } unaryExpr && IsParseObjectGet(node) && (type == typeof(bool) || type == typeof(bool?)))
+        {
+            // This is a raw boolean field access like 'where !obj.Get("foo")'.
 
-        /// 
-        /// Performs a subsequent ordering of a query based upon the key selector provided.
-        /// 
-        /// The type of ParseObject being queried for.
-        /// The type of key returned by keySelector.
-        /// The query to order.
-        /// A function to extract a key from the ParseObject.
-        /// A new ParseQuery based on source whose results will be ordered by
-        /// the key specified in the keySelector.
-        public static ParseQuery ThenBy(this ParseQuery source, Expression> keySelector) where TSource : ParseObject => source.ThenBy(GetOrderByPath(keySelector));
+            return source.WhereNotEqualTo(GetValue(node.Arguments[0]) as string, true);
+        }
 
-        /// 
-        /// Performs a subsequent ordering of a query based upon the key selector provided.
-        /// 
-        /// The type of ParseObject being queried for.
-        /// The type of key returned by keySelector.
-        /// The query to order.
-        /// A function to extract a key from the ParseObject.
-        /// A new ParseQuery based on source whose results will be ordered by
-        /// the key specified in the keySelector.
-        public static ParseQuery ThenByDescending(this ParseQuery source, Expression> keySelector) where TSource : ParseObject => source.ThenByDescending(GetOrderByPath(keySelector));
+        throw new InvalidOperationException("Encountered an unsupported expression for ParseQueries.");
+    }
 
-        /// 
-        /// Correlates the elements of two queries based on matching keys.
-        /// 
-        /// The type of ParseObjects of the first query.
-        /// The type of ParseObjects of the second query.
-        /// The type of the keys returned by the key selector
-        /// functions.
-        /// The type of the result. This must match either
-        /// TOuter or TInner
-        /// The first query to join.
-        /// The query to join to the first query.
-        /// A function to extract a join key from the results of
-        /// the first query.
-        /// A function to extract a join key from the results of
-        /// the second query.
-        /// A function to select either the outer or inner query
-        /// result to determine which query is the base query.
-        /// A new ParseQuery with a WhereMatchesQuery or WhereMatchesKeyInQuery
-        /// clause based upon the query indicated in the .
-        public static ParseQuery Join(this ParseQuery outer, ParseQuery inner, Expression> outerKeySelector, Expression> innerKeySelector, Expression> resultSelector) where TOuter : ParseObject where TInner : ParseObject where TResult : ParseObject
+    /// 
+    /// Normalizes an OrderBy's keySelector expression and then extracts the path
+    /// from the ParseObject.Get() call.
+    /// 
+    static string GetOrderByPath(Expression> keySelector)
+    {
+        string result = null;
+        Expression normalized = new ObjectNormalizer().Visit(keySelector.Body);
+        MethodCallExpression callExpr = normalized as MethodCallExpression;
+
+        if (IsParseObjectGet(callExpr) && callExpr.Object == keySelector.Parameters[0])
         {
-            // resultSelector must select either the inner object or the outer object. If it's the inner object, reverse the query.
+            // We're operating on the parameter
 
-            if (resultSelector.Body == resultSelector.Parameters[1])
-            {
-                // The inner object was selected.
+            result = GetValue(callExpr.Arguments[0]) as string;
+        }
 
-                return inner.Join(outer, innerKeySelector, outerKeySelector, (i, o) => i) as ParseQuery;
-            }
+        if (result == null)
+        {
+            throw new InvalidOperationException("OrderBy expression must be a field access on a ParseObject.");
+        }
 
-            if (resultSelector.Body != resultSelector.Parameters[0])
-            {
-                throw new InvalidOperationException("Joins must select either the outer or inner object.");
-            }
+        return result;
+    }
 
-            // Normalize both selectors
-            Expression outerNormalized = new ObjectNormalizer().Visit(outerKeySelector.Body), innerNormalized = new ObjectNormalizer().Visit(innerKeySelector.Body);
-            MethodCallExpression outerAsGet = outerNormalized as MethodCallExpression, innerAsGet = innerNormalized as MethodCallExpression;
+    /// 
+    /// Orders a query based upon the key selector provided.
+    /// 
+    /// The type of ParseObject being queried for.
+    /// The type of key returned by keySelector.
+    /// The query to order.
+    /// A function to extract a key from the ParseObject.
+    /// A new ParseQuery based on source whose results will be ordered by
+    /// the key specified in the keySelector.
+    public static ParseQuery OrderBy(this ParseQuery source, Expression> keySelector) where TSource : ParseObject
+    {
+        return source.OrderBy(GetOrderByPath(keySelector));
+    }
 
-            if (IsParseObjectGet(outerAsGet) && outerAsGet.Object == outerKeySelector.Parameters[0])
-            {
-                string outerKey = GetValue(outerAsGet.Arguments[0]) as string;
+    /// 
+    /// Orders a query based upon the key selector provided.
+    /// 
+    /// The type of ParseObject being queried for.
+    /// The type of key returned by keySelector.
+    /// The query to order.
+    /// A function to extract a key from the ParseObject.
+    /// A new ParseQuery based on source whose results will be ordered by
+    /// the key specified in the keySelector.
+    public static ParseQuery OrderByDescending(this ParseQuery source, Expression> keySelector) where TSource : ParseObject
+    {
+        return source.OrderByDescending(GetOrderByPath(keySelector));
+    }
 
-                if (IsParseObjectGet(innerAsGet) && innerAsGet.Object == innerKeySelector.Parameters[0])
-                {
-                    // Both are key accesses, so treat this as a WhereMatchesKeyInQuery.
+    /// 
+    /// Performs a subsequent ordering of a query based upon the key selector provided.
+    /// 
+    /// The type of ParseObject being queried for.
+    /// The type of key returned by keySelector.
+    /// The query to order.
+    /// A function to extract a key from the ParseObject.
+    /// A new ParseQuery based on source whose results will be ordered by
+    /// the key specified in the keySelector.
+    public static ParseQuery ThenBy(this ParseQuery source, Expression> keySelector) where TSource : ParseObject
+    {
+        return source.ThenBy(GetOrderByPath(keySelector));
+    }
 
-                    return outer.WhereMatchesKeyInQuery(outerKey, GetValue(innerAsGet.Arguments[0]) as string, inner) as ParseQuery;
-                }
+    /// 
+    /// Performs a subsequent ordering of a query based upon the key selector provided.
+    /// 
+    /// The type of ParseObject being queried for.
+    /// The type of key returned by keySelector.
+    /// The query to order.
+    /// A function to extract a key from the ParseObject.
+    /// A new ParseQuery based on source whose results will be ordered by
+    /// the key specified in the keySelector.
+    public static ParseQuery ThenByDescending(this ParseQuery source, Expression> keySelector) where TSource : ParseObject
+    {
+        return source.ThenByDescending(GetOrderByPath(keySelector));
+    }
 
-                if (innerKeySelector.Body == innerKeySelector.Parameters[0])
-                {
-                    // The inner selector is on the result of the query itself, so treat this as a WhereMatchesQuery.
+    /// 
+    /// Correlates the elements of two queries based on matching keys.
+    /// 
+    /// The type of ParseObjects of the first query.
+    /// The type of ParseObjects of the second query.
+    /// The type of the keys returned by the key selector
+    /// functions.
+    /// The type of the result. This must match either
+    /// TOuter or TInner
+    /// The first query to join.
+    /// The query to join to the first query.
+    /// A function to extract a join key from the results of
+    /// the first query.
+    /// A function to extract a join key from the results of
+    /// the second query.
+    /// A function to select either the outer or inner query
+    /// result to determine which query is the base query.
+    /// A new ParseQuery with a WhereMatchesQuery or WhereMatchesKeyInQuery
+    /// clause based upon the query indicated in the .
+    public static ParseQuery Join(this ParseQuery outer, ParseQuery inner, Expression> outerKeySelector, Expression> innerKeySelector, Expression> resultSelector) where TOuter : ParseObject where TInner : ParseObject where TResult : ParseObject
+    {
+        // resultSelector must select either the inner object or the outer object. If it's the inner object, reverse the query.
 
-                    return outer.WhereMatchesQuery(outerKey, inner) as ParseQuery;
-                }
+        if (resultSelector.Body == resultSelector.Parameters[1])
+        {
+            // The inner object was selected.
+
+            return inner.Join(outer, innerKeySelector, outerKeySelector, (i, o) => i) as ParseQuery;
+        }
+
+        if (resultSelector.Body != resultSelector.Parameters[0])
+        {
+            throw new InvalidOperationException("Joins must select either the outer or inner object.");
+        }
 
-                throw new InvalidOperationException("The key for the joined object must be a ParseObject or a field access on the ParseObject.");
+        // Normalize both selectors
+        Expression outerNormalized = new ObjectNormalizer().Visit(outerKeySelector.Body), innerNormalized = new ObjectNormalizer().Visit(innerKeySelector.Body);
+        MethodCallExpression outerAsGet = outerNormalized as MethodCallExpression, innerAsGet = innerNormalized as MethodCallExpression;
+
+        if (IsParseObjectGet(outerAsGet) && outerAsGet.Object == outerKeySelector.Parameters[0])
+        {
+            string outerKey = GetValue(outerAsGet.Arguments[0]) as string;
+
+            if (IsParseObjectGet(innerAsGet) && innerAsGet.Object == innerKeySelector.Parameters[0])
+            {
+                // Both are key accesses, so treat this as a WhereMatchesKeyInQuery.
+
+                return outer.WhereMatchesKeyInQuery(outerKey, GetValue(innerAsGet.Arguments[0]) as string, inner) as ParseQuery;
             }
 
-            // TODO (hallucinogen): If we ever support "and" queries fully and/or support a "where this object
-            // matches some key in some other query" (as opposed to requiring a key on this query), we
-            // can add support for even more types of joins.
+            if (innerKeySelector.Body == innerKeySelector.Parameters[0])
+            {
+                // The inner selector is on the result of the query itself, so treat this as a WhereMatchesQuery.
+
+                return outer.WhereMatchesQuery(outerKey, inner) as ParseQuery;
+            }
 
-            throw new InvalidOperationException("The key for the selected object must be a field access on the ParseObject.");
+            throw new InvalidOperationException("The key for the joined object must be a ParseObject or a field access on the ParseObject.");
         }
 
-        public static string GetClassName(this ParseQuery query) where T : ParseObject => query.ClassName;
+        // TODO (hallucinogen): If we ever support "and" queries fully and/or support a "where this object
+        // matches some key in some other query" (as opposed to requiring a key on this query), we
+        // can add support for even more types of joins.
+
+        throw new InvalidOperationException("The key for the selected object must be a field access on the ParseObject.");
+    }
 
-        public static IDictionary BuildParameters(this ParseQuery query) where T : ParseObject => query.BuildParameters(false);
+    public static string GetClassName(this ParseQuery query) where T : ParseObject
+    {
+        return query.ClassName;
+    }
 
-        public static object GetConstraint(this ParseQuery query, string key) where T : ParseObject => query.GetConstraint(key);
+    public static IDictionary BuildParameters(this ParseQuery query) where T : ParseObject
+    {
+        return query.BuildParameters(false);
+    }
+
+    public static object GetConstraint(this ParseQuery query, string key) where T : ParseObject
+    {
+        return query.GetConstraint(key);
     }
 }
+
+
diff --git a/Parse/Utilities/ParseRelationExtensions.cs b/Parse/Utilities/ParseRelationExtensions.cs
index d52f6121..5a368656 100644
--- a/Parse/Utilities/ParseRelationExtensions.cs
+++ b/Parse/Utilities/ParseRelationExtensions.cs
@@ -1,21 +1,29 @@
-namespace Parse.Abstractions.Internal
+namespace Parse.Abstractions.Internal;
+
+/// 
+/// So here's the deal. We have a lot of internal APIs for ParseObject, ParseUser, etc.
+///
+/// These cannot be 'internal' anymore if we are fully modularizing things out, because
+/// they are no longer a part of the same library, especially as we create things like
+/// Installation inside push library.
+///
+/// So this class contains a bunch of extension methods that can live inside another
+/// namespace, which 'wrap' the intenral APIs that already exist.
+/// 
+public static class ParseRelationExtensions
 {
-    /// 
-    /// So here's the deal. We have a lot of internal APIs for ParseObject, ParseUser, etc.
-    ///
-    /// These cannot be 'internal' anymore if we are fully modularizing things out, because
-    /// they are no longer a part of the same library, especially as we create things like
-    /// Installation inside push library.
-    ///
-    /// So this class contains a bunch of extension methods that can live inside another
-    /// namespace, which 'wrap' the intenral APIs that already exist.
-    /// 
-    public static class ParseRelationExtensions
+    public static ParseRelation Create(ParseObject parent, string childKey) where T : ParseObject
     {
-        public static ParseRelation Create(ParseObject parent, string childKey) where T : ParseObject => new ParseRelation(parent, childKey);
+        return new ParseRelation(parent, childKey);
+    }
 
-        public static ParseRelation Create(ParseObject parent, string childKey, string targetClassName) where T : ParseObject => new ParseRelation(parent, childKey, targetClassName);
+    public static ParseRelation Create(ParseObject parent, string childKey, string targetClassName) where T : ParseObject
+    {
+        return new ParseRelation(parent, childKey, targetClassName);
+    }
 
-        public static string GetTargetClassName(this ParseRelation relation) where T : ParseObject => relation.TargetClassName;
+    public static string GetTargetClassName(this ParseRelation relation) where T : ParseObject
+    {
+        return relation.TargetClassName;
     }
 }
diff --git a/Parse/Utilities/ParseUserExtensions.cs b/Parse/Utilities/ParseUserExtensions.cs
index 2479a6af..4abd0ece 100644
--- a/Parse/Utilities/ParseUserExtensions.cs
+++ b/Parse/Utilities/ParseUserExtensions.cs
@@ -2,26 +2,37 @@
 using System.Threading;
 using System.Threading.Tasks;
 
-namespace Parse.Abstractions.Internal
+namespace Parse.Abstractions.Internal;
+
+/// 
+/// So here's the deal. We have a lot of internal APIs for ParseObject, ParseUser, etc.
+///
+/// These cannot be 'internal' anymore if we are fully modularizing things out, because
+/// they are no longer a part of the same library, especially as we create things like
+/// Installation inside push library.
+///
+/// So this class contains a bunch of extension methods that can live inside another
+/// namespace, which 'wrap' the intenral APIs that already exist.
+/// 
+public static class ParseUserExtensions
 {
-    /// 
-    /// So here's the deal. We have a lot of internal APIs for ParseObject, ParseUser, etc.
-    ///
-    /// These cannot be 'internal' anymore if we are fully modularizing things out, because
-    /// they are no longer a part of the same library, especially as we create things like
-    /// Installation inside push library.
-    ///
-    /// So this class contains a bunch of extension methods that can live inside another
-    /// namespace, which 'wrap' the intenral APIs that already exist.
-    /// 
-    public static class ParseUserExtensions
+    public static Task UnlinkFromAsync(this ParseUser user, string authType, CancellationToken cancellationToken)
     {
-        public static Task UnlinkFromAsync(this ParseUser user, string authType, CancellationToken cancellationToken) => user.UnlinkFromAsync(authType, cancellationToken);
+        return user.UnlinkFromAsync(authType, cancellationToken);
+    }
 
-        public static Task LinkWithAsync(this ParseUser user, string authType, CancellationToken cancellationToken) => user.LinkWithAsync(authType, cancellationToken);
+    public static Task LinkWithAsync(this ParseUser user, string authType, CancellationToken cancellationToken)
+    {
+        return user.LinkWithAsync(authType, cancellationToken);
+    }
 
-        public static Task LinkWithAsync(this ParseUser user, string authType, IDictionary data, CancellationToken cancellationToken) => user.LinkWithAsync(authType, data, cancellationToken);
+    public static Task LinkWithAsync(this ParseUser user, string authType, IDictionary data, CancellationToken cancellationToken)
+    {
+        return user.LinkWithAsync(authType, data, cancellationToken);
+    }
 
-        public static Task UpgradeToRevocableSessionAsync(this ParseUser user, CancellationToken cancellationToken) => user.UpgradeToRevocableSessionAsync(cancellationToken);
+    public static Task UpgradeToRevocableSessionAsync(this ParseUser user, CancellationToken cancellationToken)
+    {
+        return user.UpgradeToRevocableSessionAsync(cancellationToken);
     }
 }
diff --git a/Parse/Utilities/PushServiceExtensions.cs b/Parse/Utilities/PushServiceExtensions.cs
index caa32d53..2175c9c0 100644
--- a/Parse/Utilities/PushServiceExtensions.cs
+++ b/Parse/Utilities/PushServiceExtensions.cs
@@ -6,195 +6,232 @@
 using Parse.Infrastructure.Utilities;
 using Parse.Platform.Push;
 
-namespace Parse
+namespace Parse;
+
+public static class PushServiceExtensions
 {
-    public static class PushServiceExtensions
+    /// 
+    /// Pushes a simple message to every device. This is shorthand for:
+    ///
+    /// 
+    /// var push = new ParsePush();
+    /// push.Data = new Dictionary<string, object>{{"alert", alert}};
+    /// return push.SendAsync();
+    /// 
+    /// 
+    /// The alert message to send.
+    public static Task SendPushAlertAsync(this IServiceHub serviceHub, string alert)
+    {
+        return new ParsePush(serviceHub) { Alert = alert }.SendAsync();
+    }
+
+    /// 
+    /// Pushes a simple message to every device subscribed to channel. This is shorthand for:
+    ///
+    /// 
+    /// var push = new ParsePush();
+    /// push.Channels = new List<string> { channel };
+    /// push.Data = new Dictionary<string, object>{{"alert", alert}};
+    /// return push.SendAsync();
+    /// 
+    /// 
+    /// The alert message to send.
+    /// An Installation must be subscribed to channel to receive this Push Notification.
+    public static Task SendPushAlertAsync(this IServiceHub serviceHub, string alert, string channel)
+    {
+        return new ParsePush(serviceHub) { Channels = new List { channel }, Alert = alert }.SendAsync();
+    }
+
+    /// 
+    /// Pushes a simple message to every device subscribed to any of channels. This is shorthand for:
+    ///
+    /// 
+    /// var push = new ParsePush();
+    /// push.Channels = channels;
+    /// push.Data = new Dictionary<string, object>{{"alert", alert}};
+    /// return push.SendAsync();
+    /// 
+    /// 
+    /// The alert message to send.
+    /// An Installation must be subscribed to any of channels to receive this Push Notification.
+    public static Task SendPushAlertAsync(this IServiceHub serviceHub, string alert, IEnumerable channels)
+    {
+        return new ParsePush(serviceHub) { Channels = channels, Alert = alert }.SendAsync();
+    }
+
+    /// 
+    /// Pushes a simple message to every device matching the target query. This is shorthand for:
+    ///
+    /// 
+    /// var push = new ParsePush();
+    /// push.Query = query;
+    /// push.Data = new Dictionary<string, object>{{"alert", alert}};
+    /// return push.SendAsync();
+    /// 
+    /// 
+    /// The alert message to send.
+    /// A query filtering the devices which should receive this Push Notification.
+    public static Task SendPushAlertAsync(this IServiceHub serviceHub, string alert, ParseQuery query)
+    {
+        return new ParsePush(serviceHub) { Query = query, Alert = alert }.SendAsync();
+    }
+
+    /// 
+    /// Pushes an arbitrary payload to every device. This is shorthand for:
+    ///
+    /// 
+    /// var push = new ParsePush();
+    /// push.Data = data;
+    /// return push.SendAsync();
+    /// 
+    /// 
+    /// A push payload. See the ParsePush.Data property for more information.
+    public static Task SendPushDataAsync(this IServiceHub serviceHub, IDictionary data)
+    {
+        return new ParsePush(serviceHub) { Data = data }.SendAsync();
+    }
+
+    /// 
+    /// Pushes an arbitrary payload to every device subscribed to channel. This is shorthand for:
+    ///
+    /// 
+    /// var push = new ParsePush();
+    /// push.Channels = new List<string> { channel };
+    /// push.Data = data;
+    /// return push.SendAsync();
+    /// 
+    /// 
+    /// A push payload. See the ParsePush.Data property for more information.
+    /// An Installation must be subscribed to channel to receive this Push Notification.
+    public static Task SendPushDataAsync(this IServiceHub serviceHub, IDictionary data, string channel)
+    {
+        return new ParsePush(serviceHub) { Channels = new List { channel }, Data = data }.SendAsync();
+    }
+
+    /// 
+    /// Pushes an arbitrary payload to every device subscribed to any of channels. This is shorthand for:
+    ///
+    /// 
+    /// var push = new ParsePush();
+    /// push.Channels = channels;
+    /// push.Data = data;
+    /// return push.SendAsync();
+    /// 
+    /// 
+    /// A push payload. See the ParsePush.Data property for more information.
+    /// An Installation must be subscribed to any of channels to receive this Push Notification.
+    public static Task SendPushDataAsync(this IServiceHub serviceHub, IDictionary data, IEnumerable channels)
+    {
+        return new ParsePush(serviceHub) { Channels = channels, Data = data }.SendAsync();
+    }
+
+    /// 
+    /// Pushes an arbitrary payload to every device matching target. This is shorthand for:
+    ///
+    /// 
+    /// var push = new ParsePush();
+    /// push.Query = query
+    /// push.Data = data;
+    /// return push.SendAsync();
+    /// 
+    /// 
+    /// A push payload. See the ParsePush.Data property for more information.
+    /// A query filtering the devices which should receive this Push Notification.
+    public static Task SendPushDataAsync(this IServiceHub serviceHub, IDictionary data, ParseQuery query)
     {
-        /// 
-        /// Pushes a simple message to every device. This is shorthand for:
-        ///
-        /// 
-        /// var push = new ParsePush();
-        /// push.Data = new Dictionary<string, object>{{"alert", alert}};
-        /// return push.SendAsync();
-        /// 
-        /// 
-        /// The alert message to send.
-        public static Task SendPushAlertAsync(this IServiceHub serviceHub, string alert) => new ParsePush(serviceHub) { Alert = alert }.SendAsync();
-
-        /// 
-        /// Pushes a simple message to every device subscribed to channel. This is shorthand for:
-        ///
-        /// 
-        /// var push = new ParsePush();
-        /// push.Channels = new List<string> { channel };
-        /// push.Data = new Dictionary<string, object>{{"alert", alert}};
-        /// return push.SendAsync();
-        /// 
-        /// 
-        /// The alert message to send.
-        /// An Installation must be subscribed to channel to receive this Push Notification.
-        public static Task SendPushAlertAsync(this IServiceHub serviceHub, string alert, string channel) => new ParsePush(serviceHub) { Channels = new List { channel }, Alert = alert }.SendAsync();
-
-        /// 
-        /// Pushes a simple message to every device subscribed to any of channels. This is shorthand for:
-        ///
-        /// 
-        /// var push = new ParsePush();
-        /// push.Channels = channels;
-        /// push.Data = new Dictionary<string, object>{{"alert", alert}};
-        /// return push.SendAsync();
-        /// 
-        /// 
-        /// The alert message to send.
-        /// An Installation must be subscribed to any of channels to receive this Push Notification.
-        public static Task SendPushAlertAsync(this IServiceHub serviceHub, string alert, IEnumerable channels) => new ParsePush(serviceHub) { Channels = channels, Alert = alert }.SendAsync();
-
-        /// 
-        /// Pushes a simple message to every device matching the target query. This is shorthand for:
-        ///
-        /// 
-        /// var push = new ParsePush();
-        /// push.Query = query;
-        /// push.Data = new Dictionary<string, object>{{"alert", alert}};
-        /// return push.SendAsync();
-        /// 
-        /// 
-        /// The alert message to send.
-        /// A query filtering the devices which should receive this Push Notification.
-        public static Task SendPushAlertAsync(this IServiceHub serviceHub, string alert, ParseQuery query) => new ParsePush(serviceHub) { Query = query, Alert = alert }.SendAsync();
-
-        /// 
-        /// Pushes an arbitrary payload to every device. This is shorthand for:
-        ///
-        /// 
-        /// var push = new ParsePush();
-        /// push.Data = data;
-        /// return push.SendAsync();
-        /// 
-        /// 
-        /// A push payload. See the ParsePush.Data property for more information.
-        public static Task SendPushDataAsync(this IServiceHub serviceHub, IDictionary data) => new ParsePush(serviceHub) { Data = data }.SendAsync();
-
-        /// 
-        /// Pushes an arbitrary payload to every device subscribed to channel. This is shorthand for:
-        ///
-        /// 
-        /// var push = new ParsePush();
-        /// push.Channels = new List<string> { channel };
-        /// push.Data = data;
-        /// return push.SendAsync();
-        /// 
-        /// 
-        /// A push payload. See the ParsePush.Data property for more information.
-        /// An Installation must be subscribed to channel to receive this Push Notification.
-        public static Task SendPushDataAsync(this IServiceHub serviceHub, IDictionary data, string channel) => new ParsePush(serviceHub) { Channels = new List { channel }, Data = data }.SendAsync();
-
-        /// 
-        /// Pushes an arbitrary payload to every device subscribed to any of channels. This is shorthand for:
-        ///
-        /// 
-        /// var push = new ParsePush();
-        /// push.Channels = channels;
-        /// push.Data = data;
-        /// return push.SendAsync();
-        /// 
-        /// 
-        /// A push payload. See the ParsePush.Data property for more information.
-        /// An Installation must be subscribed to any of channels to receive this Push Notification.
-        public static Task SendPushDataAsync(this IServiceHub serviceHub, IDictionary data, IEnumerable channels) => new ParsePush(serviceHub) { Channels = channels, Data = data }.SendAsync();
-
-        /// 
-        /// Pushes an arbitrary payload to every device matching target. This is shorthand for:
-        ///
-        /// 
-        /// var push = new ParsePush();
-        /// push.Query = query
-        /// push.Data = data;
-        /// return push.SendAsync();
-        /// 
-        /// 
-        /// A push payload. See the ParsePush.Data property for more information.
-        /// A query filtering the devices which should receive this Push Notification.
-        public static Task SendPushDataAsync(this IServiceHub serviceHub, IDictionary data, ParseQuery query) => new ParsePush(serviceHub) { Query = query, Data = data }.SendAsync();
-
-        #region Receiving Push
+        return new ParsePush(serviceHub) { Query = query, Data = data }.SendAsync();
+    }
+
+    #region Receiving Push
 
+#pragma warning disable CS1030 // #warning directive
 #warning Check if this should be moved into IParsePushController.
 
-        /// 
-        /// An event fired when a push notification is received.
-        /// 
-        public static event EventHandler ParsePushNotificationReceived
+    /// 
+    /// An event fired when a push notification is received.
+    /// 
+    public static event EventHandler ParsePushNotificationReceived
+#pragma warning restore CS1030 // #warning directive
+    {
+        add
+        {
+            parsePushNotificationReceived.Add(value);
+        }
+        remove
         {
-            add
-            {
-                parsePushNotificationReceived.Add(value);
-            }
-            remove
-            {
-                parsePushNotificationReceived.Remove(value);
-            }
+            parsePushNotificationReceived.Remove(value);
         }
+    }
+
+    internal static readonly SynchronizedEventHandler parsePushNotificationReceived = new SynchronizedEventHandler();
+
+    #endregion
 
-        internal static readonly SynchronizedEventHandler parsePushNotificationReceived = new SynchronizedEventHandler();
-
-        #endregion
-
-        #region Push Subscription
-
-        /// 
-        /// Subscribe the current installation to this channel. This is shorthand for:
-        ///
-        /// 
-        /// var installation = ParseInstallation.CurrentInstallation;
-        /// installation.AddUniqueToList("channels", channel);
-        /// installation.SaveAsync(cancellationToken);
-        /// 
-        /// 
-        /// The channel to which this installation should subscribe.
-        /// CancellationToken to cancel the current operation.
-        public static Task SubscribeToPushChannelAsync(this IServiceHub serviceHub, string channel, CancellationToken cancellationToken = default) => SubscribeToPushChannelsAsync(serviceHub, new List { channel }, cancellationToken);
-
-        /// 
-        /// Subscribe the current installation to these channels. This is shorthand for:
-        ///
-        /// 
-        /// var installation = ParseInstallation.CurrentInstallation;
-        /// installation.AddRangeUniqueToList("channels", channels);
-        /// installation.SaveAsync(cancellationToken);
-        /// 
-        /// 
-        /// The channels to which this installation should subscribe.
-        /// CancellationToken to cancel the current operation.
-        public static Task SubscribeToPushChannelsAsync(this IServiceHub serviceHub, IEnumerable channels, CancellationToken cancellationToken = default) => serviceHub.PushChannelsController.SubscribeAsync(channels, serviceHub, cancellationToken);
-
-        /// 
-        /// Unsubscribe the current installation from this channel. This is shorthand for:
-        ///
-        /// 
-        /// var installation = ParseInstallation.CurrentInstallation;
-        /// installation.Remove("channels", channel);
-        /// installation.SaveAsync(cancellationToken);
-        /// 
-        /// 
-        /// The channel from which this installation should unsubscribe.
-        /// CancellationToken to cancel the current operation.
-        public static Task UnsubscribeToPushChannelAsync(this IServiceHub serviceHub, string channel, CancellationToken cancellationToken = default) => UnsubscribeToPushChannelsAsync(serviceHub, new List { channel }, cancellationToken);
-
-        /// 
-        /// Unsubscribe the current installation from these channels. This is shorthand for:
-        ///
-        /// 
-        /// var installation = ParseInstallation.CurrentInstallation;
-        /// installation.RemoveAllFromList("channels", channels);
-        /// installation.SaveAsync(cancellationToken);
-        /// 
-        /// 
-        /// The channels from which this installation should unsubscribe.
-        /// CancellationToken to cancel the current operation.
-        public static Task UnsubscribeToPushChannelsAsync(this IServiceHub serviceHub, IEnumerable channels, CancellationToken cancellationToken = default) => serviceHub.PushChannelsController.UnsubscribeAsync(channels, serviceHub, cancellationToken);
-
-        #endregion
+    #region Push Subscription
+
+    /// 
+    /// Subscribe the current installation to this channel. This is shorthand for:
+    ///
+    /// 
+    /// var installation = ParseInstallation.CurrentInstallation;
+    /// installation.AddUniqueToList("channels", channel);
+    /// installation.SaveAsync(cancellationToken);
+    /// 
+    /// 
+    /// The channel to which this installation should subscribe.
+    /// CancellationToken to cancel the current operation.
+    public static Task SubscribeToPushChannelAsync(this IServiceHub serviceHub, string channel, CancellationToken cancellationToken = default)
+    {
+        return SubscribeToPushChannelsAsync(serviceHub, new List { channel }, cancellationToken);
     }
+
+    /// 
+    /// Subscribe the current installation to these channels. This is shorthand for:
+    ///
+    /// 
+    /// var installation = ParseInstallation.CurrentInstallation;
+    /// installation.AddRangeUniqueToList("channels", channels);
+    /// installation.SaveAsync(cancellationToken);
+    /// 
+    /// 
+    /// The channels to which this installation should subscribe.
+    /// CancellationToken to cancel the current operation.
+    public static Task SubscribeToPushChannelsAsync(this IServiceHub serviceHub, IEnumerable channels, CancellationToken cancellationToken = default)
+    {
+        return serviceHub.PushChannelsController.SubscribeAsync(channels, serviceHub, cancellationToken);
+    }
+
+    /// 
+    /// Unsubscribe the current installation from this channel. This is shorthand for:
+    ///
+    /// 
+    /// var installation = ParseInstallation.CurrentInstallation;
+    /// installation.Remove("channels", channel);
+    /// installation.SaveAsync(cancellationToken);
+    /// 
+    /// 
+    /// The channel from which this installation should unsubscribe.
+    /// CancellationToken to cancel the current operation.
+    public static Task UnsubscribeToPushChannelAsync(this IServiceHub serviceHub, string channel, CancellationToken cancellationToken = default)
+    {
+        return UnsubscribeToPushChannelsAsync(serviceHub, new List { channel }, cancellationToken);
+    }
+
+    /// 
+    /// Unsubscribe the current installation from these channels. This is shorthand for:
+    ///
+    /// 
+    /// var installation = ParseInstallation.CurrentInstallation;
+    /// installation.RemoveAllFromList("channels", channels);
+    /// installation.SaveAsync(cancellationToken);
+    /// 
+    /// 
+    /// The channels from which this installation should unsubscribe.
+    /// CancellationToken to cancel the current operation.
+    public static Task UnsubscribeToPushChannelsAsync(this IServiceHub serviceHub, IEnumerable channels, CancellationToken cancellationToken = default)
+    {
+        return serviceHub.PushChannelsController.UnsubscribeAsync(channels, serviceHub, cancellationToken);
+    }
+
+    #endregion
 }
diff --git a/Parse/Utilities/QueryServiceExtensions.cs b/Parse/Utilities/QueryServiceExtensions.cs
index 1fa20754..00099bd2 100644
--- a/Parse/Utilities/QueryServiceExtensions.cs
+++ b/Parse/Utilities/QueryServiceExtensions.cs
@@ -4,62 +4,67 @@
 using System.Linq;
 using Parse.Abstractions.Infrastructure;
 
-namespace Parse
+namespace Parse;
+
+public static class QueryServiceExtensions
 {
-    public static class QueryServiceExtensions
+    public static ParseQuery GetQuery(this IServiceHub serviceHub) where T : ParseObject
     {
-        public static ParseQuery GetQuery(this IServiceHub serviceHub) where T : ParseObject => new ParseQuery(serviceHub);
-
-        // ALTERNATE NAME: BuildOrQuery
+        return new ParseQuery(serviceHub);
+    }
 
-        /// 
-        /// Constructs a query that is the or of the given queries.
-        /// 
-        /// The type of ParseObject being queried.
-        /// An initial query to 'or' with additional queries.
-        /// The list of ParseQueries to 'or' together.
-        /// A query that is the or of the given queries.
-        public static ParseQuery ConstructOrQuery(this IServiceHub serviceHub, ParseQuery source, params ParseQuery[] queries) where T : ParseObject => serviceHub.ConstructOrQuery(queries.Concat(new[] { source }));
+    // ALTERNATE NAME: BuildOrQuery
 
-        /// 
-        /// Constructs a query that is the or of the given queries.
-        /// 
-        /// The list of ParseQueries to 'or' together.
-        /// A ParseQquery that is the 'or' of the passed in queries.
-        public static ParseQuery ConstructOrQuery(this IServiceHub serviceHub, IEnumerable> queries) where T : ParseObject
-        {
-            string className = default;
-            List> orValue = new List> { };
+    /// 
+    /// Constructs a query that is the or of the given queries.
+    /// 
+    /// The type of ParseObject being queried.
+    /// An initial query to 'or' with additional queries.
+    /// The list of ParseQueries to 'or' together.
+    /// A query that is the or of the given queries.
+    public static ParseQuery ConstructOrQuery(this IServiceHub serviceHub, ParseQuery source, params ParseQuery[] queries) where T : ParseObject
+    {
+        return serviceHub.ConstructOrQuery(queries.Concat(new[] { source }));
+    }
 
-            // We need to cast it to non-generic IEnumerable because of AOT-limitation
+    /// 
+    /// Constructs a query that is the or of the given queries.
+    /// 
+    /// The list of ParseQueries to 'or' together.
+    /// A ParseQquery that is the 'or' of the passed in queries.
+    public static ParseQuery ConstructOrQuery(this IServiceHub serviceHub, IEnumerable> queries) where T : ParseObject
+    {
+        string className = default;
+        List> orValue = new List> { };
 
-            IEnumerable nonGenericQueries = queries;
-            foreach (object obj in nonGenericQueries)
-            {
-                ParseQuery query = obj as ParseQuery;
+        // We need to cast it to non-generic IEnumerable because of AOT-limitation
 
-                if (className is { } && query.ClassName != className)
-                {
-                    throw new ArgumentException("All of the queries in an or query must be on the same class.");
-                }
+        IEnumerable nonGenericQueries = queries;
+        foreach (object obj in nonGenericQueries)
+        {
+            ParseQuery query = obj as ParseQuery;
 
-                className = query.ClassName;
-                IDictionary parameters = query.BuildParameters();
+            if (className is { } && query.ClassName != className)
+            {
+                throw new ArgumentException("All of the queries in an or query must be on the same class.");
+            }
 
-                if (parameters.Count == 0)
-                {
-                    continue;
-                }
+            className = query.ClassName;
+            IDictionary parameters = query.BuildParameters();
 
-                if (!parameters.TryGetValue("where", out object where) || parameters.Count > 1)
-                {
-                    throw new ArgumentException("None of the queries in an or query can have non-filtering clauses");
-                }
+            if (parameters.Count == 0)
+            {
+                continue;
+            }
 
-                orValue.Add(where as IDictionary);
+            if (!parameters.TryGetValue("where", out object where) || parameters.Count > 1)
+            {
+                throw new ArgumentException("None of the queries in an or query can have non-filtering clauses");
             }
 
-            return new ParseQuery(new ParseQuery(serviceHub, className), where: new Dictionary { ["$or"] = orValue });
+            orValue.Add(where as IDictionary);
         }
+
+        return new ParseQuery(new ParseQuery(serviceHub, className), where: new Dictionary { ["$or"] = orValue });
     }
 }
diff --git a/Parse/Utilities/RoleServiceExtensions.cs b/Parse/Utilities/RoleServiceExtensions.cs
index 667c81d8..2076ecdd 100644
--- a/Parse/Utilities/RoleServiceExtensions.cs
+++ b/Parse/Utilities/RoleServiceExtensions.cs
@@ -1,12 +1,14 @@
 using Parse.Abstractions.Infrastructure;
 
-namespace Parse
+namespace Parse;
+
+public static class RoleServiceExtensions
 {
-    public static class RoleServiceExtensions
+    /// 
+    /// Gets a  over the Role collection.
+    /// 
+    public static ParseQuery GetRoleQuery(this IServiceHub serviceHub)
     {
-        /// 
-        /// Gets a  over the Role collection.
-        /// 
-        public static ParseQuery GetRoleQuery(this IServiceHub serviceHub) => serviceHub.GetQuery();
+        return serviceHub.GetQuery();
     }
 }
diff --git a/Parse/Utilities/SessionsServiceExtensions.cs b/Parse/Utilities/SessionsServiceExtensions.cs
index bc8cc58f..6c7a7edb 100644
--- a/Parse/Utilities/SessionsServiceExtensions.cs
+++ b/Parse/Utilities/SessionsServiceExtensions.cs
@@ -1,35 +1,69 @@
 using System.Threading;
 using System.Threading.Tasks;
 using Parse.Abstractions.Infrastructure;
-using Parse.Infrastructure.Utilities;
 
-namespace Parse
+namespace Parse;
+
+public static class SessionsServiceExtensions
 {
-    public static class SessionsServiceExtensions
+    /// 
+    /// Constructs a  for ParseSession.
+    /// 
+    public static ParseQuery GetSessionQuery(this IServiceHub serviceHub)
+    {
+        return serviceHub.GetQuery();
+    }
+
+    /// 
+    /// Gets the current  object related to the current user.
+    /// 
+    public static Task GetCurrentSessionAsync(this IServiceHub serviceHub)
+    {
+        return GetCurrentSessionAsync(serviceHub, CancellationToken.None);
+    }
+
+    /// 
+    /// Gets the current  object related to the current user.
+    /// 
+    /// The cancellation token
+    public static async Task GetCurrentSessionAsync(this IServiceHub serviceHub, CancellationToken cancellationToken)
     {
-        /// 
-        /// Constructs a  for ParseSession.
-        /// 
-        public static ParseQuery GetSessionQuery(this IServiceHub serviceHub) => serviceHub.GetQuery();
-
-        /// 
-        /// Gets the current  object related to the current user.
-        /// 
-        public static Task GetCurrentSessionAsync(this IServiceHub serviceHub) => GetCurrentSessionAsync(serviceHub, CancellationToken.None);
-
-        /// 
-        /// Gets the current  object related to the current user.
-        /// 
-        /// The cancellation token
-        public static Task GetCurrentSessionAsync(this IServiceHub serviceHub, CancellationToken cancellationToken) => serviceHub.GetCurrentUserAsync().OnSuccess(task => task.Result switch
+        var currentUser = await serviceHub.GetCurrentUserAsync().ConfigureAwait(false);
+
+        if (currentUser == null || currentUser.SessionToken == null)
         {
-            null => Task.FromResult(default),
-            { SessionToken: null } => Task.FromResult(default),
-            { SessionToken: { } sessionToken } => serviceHub.SessionController.GetSessionAsync(sessionToken, serviceHub, cancellationToken).OnSuccess(successTask => serviceHub.GenerateObjectFromState(successTask.Result, "_Session"))
-        }).Unwrap();
+            // Return null if there is no current user or session token
+            return null;
+        }
 
-        public static Task RevokeSessionAsync(this IServiceHub serviceHub, string sessionToken, CancellationToken cancellationToken) => sessionToken is null || !serviceHub.SessionController.IsRevocableSessionToken(sessionToken) ? Task.CompletedTask : serviceHub.SessionController.RevokeAsync(sessionToken, cancellationToken);
+        // Fetch the session using the session token
+        var sessionState = await serviceHub.SessionController
+            .GetSessionAsync(currentUser.SessionToken, serviceHub, cancellationToken)
+            .ConfigureAwait(false);
 
-        public static Task UpgradeToRevocableSessionAsync(this IServiceHub serviceHub, string sessionToken, CancellationToken cancellationToken) => sessionToken is null || serviceHub.SessionController.IsRevocableSessionToken(sessionToken) ? Task.FromResult(sessionToken) : serviceHub.SessionController.UpgradeToRevocableSessionAsync(sessionToken, serviceHub, cancellationToken).OnSuccess(task => serviceHub.GenerateObjectFromState(task.Result, "_Session").SessionToken);
+        // Generate and return the ParseSession object
+        return serviceHub.GenerateObjectFromState(sessionState, "_Session");
     }
+
+
+    public static Task RevokeSessionAsync(this IServiceHub serviceHub, string sessionToken, CancellationToken cancellationToken)
+    {
+        return sessionToken is null || !serviceHub.SessionController.IsRevocableSessionToken(sessionToken) ? Task.CompletedTask : serviceHub.SessionController.RevokeAsync(sessionToken, cancellationToken);
+    }
+
+    public static async Task UpgradeToRevocableSessionAsync(this IServiceHub serviceHub, string sessionToken, CancellationToken cancellationToken)
+    {
+        if (sessionToken is null || serviceHub.SessionController.IsRevocableSessionToken(sessionToken))
+        {
+            return sessionToken;
+        }
+
+        // Perform the upgrade asynchronously
+        var upgradeResult = await serviceHub.SessionController.UpgradeToRevocableSessionAsync(sessionToken, serviceHub, cancellationToken).ConfigureAwait(false);
+
+        // Generate the session object from the result and return the session token
+        var session = serviceHub.GenerateObjectFromState(upgradeResult, "_Session");
+        return session.SessionToken;
+    }
+
 }
diff --git a/Parse/Utilities/UserServiceExtensions.cs b/Parse/Utilities/UserServiceExtensions.cs
index 01eef386..0a9fd95d 100644
--- a/Parse/Utilities/UserServiceExtensions.cs
+++ b/Parse/Utilities/UserServiceExtensions.cs
@@ -2,237 +2,302 @@
 using System.Threading;
 using System.Threading.Tasks;
 using Parse.Abstractions.Infrastructure;
-using Parse.Abstractions.Internal;
 using Parse.Abstractions.Platform.Authentication;
-using Parse.Infrastructure.Utilities;
 
-namespace Parse
+namespace Parse;
+
+public static class UserServiceExtensions
 {
-    public static class UserServiceExtensions
+    internal static async Task GetCurrentSessionToken(this IServiceHub serviceHub)
     {
-        internal static string GetCurrentSessionToken(this IServiceHub serviceHub)
-        {
-            Task sessionTokenTask = GetCurrentSessionTokenAsync(serviceHub);
-            sessionTokenTask.Wait();
-            return sessionTokenTask.Result;
-        }
+        string sessionToken = await serviceHub.CurrentUserController.GetCurrentSessionTokenAsync(serviceHub);
 
-        internal static Task GetCurrentSessionTokenAsync(this IServiceHub serviceHub, CancellationToken cancellationToken = default) => serviceHub.CurrentUserController.GetCurrentSessionTokenAsync(serviceHub, cancellationToken);
-
-        // TODO: Consider renaming SignUpAsync and LogInAsync to SignUpWithAsync and LogInWithAsync, respectively.
-        // TODO: Consider returning the created user from the SignUpAsync overload that accepts a username and password.
-
-        /// 
-        /// Creates a new , saves it with the target Parse Server instance, and then authenticates it on the target client.
-        /// 
-        /// The  instance to target when creating the user and authenticating.
-        /// The value that should be used for .
-        /// The value that should be used for .
-        /// The cancellation token.
-        public static Task SignUpAsync(this IServiceHub serviceHub, string username, string password, CancellationToken cancellationToken = default) => new ParseUser { Services = serviceHub, Username = username, Password = password }.SignUpAsync(cancellationToken);
-
-        /// 
-        /// Saves the provided  instance with the target Parse Server instance and then authenticates it on the target client. This method should only be used once  has been called and  is the wanted bind target, or if  has already been set or  has already been called on the .
-        /// 
-        /// The  instance to target when creating the user and authenticating.
-        /// The  instance to save on the target Parse Server instance and authenticate.
-        /// The cancellation token.
-        public static Task SignUpAsync(this IServiceHub serviceHub, ParseUser user, CancellationToken cancellationToken = default)
-        {
-            user.Bind(serviceHub);
-            return user.SignUpAsync(cancellationToken);
-        }
+        return sessionToken;
+    }
 
-        /// 
-        /// Logs in a user with a username and password. On success, this saves the session to disk or to memory so you can retrieve the currently logged in user using .
-        /// 
-        /// The  instance to target when logging in.
-        /// The username to log in with.
-        /// The password to log in with.
-        /// The cancellation token.
-        /// The newly logged-in user.
-        public static Task LogInAsync(this IServiceHub serviceHub, string username, string password, CancellationToken cancellationToken = default) => serviceHub.UserController.LogInAsync(username, password, serviceHub, cancellationToken).OnSuccess(task =>
-        {
-            ParseUser user = serviceHub.GenerateObjectFromState(task.Result, "_User");
-            return SaveCurrentUserAsync(serviceHub, user).OnSuccess(_ => user);
-        }).Unwrap();
-
-        /// 
-        /// Logs in a user with a username and password. On success, this saves the session to disk so you
-        /// can retrieve the currently logged in user using .
-        /// 
-        /// The session token to authorize with
-        /// The cancellation token.
-        /// The user if authorization was successful
-        public static Task BecomeAsync(this IServiceHub serviceHub, string sessionToken, CancellationToken cancellationToken = default) => serviceHub.UserController.GetUserAsync(sessionToken, serviceHub, cancellationToken).OnSuccess(t =>
-        {
-            ParseUser user = serviceHub.GenerateObjectFromState(t.Result, "_User");
-            return SaveCurrentUserAsync(serviceHub, user).OnSuccess(_ => user);
-        }).Unwrap();
-
-        /// 
-        /// Logs out the currently logged in user session. This will remove the session from disk, log out of
-        /// linked services, and future calls to  will return null.
-        /// 
-        /// 
-        /// Typically, you should use , unless you are managing your own threading.
-        /// 
-        public static void LogOut(this IServiceHub serviceHub) => LogOutAsync(serviceHub).Wait(); // TODO (hallucinogen): this will without a doubt fail in Unity. But what else can we do?
-
-        /// 
-        /// Logs out the currently logged in user session. This will remove the session from disk, log out of
-        /// linked services, and future calls to  will return null.
-        /// 
-        /// 
-        /// This is preferable to using , unless your code is already running from a
-        /// background thread.
-        /// 
-        public static Task LogOutAsync(this IServiceHub serviceHub) => LogOutAsync(serviceHub, CancellationToken.None);
-
-        /// 
-        /// Logs out the currently logged in user session. This will remove the session from disk, log out of
-        /// linked services, and future calls to  will return null.
-        ///
-        /// This is preferable to using , unless your code is already running from a
-        /// background thread.
-        /// 
-        public static Task LogOutAsync(this IServiceHub serviceHub, CancellationToken cancellationToken) => GetCurrentUserAsync(serviceHub).OnSuccess(task =>
-        {
-            LogOutWithProviders();
-            return task.Result is { } user ? user.TaskQueue.Enqueue(toAwait => user.LogOutAsync(toAwait, cancellationToken), cancellationToken) : Task.CompletedTask;
-        }).Unwrap();
 
-        static void LogOutWithProviders()
-        {
-            foreach (IParseAuthenticationProvider provider in ParseUser.Authenticators.Values)
-            {
-                provider.Deauthenticate();
-            }
-        }
+    // DONE: Consider renaming SignUpAsync and LogInAsync to SignUpWithAsync and LogInWithAsync, respectively.
+    // DONE: Consider returning the created user from the SignUpAsync overload that accepts a username and password.
+    
+    /// 
+    /// Creates a new , saves it with the target Parse Server instance, and then authenticates it on the target client.
+    /// 
+    /// The  instance to target when creating the user and authenticating.
+    /// The value that should be used for .
+    /// The value that should be used for .
+    /// The cancellation token.
+    public static Task SignUpWithAsync(this IServiceHub serviceHub, string username, string password, CancellationToken cancellationToken = default)
+    {
+        var ee = new ParseUser { Services = serviceHub, Username = username, Password = password };
+        return ee.SignUpAsync(cancellationToken);
+        
+    }
 
-        /// 
-        /// Gets the currently logged in ParseUser with a valid session, either from memory or disk
-        /// if necessary.
-        /// 
-        public static ParseUser GetCurrentUser(this IServiceHub serviceHub)
-        {
-            Task userTask = GetCurrentUserAsync(serviceHub);
+    /// 
+    /// Saves the provided  instance with the target Parse Server instance and then authenticates it on the target client. This method should only be used once  has been called and  is the wanted bind target, or if  has already been set or  has already been called on the .
+    /// 
+    /// The  instance to target when creating the user and authenticating.
+    /// The  instance to save on the target Parse Server instance and authenticate.
+    /// The cancellation token.
+    public static Task SignUpWithAsync(this IServiceHub serviceHub, ParseUser user, CancellationToken cancellationToken = default)
+    {
+        user.Bind(serviceHub);
+        return user.SignUpAsync(cancellationToken);
+    }
 
-            // TODO (hallucinogen): this will without a doubt fail in Unity. How should we fix it?
+    /// 
+    /// Logs in a user with a username and password. On success, this saves the session to disk or to memory so you can retrieve the currently logged-in user using .
+    /// 
+    /// The  instance to target when logging in.
+    /// The username to log in with.
+    /// The password to log in with.
+    /// The cancellation token.
+    /// The newly logged-in user.
+    public static async Task LogInWithAsync(this IServiceHub serviceHub, string username, string password, CancellationToken cancellationToken = default)
+    {
+        // Log in the user and get the user state
+        var userState = await serviceHub.UserController
+            .LogInAsync(username, password, serviceHub, cancellationToken)
+            .ConfigureAwait(false);
 
-            userTask.Wait();
-            return userTask.Result;
-        }
+        // Generate the ParseUser object from the returned state
+        var user = serviceHub.GenerateObjectFromState(userState, "_User");
 
-        /// 
-        /// Gets the currently logged in ParseUser with a valid session, either from memory or disk
-        /// if necessary, asynchronously.
-        /// 
-        internal static Task GetCurrentUserAsync(this IServiceHub serviceHub, CancellationToken cancellationToken = default) => serviceHub.CurrentUserController.GetAsync(serviceHub, cancellationToken);
+        // Save the user locally
+        await SaveAndReturnCurrentUserAsync(serviceHub, user).ConfigureAwait(false);
 
-        internal static Task SaveCurrentUserAsync(this IServiceHub serviceHub, ParseUser user, CancellationToken cancellationToken = default) => serviceHub.CurrentUserController.SetAsync(user, cancellationToken);
+        // Set the authenticated user as the current instance
+        InstanceUser = user;
 
-        internal static void ClearInMemoryUser(this IServiceHub serviceHub) => serviceHub.CurrentUserController.ClearFromMemory();
+        return user;
+    }
 
-        /// 
-        /// Constructs a  for s.
-        /// 
-        public static ParseQuery GetUserQuery(this IServiceHub serviceHub) => serviceHub.GetQuery();
+    public static ParseUser InstanceUser { get; set; }
 
-        #region Legacy / Revocable Session Tokens
 
-        /// 
-        /// Tells server to use revocable session on LogIn and SignUp, even when App's Settings
-        /// has "Require Revocable Session" turned off. Issues network request in background to
-        /// migrate the sessionToken on disk to revocable session.
-        /// 
-        /// The Task that upgrades the session.
-        public static Task EnableRevocableSessionAsync(this IServiceHub serviceHub, CancellationToken cancellationToken = default)
-        {
-            lock (serviceHub.UserController.RevocableSessionEnabledMutex)
-            {
-                serviceHub.UserController.RevocableSessionEnabled = true;
-            }
+    /// 
+    /// Logs in a user with a username and password. On success, this saves the session to disk so you
+    /// can retrieve the currently logged-in user using .
+    /// 
+    /// The session token to authorize with
+    /// The cancellation token.
+    /// The user if authorization was successful
+    public static async Task BecomeAsync(this IServiceHub serviceHub, string sessionToken, CancellationToken cancellationToken = default)
+    {
+        // Fetch the user state using the session token
+        var userState = await serviceHub.UserController.GetUserAsync(sessionToken, serviceHub, cancellationToken).ConfigureAwait(false);
 
-            return GetCurrentUserAsync(serviceHub, cancellationToken).OnSuccess(task => task.Result.UpgradeToRevocableSessionAsync(cancellationToken));
-        }
+        // Generate the ParseUser object from the returned state
+        var user = serviceHub.GenerateObjectFromState(userState, "_User");
+
+        // Save the user locally
+        await SaveAndReturnCurrentUserAsync(serviceHub, user).ConfigureAwait(false);
 
-        internal static void DisableRevocableSession(this IServiceHub serviceHub)
+        // Set the authenticated user as the current instance only after successful save
+        InstanceUser = user;
+
+        return user;
+    }
+
+    /// 
+    /// Logs out the currently logged in user session. This will remove the session from disk, log out of
+    /// linked services, and future calls to  will return null.
+    /// 
+    /// 
+    /// This is preferable to using , unless your code is already running from a
+    /// background thread.
+    /// 
+    public static void LogOut(this IServiceHub serviceHub)
+    {        
+        _ = LogOutAsync(serviceHub, CancellationToken.None);
+    }
+
+    /// 
+    /// Logs out the currently logged in user session. This will remove the session from disk, log out of
+    /// linked services, and future calls to  will return null.
+    ///
+    /// This is preferable to use , unless your code is already running from a
+    /// background thread.
+    /// 
+    public static async Task LogOutAsync(this IServiceHub serviceHub, CancellationToken cancellationToken)
+    {
+        // Fetch the current user
+        var user = await GetCurrentUserAsync(serviceHub, cancellationToken).ConfigureAwait(false);
+
+        // Log out with providers
+        LogOutWithProviders();
+
+        // If a user is logged in, log them out and return the result, otherwise, complete immediately
+        if (user != null)
         {
-            lock (serviceHub.UserController.RevocableSessionEnabledMutex)
-            {
-                serviceHub.UserController.RevocableSessionEnabled = false;
-            }
+            await user.TaskQueue.Enqueue(toAwait => user.LogOutAsync(cancellationToken), cancellationToken).ConfigureAwait(false);
         }
+    }
+
 
-        internal static bool GetIsRevocableSessionEnabled(this IServiceHub serviceHub)
+    static void LogOutWithProviders()
+    {
+        foreach (IParseAuthenticationProvider provider in ParseUser.Authenticators.Values)
         {
-            lock (serviceHub.UserController.RevocableSessionEnabledMutex)
-            {
-                return serviceHub.UserController.RevocableSessionEnabled;
-            }
+            provider.Deauthenticate();
         }
+    }
 
-        #endregion
+    /// 
+    /// Gets the currently logged in ParseUser with a valid session, either from memory or disk
+    /// if necessary.
+    /// 
+    public static async Task GetCurrentUser(this IServiceHub serviceHub)
+    {
+        ParseUser userTask = await GetCurrentUserAsync(serviceHub);
 
-        /// 
-        /// Requests a password reset email to be sent to the specified email address associated with the
-        /// user account. This email allows the user to securely reset their password on the Parse site.
-        /// 
-        /// The email address associated with the user that forgot their password.
-        public static Task RequestPasswordResetAsync(this IServiceHub serviceHub, string email) => RequestPasswordResetAsync(serviceHub, email, CancellationToken.None);
+        return userTask;
+        //  (hallucinogen): this will without a doubt fail in Unity. How should we fix it?
+        //Fixed
+    }
 
-        /// 
-        /// Requests a password reset email to be sent to the specified email address associated with the
-        /// user account. This email allows the user to securely reset their password on the Parse site.
-        /// 
-        /// The email address associated with the user that forgot their password.
-        /// The cancellation token.
-        public static Task RequestPasswordResetAsync(this IServiceHub serviceHub, string email, CancellationToken cancellationToken) => serviceHub.UserController.RequestPasswordResetAsync(email, cancellationToken);
+    /// 
+    /// Gets the currently logged in ParseUser with a valid session, either from memory or disk
+    /// if necessary, asynchronously.
+    /// 
+    internal static async Task GetCurrentUserAsync(this IServiceHub serviceHub, CancellationToken cancellationToken = default)
+    {
+        var user = await serviceHub.CurrentUserController.GetAsync(serviceHub, cancellationToken);
+        return user;
+    }
 
-        public static Task LogInWithAsync(this IServiceHub serviceHub, string authType, IDictionary data, CancellationToken cancellationToken)
-        {
-            ParseUser user = null;
+    internal static Task SaveCurrentUserAsync(this IServiceHub serviceHub, ParseUser user, CancellationToken cancellationToken = default)
+    {
+       return serviceHub.CurrentUserController.SetAsync(user, cancellationToken);
+    }
+    internal static async Task SaveAndReturnCurrentUserAsync(this IServiceHub serviceHub, ParseUser user, CancellationToken cancellationToken = default)
+    {
+        var usr = await serviceHub.CurrentUserController.SetAsync(user,cancellationToken);
+        return usr;
+    }
 
-            return serviceHub.UserController.LogInAsync(authType, data, serviceHub, cancellationToken).OnSuccess(task =>
-            {
-                user = serviceHub.GenerateObjectFromState(task.Result, "_User");
+    internal static void ClearInMemoryUser(this IServiceHub serviceHub)
+    {
+        serviceHub.CurrentUserController.ClearFromMemory();
+    }
 
-                lock (user.Mutex)
-                {
-                    if (user.AuthData == null)
-                    {
-                        user.AuthData = new Dictionary>();
-                    }
+    /// 
+    /// Constructs a  for s.
+    /// 
+    public static ParseQuery GetUserQuery(this IServiceHub serviceHub)
+    {
+        return serviceHub.GetQuery();
+    }
 
-                    user.AuthData[authType] = data;
+    #region Legacy / Revocable Session Tokens
+
+    /// 
+    /// Tells server to use revocable session on LogIn and SignUp, even when App's Settings
+    /// has "Require Revocable Session" turned off. Issues network request in background to
+    /// migrate the sessionToken on disk to revocable session.
+    /// 
+    /// The Task that upgrades the session.
+    //public static Task EnableRevocableSessionAsync(this IServiceHub serviceHub, CancellationToken cancellationToken = default)
+    //{
+    //    lock (serviceHub.UserController.RevocableSessionEnabledMutex)
+    //    {
+    //        serviceHub.UserController.RevocableSessionEnabled = true;
+    //    }
+
+    //    return GetCurrentUserAsync(serviceHub, cancellationToken).OnSuccess(task => task.Result.UpgradeToRevocableSessionAsync(cancellationToken));
+    //}
+
+    //internal static void DisableRevocableSession(this IServiceHub serviceHub)
+    //{
+    //    lock (serviceHub.UserController.RevocableSessionEnabledMutex)
+    //    {
+    //        serviceHub.UserController.RevocableSessionEnabled = false;
+    //    }
+    //}
+
+    //internal static bool GetIsRevocableSessionEnabled(this IServiceHub serviceHub)
+    //{
+    //    lock (serviceHub.UserController.RevocableSessionEnabledMutex)
+    //    {
+    //        return serviceHub.UserController.RevocableSessionEnabled;
+    //    }
+    //}
+
+    #endregion
+
+    /// 
+    /// Requests a password reset email to be sent to the specified email address associated with the
+    /// user account. This email allows the user to securely reset their password on the Parse site.
+    /// 
+    /// The email address associated with the user that forgot their password.
+    public static Task RequestPasswordResetAsync(this IServiceHub serviceHub, string email)
+    {
+        return RequestPasswordResetAsync(serviceHub, email, CancellationToken.None);
+    }
 
-#warning Check if SynchronizeAllAuthData should accept an IServiceHub for consistency on which actions take place on which IServiceHub implementation instance.
+    /// 
+    /// Requests a password reset email to be sent to the specified email address associated with the
+    /// user account. This email allows the user to securely reset their password on the Parse site.
+    /// 
+    /// The email address associated with the user that forgot their password.
+    /// The cancellation token.
+    public static Task RequestPasswordResetAsync(this IServiceHub serviceHub, string email, CancellationToken cancellationToken)
+    {
+        return serviceHub.UserController.RequestPasswordResetAsync(email, cancellationToken);
+    }
 
-                    user.SynchronizeAllAuthData();
-                }
+    public static async Task LogInWithAsync(this IServiceHub serviceHub, string authType, IDictionary data, CancellationToken cancellationToken)
+    {
+        // Log in the user with the provided authType and data
+        var userState = await serviceHub.UserController
+            .LogInAsync(authType, data, serviceHub, cancellationToken)
+            .ConfigureAwait(false);
 
-                return SaveCurrentUserAsync(serviceHub, user);
-            }).Unwrap().OnSuccess(t => user);
-        }
+        // Generate the ParseUser object from the user state
+        var user = serviceHub.GenerateObjectFromState(userState, "_User");
 
-        public static Task LogInWithAsync(this IServiceHub serviceHub, string authType, CancellationToken cancellationToken)
+        // Synchronize the user data in a thread-safe way
+        lock (user.Mutex)
         {
-            IParseAuthenticationProvider provider = ParseUser.GetProvider(authType);
-            return provider.AuthenticateAsync(cancellationToken).OnSuccess(authData => LogInWithAsync(serviceHub, authType, authData.Result, cancellationToken)).Unwrap();
+            user.AuthData ??= new Dictionary>();
+
+            user.AuthData[authType] = data;
+
+            // Synchronize authentication data for all providers
+            user.SynchronizeAllAuthData();
         }
 
-        internal static void RegisterProvider(this IServiceHub serviceHub, IParseAuthenticationProvider provider)
-        {
-            ParseUser.Authenticators[provider.AuthType] = provider;
-            ParseUser curUser = GetCurrentUser(serviceHub);
+        // Save the current user locally
+        await SaveAndReturnCurrentUserAsync(serviceHub, user).ConfigureAwait(false);
+
+        return user;
+    }
 
-            if (curUser != null)
-            {
+    public static async Task LogInWithAsync(this IServiceHub serviceHub, string authType, CancellationToken cancellationToken)
+    {
+        // Get the authentication provider based on the provided authType
+        IParseAuthenticationProvider provider = ParseUser.GetProvider(authType);
+
+        // Authenticate using the provider
+        var authData = await provider.AuthenticateAsync(cancellationToken).ConfigureAwait(false);
+
+        // Log in using the authenticated data
+        return await LogInWithAsync(serviceHub, authType, authData, cancellationToken).ConfigureAwait(false);
+    }
+
+
+    internal static async void RegisterProvider(this IServiceHub serviceHub, IParseAuthenticationProvider provider)
+    {
+        ParseUser.Authenticators[provider.AuthType] = provider;
+        ParseUser curUser = await GetCurrentUser(serviceHub);
+
+        if (curUser != null)
+        {
+#pragma warning disable CS1030 // #warning directive
 #warning Check if SynchronizeAllAuthData should accept an IServiceHub for consistency on which actions take place on which IServiceHub implementation instance.
 
-                curUser.SynchronizeAuthData(provider);
-            }
+            curUser.SynchronizeAuthData(provider);
+#pragma warning restore CS1030 // #warning directive
         }
     }
 }
diff --git a/README.md b/README.md
index bfea07e4..56374b42 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,7 @@
 [![Coverage](https://img.shields.io/codecov/c/github/parse-community/Parse-SDK-dotNET/master.svg)](https://codecov.io/github/parse-community/Parse-SDK-dotNET?branch=master)
 [![auto-release](https://img.shields.io/badge/%F0%9F%9A%80-auto--release-9e34eb.svg)](https://github.com/parse-community/Parse-SDK-dotNET/releases)
 
+[![.NET Version](https://img.shields.io/badge/.NET-6,_7,_8,_9-5234CE.svg?logo=.net&style=flat)](https://dotnet.microsoft.com)
 [![Nuget](https://img.shields.io/nuget/v/parse.svg)](http://nuget.org/packages/parse)
 
 [![Backers on Open Collective](https://opencollective.com/parse-server/backers/badge.svg)][open-collective-link]
@@ -22,6 +23,8 @@ A library that gives you access to the powerful Parse Server backend from any pl
 
 - [Parse SDK for .NET](#parse-sdk-for-net)
   - [Getting Started](#getting-started)
+  - [Compatibility](#compatibility)
+    - [.NET](#net)
   - [Using the Code](#using-the-code)
     - [Common Definitions](#common-definitions)
     - [Client-Side Use](#client-side-use)
@@ -42,6 +45,19 @@ The latest development release is also available as [a NuGet package (Prerelease
 Note that the previous stable package currently available on the official distribution channel is quite old.
 To use the most up-to-date code, either build this project and reference the generated NuGet package, download the pre-built assembly from [releases][releases-link] or check the [NuGet package (Prerelease)][nuget-link-prerelease] on NuGet.
 
+## Compatibility
+
+### .NET
+
+Parse .NET SDK is continuously tested with the most recent releases of .NET to ensure compatibility. We follow the [.NET Long Term Support plan](https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core) and only test against versions that are officially supported and have not reached their end-of-life date.
+
+| .NET Version | End-of-Life   | Parse .NET SDK Version |
+|--------------|---------------|------------------------|
+| 6.0          | November 2024 | >= 1.0                 |
+| 7.0          | May 2024      | >= 1.0                 |
+| 8.0          | November 2026 | >= 1.0                 |
+| 9.0          | May 2026      | >= 1.0                 |
+
 ## Using the Code
 Make sure you are using the project's root namespace.