Add Redis IXmlRepository implementation (#173)

This commit is contained in:
Pavel Krymets 2016-09-02 11:56:47 -07:00 committed by GitHub
parent e94106e28c
commit b340b0f0f7
12 changed files with 389 additions and 2 deletions

View File

@ -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

View File

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

33
samples/Redis/Program.cs Normal file
View File

@ -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
samples/Redis/Redis.xproj Normal file
View File

@ -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>

View File

@ -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": { }
}
}

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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));
}
}
}

View File

@ -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
}
}

View File

@ -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();
}
}
}

View File

@ -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>

View File

@ -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"
}
}
}