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) + + + + + + + + + + +