diff --git a/DataProtection.sln b/DataProtection.sln
index c08ab6a1ce..7fb7eb0592 100644
--- a/DataProtection.sln
+++ b/DataProtection.sln
@@ -77,6 +77,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureKeyVault", "samples\Az
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test", "test\Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test\Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test.csproj", "{C85ED942-8121-453F-8308-9DB730843B63}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test", "test\Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test\Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test.csproj", "{06728BF2-C5EB-44C7-9F30-14FAA5649E14}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataProtection.EntityFrameworkCore", "src\Microsoft.AspNetCore.DataProtection.EntityFrameworkCore\Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.csproj", "{3E4CA7FE-741B-4C78-A775-220E0E3C1B03}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityFrameworkCoreSample", "samples\EntityFrameworkCoreSample\EntityFrameworkCoreSample.csproj", "{22BA4EAB-641E-42B2-BB37-9C3BCFD99F76}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -265,6 +271,30 @@ Global
{C85ED942-8121-453F-8308-9DB730843B63}.Release|Any CPU.Build.0 = Release|Any CPU
{C85ED942-8121-453F-8308-9DB730843B63}.Release|x86.ActiveCfg = Release|Any CPU
{C85ED942-8121-453F-8308-9DB730843B63}.Release|x86.Build.0 = Release|Any CPU
+ {06728BF2-C5EB-44C7-9F30-14FAA5649E14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {06728BF2-C5EB-44C7-9F30-14FAA5649E14}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {06728BF2-C5EB-44C7-9F30-14FAA5649E14}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {06728BF2-C5EB-44C7-9F30-14FAA5649E14}.Debug|x86.Build.0 = Debug|Any CPU
+ {06728BF2-C5EB-44C7-9F30-14FAA5649E14}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {06728BF2-C5EB-44C7-9F30-14FAA5649E14}.Release|Any CPU.Build.0 = Release|Any CPU
+ {06728BF2-C5EB-44C7-9F30-14FAA5649E14}.Release|x86.ActiveCfg = Release|Any CPU
+ {06728BF2-C5EB-44C7-9F30-14FAA5649E14}.Release|x86.Build.0 = Release|Any CPU
+ {3E4CA7FE-741B-4C78-A775-220E0E3C1B03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3E4CA7FE-741B-4C78-A775-220E0E3C1B03}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3E4CA7FE-741B-4C78-A775-220E0E3C1B03}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3E4CA7FE-741B-4C78-A775-220E0E3C1B03}.Debug|x86.Build.0 = Debug|Any CPU
+ {3E4CA7FE-741B-4C78-A775-220E0E3C1B03}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3E4CA7FE-741B-4C78-A775-220E0E3C1B03}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3E4CA7FE-741B-4C78-A775-220E0E3C1B03}.Release|x86.ActiveCfg = Release|Any CPU
+ {3E4CA7FE-741B-4C78-A775-220E0E3C1B03}.Release|x86.Build.0 = Release|Any CPU
+ {22BA4EAB-641E-42B2-BB37-9C3BCFD99F76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {22BA4EAB-641E-42B2-BB37-9C3BCFD99F76}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {22BA4EAB-641E-42B2-BB37-9C3BCFD99F76}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {22BA4EAB-641E-42B2-BB37-9C3BCFD99F76}.Debug|x86.Build.0 = Debug|Any CPU
+ {22BA4EAB-641E-42B2-BB37-9C3BCFD99F76}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {22BA4EAB-641E-42B2-BB37-9C3BCFD99F76}.Release|Any CPU.Build.0 = Release|Any CPU
+ {22BA4EAB-641E-42B2-BB37-9C3BCFD99F76}.Release|x86.ActiveCfg = Release|Any CPU
+ {22BA4EAB-641E-42B2-BB37-9C3BCFD99F76}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -293,6 +323,9 @@ Global
{4E76B2A8-9DC3-46E6-B5FC-097A1D1DFBE9} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
{295E8539-5450-4764-B3F5-51F968628022} = {5A3A5DE3-49AD-431C-971D-B01B62D94AE2}
{C85ED942-8121-453F-8308-9DB730843B63} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
+ {06728BF2-C5EB-44C7-9F30-14FAA5649E14} = {60336AB3-948D-4D15-A5FB-F32A2B91E814}
+ {3E4CA7FE-741B-4C78-A775-220E0E3C1B03} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}
+ {22BA4EAB-641E-42B2-BB37-9C3BCFD99F76} = {5A3A5DE3-49AD-431C-971D-B01B62D94AE2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DD305D75-BD1B-43AE-BF04-869DA6A0858F}
diff --git a/build/dependencies.props b/build/dependencies.props
index 435ff56004..c75b435559 100644
--- a/build/dependencies.props
+++ b/build/dependencies.props
@@ -8,6 +8,8 @@
3.0.0-alpha1-10352
3.0.0-alpha1-10352
2.3.2
+ 3.0.0-alpha1-10352
+ 3.0.0-alpha1-10352
3.0.0-alpha1-10352
3.0.0-alpha1-10352
3.0.0-alpha1-10352
diff --git a/samples/EntityFrameworkCoreSample/EntityFrameworkCoreSample.csproj b/samples/EntityFrameworkCoreSample/EntityFrameworkCoreSample.csproj
new file mode 100644
index 0000000000..212ec9d566
--- /dev/null
+++ b/samples/EntityFrameworkCoreSample/EntityFrameworkCoreSample.csproj
@@ -0,0 +1,18 @@
+
+
+
+ exe
+ net461;netcoreapp2.1
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/EntityFrameworkCoreSample/Program.cs b/samples/EntityFrameworkCoreSample/Program.cs
new file mode 100644
index 0000000000..d4e978a7b8
--- /dev/null
+++ b/samples/EntityFrameworkCoreSample/Program.cs
@@ -0,0 +1,48 @@
+// 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;
+using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace EntityFrameworkCoreSample
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ // Configure
+ var services = new ServiceCollection()
+ .AddLogging(o => o.AddConsole().SetMinimumLevel(LogLevel.Debug))
+ .AddDbContext(o =>
+ {
+ o.UseInMemoryDatabase("DataProtection_EntityFrameworkCore");
+ o.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
+ o.EnableSensitiveDataLogging();
+ })
+ .AddDataProtection()
+ .PersistKeysToDbContext()
+ .SetDefaultKeyLifetime(TimeSpan.FromDays(7))
+ .Services
+ .BuildServiceProvider(validateScopes: true);
+
+ using(services)
+ {
+ // Run a sample payload
+ var protector = services.GetDataProtector("sample-purpose");
+ var protectedData = protector.Protect("Hello world!");
+ Console.WriteLine(protectedData);
+ }
+ }
+ }
+
+ class DataProtectionKeyContext : DbContext, IDataProtectionKeyContext
+ {
+ public DataProtectionKeyContext(DbContextOptions options) : base(options) { }
+
+ public DbSet DataProtectionKeys { get; set; }
+ }
+}
diff --git a/samples/Redis/Program.cs b/samples/Redis/Program.cs
index f8f213cfad..aa1cdf5164 100644
--- a/samples/Redis/Program.cs
+++ b/samples/Redis/Program.cs
@@ -7,7 +7,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
-namespace Redis
+namespace RedisSample
{
public class Program
{
diff --git a/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/DataProtectionKey.cs b/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/DataProtectionKey.cs
new file mode 100644
index 0000000000..c236d5cb89
--- /dev/null
+++ b/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/DataProtectionKey.cs
@@ -0,0 +1,26 @@
+// 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.
+
+namespace Microsoft.AspNetCore.DataProtection.EntityFrameworkCore
+{
+ ///
+ /// Code first model used by .
+ ///
+ public class DataProtectionKey
+ {
+ ///
+ /// The entity identifier of the .
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// The friendly name of the .
+ ///
+ public string FriendlyName { get; set; }
+
+ ///
+ /// The XML representation of the .
+ ///
+ public string Xml { get; set; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/EntityFrameworkCoreDataProtectionExtensions.cs b/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/EntityFrameworkCoreDataProtectionExtensions.cs
new file mode 100644
index 0000000000..ff24b58eb9
--- /dev/null
+++ b/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/EntityFrameworkCoreDataProtectionExtensions.cs
@@ -0,0 +1,39 @@
+// 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 Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ ///
+ /// Extension method class for configuring instances of
+ ///
+ public static class EntityFrameworkCoreDataProtectionExtensions
+ {
+ ///
+ /// Configures the data protection system to persist keys to an EntityFrameworkCore datastore
+ ///
+ /// The instance to modify.
+ /// The value .
+ public static IDataProtectionBuilder PersistKeysToDbContext(this IDataProtectionBuilder builder)
+ where TContext : DbContext, IDataProtectionKeyContext
+ {
+ builder.Services.AddSingleton>(services =>
+ {
+ var loggerFactory = services.GetService() ?? NullLoggerFactory.Instance;
+ return new ConfigureOptions(options =>
+ {
+ options.XmlRepository = new EntityFrameworkCoreXmlRepository(services, loggerFactory);
+ });
+ });
+
+ return builder;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/EntityFrameworkCoreXmlRepository.cs b/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/EntityFrameworkCoreXmlRepository.cs
new file mode 100644
index 0000000000..62250cf3ef
--- /dev/null
+++ b/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/EntityFrameworkCoreXmlRepository.cs
@@ -0,0 +1,81 @@
+// 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 Microsoft.AspNetCore.DataProtection.Repositories;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.DataProtection.EntityFrameworkCore
+{
+ ///
+ /// An backed by an EntityFrameworkCore datastore.
+ ///
+ public class EntityFrameworkCoreXmlRepository : IXmlRepository
+ where TContext : DbContext, IDataProtectionKeyContext
+ {
+ private readonly IServiceProvider _services;
+ private readonly ILogger _logger;
+
+ ///
+ /// Creates a new instance of the .
+ ///
+ ///
+ /// The .
+ public EntityFrameworkCoreXmlRepository(IServiceProvider services, ILoggerFactory loggerFactory)
+ {
+ if (loggerFactory == null)
+ {
+ throw new ArgumentNullException(nameof(loggerFactory));
+ }
+
+ _logger = loggerFactory.CreateLogger>();
+ _services = services ?? throw new ArgumentNullException(nameof(services));
+ }
+
+ ///
+ public virtual IReadOnlyCollection GetAllElements()
+ {
+ using (var scope = _services.CreateScope())
+ {
+ var context = scope.ServiceProvider.GetRequiredService();
+ return context.DataProtectionKeys.AsNoTracking().Select(key => TryParseKeyXml(key.Xml)).ToList().AsReadOnly();
+ }
+ }
+
+ ///
+ public void StoreElement(XElement element, string friendlyName)
+ {
+ using (var scope = _services.CreateScope())
+ {
+ var context = scope.ServiceProvider.GetRequiredService();
+ var newKey = new DataProtectionKey()
+ {
+ FriendlyName = friendlyName,
+ Xml = element.ToString(SaveOptions.DisableFormatting)
+ };
+
+ context.DataProtectionKeys.Add(newKey);
+ _logger.LogSavingKeyToDbContext(friendlyName, typeof(TContext).Name);
+ context.SaveChanges();
+ }
+ }
+
+ private XElement TryParseKeyXml(string xml)
+ {
+ try
+ {
+ return XElement.Parse(xml);
+ }
+ catch (Exception e)
+ {
+ _logger?.LogExceptionWhileParsingKeyXml(xml, e);
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/IDataProtectionKeyContext.cs b/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/IDataProtectionKeyContext.cs
new file mode 100644
index 0000000000..39998d2a79
--- /dev/null
+++ b/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/IDataProtectionKeyContext.cs
@@ -0,0 +1,18 @@
+// 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 Microsoft.EntityFrameworkCore;
+
+namespace Microsoft.AspNetCore.DataProtection.EntityFrameworkCore
+{
+ ///
+ /// Interface used to store instances of in a
+ ///
+ public interface IDataProtectionKeyContext
+ {
+ ///
+ /// A collection of
+ ///
+ DbSet DataProtectionKeys { get; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/LoggingExtensions.cs b/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/LoggingExtensions.cs
new file mode 100644
index 0000000000..d0aeb09271
--- /dev/null
+++ b/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/LoggingExtensions.cs
@@ -0,0 +1,31 @@
+// 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;
+
+namespace Microsoft.Extensions.Logging
+{
+ internal static class LoggingExtensions
+ {
+ private static readonly Action _anExceptionOccurredWhileParsingKeyXml;
+ private static readonly Action _savingKeyToDbContext;
+
+ static LoggingExtensions()
+ {
+ _anExceptionOccurredWhileParsingKeyXml = LoggerMessage.Define(
+ eventId: 1,
+ logLevel: LogLevel.Warning,
+ formatString: "An exception occurred while parsing the key xml '{Xml}'.");
+ _savingKeyToDbContext = LoggerMessage.Define(
+ eventId: 2,
+ logLevel: LogLevel.Debug,
+ formatString: "Saving key '{FriendlyName}' to '{DbContext}'.");
+ }
+
+ public static void LogExceptionWhileParsingKeyXml(this ILogger logger, string keyXml, Exception exception)
+ => _anExceptionOccurredWhileParsingKeyXml(logger, keyXml, exception);
+
+ public static void LogSavingKeyToDbContext(this ILogger logger, string friendlyName, string contextName)
+ => _savingKeyToDbContext(logger, friendlyName, contextName, null);
+ }
+}
diff --git a/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.csproj b/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.csproj
new file mode 100644
index 0000000000..e1715d94f2
--- /dev/null
+++ b/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.csproj
@@ -0,0 +1,23 @@
+
+
+
+ Support for storing keys using Entity Framework Core.
+ netstandard2.0
+ true
+ true
+ aspnetcore;dataprotection;entityframeworkcore
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/baseline.netcore.json b/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/baseline.netcore.json
new file mode 100644
index 0000000000..9a9a7ebc1c
--- /dev/null
+++ b/src/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/baseline.netcore.json
@@ -0,0 +1,203 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.DataProtection.EntityFrameworkCore, Version=2.2.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.EntityFrameworkCoreDataProtectionExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "PersistKeysToDbContext",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": [
+ {
+ "ParameterName": "TContext",
+ "ParameterPosition": 0,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.EntityFrameworkCore.DbContext",
+ "Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.IDataProtectionKeyContext"
+ ]
+ }
+ ]
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Id",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Id",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Int32"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_FriendlyName",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_FriendlyName",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Xml",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Xml",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.EntityFrameworkCoreXmlRepository",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GetAllElements",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IReadOnlyCollection",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "StoreElement",
+ "Parameters": [
+ {
+ "Name": "element",
+ "Type": "System.Xml.Linq.XElement"
+ },
+ {
+ "Name": "friendlyName",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "System.IServiceProvider"
+ },
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": [
+ {
+ "ParameterName": "TContext",
+ "ParameterPosition": 0,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.EntityFrameworkCore.DbContext",
+ "Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.IDataProtectionKeyContext"
+ ]
+ }
+ ]
+ },
+ {
+ "Name": "Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.IDataProtectionKeyContext",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_DataProtectionKeys",
+ "Parameters": [],
+ "ReturnType": "Microsoft.EntityFrameworkCore.DbSet",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/DataProtectionEntityFrameworkTests.cs b/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/DataProtectionEntityFrameworkTests.cs
new file mode 100644
index 0000000000..c298d8e64f
--- /dev/null
+++ b/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/DataProtectionEntityFrameworkTests.cs
@@ -0,0 +1,69 @@
+// 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.Linq;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
+using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection
+{
+ public class DataProtectionEntityFrameworkTests
+ {
+ [Fact]
+ public void CreateRepository_ThrowsIf_ContextIsNull()
+ {
+ Assert.Throws(() => new EntityFrameworkCoreXmlRepository(null, null));
+ }
+
+ [Fact]
+ public void StoreElement_PersistsData()
+ {
+ var element = XElement.Parse("");
+ var friendlyName = "Element1";
+ var key = new DataProtectionKey() { FriendlyName = friendlyName, Xml = element.ToString() };
+
+ var services = GetServices(nameof(StoreElement_PersistsData));
+ var service = new EntityFrameworkCoreXmlRepository(services, NullLoggerFactory.Instance);
+ service.StoreElement(element, friendlyName);
+
+ // Use a separate instance of the context to verify correct data was saved to database
+ using (var context = services.CreateScope().ServiceProvider.GetRequiredService< DataProtectionKeyContext>())
+ {
+ Assert.Equal(1, context.DataProtectionKeys.Count());
+ Assert.Equal(key.FriendlyName, context.DataProtectionKeys.Single()?.FriendlyName);
+ Assert.Equal(key.Xml, context.DataProtectionKeys.Single()?.Xml);
+ }
+ }
+
+ [Fact]
+ public void GetAllElements_ReturnsAllElements()
+ {
+ var element1 = XElement.Parse("");
+ var element2 = XElement.Parse("");
+
+ var services = GetServices(nameof(GetAllElements_ReturnsAllElements));
+ var service1 = CreateRepo(services);
+ service1.StoreElement(element1, "element1");
+ service1.StoreElement(element2, "element2");
+
+ // Use a separate instance of the context to verify correct data was saved to database
+ var service2 = CreateRepo(services);
+ var elements = service2.GetAllElements();
+ Assert.Equal(2, elements.Count);
+ }
+
+ private EntityFrameworkCoreXmlRepository CreateRepo(IServiceProvider services)
+ => new EntityFrameworkCoreXmlRepository(services, NullLoggerFactory.Instance);
+
+ private IServiceProvider GetServices(string dbName)
+ => new ServiceCollection()
+ .AddDbContext(o => o.UseInMemoryDatabase(dbName))
+ .BuildServiceProvider(validateScopes: true);
+ }
+}
diff --git a/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/DataProtectionKeyContext.cs b/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/DataProtectionKeyContext.cs
new file mode 100644
index 0000000000..96151de0bb
--- /dev/null
+++ b/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/DataProtectionKeyContext.cs
@@ -0,0 +1,14 @@
+// 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 Microsoft.EntityFrameworkCore;
+
+namespace Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test
+{
+ class DataProtectionKeyContext : DbContext, IDataProtectionKeyContext
+ {
+ public DataProtectionKeyContext(DbContextOptions options) : base(options) { }
+
+ public DbSet DataProtectionKeys { get; set; }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/EntityFrameworkCoreDataProtectionBuilderExtensionsTests.cs b/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/EntityFrameworkCoreDataProtectionBuilderExtensionsTests.cs
new file mode 100644
index 0000000000..55b67d98e3
--- /dev/null
+++ b/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/EntityFrameworkCoreDataProtectionBuilderExtensionsTests.cs
@@ -0,0 +1,26 @@
+// 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 Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test
+{
+ public class EntityFrameworkCoreDataProtectionBuilderExtensionsTests
+ {
+ [Fact]
+ public void PersistKeysToEntityFrameworkCore_UsesEntityFrameworkCoreXmlRepository()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection
+ .AddDbContext()
+ .AddDataProtection()
+ .PersistKeysToDbContext();
+ var serviceProvider = serviceCollection.BuildServiceProvider(validateScopes: true);
+ var keyManagementOptions = serviceProvider.GetRequiredService>();
+ Assert.IsType>(keyManagementOptions.Value.XmlRepository);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test.csproj b/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test.csproj
new file mode 100644
index 0000000000..ed07b79f25
--- /dev/null
+++ b/test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.Test.csproj
@@ -0,0 +1,15 @@
+
+
+
+ $(StandardTestTfms)
+
+
+
+
+
+
+
+
+
+
+