diff --git a/samples/backend/csharp/ChatProtocolBackend.csproj b/samples/backend/csharp/ChatProtocolBackend.csproj
index 69b7a54..d4950ff 100644
--- a/samples/backend/csharp/ChatProtocolBackend.csproj
+++ b/samples/backend/csharp/ChatProtocolBackend.csproj
@@ -7,10 +7,10 @@
-
-
-
-
+
+
+
+
diff --git a/samples/backend/csharp/Model/AIChatCompletion.cs b/samples/backend/csharp/Model/AIChatCompletion.cs
index dba86cb..fd36513 100644
--- a/samples/backend/csharp/Model/AIChatCompletion.cs
+++ b/samples/backend/csharp/Model/AIChatCompletion.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
+using System.Text.Json;
using System.Text.Json.Serialization;
namespace Backend.Model;
@@ -11,5 +12,19 @@ public record AIChatCompletion([property: JsonPropertyName("message")] AIChatMes
public Guid? SessionState;
[JsonInclude, JsonPropertyName("context"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public BinaryData? Context;
+ [JsonConverter(typeof(BinaryDataJsonConverter))]
+ public BinaryData? Context { get; set; }
+
+ public class BinaryDataJsonConverter : JsonConverter
+ {
+ public override BinaryData? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
+ reader.TokenType is JsonTokenType.String && reader.TryGetBytesFromBase64(out var b64bytes)
+ ? BinaryData.FromBytes(b64bytes)
+ : new BinaryData(reader.GetString() ?? string.Empty);
+
+ public override void Write(Utf8JsonWriter writer, BinaryData value, JsonSerializerOptions options)
+ {
+ writer.WriteBase64StringValue(value);
+ }
+ }
}
diff --git a/samples/backend/csharp/Model/AIChatFile.cs b/samples/backend/csharp/Model/AIChatFile.cs
index 50773c5..5fc2d7d 100644
--- a/samples/backend/csharp/Model/AIChatFile.cs
+++ b/samples/backend/csharp/Model/AIChatFile.cs
@@ -7,6 +7,9 @@ namespace Backend.Model;
public struct AIChatFile
{
+ [JsonPropertyName("filename")]
+ public string Filename { get; set; }
+
[JsonPropertyName("contentType")]
public string ContentType { get; set; }
diff --git a/samples/frontend/dotnet/Console/Console.csproj b/samples/frontend/dotnet/Console/Console.csproj
new file mode 100644
index 0000000..6df9592
--- /dev/null
+++ b/samples/frontend/dotnet/Console/Console.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ c18ba783-dae7-4a3f-b550-8c2b6acfa1e4
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/frontend/dotnet/Console/Program.cs b/samples/frontend/dotnet/Console/Program.cs
new file mode 100644
index 0000000..ea98135
--- /dev/null
+++ b/samples/frontend/dotnet/Console/Program.cs
@@ -0,0 +1,27 @@
+using Client.Extensions;
+
+using Console;
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+HostApplicationBuilder b = new(args);
+b.Configuration
+ .AddUserSecrets()
+ .AddEnvironmentVariables("AICHATPROTOCOL_")
+ .AddCommandLine(args);
+
+b.Services
+ .AddHostedService()
+ .AddAiChatProtocolClient();
+
+CancellationTokenSource cancellationTokenSource = new();
+System.Console.CancelKeyPress += (_, args) =>
+{
+ cancellationTokenSource.Cancel();
+
+ args.Cancel = true;
+};
+
+await b.Build().RunAsync(cancellationTokenSource.Token);
diff --git a/samples/frontend/dotnet/Console/Repl.cs b/samples/frontend/dotnet/Console/Repl.cs
new file mode 100644
index 0000000..2cc8d59
--- /dev/null
+++ b/samples/frontend/dotnet/Console/Repl.cs
@@ -0,0 +1,59 @@
+namespace Console;
+
+using Backend.Model;
+
+using Client.Interfaces;
+
+using Microsoft.Extensions.Hosting;
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+internal class Repl(IAiChatProtocolClient _protocolClient) : IHostedService
+{
+ private readonly TaskCompletionSource _cts = new();
+
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ var cancelTask = Task.Delay(Timeout.Infinite, cancellationToken);
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ Console.Write("Enter your message: ");
+
+ string? message = null;
+ do
+ {
+ var readTask = Task.Run(Console.ReadLine, cancellationToken);
+ var completedTask = await Task.WhenAny(readTask, cancelTask);
+ if (completedTask == cancelTask)
+ {
+ // Cancellation was requested
+ break;
+ }
+
+ message = await readTask;
+ } while (!cancellationToken.IsCancellationRequested && string.IsNullOrWhiteSpace(message));
+
+ if (cancellationToken.IsCancellationRequested)
+ {
+ // Cancellation was requested
+ break;
+ }
+
+ if (string.IsNullOrWhiteSpace(message))
+ {
+ Console.WriteLine("Message cannot be empty.");
+ continue;
+ }
+
+ var request = new AIChatRequest([new AIChatMessage { Content = message }]);
+ var response = await _protocolClient.CompleteAsync(request, cancellationToken);
+
+ Console.WriteLine($"Response: {response.Message.Content}");
+ Console.WriteLine();
+ }
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+}
diff --git a/sdk/dotnet/Client/AIChatClient.cs b/sdk/dotnet/Client/AIChatClient.cs
new file mode 100644
index 0000000..f602d23
--- /dev/null
+++ b/sdk/dotnet/Client/AIChatClient.cs
@@ -0,0 +1,89 @@
+namespace Client;
+
+using Backend.Model;
+
+using Client.Interfaces;
+
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Http.Json;
+using System.Text.Json;
+
+internal class AiChatProtocolClient(IOptions config, IHttpClientFactory factory, IOptions serializerOptions, ILogger? _log = null) : IAiChatProtocolClient
+{
+ public const string HttpClientName = "AIChatClient";
+
+ private readonly HttpClient _httpClient = ConfigureHttpClient(factory.CreateClient(HttpClientName), config.Value);
+ private readonly JsonSerializerOptions _serializerOptions = serializerOptions.Value;
+
+ private static HttpClient ConfigureHttpClient(HttpClient client, AIChatClientOptions config)
+ {
+ if (client.BaseAddress is null)
+ {
+ client.BaseAddress = config.ChatEndpointUri;
+ client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+ }
+
+ return client;
+ }
+
+ public async Task CompleteAsync(AIChatRequest request, CancellationToken cancellationToken)
+ {
+ _log?.LogTrace("Sending request to chat endpoint");
+ var response = await _httpClient.PostAsync(string.Empty, CreateContent(request, cancellationToken), cancellationToken);
+ _log?.LogDebug("Received response from chat endpoint: {StatusCode}", response.StatusCode);
+
+ response.EnsureSuccessStatusCode();
+
+ _log?.LogDebug("Deserializing response from chat endpoint");
+ if (_log?.IsEnabled(LogLevel.Trace) is true)
+ {
+ var respString = await response.Content.ReadAsStringAsync(cancellationToken);
+ _log?.LogTrace("Response content: {Content}", respString);
+
+ return JsonSerializer.Deserialize(respString, _serializerOptions);
+ }
+ else
+ {
+ return await response.Content.ReadFromJsonAsync(_serializerOptions, cancellationToken: cancellationToken);
+ }
+ }
+
+ private static HttpContent CreateContent(AIChatRequest request, CancellationToken cancellationToken)
+ {
+ if (request.Messages.Any(message => message.Files?.Count is not null and not 0))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var boundary = $"---Part-{Guid.NewGuid()}";
+
+ // Strip off the Files from each message since we add them as parts to the form
+ var c = JsonContent.Create(request with { Messages = request.Messages.Select(message => message with { Files = null }).ToList() });
+ c.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { Name = "json" };
+
+ var multiPartContent = new MultipartFormDataContent(boundary) { c };
+
+ foreach (var part in request.Messages
+ .SelectMany((message, index) => message.Files!.Select((file, fileIndex) =>
+ new
+ {
+ Content = new ReadOnlyMemoryContent(file.Data) { Headers = { ContentType = new MediaTypeHeaderValue(file.ContentType) } },
+ Name = $"messages[{index}].files[{fileIndex}]",
+ file.Filename,
+ }))
+ .Where(part => part is not null))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ multiPartContent.Add(part.Content, part.Name, part.Filename);
+ }
+
+ return multiPartContent;
+ }
+
+ return JsonContent.Create(request);
+ }
+}
diff --git a/sdk/dotnet/Client/AIChatClientOptions.cs b/sdk/dotnet/Client/AIChatClientOptions.cs
new file mode 100644
index 0000000..525b1ee
--- /dev/null
+++ b/sdk/dotnet/Client/AIChatClientOptions.cs
@@ -0,0 +1,37 @@
+namespace Client;
+
+using Microsoft.AspNetCore.Http.Json;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+
+using System;
+using System.Text.Json;
+
+using static Backend.Model.AIChatCompletion;
+
+public class AIChatClientOptions() : IConfigureOptions, IPostConfigureOptions, IValidateOptions
+{
+ public AIChatClientOptions(IConfiguration config, IOptions globalSerializerOptions, IOptions httpJsonOptions) : this()
+ {
+ _config = config;
+ _globalSerializerOptions = globalSerializerOptions;
+ _httpJsonOptions = httpJsonOptions;
+ }
+
+ required public Uri ChatEndpointUri { get; init; }
+
+ private static readonly BinaryDataJsonConverter converter = new();
+ private readonly IConfiguration? _config;
+ private readonly IOptions? _globalSerializerOptions;
+ private readonly IOptions? _httpJsonOptions;
+
+ public void Configure(AIChatClientOptions options) => _config?.GetRequiredSection("AiChatProtocol").Bind(options);
+
+ public void PostConfigure(string? name, AIChatClientOptions options)
+ {
+ _globalSerializerOptions?.Value.Converters.Add(converter);
+ _httpJsonOptions?.Value.SerializerOptions.Converters.Add(converter);
+ }
+
+ public ValidateOptionsResult Validate(string? name, AIChatClientOptions options) => options.ChatEndpointUri is not null ? ValidateOptionsResult.Success : ValidateOptionsResult.Fail("ChatEndpointUri must be set");
+}
diff --git a/sdk/dotnet/Client/Client.csproj b/sdk/dotnet/Client/Client.csproj
new file mode 100644
index 0000000..91c00c5
--- /dev/null
+++ b/sdk/dotnet/Client/Client.csproj
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/sdk/dotnet/Client/Extensions/DependencyInjectionExtensions.cs b/sdk/dotnet/Client/Extensions/DependencyInjectionExtensions.cs
new file mode 100644
index 0000000..ddab531
--- /dev/null
+++ b/sdk/dotnet/Client/Extensions/DependencyInjectionExtensions.cs
@@ -0,0 +1,35 @@
+namespace Client.Extensions;
+
+using Client.Interfaces;
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+
+using System.Net.Http.Headers;
+
+public static class DependencyInjectionExtensions
+{
+ public static IServiceCollection AddAiChatProtocolClient(this IServiceCollection services, IAiChatProtocolClient? client = null)
+ {
+ services.ConfigureOptions();
+
+ if (client is not null)
+ {
+ services.AddSingleton(client);
+ }
+ else
+ {
+ services.AddSingleton();
+ }
+
+ services.AddHttpClient(AiChatProtocolClient.HttpClientName, (sp, client) =>
+ {
+ var config = sp.GetRequiredService>().Value;
+ client.BaseAddress = config.ChatEndpointUri;
+ client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+ });
+
+ return services;
+ }
+}
diff --git a/sdk/dotnet/Client/Interfaces/IAiChatProtocolClient.cs b/sdk/dotnet/Client/Interfaces/IAiChatProtocolClient.cs
new file mode 100644
index 0000000..d372805
--- /dev/null
+++ b/sdk/dotnet/Client/Interfaces/IAiChatProtocolClient.cs
@@ -0,0 +1,10 @@
+namespace Client.Interfaces;
+
+using Backend.Model;
+
+using System.Threading;
+
+public interface IAiChatProtocolClient
+{
+ Task CompleteAsync(AIChatRequest iChatRequest, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/sdk/dotnet/Client/Properties/AssemblyInfo.cs b/sdk/dotnet/Client/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..5d7769c
--- /dev/null
+++ b/sdk/dotnet/Client/Properties/AssemblyInfo.cs
@@ -0,0 +1,4 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Client.Tests")]
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
\ No newline at end of file
diff --git a/sdk/dotnet/Directory.Build.props b/sdk/dotnet/Directory.Build.props
new file mode 100644
index 0000000..75aba62
--- /dev/null
+++ b/sdk/dotnet/Directory.Build.props
@@ -0,0 +1,9 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
\ No newline at end of file
diff --git a/sdk/dotnet/Directory.Packages.props b/sdk/dotnet/Directory.Packages.props
new file mode 100644
index 0000000..3c9c3cb
--- /dev/null
+++ b/sdk/dotnet/Directory.Packages.props
@@ -0,0 +1,11 @@
+
+
+ true
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sdk/dotnet/Tests/Client.Tests/AiChatProtocolClientTests.cs b/sdk/dotnet/Tests/Client.Tests/AiChatProtocolClientTests.cs
new file mode 100644
index 0000000..ef50a05
--- /dev/null
+++ b/sdk/dotnet/Tests/Client.Tests/AiChatProtocolClientTests.cs
@@ -0,0 +1,134 @@
+namespace Client.Tests;
+
+using Backend.Model;
+
+using Client;
+
+using Microsoft.Extensions.Options;
+
+using Moq;
+using Moq.AutoMock;
+using Moq.Protected;
+
+using System.Net;
+using System.Net.Http.Json;
+
+public class AiChatProtocolClientTests
+{
+ private readonly AIChatClientOptions _config = new()
+ {
+ ChatEndpointUri = new Uri("https://api.example.com/chat")
+ };
+
+ private static readonly AutoMocker mock = new();
+
+ private static void RegisterFactoryWithMockedHttpResponse(HttpResponseMessage expectedResponse, Action? requestValidator = null)
+ {
+ var handlerMock = mock.GetMock(enablePrivate: true);
+ handlerMock.Protected().Setup("Send", ItExpr.IsAny(), ItExpr.IsAny())
+ .Returns((HttpRequestMessage req, CancellationToken _) =>
+ {
+ requestValidator?.Invoke(req);
+ return expectedResponse;
+ });
+
+ handlerMock.Protected().Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny())
+ .ReturnsAsync((HttpRequestMessage req, CancellationToken _) =>
+ {
+ requestValidator?.Invoke(req);
+ return expectedResponse;
+ });
+
+ mock.GetMock().Setup(f => f.CreateClient(AiChatProtocolClient.HttpClientName))
+ .Returns(() => new HttpClient(handlerMock.Object));
+ }
+
+ public AiChatProtocolClientTests() => mock.Use(Options.Create(_config));
+
+ [Fact]
+ public async Task CompleteAsync_ShouldReturnAIChatCompletion_WhenRequestIsSuccessful()
+ {
+ var request = new AIChatRequest([]);
+ var expectedResponse = new AIChatCompletion(new AIChatMessage() { Content = "hi" });
+
+ RegisterFactoryWithMockedHttpResponse(new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = JsonContent.Create(expectedResponse)
+ });
+
+ var client = mock.CreateInstance();
+
+ var result = await client.CompleteAsync(request, default);
+
+ Assert.NotNull(result);
+ Assert.Equal(expectedResponse, result);
+ }
+
+ [Fact]
+ public async Task CompleteAsync_ShouldThrowException_WhenRequestFails()
+ {
+ RegisterFactoryWithMockedHttpResponse(new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.BadRequest
+ });
+
+ var client = mock.CreateInstance();
+
+ await Assert.ThrowsAsync(() => client.CompleteAsync(new([]), default));
+ }
+
+ [Fact]
+ public async Task CompleteAsync_WithFilesInRequest_IsSuccessful()
+ {
+ // Arrange
+ var expectedResponse = new AIChatCompletion(new AIChatMessage() { Content = "hi" });
+ var chatRequest = new AIChatRequest([
+ new()
+ {
+ Content = "Analyze this file for me",
+ Files =
+ [
+ new AIChatFile { Data = new([ 1, 2, 3 ]), ContentType = "application/octet-stream", Filename = "test.txt" }
+ ]
+ }
+ ])
+ { SessionState = Guid.NewGuid() };
+
+ RegisterFactoryWithMockedHttpResponse(new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = JsonContent.Create(expectedResponse)
+ }, async (HttpRequestMessage r) =>
+ {
+ var content = r.Content as MultipartFormDataContent;
+ Assert.NotNull(content);
+ Assert.Equal(2, content.Count());
+
+ var jsonPart = content.First() as JsonContent;
+ Assert.NotNull(jsonPart);
+ Assert.Equal(typeof(AIChatRequest), jsonPart.ObjectType);
+
+ // The json value in the multipart form should not contain the files
+ Assert.Equivalent(chatRequest with { Messages = chatRequest.Messages.Select(i => i with { Files = null }).ToList() }, jsonPart.Value);
+
+ var fileFormContent = content.Skip(1).First() as ReadOnlyMemoryContent;
+ Assert.NotNull(fileFormContent);
+ Assert.Equal("application/octet-stream", fileFormContent.Headers.ContentType?.MediaType);
+
+ var disp = fileFormContent.Headers.ContentDisposition;
+ Assert.Equal("test.txt", disp?.FileName);
+ Assert.Equal("test.txt", disp?.FileNameStar);
+ Assert.Equal(@"""messages[0].files[0]""", disp?.Name);
+ Assert.Equal(3, disp?.Parameters.Count);
+ Assert.Equal([1, 2, 3], await fileFormContent.ReadAsByteArrayAsync());
+ });
+
+ var client = mock.CreateInstance();
+
+ var result = await client.CompleteAsync(chatRequest, default);
+
+ Assert.NotNull(result);
+ Assert.Equal(expectedResponse, result);
+ }
+}
diff --git a/sdk/dotnet/Tests/Client.Tests/Client.Tests.csproj b/sdk/dotnet/Tests/Client.Tests/Client.Tests.csproj
new file mode 100644
index 0000000..7484d5f
--- /dev/null
+++ b/sdk/dotnet/Tests/Client.Tests/Client.Tests.csproj
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/sdk/dotnet/Tests/Directory.Build.props b/sdk/dotnet/Tests/Directory.Build.props
new file mode 100644
index 0000000..12d3319
--- /dev/null
+++ b/sdk/dotnet/Tests/Directory.Build.props
@@ -0,0 +1,20 @@
+
+
+
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sdk/dotnet/Tests/Directory.Packages.props b/sdk/dotnet/Tests/Directory.Packages.props
new file mode 100644
index 0000000..0326d50
--- /dev/null
+++ b/sdk/dotnet/Tests/Directory.Packages.props
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sdk/dotnet/dotnet.sln b/sdk/dotnet/dotnet.sln
new file mode 100644
index 0000000..a882efb
--- /dev/null
+++ b/sdk/dotnet/dotnet.sln
@@ -0,0 +1,54 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.11.35327.3
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Client", "Client\Client.csproj", "{AD754304-75EF-4A0C-A785-A4C68B901EAE}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solution Items", "{1A4D2496-4669-4EB2-9669-05E15954447F}"
+ ProjectSection(SolutionItems) = preProject
+ Directory.Build.props = Directory.Build.props
+ Directory.Packages.props = Directory.Packages.props
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChatProtocolBackend", "..\..\samples\backend\csharp\ChatProtocolBackend.csproj", "{4D9259DE-46D5-4C03-92CB-C82A952E3D2A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Tests", "Tests\Client.Tests\Client.Tests.csproj", "{E4281AF2-D178-46EC-9ABE-908B2FF6F01C}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{31CAADBE-1FF2-450B-9A8E-4EE517F4F60A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Console", "..\..\samples\frontend\dotnet\Console\Console.csproj", "{B159000D-1A1E-43C9-ABF8-F4A64971E95F}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {AD754304-75EF-4A0C-A785-A4C68B901EAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AD754304-75EF-4A0C-A785-A4C68B901EAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AD754304-75EF-4A0C-A785-A4C68B901EAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AD754304-75EF-4A0C-A785-A4C68B901EAE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4D9259DE-46D5-4C03-92CB-C82A952E3D2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4D9259DE-46D5-4C03-92CB-C82A952E3D2A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4D9259DE-46D5-4C03-92CB-C82A952E3D2A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4D9259DE-46D5-4C03-92CB-C82A952E3D2A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E4281AF2-D178-46EC-9ABE-908B2FF6F01C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E4281AF2-D178-46EC-9ABE-908B2FF6F01C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E4281AF2-D178-46EC-9ABE-908B2FF6F01C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E4281AF2-D178-46EC-9ABE-908B2FF6F01C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B159000D-1A1E-43C9-ABF8-F4A64971E95F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B159000D-1A1E-43C9-ABF8-F4A64971E95F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B159000D-1A1E-43C9-ABF8-F4A64971E95F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B159000D-1A1E-43C9-ABF8-F4A64971E95F}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {B159000D-1A1E-43C9-ABF8-F4A64971E95F} = {31CAADBE-1FF2-450B-9A8E-4EE517F4F60A}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {0D6B5B21-972F-482B-B9F4-9020FD9C94AE}
+ EndGlobalSection
+EndGlobal