diff --git a/eng/.docsettings.yml b/eng/.docsettings.yml
index 4a18a0522ef7..237e65e94514 100644
--- a/eng/.docsettings.yml
+++ b/eng/.docsettings.yml
@@ -70,6 +70,7 @@ known_presence_issues:
- ['sdk/keyvault','#5499']
- ['sdk/eventhub','#5499']
- ['sdk/attestation/Microsoft.Azure.Attestation','#5499']
+ - ['sdk/storage/Azure.AspNetCore.DataProtection.Blobs','#9960']
- ['sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration', '#9939']
- ['sdk/keyvault/Azure.Security.KeyVault.Secrets.AspNetCore.DataProtection','#9955']
diff --git a/eng/Packages.Data.props b/eng/Packages.Data.props
index 89ff91ab7170..7594d940b23f 100755
--- a/eng/Packages.Data.props
+++ b/eng/Packages.Data.props
@@ -23,8 +23,6 @@
-
-
@@ -49,12 +47,6 @@
-
-
-
-
-
-
@@ -114,20 +106,45 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sdk/core/Microsoft.Extensions.Azure/src/Microsoft.Extensions.Azure.csproj b/sdk/core/Microsoft.Extensions.Azure/src/Microsoft.Extensions.Azure.csproj
index 230dd29dead2..7f8a1d6da3ff 100644
--- a/sdk/core/Microsoft.Extensions.Azure/src/Microsoft.Extensions.Azure.csproj
+++ b/sdk/core/Microsoft.Extensions.Azure/src/Microsoft.Extensions.Azure.csproj
@@ -12,6 +12,7 @@
true
false
+ true
diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets.AspNetCore.DataProtection/src/Azure.Security.KeyVault.Secrets.AspNetCore.DataProtection.csproj b/sdk/keyvault/Azure.Security.KeyVault.Secrets.AspNetCore.DataProtection/src/Azure.Security.KeyVault.Secrets.AspNetCore.DataProtection.csproj
index 5de460712cb1..4495c78b982a 100644
--- a/sdk/keyvault/Azure.Security.KeyVault.Secrets.AspNetCore.DataProtection/src/Azure.Security.KeyVault.Secrets.AspNetCore.DataProtection.csproj
+++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets.AspNetCore.DataProtection/src/Azure.Security.KeyVault.Secrets.AspNetCore.DataProtection.csproj
@@ -5,6 +5,7 @@
aspnetcore;dataprotection;azure;keyvault
1.0.0-preview.1
false
+ true
diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/Azure.Security.KeyVault.Secrets.Extensions.Configuration.csproj b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/Azure.Security.KeyVault.Secrets.Extensions.Configuration.csproj
index 0f741f5a3862..42daed390afe 100644
--- a/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/Azure.Security.KeyVault.Secrets.Extensions.Configuration.csproj
+++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets.Extensions.Configuration/src/Azure.Security.KeyVault.Secrets.Extensions.Configuration.csproj
@@ -6,6 +6,7 @@
$(PackageTags);azure;keyvault
1.0.0-preview.1
false
+ true
diff --git a/sdk/storage/Azure.AspNetCore.DataProtection.Blobs/api/Azure.AspNetCore.DataProtection.Blobs.netstandard2.0.cs b/sdk/storage/Azure.AspNetCore.DataProtection.Blobs/api/Azure.AspNetCore.DataProtection.Blobs.netstandard2.0.cs
new file mode 100644
index 000000000000..f23bb8f7b2b8
--- /dev/null
+++ b/sdk/storage/Azure.AspNetCore.DataProtection.Blobs/api/Azure.AspNetCore.DataProtection.Blobs.netstandard2.0.cs
@@ -0,0 +1,18 @@
+namespace Azure.AspNetCore.DataProtection.Blobs
+{
+ public sealed partial class AzureBlobXmlRepository : Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository
+ {
+ public AzureBlobXmlRepository(Azure.Storage.Blobs.BlobClient blobClient) { }
+ public System.Collections.Generic.IReadOnlyCollection GetAllElements() { throw null; }
+ public void StoreElement(System.Xml.Linq.XElement element, string friendlyName) { }
+ }
+}
+namespace Microsoft.AspNetCore.DataProtection
+{
+ public static partial class AzureStorageBlobDataProtectionBuilderExtensions
+ {
+ public static Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder PersistKeysToAzureBlobStorage(this Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder builder, Azure.Storage.Blobs.BlobClient blobClient) { throw null; }
+ public static Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder PersistKeysToAzureBlobStorage(this Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder builder, System.Uri blobUri) { throw null; }
+ public static Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder PersistKeysToAzureBlobStorage(this Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder builder, System.Uri blobUri, Azure.Core.TokenCredential tokenCredential) { throw null; }
+ }
+}
diff --git a/sdk/storage/Azure.AspNetCore.DataProtection.Blobs/src/Azure.AspNetCore.DataProtection.Blobs.csproj b/sdk/storage/Azure.AspNetCore.DataProtection.Blobs/src/Azure.AspNetCore.DataProtection.Blobs.csproj
new file mode 100644
index 000000000000..61b236c45604
--- /dev/null
+++ b/sdk/storage/Azure.AspNetCore.DataProtection.Blobs/src/Azure.AspNetCore.DataProtection.Blobs.csproj
@@ -0,0 +1,18 @@
+
+
+
+ $(RequiredTargetFrameworks)
+ Microsoft Azure Blob storage support as key store (https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/implementation/key-storage-providers).
+ aspnetcore;dataprotection;azure;blob;key store
+ 1.0.0-preview.1
+ false
+ true
+
+
+
+
+
+
+
+
+
diff --git a/sdk/storage/Azure.AspNetCore.DataProtection.Blobs/src/AzureBlobXmlRepository.cs b/sdk/storage/Azure.AspNetCore.DataProtection.Blobs/src/AzureBlobXmlRepository.cs
new file mode 100644
index 000000000000..ad6d91322de2
--- /dev/null
+++ b/sdk/storage/Azure.AspNetCore.DataProtection.Blobs/src/AzureBlobXmlRepository.cs
@@ -0,0 +1,266 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+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 Azure;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Microsoft.AspNetCore.DataProtection.Repositories;
+
+#pragma warning disable AZC0001 //
+namespace Azure.AspNetCore.DataProtection.Blobs
+#pragma warning restore
+{
+ ///
+ /// An which is backed by Azure Blob Storage.
+ ///
+ ///
+ /// Instances of this type are thread-safe.
+ ///
+ 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 static BlobHttpHeaders _blobHttpHeaders = new BlobHttpHeaders() { ContentType = "application/xml; charset=utf-8" };
+
+ private readonly Random _random;
+ private BlobData _cachedBlobData;
+ private readonly BlobClient _blobClient;
+
+ ///
+ /// Creates a new instance of the .
+ ///
+ /// A that is connected to the blob we are reading from and writing to.
+ public AzureBlobXmlRepository(BlobClient blobClient)
+ {
+ _random = new Random();
+ _blobClient = blobClient;
+ }
+
+ ///
+ public IReadOnlyCollection GetAllElements()
+ {
+ // 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()).GetAwaiter().GetResult();
+ return new ReadOnlyCollection(elements);
+ }
+
+ ///
+ public void StoreElement(XElement element, string friendlyName)
+ {
+ if (element == null)
+ {
+ throw new ArgumentNullException(nameof(element));
+ }
+
+ // 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(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 async Task> GetAllElementsAsync()
+ {
+ var data = await GetLatestDataAsync().ConfigureAwait(false);
+
+ if (data == null || data.BlobContents.Length == 0)
+ {
+ // no data in blob storage
+ return Array.Empty();
+ }
+
+ // The document will look like this:
+ //
+ //
+ //
+ //
+ // ...
+ //
+ //
+ // 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 GetLatestDataAsync()
+ {
+ // 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 requestCondition = (latestCachedData != null)
+ ? new BlobRequestConditions() { IfNoneMatch = latestCachedData.ETag }
+ : null;
+
+ try
+ {
+ using (var memoryStream = new MemoryStream())
+ {
+ var response = await _blobClient.DownloadToAsync(
+ destination: memoryStream,
+ conditions: requestCondition).ConfigureAwait(false);
+
+ // 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 = response.Headers.ETag
+ };
+
+ }
+ Volatile.Write(ref _cachedBlobData, latestCachedData);
+ }
+ catch (RequestFailedException ex) when (ex.Status == 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 (RequestFailedException ex) when (ex.Status == 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(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()).ConfigureAwait(false);
+ }
+
+ if (i > 0)
+ {
+ // If at least one conflict occurred, make sure we have an up-to-date
+ // view of the blob contents.
+ await GetLatestDataAsync().ConfigureAwait(false);
+ }
+
+ // 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);
+ serializedDoc.Position = 0;
+
+ // Generate the appropriate precondition header based on whether or not
+ // we believe data already exists in storage.
+
+ BlobRequestConditions requestConditions;
+ if (latestData != null)
+ {
+ requestConditions = new BlobRequestConditions() { IfMatch = latestData.ETag };
+ }
+ else
+ {
+ requestConditions = new BlobRequestConditions() { IfNoneMatch = ETag.All };
+ }
+
+ try
+ {
+ // Send the request up to the server.
+ var response = await _blobClient.UploadAsync(
+ serializedDoc,
+ httpHeaders: _blobHttpHeaders,
+ conditions: requestConditions).ConfigureAwait(false);
+
+ // If we got this far, success!
+ // We can update the cached view of the remote contents.
+
+ Volatile.Write(ref _cachedBlobData, new BlobData()
+ {
+ BlobContents = serializedDoc.ToArray(),
+ ETag = response.Value.ETag // was updated by Upload routine
+ });
+
+ return;
+ }
+ catch (RequestFailedException ex)
+ when (ex.Status == 409 || ex.Status == 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 ETag? ETag;
+ }
+ }
+}
diff --git a/sdk/storage/Azure.AspNetCore.DataProtection.Blobs/src/AzureStorageBlobDataProtectionBuilderExtensions.cs b/sdk/storage/Azure.AspNetCore.DataProtection.Blobs/src/AzureStorageBlobDataProtectionBuilderExtensions.cs
new file mode 100644
index 000000000000..a4d732274f35
--- /dev/null
+++ b/sdk/storage/Azure.AspNetCore.DataProtection.Blobs/src/AzureStorageBlobDataProtectionBuilderExtensions.cs
@@ -0,0 +1,125 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using Azure.Core;
+using Azure.Identity;
+using Azure.Storage.Blobs;
+using Azure.AspNetCore.DataProtection.Blobs;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.Extensions.DependencyInjection;
+
+#pragma warning disable AZC0001 // Extension methods have to be in the correct namespace to appear in intellisense.
+namespace Microsoft.AspNetCore.DataProtection
+#pragma warning disable
+{
+ ///
+ /// Contains Azure-specific extension methods for modifying a
+ /// .
+ ///
+ public static class AzureStorageBlobDataProtectionBuilderExtensions
+ {
+ ///
+ /// Configures the data protection system to persist keys to the specified path
+ /// in Azure Blob Storage.
+ ///
+ /// The builder instance to modify.
+ /// The full URI where the key file should be stored.
+ /// The URI must contain the SAS token as a query string parameter.
+ /// The value .
+ ///
+ /// The container referenced by must already exist.
+ ///
+ 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 BlobUriBuilder(blobUri);
+ BlobClient client;
+
+ // The SAS token is present in the query string.
+ if (uriBuilder.Sas == null)
+ {
+ client = new BlobClient(blobUri, new DefaultAzureCredential());
+ }
+ else
+ {
+ client = new BlobClient(blobUri);
+ }
+
+ return PersistKeystoAzureBlobStorageInternal(builder, client);
+ }
+
+ ///
+ /// Configures the data protection system to persist keys to the specified path
+ /// in Azure Blob Storage.
+ ///
+ /// The builder instance to modify.
+ /// The full URI where the key file should be stored.
+ /// The URI must contain the SAS token as a query string parameter.
+ /// The credentials to connect to the blob.
+ /// The value .
+ ///
+ /// The container referenced by must already exist.
+ ///
+ public static IDataProtectionBuilder PersistKeysToAzureBlobStorage(this IDataProtectionBuilder builder, Uri blobUri, TokenCredential tokenCredential)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+ if (blobUri == null)
+ {
+ throw new ArgumentNullException(nameof(blobUri));
+ }
+ if (tokenCredential == null)
+ {
+ throw new ArgumentNullException(nameof(tokenCredential));
+ }
+
+ var client = new BlobClient(blobUri, tokenCredential);
+
+ return PersistKeystoAzureBlobStorageInternal(builder, client);
+ }
+
+ ///
+ /// Configures the data protection system to persist keys to the specified path
+ /// in Azure Blob Storage.
+ ///
+ /// The builder instance to modify.
+ /// The in which the
+ /// key file should be stored.
+ /// The value .
+ ///
+ /// The blob referenced by must already exist.
+ ///
+ public static IDataProtectionBuilder PersistKeysToAzureBlobStorage(this IDataProtectionBuilder builder, BlobClient blobClient)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+ if (blobClient == null)
+ {
+ throw new ArgumentNullException(nameof(blobClient));
+ }
+ return PersistKeystoAzureBlobStorageInternal(builder, blobClient);
+ }
+
+ private static IDataProtectionBuilder PersistKeystoAzureBlobStorageInternal(IDataProtectionBuilder builder, BlobClient blobClient)
+ {
+ builder.Services.Configure(options =>
+ {
+ options.XmlRepository = new AzureBlobXmlRepository(blobClient);
+ });
+ return builder;
+ }
+ }
+}
diff --git a/sdk/storage/Azure.AspNetCore.DataProtection.Blobs/tests/Azure.AspNetCore.DataProtection.Blobs.Tests.csproj b/sdk/storage/Azure.AspNetCore.DataProtection.Blobs/tests/Azure.AspNetCore.DataProtection.Blobs.Tests.csproj
new file mode 100644
index 000000000000..6b9b1b0686c6
--- /dev/null
+++ b/sdk/storage/Azure.AspNetCore.DataProtection.Blobs/tests/Azure.AspNetCore.DataProtection.Blobs.Tests.csproj
@@ -0,0 +1,20 @@
+
+
+
+ $(RequiredTargetFrameworks)
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sdk/storage/Azure.AspNetCore.DataProtection.Blobs/tests/AzureBlobXmlRepositoryTests.cs b/sdk/storage/Azure.AspNetCore.DataProtection.Blobs/tests/AzureBlobXmlRepositoryTests.cs
new file mode 100644
index 000000000000..a3d2a9c3afa9
--- /dev/null
+++ b/sdk/storage/Azure.AspNetCore.DataProtection.Blobs/tests/AzureBlobXmlRepositoryTests.cs
@@ -0,0 +1,140 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using Azure;
+using Azure.Core;
+using Azure.Core.Testing;
+using Azure.Storage;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Moq;
+using NUnit.Framework;
+
+namespace Azure.AspNetCore.DataProtection.Blobs.Tests
+{
+ public class AzureBlobXmlRepositoryTests
+ {
+ [Test]
+ public void StoreCreatesBlobWhenNotExist()
+ {
+ BlobRequestConditions uploadConditions = null;
+ byte[] bytes = null;
+ string contentType = null;
+
+ var mock = new Mock();
+
+ mock.Setup(c => c.UploadAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .Returns(async (Stream strm, BlobHttpHeaders headers, IDictionary metaData, BlobRequestConditions conditions, IProgress progress, AccessTier? access, StorageTransferOptions transfer, CancellationToken token) =>
+ {
+ using var memoryStream = new MemoryStream();
+ strm.CopyTo(memoryStream);
+ bytes = memoryStream.ToArray();
+ uploadConditions = conditions;
+ contentType = headers?.ContentType;
+
+ await Task.Yield();
+
+ var mockResponse = new Mock>();
+ var blobContentInfo = BlobsModelFactory.BlobContentInfo(ETag.All, DateTimeOffset.Now.AddDays(-1), Array.Empty(), "", 1);
+
+ mockResponse.Setup(c => c.Value).Returns(blobContentInfo);
+ return mockResponse.Object;
+ });
+
+ var repository = new AzureBlobXmlRepository(mock.Object);
+ repository.StoreElement(new XElement("Element"), null);
+
+ Assert.AreEqual("*", uploadConditions.IfNoneMatch.ToString());
+ Assert.AreEqual("application/xml; charset=utf-8", contentType);
+ var element = "";
+
+ Assert.AreEqual(bytes, GetEnvelopedContent(element));
+ }
+
+ [Test]
+ public void StoreUpdatesWhenExistsAndNewerExists()
+ {
+ byte[] bytes = null;
+
+ var mock = new Mock();
+
+ mock.Setup(c => c.DownloadToAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .Returns(async (Stream target, BlobRequestConditions conditions, StorageTransferOptions options, CancellationToken token) =>
+ {
+ var data = GetEnvelopedContent("");
+ await target.WriteAsync(data, 0, data.Length);
+
+ var response = new MockResponse(200);
+ response.AddHeader(new HttpHeader("ETag", "*"));
+ return response;
+ })
+ .Verifiable();
+
+ mock.Setup(c => c.UploadAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny>(),
+ It.Is((BlobRequestConditions conditions) => conditions.IfNoneMatch == ETag.All),
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .Throws(new RequestFailedException(status: 412, message: ""))
+ .Verifiable();
+
+ mock.Setup(c => c.UploadAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny>(),
+ It.Is((BlobRequestConditions conditions) => conditions.IfNoneMatch != ETag.All),
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .Returns(async (Stream strm, BlobHttpHeaders headers, IDictionary metaData, BlobRequestConditions conditions, IProgress progress, AccessTier? access, StorageTransferOptions transfer, CancellationToken token) =>
+ {
+ using var memoryStream = new MemoryStream();
+ strm.CopyTo(memoryStream);
+ bytes = memoryStream.ToArray();
+
+ await Task.Yield();
+
+ var mockResponse = new Mock>();
+ var blobContentInfo = BlobsModelFactory.BlobContentInfo(ETag.All, DateTimeOffset.Now.AddDays(-1), Array.Empty(), "", 1);
+ mockResponse.Setup(c => c.Value).Returns(blobContentInfo);
+ return mockResponse.Object;
+ })
+ .Verifiable();
+
+ var repository = new AzureBlobXmlRepository(mock.Object);
+ repository.StoreElement(new XElement("Element2"), null);
+
+ mock.Verify();
+ Assert.AreEqual(bytes, GetEnvelopedContent(""));
+ }
+
+ private static byte[] GetEnvelopedContent(string element)
+ {
+ return Encoding.UTF8.GetBytes($"{element}");
+ }
+ }
+}
diff --git a/sdk/storage/Azure.AspNetCore.DataProtection.Blobs/tests/AzureDataProtectionBuilderExtensionsTest.cs b/sdk/storage/Azure.AspNetCore.DataProtection.Blobs/tests/AzureDataProtectionBuilderExtensionsTest.cs
new file mode 100644
index 000000000000..ee59a0a170a0
--- /dev/null
+++ b/sdk/storage/Azure.AspNetCore.DataProtection.Blobs/tests/AzureDataProtectionBuilderExtensionsTest.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using Azure.Storage.Blobs;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using NUnit.Framework;
+
+namespace Azure.AspNetCore.DataProtection.Blobs.Tests
+{
+ public class AzureDataProtectionBuilderExtensionsTest
+ {
+ [Test]
+ public void PersistKeysToAzureBlobStorage_UsesAzureBlobXmlRepository()
+ {
+ // Arrange
+ var client = new BlobClient(new Uri("http://www.example.com"));
+ var serviceCollection = new ServiceCollection();
+ var builder = serviceCollection.AddDataProtection();
+
+ // Act
+ builder.PersistKeysToAzureBlobStorage(client);
+ var services = serviceCollection.BuildServiceProvider();
+
+ // Assert
+ var options = services.GetRequiredService>();
+ Assert.IsInstanceOf(options.Value.XmlRepository);
+ }
+ }
+}
diff --git a/sdk/storage/Azure.Storage.sln b/sdk/storage/Azure.Storage.sln
index 612768e7156e..597f73a984c8 100644
--- a/sdk/storage/Azure.Storage.sln
+++ b/sdk/storage/Azure.Storage.sln
@@ -113,6 +113,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Storage.Blobs.Batch.T
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiCompat", "..\..\eng\ApiCompat\ApiCompat.csproj", "{16260507-C87E-44D2-883F-8E338B7A519F}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.AspNetCore.DataProtection.Blobs", "Azure.AspNetCore.DataProtection.Blobs\src\Azure.AspNetCore.DataProtection.Blobs.csproj", "{E6B80592-AF36-4D07-8655-963A65103FDF}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.AspNetCore.DataProtection.Blobs.Tests", "Azure.AspNetCore.DataProtection.Blobs\tests\Azure.AspNetCore.DataProtection.Blobs.Tests.csproj", "{0DB3D27F-8C6F-4CFE-9059-891F63EFBF4E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -203,6 +207,14 @@ Global
{16260507-C87E-44D2-883F-8E338B7A519F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{16260507-C87E-44D2-883F-8E338B7A519F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{16260507-C87E-44D2-883F-8E338B7A519F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E6B80592-AF36-4D07-8655-963A65103FDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E6B80592-AF36-4D07-8655-963A65103FDF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E6B80592-AF36-4D07-8655-963A65103FDF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E6B80592-AF36-4D07-8655-963A65103FDF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0DB3D27F-8C6F-4CFE-9059-891F63EFBF4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0DB3D27F-8C6F-4CFE-9059-891F63EFBF4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0DB3D27F-8C6F-4CFE-9059-891F63EFBF4E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0DB3D27F-8C6F-4CFE-9059-891F63EFBF4E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE