Browse Source

Add Redis IXmlRepository implementation (#173)

Pavel Krymets 9 years ago
parent
commit
b340b0f0f7

+ 36 - 2
DataProtection.sln

@@ -1,7 +1,6 @@
-
 Microsoft Visual Studio Solution File, Format Version 12.00
 # Visual Studio 14
-VisualStudioVersion = 14.0.22710.0
+VisualStudioVersion = 14.0.25420.1
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}"
 EndProject
@@ -33,6 +32,14 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.DataPr
 EndProject
 Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.DataProtection.Extensions", "src\Microsoft.AspNetCore.DataProtection.Extensions\Microsoft.AspNetCore.DataProtection.Extensions.xproj", "{BF8681DB-C28B-441F-BD92-0DCFE9537A9F}"
 EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.DataProtection.Redis", "src\Microsoft.AspNetCore.DataProtection.Redis\Microsoft.AspNetCore.DataProtection.Redis.xproj", "{0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}"
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Redis", "samples\Redis\Redis.xproj", "{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{3A6C77DB-FD3D-4B20-A52B-34F7A7E1AED2}"
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.DataProtection.Redis.Test", "test\Microsoft.AspNetCore.DataProtection.Redis.Test\Microsoft.AspNetCore.DataProtection.Redis.Test.xproj", "{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -141,6 +148,30 @@ Global
 		{BF8681DB-C28B-441F-BD92-0DCFE9537A9F}.Release|Any CPU.Build.0 = Release|Any CPU
 		{BF8681DB-C28B-441F-BD92-0DCFE9537A9F}.Release|x86.ActiveCfg = Release|Any CPU
 		{BF8681DB-C28B-441F-BD92-0DCFE9537A9F}.Release|x86.Build.0 = Release|Any CPU
+		{0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}.Debug|x86.Build.0 = Debug|Any CPU
+		{0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}.Release|Any CPU.Build.0 = Release|Any CPU
+		{0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}.Release|x86.ActiveCfg = Release|Any CPU
+		{0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9}.Release|x86.Build.0 = Release|Any CPU
+		{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Debug|x86.Build.0 = Debug|Any CPU
+		{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Release|Any CPU.Build.0 = Release|Any CPU
+		{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Release|x86.ActiveCfg = Release|Any CPU
+		{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}.Release|x86.Build.0 = Release|Any CPU
+		{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Debug|x86.Build.0 = Debug|Any CPU
+		{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Release|Any CPU.Build.0 = Release|Any CPU
+		{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Release|x86.ActiveCfg = Release|Any CPU
+		{ABCF00E5-5B2F-469C-90DC-908C5A04C08D}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -159,5 +190,8 @@ Global
 		{E3552DEB-4173-43AE-BF69-3C10DFF3BAB6} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
 		{04AA8E60-A053-4D50-89FE-E76C3DF45200} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
 		{BF8681DB-C28B-441F-BD92-0DCFE9537A9F} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
+		{0508ADB0-9D2E-4506-9AA3-C15D7BEAE7C9} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
+		{24AAEC96-DF46-4F61-B2FF-3D5E056685D9} = {3A6C77DB-FD3D-4B20-A52B-34F7A7E1AED2}
+		{ABCF00E5-5B2F-469C-90DC-908C5A04C08D} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
 	EndGlobalSection
 EndGlobal

+ 1 - 0
NuGetPackageVerifier.json

@@ -9,6 +9,7 @@
             "Microsoft.AspNetCore.DataProtection": { },
             "Microsoft.AspNetCore.DataProtection.Abstractions": { },
             "Microsoft.AspNetCore.DataProtection.Extensions": { },
+            "Microsoft.AspNetCore.DataProtection.Redis": { },
             "Microsoft.AspNetCore.DataProtection.SystemWeb": { }
         }
     },

+ 33 - 0
samples/Redis/Program.cs

@@ -0,0 +1,33 @@
+using System;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.AspNetCore.DataProtection.Redis;
+using StackExchange.Redis;
+
+namespace Redis
+{
+    public class Program
+    {
+        public static void Main(string[] args)
+        {
+            // Connect
+            var redis = ConnectionMultiplexer.Connect("localhost:6379");
+
+            // Configure
+            var serviceCollection = new ServiceCollection();
+            serviceCollection.AddLogging();
+            serviceCollection.AddDataProtection()
+                .PersistKeysToRedis(redis, "DataProtection-Keys");
+
+            var services = serviceCollection.BuildServiceProvider();
+            var loggerFactory = services.GetService<ILoggerFactory>();
+            loggerFactory.AddConsole(LogLevel.Trace);
+
+            // Run a sample payload
+            var protector = services.GetDataProtector("sample-purpose");
+            var protectedData = protector.Protect("Hello world!");
+            Console.WriteLine(protectedData);
+        }
+    }
+}

+ 19 - 0
samples/Redis/Redis.xproj

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="14.0.25420" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0.25420</VisualStudioVersion>
+    <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
+  </PropertyGroup>
+  <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
+  <PropertyGroup Label="Globals">
+    <ProjectGuid>24aaec96-df46-4f61-b2ff-3d5e056685d9</ProjectGuid>
+    <RootNamespace>Redis</RootNamespace>
+    <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
+    <OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <SchemaVersion>2.0</SchemaVersion>
+  </PropertyGroup>
+  <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
+</Project>

+ 16 - 0
samples/Redis/project.json

@@ -0,0 +1,16 @@
+{
+  "version": "1.0.0-*",
+  "buildOptions": {
+    "debugType": "portable",
+    "emitEntryPoint": true
+  },
+  "dependencies": {
+    "Microsoft.AspNetCore.DataProtection.Redis": "1.1.0-*",
+    "Microsoft.Extensions.DependencyInjection": "1.1.0-*",
+    "Microsoft.Extensions.Logging": "1.1.0-*",
+    "Microsoft.Extensions.Logging.Console": "1.1.0-*"
+  },
+  "frameworks": {
+    "net451": { }
+  }
+}

+ 19 - 0
src/Microsoft.AspNetCore.DataProtection.Redis/Microsoft.AspNetCore.DataProtection.Redis.xproj

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="14.0.25420" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0.25420</VisualStudioVersion>
+    <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
+  </PropertyGroup>
+  <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
+  <PropertyGroup Label="Globals">
+    <ProjectGuid>0508adb0-9d2e-4506-9aa3-c15d7beae7c9</ProjectGuid>
+    <RootNamespace>Microsoft.AspNetCore.DataProtection.Redis</RootNamespace>
+    <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
+    <OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <SchemaVersion>2.0</SchemaVersion>
+  </PropertyGroup>
+  <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
+</Project>

+ 76 - 0
src/Microsoft.AspNetCore.DataProtection.Redis/RedisDataProtectionBuilderExtensions.cs

@@ -0,0 +1,76 @@
+// 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 StackExchange.Redis;
+using Microsoft.AspNetCore.DataProtection.Repositories;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+    /// <summary>
+    /// Contains Redis-specific extension methods for modifying a <see cref="IDataProtectionBuilder"/>.
+    /// </summary>
+    public static class RedisDataProtectionBuilderExtensions
+    {
+        private const string DataProtectionKeysName = "DataProtection-Keys";
+
+        /// <summary>
+        /// Configures the data protection system to persist keys to specified key in Redis database
+        /// </summary>
+        /// <param name="builder">The builder instance to modify.</param>
+        /// <param name="databaseFactory">The delegate used to create <see cref="IDatabase"/> instances.</param>
+        /// <param name="key">The <see cref="RedisKey"/> used to store key list.</param>
+        /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+        public static IDataProtectionBuilder PersistKeysToRedis(this IDataProtectionBuilder builder, Func<IDatabase> databaseFactory, RedisKey key)
+        {
+            if (builder == null)
+            {
+                throw new ArgumentNullException(nameof(builder));
+            }
+            if (databaseFactory == null)
+            {
+                throw new ArgumentNullException(nameof(databaseFactory));
+            }
+            return PersistKeysToRedisInternal(builder, databaseFactory, key);
+        }
+
+        /// <summary>
+        /// Configures the data protection system to persist keys to the default key ('DataProtection-Keys') in Redis database
+        /// </summary>
+        /// <param name="builder">The builder instance to modify.</param>
+        /// <param name="connectionMultiplexer">The <see cref="IConnectionMultiplexer"/> for database access.</param>
+        /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+        public static IDataProtectionBuilder PersistKeysToRedis(this IDataProtectionBuilder builder, IConnectionMultiplexer connectionMultiplexer)
+        {
+            return PersistKeysToRedis(builder, connectionMultiplexer, DataProtectionKeysName);
+        }
+
+        /// <summary>
+        /// Configures the data protection system to persist keys to the specified key in Redis database
+        /// </summary>
+        /// <param name="builder">The builder instance to modify.</param>
+        /// <param name="connectionMultiplexer">The <see cref="IConnectionMultiplexer"/> for database access.</param>
+        /// <param name="key">The <see cref="RedisKey"/> used to store key list.</param>
+        /// <returns>A reference to the <see cref="IDataProtectionBuilder" /> after this operation has completed.</returns>
+        public static IDataProtectionBuilder PersistKeysToRedis(this IDataProtectionBuilder builder, IConnectionMultiplexer connectionMultiplexer, RedisKey key)
+        {
+            if (builder == null)
+            {
+                throw new ArgumentNullException(nameof(builder));
+            }
+            if (connectionMultiplexer == null)
+            {
+                throw new ArgumentNullException(nameof(connectionMultiplexer));
+            }
+            return PersistKeysToRedisInternal(builder, () => connectionMultiplexer.GetDatabase(), key);
+        }
+
+        private static IDataProtectionBuilder PersistKeysToRedisInternal(IDataProtectionBuilder config, Func<IDatabase> databaseFactory, RedisKey key)
+        {
+            config.Services.TryAddSingleton<IXmlRepository>(services => new RedisXmlRepository(databaseFactory, key));
+            return config;
+        }
+    }
+}

+ 59 - 0
src/Microsoft.AspNetCore.DataProtection.Redis/RedisXmlRepository.cs

@@ -0,0 +1,59 @@
+// 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.Xml.Linq;
+using StackExchange.Redis;
+using Microsoft.AspNetCore.DataProtection.Repositories;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+    /// <summary>
+    /// An XML repository backed by a Redis list entry.
+    /// </summary>
+    public class RedisXmlRepository: IXmlRepository
+    {
+        private readonly Func<IDatabase> _databaseFactory;
+        private readonly RedisKey _key;
+
+        /// <summary>
+        /// Creates a <see cref="RedisXmlRepository"/> with keys stored at the given directory.
+        /// </summary>
+        /// <param name="databaseFactory">The delegate used to create <see cref="IDatabase"/> instances.</param>
+        /// <param name="key">The <see cref="RedisKey"/> used to store key list.</param>
+        public RedisXmlRepository(Func<IDatabase> databaseFactory, RedisKey key)
+        {
+            _databaseFactory = databaseFactory;
+            _key = key;
+        }
+
+        /// <inheritdoc />
+        public IReadOnlyCollection<XElement> GetAllElements()
+        {
+            return GetAllElementsCore().ToList().AsReadOnly();
+        }
+
+        private IEnumerable<XElement> GetAllElementsCore()
+        {
+            // Note: Inability to read any value is considered a fatal error (since the file may contain
+            // revocation information), and we'll fail the entire operation rather than return a partial
+            // set of elements. If a value contains well-formed XML but its contents are meaningless, we
+            // won't fail that operation here. The caller is responsible for failing as appropriate given
+            // that scenario.
+            var database = _databaseFactory();
+            foreach (var value in database.ListRange(_key))
+            {
+                yield return XElement.Parse(value);
+            }
+        }
+
+        /// <inheritdoc />
+        public void StoreElement(XElement element, string friendlyName)
+        {
+            var database = _databaseFactory();
+            database.ListRightPush(_key, element.ToString(SaveOptions.DisableFormatting));
+        }
+    }
+}

+ 31 - 0
src/Microsoft.AspNetCore.DataProtection.Redis/project.json

@@ -0,0 +1,31 @@
+{
+  "version": "0.1.0-*",
+  "description": "Redis storrage support as key store.",
+  "packOptions": {
+    "repository": {
+      "type": "git",
+      "url": "git://github.com/aspnet/dataprotection"
+    },
+    "tags": [
+      "aspnetcore",
+      "dataprotection",
+      "redis"
+    ]
+  },
+  "dependencies": {
+    "Microsoft.AspNetCore.DataProtection": "1.1.0-*",
+    "StackExchange.Redis.StrongName": "1.1.603"
+  },
+  "frameworks": {
+    "net451": {}
+  },
+  "buildOptions": {
+    "allowUnsafe": true,
+    "warningsAsErrors": true,
+    "keyFile": "../../tools/Key.snk",
+    "nowarn": [
+      "CS1591"
+    ],
+    "xmlDoc": true
+  }
+}

+ 59 - 0
test/Microsoft.AspNetCore.DataProtection.Redis.Test/DataProtectionRedisTests.cs

@@ -0,0 +1,59 @@
+// 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.Linq;
+using System.Xml;
+using System.Xml.Linq;
+using Moq;
+using StackExchange.Redis;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+    public class DataProtectionRedisTests
+    {
+        [Fact]
+        public void GetAllElements_ReturnsAllXmlValuesForGivenKey()
+        {
+            var database = new Mock<IDatabase>();
+            database.Setup(d => d.ListRange("Key", 0, -1, CommandFlags.None)).Returns(new RedisValue[]
+            {
+                "<Element1/>",
+                "<Element2/>",
+            }).Verifiable();
+            var repo = new RedisXmlRepository(() => database.Object, "Key");
+
+            var elements = repo.GetAllElements().ToArray();
+
+            database.Verify();
+            Assert.Equal(new XElement("Element1").ToString(), elements[0].ToString());
+            Assert.Equal(new XElement("Element2").ToString(), elements[1].ToString());
+        }
+
+        [Fact]
+        public void GetAllElements_ThrowsParsingException()
+        {
+            var database = new Mock<IDatabase>();
+            database.Setup(d => d.ListRange("Key", 0, -1, CommandFlags.None)).Returns(new RedisValue[]
+            {
+                "<Element1/>",
+                "<Element2",
+            }).Verifiable();
+            var repo = new RedisXmlRepository(() => database.Object, "Key");
+
+            Assert.Throws<XmlException>(() => repo.GetAllElements());
+        }
+
+        [Fact]
+        public void StoreElement_PushesValueToList()
+        {
+            var database = new Mock<IDatabase>();
+            database.Setup(d => d.ListRightPush("Key", "<Element2 />", When.Always, CommandFlags.None)).Verifiable();
+            var repo = new RedisXmlRepository(() => database.Object, "Key");
+
+            repo.StoreElement(new XElement("Element2"), null);
+
+            database.Verify();
+        }
+    }
+}

+ 19 - 0
test/Microsoft.AspNetCore.DataProtection.Redis.Test/Microsoft.AspNetCore.DataProtection.Redis.Test.xproj

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="14.0.25420" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0.25420</VisualStudioVersion>
+    <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
+  </PropertyGroup>
+  <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
+  <PropertyGroup Label="Globals">
+    <ProjectGuid>abcf00e5-5b2f-469c-90dc-908c5a04c08d</ProjectGuid>
+    <RootNamespace>Microsoft.AspNetCore.DataProtection.Redis.Test</RootNamespace>
+    <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
+    <OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <SchemaVersion>2.0</SchemaVersion>
+  </PropertyGroup>
+  <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
+</Project>

+ 21 - 0
test/Microsoft.AspNetCore.DataProtection.Redis.Test/project.json

@@ -0,0 +1,21 @@
+{
+  "dependencies": {
+    "dotnet-test-xunit": "2.2.0-*",
+    "Microsoft.AspNetCore.DataProtection.Abstractions": "1.1.0-*",
+    "Microsoft.AspNetCore.DataProtection.Redis": "1.1.0-*",
+    "Microsoft.AspNetCore.Testing": "1.1.0-*",
+    "Moq": "4.6.36-*",
+    "xunit": "2.2.0-*"
+  },
+  "frameworks": {
+    "net451": {}
+  },
+  "testRunner": "xunit",
+  "buildOptions": {
+    "warningsAsErrors": true,
+    "keyFile": "../../tools/Key.snk",
+    "compile": {
+      "include": "../common/**/*.cs"
+    }
+  }
+}