From a60f4da5d42538dcb24f5226a99f0f58c9bcbd61 Mon Sep 17 00:00:00 2001 From: Luiz Fernando Bicalho Date: Thu, 9 May 2024 15:40:29 -0300 Subject: [PATCH] Breaking change: Blazor StateManager does not use the provided timeout parameter (#3910) * Updated ISessionManager and SessionManager with timeout and cancellation support Updated the `ISessionManager` interface and `SessionManager` class to include a `TimeSpan` parameter in the `RetrieveSession` and `SendSession` methods for timeout specification. Overloads of these methods have been added to accept a `CancellationToken` parameter for operation cancellation. The `StateManager` class's `GetState` and `SaveState` methods have also been updated to accept a `TimeSpan` parameter for timeout. Added a new project `Csla.Blazor.WebAssembly.Tests` for unit tests of the `SessionManager` class, including tests for timeout and cancellation scenarios. The solution file and a new project settings file have been updated accordingly. The `SessionManager` class now handles `OperationCanceledException` by rethrowing the exception. * `Refactor SessionManager and StateManager for improved session handling` Refactored `SessionManagerTests.cs` and `SessionManager.cs` to improve session handling. The `SessionMessage` object has been changed from a local variable to a class-level variable, allowing it to be accessed across different methods. The `RetrieveSession` and `SendSession` methods have been updated to return the session and assert that the returned session is equal to `SessionValue.Session`. The exception type expected in these methods with zero timeout and cancelled cancellation token has been changed from `TaskCanceledException` to `TimeoutException`. The `GetCancellationToken` method has been simplified to create a `CancellationTokenSource` with a timeout directly. In `StateManager.cs`, the `GetState` method has been updated to not return a `Session` object, instead, it just retrieves the session without assigning it to a variable. * Refactor SessionManagerTests and remove certain test methods This commit includes a significant refactoring of the `SessionManagerTests.cs` file. The `GetHttpClient` method has been simplified by removing the creation of a new `MemoryStream` instance and the use of `CslaBinaryWriter`. The stream's position is no longer reset to 0, and the array is directly obtained from the stream. Several test methods have been removed, including `RetrieveSession_WithTimeoutValue_ShouldNotThrowException`, `RetrieveSession_WithValidCancellationToken_ShouldNotThrowException`, and `SendSession_WithZeroTimeout_ShouldThrowTimeoutException`. These tests were checking for specific exceptions or session values. The `SendSession_WithTimeoutValue_ShouldNotThrowException` test method has been modified by removing the assertion that was previously checking if the operation is successful. Lastly, the `RetrieveCachedSessionSession` method has been modified by removing the call to `GetCachedSession` method of `_sessionManager`. * Refactor SessionManager and update related tests Updated `SessionManager.cs` methods `RetrieveSession` and `SendSession` to handle `TaskCanceledException` internally and rethrow as `TimeoutException`. Simplified `SendSession` by removing exception handling and refactored `RetrieveSession` to move `stateResult` handling outside of try-catch block. Renamed test methods in `SessionManagerTests.cs` to reflect these changes and updated expected exception type. * `Convert SaveState to async in StateManager.cs` ` ` `The SaveState method in StateManager.cs has been converted from a synchronous method to an asynchronous one. This is indicated by the addition of the async keyword and the change in return type from void to Task. Additionally, the call to _sessionManager.SendSession(timeout) within the SaveState method has been updated to use the await keyword, making this method call awaited, in line with the change to make SaveState an asynchronous method.` * Updated SessionManager and its tests for async operations and better error handling In this commit, several updates were made to the `SessionManager.cs` and `SessionManagerTests.cs` files. The variable `_sessionManager` was renamed to `_SessionManager` in `SessionManagerTests.cs`. The `Initialize` method was converted to an asynchronous method, and the `RetrieveSession` method call in it was updated to use `await`. XML comments were added to the `RetrieveSession`, `SendSession`, and `GetSession` methods in `SessionManager.cs` for better code documentation. The `RetrieveSession` and `SendSession` methods were updated to handle `TaskCanceledException` and throw a `TimeoutException` with a custom message. The `GetSession` method was updated to handle the case where `_session` is `null`, creating and returning a new `Session` object in this case. The `SendSession` method was updated to serialize the `_session` object and send it to the server if `SyncContextWithServer` is `true`. Finally, the `RetrieveSession` method was updated to retrieve the session from the server if `SyncContextWithServer` is `true`, deserializing and storing the retrieved session in `_session` or calling `GetSession` to get or create a new session if the retrieval is unsuccessful. * Subject: Refactored test methods and updated session retrieval Refactored the `Initialize` method in `SessionManagerTests.cs` to be synchronous and removed the `RetrieveSession` call. The `RetrieveSession` call has been added to four test methods to ensure session retrieval before each test. Renamed and converted `RetrieveCAchedSessionSession` to an asynchronous method, adding a `RetrieveSession` call and an assertion for non-null cached sessions. Added a new test method `RetrieveNullCachedSessionSession` to assert null cached sessions. * Refactor variable names to follow C# naming convention Updated variable names `_SessionManager` and `SessionValue` to `_sessionManager` and `_sessionValue` respectively, to adhere to the common C# naming convention for private fields. All instances of these variables in the code, including in the `Initialize()`, `Deserialize()`, `RetrieveSession()`, `SendSession()`, and `GetCachedSession()` methods, as well as in test assertions, have been updated accordingly. * Switch from Moq to NSubstitute in Csla.Blazor.WebAssembly.Tests This commit represents a significant shift in the mocking framework used for unit testing in the `Csla.Blazor.WebAssembly.Tests.csproj` project. The `Moq` package has been replaced with `NSubstitute` in the project file and throughout the `SessionManagerTests.cs` file. This includes changes in the way mocks are created, set up, and how return values are specified for mocked methods and properties. Additionally, a new `TestHttpMessageHandler` class has been added to `SessionManagerTests.cs` to mock the behavior of an `HttpClient`. The `GetHttpClient` method has been updated to use this new class, aligning with the switch from `Moq` to `NSubstitute`. --------- Co-authored-by: Luiz Fernando Bicalho Co-authored-by: Rockford Lhotka --- .../Blazor/State/SessionManager.cs | 4 +- .../Csla.Blazor.WebAssembly.Tests.csproj | 36 ++++ .../SessionManagerTests.cs | 201 ++++++++++++++++++ .../State/SessionManager.cs | 186 ++++++++++------ Source/Csla.Blazor/State/StateManager.cs | 7 +- Source/Csla/State/ISessionManager.cs | 4 +- Source/csla.test.sln | 42 ++++ 7 files changed, 404 insertions(+), 76 deletions(-) create mode 100644 Source/Csla.Blazor.WebAssembly.Tests/Csla.Blazor.WebAssembly.Tests.csproj create mode 100644 Source/Csla.Blazor.WebAssembly.Tests/SessionManagerTests.cs diff --git a/Source/Csla.AspNetCore/Blazor/State/SessionManager.cs b/Source/Csla.AspNetCore/Blazor/State/SessionManager.cs index 6d968673c9..fba25e0108 100644 --- a/Source/Csla.AspNetCore/Blazor/State/SessionManager.cs +++ b/Source/Csla.AspNetCore/Blazor/State/SessionManager.cs @@ -79,8 +79,8 @@ public void PurgeSessions(TimeSpan expiration) } // wasm client-side methods - Task ISessionManager.RetrieveSession() => throw new NotImplementedException(); + Task ISessionManager.RetrieveSession(TimeSpan timeout) => throw new NotImplementedException(); Session ISessionManager.GetCachedSession() => throw new NotImplementedException(); - Task ISessionManager.SendSession() => throw new NotImplementedException(); + Task ISessionManager.SendSession(TimeSpan timeout) => throw new NotImplementedException(); } } diff --git a/Source/Csla.Blazor.WebAssembly.Tests/Csla.Blazor.WebAssembly.Tests.csproj b/Source/Csla.Blazor.WebAssembly.Tests/Csla.Blazor.WebAssembly.Tests.csproj new file mode 100644 index 0000000000..8af20c9d4a --- /dev/null +++ b/Source/Csla.Blazor.WebAssembly.Tests/Csla.Blazor.WebAssembly.Tests.csproj @@ -0,0 +1,36 @@ + + + + net8.0 + false + + Library + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + diff --git a/Source/Csla.Blazor.WebAssembly.Tests/SessionManagerTests.cs b/Source/Csla.Blazor.WebAssembly.Tests/SessionManagerTests.cs new file mode 100644 index 0000000000..8788766da0 --- /dev/null +++ b/Source/Csla.Blazor.WebAssembly.Tests/SessionManagerTests.cs @@ -0,0 +1,201 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Threading; +using System.Threading.Tasks; +using Csla.Blazor.WebAssembly.State; +using Csla.State; +using System.Net.Http; +using Csla.Blazor.WebAssembly.Configuration; +using Csla.Core; +using Csla.Runtime; +using System.Net; +using Csla.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using Csla.Serialization.Mobile; +using Microsoft.AspNetCore.Components.Authorization; +using NSubstitute; + +namespace Csla.Test.State +{ + [TestClass] + public class SessionManagerTests + { + private SessionManager _sessionManager; + private SessionMessage _sessionValue; + + [TestInitialize] + public void Initialize() + { + var mockServiceProvider = Substitute.For(); + + // Mock AuthenticationStateProvider + var mockAuthStateProvider = Substitute.For(); + + // Mock IServiceProvider + mockServiceProvider.GetService(typeof(AuthenticationStateProvider)).Returns(mockAuthStateProvider); + + _sessionValue = new SessionMessage + { + // Set properties here + // For example: + Principal = new Security.CslaClaimsPrincipal() { }, + Session = [] + }; + + // Mock ISerializationFormatter + var mockFormatter = Substitute.For(); + mockFormatter.Serialize(Arg.Any(), Arg.Any()); + mockFormatter.Deserialize(Arg.Any()).Returns(_sessionValue); + + // Mock IServiceProvider + mockServiceProvider.GetService(typeof(Csla.Serialization.Mobile.MobileFormatter)).Returns(mockFormatter); + + var mockActivator = Substitute.For(); + mockActivator.CreateInstance(Arg.Is(t => t == typeof(Csla.Serialization.Mobile.MobileFormatter))).Returns(mockFormatter); + mockActivator.InitializeInstance(Arg.Any()); + + // Mock IServiceProvider + mockServiceProvider.GetService(typeof(Csla.Server.IDataPortalActivator)).Returns(mockActivator); + + // Mock IContextManager + var mockContextManager = Substitute.For(); + mockContextManager.IsValid.Returns(true); + + // Mock IContextManagerLocal + var mockLocalContextManager = Substitute.For(); + + // Mock IServiceProvider + mockServiceProvider.GetService(typeof(IRuntimeInfo)).Returns(new RuntimeInfo()); + + // Mock IEnumerable + var mockContextManagerList = new List { mockContextManager }; + + // Mock ApplicationContextAccessor + var mockApplicationContextAccessor = Substitute.For(mockContextManagerList, mockLocalContextManager, mockServiceProvider); + + var _applicationContext = new ApplicationContext(mockApplicationContextAccessor); + + _sessionManager = new SessionManager(_applicationContext, GetHttpClient(_sessionValue, _applicationContext), new BlazorWebAssemblyConfigurationOptions { SyncContextWithServer = true }); + } + + public class TestHttpMessageHandler(SessionMessage session, ApplicationContext _applicationContext) : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var response = new HttpResponseMessage() + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("{\"ResultStatus\":0, \"SessionData\":\"" + Convert.ToBase64String(GetSession(session, _applicationContext)) + "\"}"), + }; + return Task.FromResult(response); + } + } + private static HttpClient GetHttpClient(SessionMessage session, ApplicationContext _applicationContext) + { + var handlerMock = new TestHttpMessageHandler(session,_applicationContext); + // use real http client with mocked handler here + var httpClient = new HttpClient(handlerMock) + { + BaseAddress = new Uri("http://test.com/"), + }; + return httpClient; + } + + private static byte[] GetSession(SessionMessage session, ApplicationContext _applicationContext) + { + var info = new MobileFormatter(_applicationContext).SerializeToDTO(session); + var ms = new MemoryStream(); + new CslaBinaryWriter(_applicationContext).Write(ms, info); + ms.Position = 0; + var array = ms.ToArray(); + return array; + } + + [TestMethod] + public async Task RetrieveSession_WithTimeoutValue_ShouldNotThrowException() + { + var timeout = TimeSpan.FromHours(1); + var session = await _sessionManager.RetrieveSession(timeout); + Assert.AreEqual(session, _sessionValue.Session); + } + + [TestMethod] + public async Task RetrieveSession_WithZeroTimeout_ShouldThrowTimeoutException() + { + var timeout = TimeSpan.Zero; + await Assert.ThrowsExceptionAsync(() => _sessionManager.RetrieveSession(timeout)); + } + + [TestMethod] + public async Task RetrieveSession_WithValidCancellationToken_ShouldNotThrowException() + { + var cts = new CancellationTokenSource(); + var session = await _sessionManager.RetrieveSession(cts.Token); + Assert.AreEqual(session, _sessionValue.Session); + } + + [TestMethod] + public async Task RetrieveSession_WithCancelledCancellationToken_ShouldThrowTaskCanceledException() + { + var cts = new CancellationTokenSource(); + cts.Cancel(); + await Assert.ThrowsExceptionAsync(() => _sessionManager.RetrieveSession(cts.Token)); + } + + [TestMethod] + public async Task SendSession_WithTimeoutValue_ShouldNotThrowException() + { + await _sessionManager.RetrieveSession(TimeSpan.FromHours(1)); + + var timeout = TimeSpan.FromHours(1); + await _sessionManager.SendSession(timeout); + Assert.IsTrue(true); + } + + [TestMethod] + public async Task SendSession_WithZeroTimeout_ShouldThrowTimeoutException() + { + await _sessionManager.RetrieveSession(TimeSpan.FromHours(1)); + + var timeout = TimeSpan.Zero; + await Assert.ThrowsExceptionAsync(() => _sessionManager.SendSession(timeout)); + } + + [TestMethod] + public async Task SendSession_WithValidCancellationToken_ShouldNotThrowException() + { + await _sessionManager.RetrieveSession(TimeSpan.FromHours(1)); + + var cts = new CancellationTokenSource(); + await _sessionManager.SendSession(cts.Token); + Assert.IsTrue(true); + } + + [TestMethod] + public async Task SendSession_WithCancelledCancellationToken_ShouldThrowTaskCanceledException() + { + await _sessionManager.RetrieveSession(TimeSpan.FromHours(1)); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + await Assert.ThrowsExceptionAsync(() => _sessionManager.SendSession(cts.Token)); + } + + + [TestMethod] + public async Task RetrieveCachedSessionSession() + { + await _sessionManager.RetrieveSession(TimeSpan.FromHours(1)); + + var session = _sessionManager.GetCachedSession(); + Assert.IsNotNull(session); + } + [TestMethod] + public void RetrieveNullCachedSessionSession() + { + var session = _sessionManager.GetCachedSession(); + Assert.IsNull(session); + } + } +} diff --git a/Source/Csla.Blazor.WebAssembly/State/SessionManager.cs b/Source/Csla.Blazor.WebAssembly/State/SessionManager.cs index b8c61c0328..36458f3ab4 100644 --- a/Source/Csla.Blazor.WebAssembly/State/SessionManager.cs +++ b/Source/Csla.Blazor.WebAssembly/State/SessionManager.cs @@ -13,6 +13,8 @@ using Csla.State; using Microsoft.AspNetCore.Components.Authorization; using Csla.Blazor.State.Messages; +using System.Reflection; +using System.Threading; namespace Csla.Blazor.WebAssembly.State { @@ -39,84 +41,132 @@ public Session GetCachedSession() return _session; } - /// - /// Retrieves the current user's session from - /// the web server to the wasm client - /// if SyncContextWithServer is true. - /// - public async Task RetrieveSession() - { - if (_options.SyncContextWithServer) - { - long lastTouched = 0; - if (_session != null) - lastTouched = _session.LastTouched; - var url = $"{_options.StateControllerName}?lastTouched={lastTouched}"; + /// + /// Retrieves the current user's session from + /// the web server to the wasm client + /// if SyncContextWithServer is true. + /// + /// The timeout duration for the operation. + /// The retrieved session. + public async Task RetrieveSession(TimeSpan timeout) + { + try + { + return await RetrieveSession(GetCancellationToken(timeout)); + } + catch (TaskCanceledException tcex) + { + throw new TimeoutException($"{this.GetType().FullName}.{nameof(RetrieveSession)}.", tcex); + } + } - var stateResult = await client.GetFromJsonAsync(url); - if (stateResult.ResultStatus == ResultStatuses.Success) + /// + /// Retrieves the current user's session from + /// the web server to the wasm client + /// if SyncContextWithServer is true. + /// + /// The cancellation token. + /// The retrieved session. + public async Task RetrieveSession(CancellationToken ct) { - var formatter = SerializationFormatterFactory.GetFormatter(ApplicationContext); - var buffer = new MemoryStream(stateResult.SessionData) - { - Position = 0 - }; - var message = (SessionMessage)formatter.Deserialize(buffer); - _session = message.Session; - if (message.Principal is not null && - ApplicationContext.GetRequiredService() is CslaAuthenticationStateProvider provider) - { - provider.SetPrincipal(message.Principal); - } + if (_options.SyncContextWithServer) + { + long lastTouched = 0; + if (_session != null) + lastTouched = _session.LastTouched; + var url = $"{_options.StateControllerName}?lastTouched={lastTouched}"; + var stateResult = await client.GetFromJsonAsync(url, ct); + if (stateResult.ResultStatus == ResultStatuses.Success) + { + var formatter = SerializationFormatterFactory.GetFormatter(ApplicationContext); + var buffer = new MemoryStream(stateResult.SessionData) + { + Position = 0 + }; + var message = (SessionMessage)formatter.Deserialize(buffer); + _session = message.Session; + if (message.Principal is not null && + ApplicationContext.GetRequiredService() is CslaAuthenticationStateProvider provider) + { + provider.SetPrincipal(message.Principal); + } + } + else // NoUpdates + { + _session = GetSession(); + } + } + else + { + _session = GetSession(); + } + return _session; } - else // NoUpdates + + /// + /// Sends the current user's session from + /// the wasm client to the web server + /// if SyncContextWithServer is true. + /// + /// The timeout duration for the operation. + /// A task representing the asynchronous operation. + public async Task SendSession(TimeSpan timeout) { - _session = GetSession(); + try + { + await SendSession(GetCancellationToken(timeout)); + } + catch (TaskCanceledException tcex) + { + throw new TimeoutException($"{this.GetType().FullName}.{nameof(SendSession)}.", tcex); + } } - } - else - { - _session = GetSession(); - } - return _session; - } - /// - /// Sends the current user's session from - /// the wasm client to the web server - /// if SyncContextWithServer is true. - /// - public async Task SendSession() + private static CancellationToken GetCancellationToken(TimeSpan timeout) { - _session.Touch(); - if (_options.SyncContextWithServer) - { - var formatter = SerializationFormatterFactory.GetFormatter(ApplicationContext); - var buffer = new MemoryStream(); - formatter.Serialize(buffer, _session); - buffer.Position = 0; - await client.PutAsJsonAsync(_options.StateControllerName, buffer.ToArray()); - } + var cts = new CancellationTokenSource(timeout); + return cts.Token; } + /// + /// Sends the current user's session from + /// the wasm client to the web server + /// if SyncContextWithServer is true. + /// + /// The cancellation token. + /// A task representing the asynchronous operation. + public async Task SendSession(CancellationToken ct) + { + _session.Touch(); + if (_options.SyncContextWithServer) + { + var formatter = SerializationFormatterFactory.GetFormatter(ApplicationContext); + var buffer = new MemoryStream(); + formatter.Serialize(buffer, _session); + buffer.Position = 0; + await client.PutAsJsonAsync(_options.StateControllerName, buffer.ToArray(), ct); + } + } - /// - /// Gets or creates the session data. - /// - private Session GetSession() - { - Session result; - if (_session != null) - { - result = _session; - } - else - { - result = []; - result.Touch(); - } - return result; - } + + /// + /// Gets or creates the session data. + /// + /// The session data. + private Session GetSession() + { + Session result; + if (_session != null) + { + result = _session; + } + else + { + result = []; + result.Touch(); + } + return result; + } // server-side methods Session ISessionManager.GetSession() => throw new NotImplementedException(); diff --git a/Source/Csla.Blazor/State/StateManager.cs b/Source/Csla.Blazor/State/StateManager.cs index b9a58c50d9..8631eeb92c 100644 --- a/Source/Csla.Blazor/State/StateManager.cs +++ b/Source/Csla.Blazor/State/StateManager.cs @@ -41,10 +41,9 @@ public Task InitializeAsync(TimeSpan timeout) /// Time to wait before timing out private async Task GetState(TimeSpan timeout) { - Session session; var isBrowser = OperatingSystem.IsBrowser(); if (isBrowser) - session = await _sessionManager.RetrieveSession(); + _ = await _sessionManager.RetrieveSession(timeout); } /// @@ -57,12 +56,12 @@ private async Task GetState(TimeSpan timeout) /// at which you know the user is navigating to another /// page. /// - public async Task SaveState() + public async Task SaveState(TimeSpan timeout) { var isBrowser = OperatingSystem.IsBrowser(); if (isBrowser) { - await _sessionManager.SendSession(); + await _sessionManager.SendSession(timeout); } } } diff --git a/Source/Csla/State/ISessionManager.cs b/Source/Csla/State/ISessionManager.cs index 6ce61594d0..c089102e62 100644 --- a/Source/Csla/State/ISessionManager.cs +++ b/Source/Csla/State/ISessionManager.cs @@ -18,7 +18,7 @@ public interface ISessionManager /// Retrieves the current user's session from /// the web server to the wasm client. /// - Task RetrieveSession(); + Task RetrieveSession(TimeSpan timeout); /// /// Gets the current user's session from the cache. /// @@ -27,7 +27,7 @@ public interface ISessionManager /// Sends the current user's session from /// the wasm client to the web server. /// - Task SendSession(); + Task SendSession(TimeSpan timeout); #endregion #region Server diff --git a/Source/csla.test.sln b/Source/csla.test.sln index 8eddba99cf..1036a141cc 100644 --- a/Source/csla.test.sln +++ b/Source/csla.test.sln @@ -60,6 +60,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphMergerTest.Business", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphMergerTest.BusinessTests", "GraphMergerTest\GraphMergerTest.BusinessTests\GraphMergerTest.BusinessTests.csproj", "{FE652EBA-94E7-4BB2-99C8-18B0382E0D9A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Csla.Blazor.WebAssembly.Tests", "Csla.Blazor.WebAssembly.Tests\Csla.Blazor.WebAssembly.Tests.csproj", "{430DDCF1-1570-4193-9B1E-774303F6FC34}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -964,6 +966,46 @@ Global {FE652EBA-94E7-4BB2-99C8-18B0382E0D9A}.Testing|x64.Build.0 = Debug|Any CPU {FE652EBA-94E7-4BB2-99C8-18B0382E0D9A}.Testing|x86.ActiveCfg = Debug|Any CPU {FE652EBA-94E7-4BB2-99C8-18B0382E0D9A}.Testing|x86.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Debug|Any CPU.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Debug|ARM.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Debug|ARM.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Debug|x64.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Debug|x64.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Debug|x86.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Debug|x86.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Debug|Any CPU.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Debug|Any CPU.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Debug|ARM.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Debug|ARM.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Debug|x64.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Debug|x64.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Debug|x86.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Debug|x86.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Release|Any CPU.ActiveCfg = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Release|Any CPU.Build.0 = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Release|ARM.ActiveCfg = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Release|ARM.Build.0 = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Release|x64.ActiveCfg = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Release|x64.Build.0 = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Release|x86.ActiveCfg = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Release|x86.Build.0 = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Release|Any CPU.ActiveCfg = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Release|Any CPU.Build.0 = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Release|ARM.ActiveCfg = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Release|ARM.Build.0 = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Release|x64.ActiveCfg = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Release|x64.Build.0 = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Release|x86.ActiveCfg = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Release|x86.Build.0 = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Testing|Any CPU.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Testing|Any CPU.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Testing|ARM.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Testing|ARM.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Testing|x64.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Testing|x64.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Testing|x86.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Testing|x86.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE