JSON error handling (#11190)

* Exception handling with SystemTextJsonInputFormatter

* Additional tests

* Update ref package

* PR feedback

* Test fixes and feedback

* Update refs

* Restructure tests

* Cleanup
This commit is contained in:
Pranav K 2019-06-24 14:30:38 -07:00 committed by GitHub
commit 151ae52661
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 382 additions and 26 deletions

View File

@ -1877,7 +1877,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
}
public partial class SystemTextJsonInputFormatter : Microsoft.AspNetCore.Mvc.Formatters.TextInputFormatter, Microsoft.AspNetCore.Mvc.Formatters.IInputFormatterExceptionPolicy
{
public SystemTextJsonInputFormatter(Microsoft.AspNetCore.Mvc.JsonOptions options) { }
public SystemTextJsonInputFormatter(Microsoft.AspNetCore.Mvc.JsonOptions options, Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonInputFormatter> logger) { }
Microsoft.AspNetCore.Mvc.Formatters.InputFormatterExceptionPolicy Microsoft.AspNetCore.Mvc.Formatters.IInputFormatterExceptionPolicy.ExceptionPolicy { get { throw null; } }
public System.Text.Json.JsonSerializerOptions SerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
[System.Diagnostics.DebuggerStepThroughAttribute]

View File

@ -8,6 +8,8 @@ using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters.Json;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.Formatters
{
@ -16,13 +18,19 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// </summary>
public class SystemTextJsonInputFormatter : TextInputFormatter, IInputFormatterExceptionPolicy
{
private readonly ILogger<SystemTextJsonInputFormatter> _logger;
/// <summary>
/// Initializes a new instance of <see cref="SystemTextJsonInputFormatter"/>.
/// </summary>
/// <param name="options">The <see cref="JsonOptions"/>.</param>
public SystemTextJsonInputFormatter(JsonOptions options)
/// <param name="logger">The <see cref="ILogger"/>.</param>
public SystemTextJsonInputFormatter(
JsonOptions options,
ILogger<SystemTextJsonInputFormatter> logger)
{
SerializerOptions = options.JsonSerializerOptions;
_logger = logger;
SupportedEncodings.Add(UTF8EncodingWithoutBOM);
SupportedEncodings.Add(UTF16EncodingLittleEndian);
@ -67,6 +75,18 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
{
model = await JsonSerializer.ReadAsync(inputStream, context.ModelType, SerializerOptions);
}
catch (JsonException jsonException)
{
var path = jsonException.Path;
var formatterException = new InputFormatterException(jsonException.Message, jsonException);
context.ModelState.TryAddModelError(path, formatterException, context.Metadata);
Log.JsonInputException(_logger, jsonException);
return InputFormatterResult.Failure();
}
finally
{
if (inputStream is TranscodingReadStream transcoding)
@ -85,6 +105,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
}
else
{
Log.JsonInputSuccess(_logger, context.ModelType);
return InputFormatterResult.Success(model);
}
}
@ -98,5 +119,29 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
return new TranscodingReadStream(httpContext.Request.Body, encoding);
}
private static class Log
{
private static readonly Action<ILogger, string, Exception> _jsonInputFormatterException;
private static readonly Action<ILogger, string, Exception> _jsonInputSuccess;
static Log()
{
_jsonInputFormatterException = LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(1, "SystemTextJsonInputException"),
"JSON input formatter threw an exception: {Message}");
_jsonInputSuccess = LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(2, "SystemTextJsonInputSuccess"),
"JSON input formatter succeeded, deserializing to type '{TypeName}'");
}
public static void JsonInputException(ILogger logger, Exception exception)
=> _jsonInputFormatterException(logger, exception.Message, exception);
public static void JsonInputSuccess(ILogger logger, Type modelType)
=> _jsonInputSuccess(logger, modelType.FullName, null);
}
}
}

View File

@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.Mvc
options.Filters.Add(new UnsupportedContentTypeFilter());
// Set up default input formatters.
options.InputFormatters.Add(new SystemTextJsonInputFormatter(_jsonOptions.Value));
options.InputFormatters.Add(new SystemTextJsonInputFormatter(_jsonOptions.Value, _loggerFactory.CreateLogger<SystemTextJsonInputFormatter>()));
// Set up default output formatters.
options.OutputFormatters.Add(new HttpNoContentOutputFormatter());

View File

@ -3,16 +3,21 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging.Testing;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Formatters
{
public abstract class JsonInputFormatterTestBase
public abstract class JsonInputFormatterTestBase : LoggedTest
{
[Theory]
[InlineData("application/json", true)]
@ -102,6 +107,61 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
Assert.Equal("abcd", stringValue);
}
[Fact]
public virtual async Task JsonFormatter_EscapedKeys_Bracket()
{
var expectedKey = JsonFormatter_EscapedKeys_Bracket_Expected;
// Arrange
var content = "[{\"It[s a key\":1234556}]";
var formatter = GetInputFormatter();
var contentBytes = Encoding.UTF8.GetBytes(content);
var httpContext = GetHttpContext(contentBytes);
var formatterContext = CreateInputFormatterContext(typeof(IEnumerable<IDictionary<string, short>>), httpContext);
// Act
var result = await formatter.ReadAsync(formatterContext);
// Assert
Assert.True(result.HasError);
Assert.Collection(
formatterContext.ModelState.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal(expectedKey, kvp.Key);
});
}
[Fact]
public virtual async Task JsonFormatter_EscapedKeys()
{
var expectedKey = JsonFormatter_EscapedKeys_Expected;
// Arrange
var content = "[{\"It\\\"s a key\": 1234556}]";
var formatter = GetInputFormatter();
var contentBytes = Encoding.UTF8.GetBytes(content);
var httpContext = GetHttpContext(contentBytes);
var formatterContext = CreateInputFormatterContext(
typeof(IEnumerable<IDictionary<string, short>>), httpContext);
// Act
var result = await formatter.ReadAsync(formatterContext);
// Assert
Assert.True(result.HasError);
Assert.Collection(
formatterContext.ModelState.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal(expectedKey, kvp.Key);
});
}
[Fact]
public virtual async Task JsonFormatterReadsDateTimeValue()
{
@ -199,6 +259,33 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
Assert.Equal(new int[] { 0, 23, 300 }, (IEnumerable<int>)result.Model);
}
[Fact]
public virtual async Task ReadAsync_ArrayOfObjects_HasCorrectKey()
{
// Arrange
var formatter = GetInputFormatter();
var content = "[{\"Age\": 5}, {\"Age\": 3}, {\"Age\": \"Cheese\"} ]";
var contentBytes = Encoding.UTF8.GetBytes(content);
var httpContext = GetHttpContext(contentBytes);
var formatterContext = CreateInputFormatterContext(typeof(List<ComplexModel>), httpContext);
var expectedKey = ReadAsync_ArrayOfObjects_HasCorrectKey_Expected;
// Act
var result = await formatter.ReadAsync(formatterContext);
// Assert
Assert.True(result.HasError, "Model should have had an error!");
Assert.Collection(formatterContext.ModelState.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal(expectedKey, kvp.Key);
Assert.Single(kvp.Value.Errors);
});
}
[Fact]
public virtual async Task ReadAsync_AddsModelValidationErrorsToModelState()
{
@ -210,15 +297,19 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
var httpContext = GetHttpContext(contentBytes);
var formatterContext = CreateInputFormatterContext(typeof(ComplexModel), httpContext);
var expectedKey = ReadAsync_AddsModelValidationErrorsToModelState_Expected;
// Act
var result = await formatter.ReadAsync(formatterContext);
// Assert
Assert.True(result.HasError);
Assert.Equal(
"Could not convert string to decimal: not-an-age. Path 'Age', line 1, position 44.",
formatterContext.ModelState["Age"].Errors[0].ErrorMessage);
Assert.True(result.HasError, "Model should have had an error!");
Assert.Collection(formatterContext.ModelState.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal(expectedKey, kvp.Key);
Assert.Single(kvp.Value.Errors);
});
}
[Fact]
@ -227,19 +318,23 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
// Arrange
var formatter = GetInputFormatter();
var content = "[0, 23, 300]";
var content = "[0, 23, 33767]";
var contentBytes = Encoding.UTF8.GetBytes(content);
var httpContext = GetHttpContext(contentBytes);
var formatterContext = CreateInputFormatterContext(typeof(byte[]), httpContext);
var formatterContext = CreateInputFormatterContext(typeof(short[]), httpContext);
var expectedValue = ReadAsync_InvalidArray_AddsOverflowErrorsToModelState_Expected;
// Act
var result = await formatter.ReadAsync(formatterContext);
// Assert
Assert.True(result.HasError);
Assert.Equal("The supplied value is invalid.", formatterContext.ModelState["[2]"].Errors[0].ErrorMessage);
Assert.Null(formatterContext.ModelState["[2]"].Errors[0].Exception);
Assert.True(result.HasError, "Model should have produced an error!");
Assert.Collection(formatterContext.ModelState.OrderBy(k => k.Key),
kvp => {
Assert.Equal(expectedValue, kvp.Key);
});
}
[Fact]
@ -253,15 +348,19 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
var httpContext = GetHttpContext(contentBytes);
var formatterContext = CreateInputFormatterContext(typeof(ComplexModel[]), httpContext, modelName: "names");
var expectedKey = ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState_Expected;
// Act
var result = await formatter.ReadAsync(formatterContext);
// Assert
Assert.True(result.HasError);
Assert.Equal(
"Error converting value 300 to type 'System.Byte'. Path '[1].Small', line 1, position 69.",
formatterContext.ModelState["names[1].Small"].Errors[0].ErrorMessage);
Assert.Collection(
formatterContext.ModelState.OrderBy(k => k.Key),
kvp => {
Assert.Equal(expectedKey, kvp.Key);
Assert.Single(kvp.Value.Errors);
});
}
[Fact]
@ -318,6 +417,65 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
Assert.Null(result.Model);
}
[Fact]
public async Task ReadAsync_ComplexPoco()
{
// Arrange
var formatter = GetInputFormatter();
var content = "{ \"Id\": 5, \"Person\": { \"Name\": \"name\", \"Numbers\": [3, 2, \"Hamburger\"]} }";
var contentBytes = Encoding.UTF8.GetBytes(content);
var httpContext = GetHttpContext(contentBytes);
var formatterContext = CreateInputFormatterContext(typeof(ComplexPoco), httpContext);
var expectedKey = ReadAsync_ComplexPoco_Expected;
// Act
var result = await formatter.ReadAsync(formatterContext);
// Assert
Assert.True(result.HasError, "Model should have had an error!");
Assert.Collection(formatterContext.ModelState.OrderBy(k => k.Key),
kvp => {
Assert.Equal(expectedKey, kvp.Key);
Assert.Single(kvp.Value.Errors);
});
}
[Fact]
public virtual async Task ReadAsync_RequiredAttribute()
{
// Arrange
var formatter = GetInputFormatter();
var content = "{ \"Id\": 5, \"Person\": {\"Numbers\": [3]} }";
var contentBytes = Encoding.UTF8.GetBytes(content);
var httpContext = GetHttpContext(contentBytes);
var formatterContext = CreateInputFormatterContext(typeof(ComplexPoco), httpContext);
// Act
var result = await formatter.ReadAsync(formatterContext);
// Assert
Assert.True(result.HasError, "Model should have had an error!");
Assert.Single(formatterContext.ModelState["Person.Name"].Errors);
}
internal abstract string JsonFormatter_EscapedKeys_Bracket_Expected { get; }
internal abstract string JsonFormatter_EscapedKeys_Expected { get; }
internal abstract string ReadAsync_ArrayOfObjects_HasCorrectKey_Expected { get; }
internal abstract string ReadAsync_AddsModelValidationErrorsToModelState_Expected { get; }
internal abstract string ReadAsync_InvalidArray_AddsOverflowErrorsToModelState_Expected { get; }
internal abstract string ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState_Expected { get; }
internal abstract string ReadAsync_ComplexPoco_Expected { get; }
protected abstract TextInputFormatter GetInputFormatter();
protected static HttpContext GetHttpContext(
@ -356,6 +514,20 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
treatEmptyInputAsDefaultValue: treatEmptyInputAsDefaultValue);
}
protected sealed class ComplexPoco
{
public int Id { get; set; }
public Person Person{ get; set; }
}
protected sealed class Person
{
[Required]
[JsonProperty(Required = Required.Always)]
public string Name { get; set; }
public IEnumerable<int> Numbers { get; set; }
}
protected sealed class ComplexModel
{
public string Name { get; set; }

View File

@ -1,40 +1,103 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Formatters
{
public class SystemTextJsonInputFormatterTest : JsonInputFormatterTestBase
{
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8474")]
[Fact]
public override Task ReadAsync_AddsModelValidationErrorsToModelState()
{
return base.ReadAsync_AddsModelValidationErrorsToModelState();
}
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8474")]
[Fact]
public override Task ReadAsync_InvalidArray_AddsOverflowErrorsToModelState()
{
return base.ReadAsync_InvalidArray_AddsOverflowErrorsToModelState();
}
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8474")]
[Fact]
public override Task ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState()
{
return base.ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState();
}
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8474")]
[Fact]
public override Task ReadAsync_UsesTryAddModelValidationErrorsToModelState()
{
return base.ReadAsync_UsesTryAddModelValidationErrorsToModelState();
}
[Fact(Skip = "https://github.com/dotnet/corefx/issues/38492")]
public override Task ReadAsync_RequiredAttribute()
{
// System.Text.Json does not yet support an equivalent of Required.
throw new NotImplementedException();
}
[Fact]
public override Task JsonFormatter_EscapedKeys()
{
return base.JsonFormatter_EscapedKeys();
}
[Fact]
public override Task JsonFormatter_EscapedKeys_Bracket()
{
return base.JsonFormatter_EscapedKeys_Bracket();
}
[Fact]
public async Task ReadAsync_SingleError()
{
// Arrange
var formatter = GetInputFormatter();
var content = "[5, 'seven', 3, notnum ]";
var contentBytes = Encoding.UTF8.GetBytes(content);
var httpContext = GetHttpContext(contentBytes);
var formatterContext = CreateInputFormatterContext(typeof(List<int>), httpContext);
// Act
await formatter.ReadAsync(formatterContext);
Assert.Collection(
formatterContext.ModelState.OrderBy(k => k),
kvp =>
{
Assert.Equal("$[1]", kvp.Key);
var error = Assert.Single(kvp.Value.Errors);
Assert.StartsWith("''' is an invalid start of a value", error.ErrorMessage);
});
}
protected override TextInputFormatter GetInputFormatter()
{
return new SystemTextJsonInputFormatter(new JsonOptions());
return new SystemTextJsonInputFormatter(new JsonOptions(), LoggerFactory.CreateLogger<SystemTextJsonInputFormatter>());
}
internal override string ReadAsync_AddsModelValidationErrorsToModelState_Expected => "$.Age";
internal override string JsonFormatter_EscapedKeys_Expected => "$[0]['It\\u0022s a key']";
internal override string JsonFormatter_EscapedKeys_Bracket_Expected => "$[0]['It[s a key']";
internal override string ReadAsync_ArrayOfObjects_HasCorrectKey_Expected => "$[2].Age";
internal override string ReadAsync_InvalidArray_AddsOverflowErrorsToModelState_Expected => "$[2]";
internal override string ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState_Expected => "$[1].Small";
internal override string ReadAsync_ComplexPoco_Expected => "$.Person.Numbers[2]";
}
}

View File

@ -226,7 +226,9 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
}
else
{
addMember = !path.EndsWith("." + member, StringComparison.Ordinal);
addMember = !path.EndsWith("." + member, StringComparison.Ordinal)
&& !path.EndsWith("['" + member + "']", StringComparison.Ordinal)
&& !path.EndsWith("[" + member + "]", StringComparison.Ordinal);
}
}
}

View File

@ -8,13 +8,13 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
{
internal static class NewtonsoftJsonLoggerExtensions
{
private static readonly Action<ILogger, Exception> _jsonInputFormatterCrashed;
private static readonly Action<ILogger, Exception> _jsonInputFormatterException;
private static readonly Action<ILogger, string, Exception> _jsonResultExecuting;
static NewtonsoftJsonLoggerExtensions()
{
_jsonInputFormatterCrashed = LoggerMessage.Define(
_jsonInputFormatterException = LoggerMessage.Define(
LogLevel.Debug,
new EventId(1, "JsonInputException"),
"JSON input formatter threw an exception.");
@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
public static void JsonInputException(this ILogger logger, Exception exception)
{
_jsonInputFormatterCrashed(logger, exception);
_jsonInputFormatterException(logger, exception);
}
public static void JsonResultExecuting(this ILogger logger, object value)

View File

@ -3,7 +3,7 @@
using System;
using System.Buffers;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@ -196,6 +196,18 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
Assert.Equal(settings.DateTimeZoneHandling, actual.DateTimeZoneHandling);
}
[Fact]
public override Task JsonFormatter_EscapedKeys()
{
return base.JsonFormatter_EscapedKeys();
}
[Fact]
public override Task JsonFormatter_EscapedKeys_Bracket()
{
return base.JsonFormatter_EscapedKeys_Bracket();
}
[Theory]
[InlineData(" ", true, true)]
[InlineData(" ", false, false)]
@ -235,6 +247,39 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
Assert.Equal(expectedMessage, modelError.ErrorMessage);
}
[Fact]
public async Task ReadAsync_AllowMultipleErrors()
{
// Arrange
var content = "[5, 'seven', 3, 'notnum']";
var formatter = CreateFormatter(allowInputFormatterExceptionMessages: true);
var contentBytes = Encoding.UTF8.GetBytes(content);
var httpContext = GetHttpContext(contentBytes);
var formatterContext = CreateInputFormatterContext(typeof(List<int>), httpContext);
// Act
var result = await formatter.ReadAsync(formatterContext);
// Assert
Assert.Collection(
formatterContext.ModelState.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal("[1]", kvp.Key);
var error = Assert.Single(kvp.Value.Errors);
Assert.StartsWith("Could not convert string to integer:", error.ErrorMessage);
},
kvp =>
{
Assert.Equal("[3]", kvp.Key);
var error = Assert.Single(kvp.Value.Errors);
Assert.StartsWith("Could not convert string to integer:", error.ErrorMessage);
});
}
[Fact]
public async Task ReadAsync_DoNotAllowInputFormatterExceptionMessages_DoesNotWrapJsonInputExceptions()
{
@ -325,6 +370,20 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
});
}
internal override string JsonFormatter_EscapedKeys_Expected => "[0]['It\"s a key']";
internal override string JsonFormatter_EscapedKeys_Bracket_Expected => "[0][\'It[s a key\']";
internal override string ReadAsync_AddsModelValidationErrorsToModelState_Expected => "Age";
internal override string ReadAsync_ArrayOfObjects_HasCorrectKey_Expected => "[2].Age";
internal override string ReadAsync_ComplexPoco_Expected => "Person.Numbers[2]";
internal override string ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState_Expected => "names[1].Small";
internal override string ReadAsync_InvalidArray_AddsOverflowErrorsToModelState_Expected => "[2]";
private class Location
{
public int Id { get; set; }

View File

@ -325,6 +325,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions.ApiDescription.S
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.ApiDescription.Server", "Extensions.ApiDescription.Server\src\Microsoft.Extensions.ApiDescription.Server.csproj", "{D7CF2A1E-A29E-45AB-9C2A-CD6C3BAE54F2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Metadata", "..\Http\Metadata\src\Microsoft.AspNetCore.Metadata.csproj", "{464195B3-022A-4D19-9104-8C66CC882D67}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -1841,6 +1843,18 @@ Global
{D7CF2A1E-A29E-45AB-9C2A-CD6C3BAE54F2}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{D7CF2A1E-A29E-45AB-9C2A-CD6C3BAE54F2}.Release|x86.ActiveCfg = Release|Any CPU
{D7CF2A1E-A29E-45AB-9C2A-CD6C3BAE54F2}.Release|x86.Build.0 = Release|Any CPU
{464195B3-022A-4D19-9104-8C66CC882D67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{464195B3-022A-4D19-9104-8C66CC882D67}.Debug|Any CPU.Build.0 = Debug|Any CPU
{464195B3-022A-4D19-9104-8C66CC882D67}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{464195B3-022A-4D19-9104-8C66CC882D67}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{464195B3-022A-4D19-9104-8C66CC882D67}.Debug|x86.ActiveCfg = Debug|Any CPU
{464195B3-022A-4D19-9104-8C66CC882D67}.Debug|x86.Build.0 = Debug|Any CPU
{464195B3-022A-4D19-9104-8C66CC882D67}.Release|Any CPU.ActiveCfg = Release|Any CPU
{464195B3-022A-4D19-9104-8C66CC882D67}.Release|Any CPU.Build.0 = Release|Any CPU
{464195B3-022A-4D19-9104-8C66CC882D67}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{464195B3-022A-4D19-9104-8C66CC882D67}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{464195B3-022A-4D19-9104-8C66CC882D67}.Release|x86.ActiveCfg = Release|Any CPU
{464195B3-022A-4D19-9104-8C66CC882D67}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -1972,6 +1986,7 @@ Global
{C6F3BCE6-1EFD-4360-932B-B98573E78926} = {45CE788D-4B69-4F83-981C-F43D8F15B0F1}
{637119E8-5BBB-4FC7-A372-DAF38FF5EBD9} = {5FE3048A-E96B-44F8-A7C4-FC590D7E04B4}
{D7CF2A1E-A29E-45AB-9C2A-CD6C3BAE54F2} = {C15AA245-9E54-4FD6-90FF-B46F47779C46}
{464195B3-022A-4D19-9104-8C66CC882D67} = {5FE3048A-E96B-44F8-A7C4-FC590D7E04B4}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {63D344F6-F86D-40E6-85B9-0AABBE338C4A}