Port DataProtection blob XmlRepository (#163)
This commit is contained in:
parent
8f8152f910
commit
0e210dadea
|
|
@ -34,12 +34,24 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.DataPr
|
|||
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}"
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.DataProtection.Azure.Blob", "src\Microsoft.AspNetCore.DataProtection.Azure.Blob\Microsoft.AspNetCore.DataProtection.Azure.Blob.xproj", "{CC799B57-81E2-4F45-8A32-0D5F49753C3F}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{3A6C77DB-FD3D-4B20-A52B-34F7A7E1AED2}"
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{5A3A5DE3-49AD-431C-971D-B01B62D94AE2}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "AzureBlob", "samples\AzureBlob\AzureBlob.xproj", "{B07435B3-CD81-4E3B-88A5-6384821E1C01}"
|
||||
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
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E1D86B1B-41D8-43C9-97FD-C2BF65C414E2}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
global.json = global.json
|
||||
NuGet.config = NuGet.config
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.DataProtection.Azure.Blob.Test", "test\Microsoft.AspNetCore.DataProtection.Azure.Blob.Test\Microsoft.AspNetCore.DataProtection.Azure.Blob.Test.xproj", "{8C41240E-48F8-402F-9388-74CFE27F4D76}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Redis", "samples\Redis\Redis.xproj", "{24AAEC96-DF46-4F61-B2FF-3D5E056685D9}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -156,14 +168,22 @@ Global
|
|||
{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
|
||||
{CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{CC799B57-81E2-4F45-8A32-0D5F49753C3F}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B07435B3-CD81-4E3B-88A5-6384821E1C01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B07435B3-CD81-4E3B-88A5-6384821E1C01}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B07435B3-CD81-4E3B-88A5-6384821E1C01}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B07435B3-CD81-4E3B-88A5-6384821E1C01}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B07435B3-CD81-4E3B-88A5-6384821E1C01}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B07435B3-CD81-4E3B-88A5-6384821E1C01}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B07435B3-CD81-4E3B-88A5-6384821E1C01}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B07435B3-CD81-4E3B-88A5-6384821E1C01}.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
|
||||
|
|
@ -172,6 +192,22 @@ Global
|
|||
{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
|
||||
{8C41240E-48F8-402F-9388-74CFE27F4D76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8C41240E-48F8-402F-9388-74CFE27F4D76}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8C41240E-48F8-402F-9388-74CFE27F4D76}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{8C41240E-48F8-402F-9388-74CFE27F4D76}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{8C41240E-48F8-402F-9388-74CFE27F4D76}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8C41240E-48F8-402F-9388-74CFE27F4D76}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8C41240E-48F8-402F-9388-74CFE27F4D76}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{8C41240E-48F8-402F-9388-74CFE27F4D76}.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
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -191,7 +227,10 @@ Global
|
|||
{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}
|
||||
{CC799B57-81E2-4F45-8A32-0D5F49753C3F} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
|
||||
{B07435B3-CD81-4E3B-88A5-6384821E1C01} = {5A3A5DE3-49AD-431C-971D-B01B62D94AE2}
|
||||
{ABCF00E5-5B2F-469C-90DC-908C5A04C08D} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
|
||||
{8C41240E-48F8-402F-9388-74CFE27F4D76} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
|
||||
{24AAEC96-DF46-4F61-B2FF-3D5E056685D9} = {5A3A5DE3-49AD-431C-971D-B01B62D94AE2}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
"Microsoft.AspNetCore.Cryptography.KeyDerivation": { },
|
||||
"Microsoft.AspNetCore.DataProtection": { },
|
||||
"Microsoft.AspNetCore.DataProtection.Abstractions": { },
|
||||
"Microsoft.AspNetCore.DataProtection.Azure.Blob": { },
|
||||
"Microsoft.AspNetCore.DataProtection.Extensions": { },
|
||||
"Microsoft.AspNetCore.DataProtection.Redis": { },
|
||||
"Microsoft.AspNetCore.DataProtection.SystemWeb": { }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>b07435b3-cd81-4e3b-88a5-6384821e1c01</ProjectGuid>
|
||||
<RootNamespace>AzureBlob</RootNamespace>
|
||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
||||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
using System;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.WindowsAzure.Storage;
|
||||
using Microsoft.AspNetCore.DataProtection.Azure.Blob;
|
||||
|
||||
namespace AzureBlob
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var storageAccount = CloudStorageAccount.DevelopmentStorageAccount;
|
||||
var client = storageAccount.CreateCloudBlobClient();
|
||||
var container = client.GetContainerReference("key-container");
|
||||
|
||||
// The container must exist before calling the DataProtection APIs.
|
||||
// The specific file within the container does not have to exist,
|
||||
// as it will be created on-demand.
|
||||
|
||||
container.CreateIfNotExistsAsync().GetAwaiter().GetResult();
|
||||
|
||||
// Configure
|
||||
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddLogging();
|
||||
serviceCollection.AddDataProtection()
|
||||
.PersistKeysToAzureBlobStorage(container, "keys.xml");
|
||||
|
||||
var services = serviceCollection.BuildServiceProvider();
|
||||
var loggerFactory = services.GetService<ILoggerFactory>();
|
||||
loggerFactory.AddConsole(Microsoft.Extensions.Logging.LogLevel.Trace);
|
||||
|
||||
// Run a sample payload
|
||||
|
||||
var protector = services.GetDataProtector("sample-purpose");
|
||||
var protectedData = protector.Protect("Hello world!");
|
||||
Console.WriteLine(protectedData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"version": "1.0.0-*",
|
||||
"buildOptions": {
|
||||
"emitEntryPoint": true
|
||||
},
|
||||
|
||||
"dependencies": {
|
||||
"Microsoft.AspNetCore.DataProtection": "1.1.0-*",
|
||||
"Microsoft.AspNetCore.DataProtection.Azure.Blob": "1.1.0-*",
|
||||
"Microsoft.Extensions.DependencyInjection": "1.1.0-*",
|
||||
"Microsoft.Extensions.Logging": "1.1.0-*",
|
||||
"Microsoft.Extensions.Logging.Console": "1.1.0-*",
|
||||
"Microsoft.NETCore.App": {
|
||||
"type": "platform",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
},
|
||||
|
||||
"frameworks": {
|
||||
"netcoreapp1.0": {
|
||||
"imports": [
|
||||
"portable-net45+win8+wp8+wpa81"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,295 @@
|
|||
// 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.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.AspNetCore.DataProtection.Repositories;
|
||||
using Microsoft.WindowsAzure.Storage;
|
||||
using Microsoft.WindowsAzure.Storage.Blob;
|
||||
|
||||
namespace Microsoft.AspNetCore.DataProtection.Azure.Blob
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IXmlRepository"/> which is backed by Azure Blob Storage.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Instances of this type are thread-safe.
|
||||
/// </remarks>
|
||||
public sealed class AzureBlobXmlRepository : IXmlRepository
|
||||
{
|
||||
private const int ConflictMaxRetries = 5;
|
||||
private static readonly TimeSpan ConflictBackoffPeriod = TimeSpan.FromMilliseconds(200);
|
||||
|
||||
private static readonly XName RepositoryElementName = "repository";
|
||||
|
||||
private readonly Func<ICloudBlob> _blobRefFactory;
|
||||
private readonly Random _random;
|
||||
private BlobData _cachedBlobData;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="AzureBlobXmlRepository"/>.
|
||||
/// </summary>
|
||||
/// <param name="blobRefFactory">A factory which can create <see cref="ICloudBlob"/>
|
||||
/// instances. The factory must be thread-safe for invocation by multiple
|
||||
/// concurrent threads, and each invocation must return a new object.</param>
|
||||
public AzureBlobXmlRepository(Func<ICloudBlob> blobRefFactory)
|
||||
{
|
||||
if (blobRefFactory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(blobRefFactory));
|
||||
}
|
||||
|
||||
_blobRefFactory = blobRefFactory;
|
||||
_random = new Random();
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<XElement> GetAllElements()
|
||||
{
|
||||
var blobRef = CreateFreshBlobRef();
|
||||
|
||||
// Shunt the work onto a ThreadPool thread so that it's independent of any
|
||||
// existing sync context or other potentially deadlock-causing items.
|
||||
|
||||
var elements = Task.Run(() => GetAllElementsAsync(blobRef)).GetAwaiter().GetResult();
|
||||
return new ReadOnlyCollection<XElement>(elements);
|
||||
}
|
||||
|
||||
public void StoreElement(XElement element, string friendlyName)
|
||||
{
|
||||
if (element == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(element));
|
||||
}
|
||||
|
||||
var blobRef = CreateFreshBlobRef();
|
||||
|
||||
// Shunt the work onto a ThreadPool thread so that it's independent of any
|
||||
// existing sync context or other potentially deadlock-causing items.
|
||||
|
||||
Task.Run(() => StoreElementAsync(blobRef, element)).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private XDocument CreateDocumentFromBlob(byte[] blob)
|
||||
{
|
||||
using (var memoryStream = new MemoryStream(blob))
|
||||
{
|
||||
var xmlReaderSettings = new XmlReaderSettings()
|
||||
{
|
||||
DtdProcessing = DtdProcessing.Prohibit, IgnoreProcessingInstructions = true
|
||||
};
|
||||
|
||||
using (var xmlReader = XmlReader.Create(memoryStream, xmlReaderSettings))
|
||||
{
|
||||
return XDocument.Load(xmlReader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ICloudBlob CreateFreshBlobRef()
|
||||
{
|
||||
// ICloudBlob instances aren't thread-safe, so we need to make sure we're working
|
||||
// with a fresh instance that won't be mutated by another thread.
|
||||
|
||||
var blobRef = _blobRefFactory();
|
||||
if (blobRef == null)
|
||||
{
|
||||
throw new InvalidOperationException("The ICloudBlob factory method returned null.");
|
||||
}
|
||||
|
||||
return blobRef;
|
||||
}
|
||||
|
||||
private async Task<IList<XElement>> GetAllElementsAsync(ICloudBlob blobRef)
|
||||
{
|
||||
var data = await GetLatestDataAsync(blobRef);
|
||||
|
||||
if (data == null)
|
||||
{
|
||||
// no data in blob storage
|
||||
return new XElement[0];
|
||||
}
|
||||
|
||||
// The document will look like this:
|
||||
//
|
||||
// <root>
|
||||
// <child />
|
||||
// <child />
|
||||
// ...
|
||||
// </root>
|
||||
//
|
||||
// We want to return the first-level child elements to our caller.
|
||||
|
||||
var doc = CreateDocumentFromBlob(data.BlobContents);
|
||||
return doc.Root.Elements().ToList();
|
||||
}
|
||||
|
||||
private async Task<BlobData> GetLatestDataAsync(ICloudBlob blobRef)
|
||||
{
|
||||
// Set the appropriate AccessCondition based on what we believe the latest
|
||||
// file contents to be, then make the request.
|
||||
|
||||
var latestCachedData = Volatile.Read(ref _cachedBlobData); // local ref so field isn't mutated under our feet
|
||||
var accessCondition = (latestCachedData != null)
|
||||
? AccessCondition.GenerateIfNoneMatchCondition(latestCachedData.ETag)
|
||||
: null;
|
||||
|
||||
try
|
||||
{
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
await blobRef.DownloadToStreamAsync(
|
||||
target: memoryStream,
|
||||
accessCondition: accessCondition,
|
||||
options: null,
|
||||
operationContext: null);
|
||||
|
||||
// At this point, our original cache either didn't exist or was outdated.
|
||||
// We'll update it now and return the updated value;
|
||||
|
||||
latestCachedData = new BlobData()
|
||||
{
|
||||
BlobContents = memoryStream.ToArray(),
|
||||
ETag = blobRef.Properties.ETag
|
||||
};
|
||||
|
||||
}
|
||||
Volatile.Write(ref _cachedBlobData, latestCachedData);
|
||||
}
|
||||
catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 304)
|
||||
{
|
||||
// 304 Not Modified
|
||||
// Thrown when we already have the latest cached data.
|
||||
// This isn't an error; we'll return our cached copy of the data.
|
||||
}
|
||||
catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404)
|
||||
{
|
||||
// 404 Not Found
|
||||
// Thrown when no file exists in storage.
|
||||
// This isn't an error; we'll delete our cached copy of data.
|
||||
|
||||
latestCachedData = null;
|
||||
Volatile.Write(ref _cachedBlobData, latestCachedData);
|
||||
}
|
||||
|
||||
return latestCachedData;
|
||||
}
|
||||
|
||||
private int GetRandomizedBackoffPeriod()
|
||||
{
|
||||
// returns a TimeSpan in the range [0.8, 1.0) * ConflictBackoffPeriod
|
||||
// not used for crypto purposes
|
||||
var multiplier = 0.8 + (_random.NextDouble() * 0.2);
|
||||
return (int) (multiplier * ConflictBackoffPeriod.Ticks);
|
||||
}
|
||||
|
||||
private async Task StoreElementAsync(ICloudBlob blobRef, XElement element)
|
||||
{
|
||||
// holds the last error in case we need to rethrow it
|
||||
ExceptionDispatchInfo lastError = null;
|
||||
|
||||
for (var i = 0; i < ConflictMaxRetries; i++)
|
||||
{
|
||||
if (i > 1)
|
||||
{
|
||||
// If multiple conflicts occurred, wait a small period of time before retrying
|
||||
// the operation so that other writers can make forward progress.
|
||||
await Task.Delay(GetRandomizedBackoffPeriod());
|
||||
}
|
||||
|
||||
if (i > 0)
|
||||
{
|
||||
// If at least one conflict occurred, make sure we have an up-to-date
|
||||
// view of the blob contents.
|
||||
await GetLatestDataAsync(blobRef);
|
||||
}
|
||||
|
||||
// Merge the new element into the document. If no document exists,
|
||||
// create a new default document and inject this element into it.
|
||||
|
||||
var latestData = Volatile.Read(ref _cachedBlobData);
|
||||
var doc = (latestData != null)
|
||||
? CreateDocumentFromBlob(latestData.BlobContents)
|
||||
: new XDocument(new XElement(RepositoryElementName));
|
||||
doc.Root.Add(element);
|
||||
|
||||
// Turn this document back into a byte[].
|
||||
|
||||
var serializedDoc = new MemoryStream();
|
||||
doc.Save(serializedDoc, SaveOptions.DisableFormatting);
|
||||
|
||||
// Generate the appropriate precondition header based on whether or not
|
||||
// we believe data already exists in storage.
|
||||
|
||||
AccessCondition accessCondition;
|
||||
if (latestData != null)
|
||||
{
|
||||
accessCondition = AccessCondition.GenerateIfMatchCondition(blobRef.Properties.ETag);
|
||||
}
|
||||
else
|
||||
{
|
||||
accessCondition = AccessCondition.GenerateIfNotExistsCondition();
|
||||
blobRef.Properties.ContentType = "application/xml; charset=utf-8"; // set content type on first write
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Send the request up to the server.
|
||||
|
||||
var serializedDocAsByteArray = serializedDoc.ToArray();
|
||||
|
||||
await blobRef.UploadFromByteArrayAsync(
|
||||
buffer: serializedDocAsByteArray,
|
||||
index: 0,
|
||||
count: serializedDocAsByteArray.Length,
|
||||
accessCondition: accessCondition,
|
||||
options: null,
|
||||
operationContext: null);
|
||||
|
||||
// If we got this far, success!
|
||||
// We can update the cached view of the remote contents.
|
||||
|
||||
Volatile.Write(ref _cachedBlobData, new BlobData()
|
||||
{
|
||||
BlobContents = serializedDocAsByteArray,
|
||||
ETag = blobRef.Properties.ETag // was updated by Upload routine
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
catch (StorageException ex)
|
||||
when (ex.RequestInformation.HttpStatusCode == 409 || ex.RequestInformation.HttpStatusCode == 412)
|
||||
{
|
||||
// 409 Conflict
|
||||
// This error is rare but can be thrown in very special circumstances,
|
||||
// such as if the blob in the process of being created. We treat it
|
||||
// as equivalent to 412 for the purposes of retry logic.
|
||||
|
||||
// 412 Precondition Failed
|
||||
// We'll get this error if another writer updated the repository and we
|
||||
// have an outdated view of its contents. If this occurs, we'll just
|
||||
// refresh our view of the remote contents and try again up to the max
|
||||
// retry limit.
|
||||
|
||||
lastError = ExceptionDispatchInfo.Capture(ex);
|
||||
}
|
||||
}
|
||||
|
||||
// if we got this far, something went awry
|
||||
lastError.Throw();
|
||||
}
|
||||
|
||||
private sealed class BlobData
|
||||
{
|
||||
internal byte[] BlobContents;
|
||||
internal string ETag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
// 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 Microsoft.AspNetCore.DataProtection.Repositories;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.WindowsAzure.Storage;
|
||||
using Microsoft.WindowsAzure.Storage.Auth;
|
||||
using Microsoft.WindowsAzure.Storage.Blob;
|
||||
|
||||
namespace Microsoft.AspNetCore.DataProtection.Azure.Blob
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains Azure-specific extension methods for modifying a
|
||||
/// <see cref="IDataProtectionBuilder"/>.
|
||||
/// </summary>
|
||||
public static class AzureDataProtectionBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures the data protection system to persist keys to the specified path
|
||||
/// in Azure Blob Storage.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance to modify.</param>
|
||||
/// <param name="storageAccount">The <see cref="CloudStorageAccount"/> which
|
||||
/// should be utilized.</param>
|
||||
/// <param name="relativePath">A relative path where the key file should be
|
||||
/// stored, generally specified as "/containerName/[subDir/]keys.xml".</param>
|
||||
/// <returns>The value <paramref name="builder"/>.</returns>
|
||||
/// <remarks>
|
||||
/// The container referenced by <paramref name="relativePath"/> must already exist.
|
||||
/// </remarks>
|
||||
public static IDataProtectionBuilder PersistKeysToAzureBlobStorage(this IDataProtectionBuilder builder, CloudStorageAccount storageAccount, string relativePath)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
if (storageAccount == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(storageAccount));
|
||||
}
|
||||
if (relativePath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(relativePath));
|
||||
}
|
||||
|
||||
// Simply concatenate the root storage endpoint with the relative path,
|
||||
// which includes the container name and blob name.
|
||||
|
||||
var uriBuilder = new UriBuilder(storageAccount.BlobEndpoint);
|
||||
uriBuilder.Path = uriBuilder.Path.TrimEnd('/') + "/" + relativePath.TrimStart('/');
|
||||
|
||||
// We can create a CloudBlockBlob from the storage URI and the creds.
|
||||
|
||||
var blobAbsoluteUri = uriBuilder.Uri;
|
||||
var credentials = storageAccount.Credentials;
|
||||
|
||||
return PersistKeystoAzureBlobStorageInternal(builder, () => new CloudBlockBlob(blobAbsoluteUri, credentials));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the data protection system to persist keys to the specified path
|
||||
/// in Azure Blob Storage.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance to modify.</param>
|
||||
/// <param name="blobUri">The full URI where the key file should be stored.
|
||||
/// The URI must contain the SAS token as a query string parameter.</param>
|
||||
/// <returns>The value <paramref name="builder"/>.</returns>
|
||||
/// <remarks>
|
||||
/// The container referenced by <paramref name="blobUri"/> must already exist.
|
||||
/// </remarks>
|
||||
public static IDataProtectionBuilder PersistKeysToAzureBlobStorage(this IDataProtectionBuilder builder, Uri blobUri)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
if (blobUri == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(blobUri));
|
||||
}
|
||||
|
||||
var uriBuilder = new UriBuilder(blobUri);
|
||||
|
||||
// The SAS token is present in the query string.
|
||||
|
||||
if (string.IsNullOrEmpty(uriBuilder.Query))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
message: "URI does not have a SAS token in the query string.",
|
||||
paramName: nameof(blobUri));
|
||||
}
|
||||
|
||||
var credentials = new StorageCredentials(uriBuilder.Query);
|
||||
uriBuilder.Query = null; // no longer needed
|
||||
var blobAbsoluteUri = uriBuilder.Uri;
|
||||
|
||||
return PersistKeystoAzureBlobStorageInternal(builder, () => new CloudBlockBlob(blobAbsoluteUri, credentials));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the data protection system to persist keys to the specified path
|
||||
/// in Azure Blob Storage.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance to modify.</param>
|
||||
/// <param name="blobReference">The <see cref="CloudBlockBlob"/> where the
|
||||
/// key file should be stored.</param>
|
||||
/// <returns>The value <paramref name="builder"/>.</returns>
|
||||
/// <remarks>
|
||||
/// The container referenced by <paramref name="blobReference"/> must already exist.
|
||||
/// </remarks>
|
||||
public static IDataProtectionBuilder PersistKeysToAzureBlobStorage(this IDataProtectionBuilder builder, CloudBlockBlob blobReference)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
if (blobReference == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(blobReference));
|
||||
}
|
||||
|
||||
// We're basically just going to make a copy of this blob.
|
||||
// Use (container, blobName) instead of (storageuri, creds) since the container
|
||||
// is tied to an existing service client, which contains user-settable defaults
|
||||
// like retry policy and secondary connection URIs.
|
||||
|
||||
var container = blobReference.Container;
|
||||
var blobName = blobReference.Name;
|
||||
|
||||
return PersistKeystoAzureBlobStorageInternal(builder, () => container.GetBlockBlobReference(blobName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the data protection system to persist keys to the specified path
|
||||
/// in Azure Blob Storage.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance to modify.</param>
|
||||
/// <param name="container">The <see cref="CloudBlobContainer"/> in which the
|
||||
/// key file should be stored.</param>
|
||||
/// <param name="blobName">The name of the key file, generally specified
|
||||
/// as "[subdir/]keys.xml"</param>
|
||||
/// <returns>The value <paramref name="builder"/>.</returns>
|
||||
/// <remarks>
|
||||
/// The container referenced by <paramref name="container"/> must already exist.
|
||||
/// </remarks>
|
||||
public static IDataProtectionBuilder PersistKeysToAzureBlobStorage(this IDataProtectionBuilder builder, CloudBlobContainer container, string blobName)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
if (container == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(container));
|
||||
}
|
||||
if (blobName == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(blobName));
|
||||
}
|
||||
return PersistKeystoAzureBlobStorageInternal(builder, () => container.GetBlockBlobReference(blobName));
|
||||
}
|
||||
|
||||
// important: the Func passed into this method must return a new instance with each call
|
||||
private static IDataProtectionBuilder PersistKeystoAzureBlobStorageInternal(IDataProtectionBuilder config, Func<CloudBlockBlob> blobRefFactory)
|
||||
{
|
||||
config.Services.AddSingleton<IXmlRepository>(services => new AzureBlobXmlRepository(blobRefFactory));
|
||||
return config;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>cc799b57-81e2-4f45-8a32-0d5f49753c3f</ProjectGuid>
|
||||
<RootNamespace>Microsoft.AspNetCore.DataProtection.Azure</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>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// 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.Reflection;
|
||||
using System.Resources;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: AssemblyMetadata("Serviceable", "True")]
|
||||
[assembly: NeutralResourcesLanguage("en-US")]
|
||||
[assembly: AssemblyCompany("Microsoft Corporation.")]
|
||||
[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")]
|
||||
[assembly: AssemblyProduct("Microsoft ASP.NET Core")]
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"version": "0.1.0-*",
|
||||
"description": "Microsoft Azure Blob storrage support as key store.",
|
||||
"packOptions": {
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/aspnet/dataprotection"
|
||||
},
|
||||
"tags": [
|
||||
"aspnetcore",
|
||||
"dataprotection",
|
||||
"azure",
|
||||
"blob"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"Microsoft.AspNetCore.DataProtection": "1.1.0-*",
|
||||
"WindowsAzure.Storage": "7.0.2-preview"
|
||||
},
|
||||
"frameworks": {
|
||||
"net451": {},
|
||||
"netstandard1.5": {
|
||||
"imports": "portable-net45+win8+wp8+wpa81"
|
||||
}
|
||||
},
|
||||
"buildOptions": {
|
||||
"allowUnsafe": true,
|
||||
"warningsAsErrors": true,
|
||||
"keyFile": "../../tools/Key.snk",
|
||||
"nowarn": [
|
||||
"CS1591"
|
||||
],
|
||||
"xmlDoc": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.AspNetCore.DataProtection.Azure.Blob;
|
||||
using Microsoft.WindowsAzure.Storage;
|
||||
using Microsoft.WindowsAzure.Storage.Blob;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.DataProtection.Azure.Test
|
||||
{
|
||||
public class AzureBlobXmlRepositoryTests
|
||||
{
|
||||
[Fact]
|
||||
public void StoreCreatesBlobWhenNotExist()
|
||||
{
|
||||
AccessCondition downloadCondition = null;
|
||||
AccessCondition uploadCondition = null;
|
||||
byte[] bytes = null;
|
||||
BlobProperties properties = new BlobProperties();
|
||||
|
||||
var mock = new Mock<ICloudBlob>();
|
||||
mock.SetupGet(c => c.Properties).Returns(properties);
|
||||
mock.Setup(c => c.UploadFromByteArrayAsync(
|
||||
It.IsAny<byte[]>(),
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<AccessCondition>(),
|
||||
It.IsAny<BlobRequestOptions>(),
|
||||
It.IsAny<OperationContext>()))
|
||||
.Returns(async (byte[] buffer, int index, int count, AccessCondition accessCondition, BlobRequestOptions options, OperationContext operationContext) =>
|
||||
{
|
||||
bytes = buffer.Skip(index).Take(count).ToArray();
|
||||
uploadCondition = accessCondition;
|
||||
await Task.Yield();
|
||||
});
|
||||
|
||||
var repository = new AzureBlobXmlRepository(() => mock.Object);
|
||||
repository.StoreElement(new XElement("Element"), null);
|
||||
|
||||
Assert.Null(downloadCondition);
|
||||
Assert.Equal("*", uploadCondition.IfNoneMatchETag);
|
||||
Assert.Equal("application/xml; charset=utf-8", properties.ContentType);
|
||||
var element = "<Element />";
|
||||
|
||||
Assert.Equal(bytes, GetEnvelopedContent(element));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StoreUpdatesWhenExistsAndNewerExists()
|
||||
{
|
||||
AccessCondition downloadCondition = null;
|
||||
byte[] bytes = null;
|
||||
BlobProperties properties = new BlobProperties();
|
||||
|
||||
var mock = new Mock<ICloudBlob>();
|
||||
mock.SetupGet(c => c.Properties).Returns(properties);
|
||||
mock.Setup(c => c.DownloadToStreamAsync(
|
||||
It.IsAny<Stream>(),
|
||||
It.IsAny<AccessCondition>(),
|
||||
null,
|
||||
null))
|
||||
.Returns(async (Stream target, AccessCondition condition, BlobRequestOptions options, OperationContext context) =>
|
||||
{
|
||||
var data = GetEnvelopedContent("<Element1 />");
|
||||
await target.WriteAsync(data, 0, data.Length);
|
||||
})
|
||||
.Verifiable();
|
||||
|
||||
mock.Setup(c => c.UploadFromByteArrayAsync(
|
||||
It.IsAny<byte[]>(),
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<int>(),
|
||||
It.Is((AccessCondition cond) => cond.IfNoneMatchETag == "*"),
|
||||
It.IsAny<BlobRequestOptions>(),
|
||||
It.IsAny<OperationContext>()))
|
||||
.Throws(new StorageException(new RequestResult { HttpStatusCode = 412 }, null, null))
|
||||
.Verifiable();
|
||||
|
||||
mock.Setup(c => c.UploadFromByteArrayAsync(
|
||||
It.IsAny<byte[]>(),
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<int>(),
|
||||
It.Is((AccessCondition cond) => cond.IfNoneMatchETag != "*"),
|
||||
It.IsAny<BlobRequestOptions>(),
|
||||
It.IsAny<OperationContext>()))
|
||||
.Returns(async (byte[] buffer, int index, int count, AccessCondition accessCondition, BlobRequestOptions options, OperationContext operationContext) =>
|
||||
{
|
||||
bytes = buffer.Skip(index).Take(count).ToArray();
|
||||
await Task.Yield();
|
||||
})
|
||||
.Verifiable();
|
||||
|
||||
var repository = new AzureBlobXmlRepository(() => mock.Object);
|
||||
repository.StoreElement(new XElement("Element2"), null);
|
||||
|
||||
mock.Verify();
|
||||
Assert.Null(downloadCondition);
|
||||
Assert.Equal(bytes, GetEnvelopedContent("<Element1 /><Element2 />"));
|
||||
}
|
||||
|
||||
private static byte[] GetEnvelopedContent(string element)
|
||||
{
|
||||
return Encoding.UTF8.GetBytes($"<?xml version=\"1.0\" encoding=\"utf-8\"?><repository>{element}</repository>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?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>8c41240e-48f8-402f-9388-74cfe27f4d76</ProjectGuid>
|
||||
<RootNamespace>Microsoft.AspNetCore.DataProtection.Azure.Test</RootNamespace>
|
||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
||||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"dotnet-test-xunit": "2.2.0-*",
|
||||
"Microsoft.AspNetCore.DataProtection": "1.1.0-*",
|
||||
"Microsoft.AspNetCore.DataProtection.Azure.Blob": "1.1.0-*",
|
||||
"Microsoft.AspNetCore.Testing": "1.1.0-*",
|
||||
"Microsoft.Extensions.DependencyInjection": "1.1.0-*",
|
||||
"xunit": "2.2.0-*",
|
||||
"Moq": "4.6.36-*"
|
||||
},
|
||||
"frameworks": {
|
||||
"netcoreapp1.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"version": "1.0.0-*",
|
||||
"type": "platform"
|
||||
},
|
||||
"System.Diagnostics.Process": "4.1.0-*",
|
||||
"System.Diagnostics.TraceSource": "4.0.0-*"
|
||||
},
|
||||
"imports": [
|
||||
"dnxcore50",
|
||||
"portable-net451+win8"
|
||||
]
|
||||
},
|
||||
"net451": {
|
||||
"frameworkAssemblies": {
|
||||
"System.Threading.Tasks": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"testRunner": "xunit",
|
||||
"buildOptions": {
|
||||
"allowUnsafe": true,
|
||||
"warningsAsErrors": true,
|
||||
"keyFile": "../../tools/Key.snk"
|
||||
}
|
||||
}
|
||||
|
|
@ -11,9 +11,11 @@
|
|||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
||||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
Loading…
Reference in New Issue