diff --git a/src/Microsoft.AspNetCore.DataProtection.Redis/Microsoft.AspNetCore.DataProtection.Redis.csproj b/src/Microsoft.AspNetCore.DataProtection.Redis/Microsoft.AspNetCore.DataProtection.Redis.csproj index e305facf8d..65b0d5c216 100644 --- a/src/Microsoft.AspNetCore.DataProtection.Redis/Microsoft.AspNetCore.DataProtection.Redis.csproj +++ b/src/Microsoft.AspNetCore.DataProtection.Redis/Microsoft.AspNetCore.DataProtection.Redis.csproj @@ -3,8 +3,8 @@ - Redis storrage support as key store. - 0.1.0 + Redis storage support as key store. + 0.3.0 net46;netstandard1.5 $(NoWarn);CS1591 true diff --git a/src/Microsoft.AspNetCore.DataProtection/DataProtectionServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.DataProtection/DataProtectionServiceCollectionExtensions.cs index 4cde160961..5a64d5e44f 100644 --- a/src/Microsoft.AspNetCore.DataProtection/DataProtectionServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.DataProtection/DataProtectionServiceCollectionExtensions.cs @@ -30,7 +30,7 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(services)); } - services.AddSingleton(); + services.TryAddSingleton(); services.AddOptions(); AddDataProtectionServices(services); diff --git a/src/Microsoft.AspNetCore.DataProtection/RC1ForwardingActivator.cs b/src/Microsoft.AspNetCore.DataProtection/RC1ForwardingActivator.cs deleted file mode 100644 index 9d76aaac49..0000000000 --- a/src/Microsoft.AspNetCore.DataProtection/RC1ForwardingActivator.cs +++ /dev/null @@ -1,42 +0,0 @@ -// 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.Extensions.Logging; - -namespace Microsoft.AspNetCore.DataProtection -{ - internal class RC1ForwardingActivator: SimpleActivator - { - private const string From = "Microsoft.AspNet.DataProtection"; - private const string To = "Microsoft.AspNetCore.DataProtection"; - private readonly ILogger _logger; - - public RC1ForwardingActivator(IServiceProvider services) : this(services, DataProtectionProviderFactory.GetDefaultLoggerFactory()) - { - } - - public RC1ForwardingActivator(IServiceProvider services, ILoggerFactory loggerFactory) : base(services) - { - _logger = loggerFactory.CreateLogger(typeof(RC1ForwardingActivator)); - } - - public override object CreateInstance(Type expectedBaseType, string implementationTypeName) - { - if (implementationTypeName.Contains(From)) - { - var forwardedImplementationTypeName = implementationTypeName.Replace(From, To); - var type = Type.GetType(forwardedImplementationTypeName, false); - if (type != null) - { - _logger.LogDebug("Forwarded activator type request from {FromType} to {ToType}", - implementationTypeName, - forwardedImplementationTypeName); - - implementationTypeName = forwardedImplementationTypeName; - } - } - return base.CreateInstance(expectedBaseType, implementationTypeName); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.DataProtection/TypeForwardingActivator.cs b/src/Microsoft.AspNetCore.DataProtection/TypeForwardingActivator.cs new file mode 100644 index 0000000000..3865adbf37 --- /dev/null +++ b/src/Microsoft.AspNetCore.DataProtection/TypeForwardingActivator.cs @@ -0,0 +1,73 @@ +// 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.Text.RegularExpressions; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.DataProtection +{ + internal class TypeForwardingActivator : SimpleActivator + { + private const string OldNamespace = "Microsoft.AspNet.DataProtection"; + private const string CurrentNamespace = "Microsoft.AspNetCore.DataProtection"; + private readonly ILogger _logger; + private static readonly Regex _versionPattern = new Regex(@",\s?Version=(\d+\.?)(\d+\.?)?(\d+\.?)?(\d+\.?)?", RegexOptions.Compiled, TimeSpan.FromSeconds(2)); + + public TypeForwardingActivator(IServiceProvider services) + : this(services, DataProtectionProviderFactory.GetDefaultLoggerFactory()) + { + } + + public TypeForwardingActivator(IServiceProvider services, ILoggerFactory loggerFactory) + : base(services) + { + _logger = loggerFactory.CreateLogger(typeof(TypeForwardingActivator)); + } + + public override object CreateInstance(Type expectedBaseType, string originalTypeName) + => CreateInstance(expectedBaseType, originalTypeName, out var _); + + // for testing + internal object CreateInstance(Type expectedBaseType, string originalTypeName, out bool forwarded) + { + var forwardedTypeName = originalTypeName; + var candidate = false; + if (originalTypeName.Contains(OldNamespace)) + { + candidate = true; + forwardedTypeName = originalTypeName.Replace(OldNamespace, CurrentNamespace); + } + +#if NET46 + if (candidate || forwardedTypeName.Contains(CurrentNamespace)) + { + candidate = true; + forwardedTypeName = RemoveVersionFromAssemblyName(forwardedTypeName); + } +#elif NETSTANDARD1_3 +#else +#error Target framework needs to be updated +#endif + + if (candidate) + { + var type = Type.GetType(forwardedTypeName, false); + if (type != null) + { + _logger.LogDebug("Forwarded activator type request from {FromType} to {ToType}", + originalTypeName, + forwardedTypeName); + forwarded = true; + return base.CreateInstance(expectedBaseType, forwardedTypeName); + } + } + + forwarded = false; + return base.CreateInstance(expectedBaseType, originalTypeName); + } + + protected string RemoveVersionFromAssemblyName(string forwardedTypeName) + => _versionPattern.Replace(forwardedTypeName, ""); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.DataProtection.Test/RC1ForwardingActivatorTests.cs b/test/Microsoft.AspNetCore.DataProtection.Test/RC1ForwardingActivatorTests.cs deleted file mode 100644 index d0f01533b7..0000000000 --- a/test/Microsoft.AspNetCore.DataProtection.Test/RC1ForwardingActivatorTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -// 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.Extensions.DependencyInjection; -using Xunit; - -namespace Microsoft.AspNetCore.DataProtection -{ - public class RC1ForwardingActivatorTests - { - [Fact] - public void CreateInstance_ForwardsToNewNamespaceIfExists() - { - // Arrange - var serviceCollection = new ServiceCollection(); - serviceCollection.AddDataProtection(); - var services = serviceCollection.BuildServiceProvider(); - var activator = services.GetActivator(); - - // Act - var name = "Microsoft.AspNet.DataProtection.RC1ForwardingActivatorTests+ClassWithParameterlessCtor, Microsoft.AspNet.DataProtection.Test"; - var instance = activator.CreateInstance(name); - - // Assert - Assert.IsType(instance); - } - - [Fact] - public void CreateInstance_DoesNotForwardIfClassDoesNotExist() - { - // Arrange - var serviceCollection = new ServiceCollection(); - serviceCollection.AddDataProtection(); - var services = serviceCollection.BuildServiceProvider(); - var activator = services.GetActivator(); - - // Act & Assert - var name = "Microsoft.AspNet.DataProtection.RC1ForwardingActivatorTests+NonExistentClassWithParameterlessCtor, Microsoft.AspNet.DataProtection.Test"; - var exception = Assert.ThrowsAny(()=> activator.CreateInstance(name)); - - Assert.Contains("Microsoft.AspNet.DataProtection.Test", exception.Message); - } - - private class ClassWithParameterlessCtor - { - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.DataProtection.Test/TypeForwardingActivatorTests.cs b/test/Microsoft.AspNetCore.DataProtection.Test/TypeForwardingActivatorTests.cs new file mode 100644 index 0000000000..1d8e02d80a --- /dev/null +++ b/test/Microsoft.AspNetCore.DataProtection.Test/TypeForwardingActivatorTests.cs @@ -0,0 +1,177 @@ +// 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.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.DataProtection +{ + public class TypeForwardingActivatorTests : MarshalByRefObject + { + [Fact] + public void CreateInstance_ForwardsToNewNamespaceIfExists() + { + // Arrange + var serviceCollection = new ServiceCollection(); + serviceCollection.AddDataProtection(); + var services = serviceCollection.BuildServiceProvider(); + var activator = services.GetActivator(); + + // Act + var name = "Microsoft.AspNet.DataProtection.TypeForwardingActivatorTests+ClassWithParameterlessCtor, Microsoft.AspNet.DataProtection.Test, Version=1.0.0.0"; + var instance = activator.CreateInstance(name); + + // Assert + Assert.IsType(instance); + } + + [Fact] + public void CreateInstance_DoesNotForwardIfClassDoesNotExist() + { + // Arrange + var serviceCollection = new ServiceCollection(); + serviceCollection.AddDataProtection(); + var services = serviceCollection.BuildServiceProvider(); + var activator = services.GetActivator(); + + // Act & Assert + var name = "Microsoft.AspNet.DataProtection.TypeForwardingActivatorTests+NonExistentClassWithParameterlessCtor, Microsoft.AspNet.DataProtection.Test"; + var exception = Assert.ThrowsAny(() => activator.CreateInstance(name)); + + Assert.Contains("Microsoft.AspNet.DataProtection.Test", exception.Message); + } + + [Theory] + [InlineData(typeof(GenericType>))] + [InlineData(typeof(GenericType))] + [InlineData(typeof(GenericType>))] + [InlineData(typeof(GenericType>))] + [InlineData(typeof(GenericType))] + [InlineData(typeof(GenericType))] + [InlineData(typeof(List))] + public void CreateInstance_Generics(Type type) + { + // Arrange + var activator = new TypeForwardingActivator(null); + var name = type.AssemblyQualifiedName; + + // Act & Assert + Assert.IsType(type, activator.CreateInstance(name)); + } + + [Theory] + [InlineData(typeof(GenericType<>))] + [InlineData(typeof(GenericType<,>))] + public void CreateInstance_ThrowsForOpenGenerics(Type type) + { + // Arrange + var activator = new TypeForwardingActivator(null); + var name = type.AssemblyQualifiedName; + + // Act & Assert + Assert.Throws(() => activator.CreateInstance(name)); + } + + [Theory] + [InlineData( + "System.Tuple`1[[Some.Type, Microsoft.AspNetCore.DataProtection, Version=1.0.0.0, Culture=neutral]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", + "System.Tuple`1[[Some.Type, Microsoft.AspNetCore.DataProtection, Culture=neutral]], mscorlib, Culture=neutral, PublicKeyToken=b77a5c561934e089")] + [InlineData( + "Some.Type`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Microsoft.AspNetCore.DataProtection, Version=1.0.0.0, Culture=neutral", + "Some.Type`1[[System.Int32, mscorlib, Culture=neutral, PublicKeyToken=b77a5c561934e089]], Microsoft.AspNetCore.DataProtection, Culture=neutral")] + [InlineData( + "System.Tuple`1[[System.Tuple`1[[Some.Type, Microsoft.AspNetCore.DataProtection, Version=1.0.0.0, Culture=neutral]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", + "System.Tuple`1[[System.Tuple`1[[Some.Type, Microsoft.AspNetCore.DataProtection, Culture=neutral]], mscorlib, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Culture=neutral, PublicKeyToken=b77a5c561934e089")] + public void ParsesFullyQualifiedTypeName(string typeName, string expected) + { + Assert.Equal(expected, new MockTypeForwardingActivator().Parse(typeName)); + } + + [Theory] + [InlineData(typeof(List))] + [InlineData(typeof(FactAttribute))] + public void CreateInstance_DoesNotForwardingTypesExternalTypes(Type type) + { + new TypeForwardingActivator(null).CreateInstance(typeof(object), type.AssemblyQualifiedName, out var forwarded); + Assert.False(forwarded, "Should not have forwarded types that are not in Microsoft.AspNetCore.DataProjection"); + } + + [Theory] + [MemberData(nameof(AssemblyVersions))] + public void CreateInstance_ForwardsAcrossVersionChanges(Version version) + { +#if NET46 + // run this test in an appdomain without testhost's custom assembly resolution hooks + var setupInfo = new AppDomainSetup + { + ApplicationBase = AppDomain.CurrentDomain.BaseDirectory + }; + var domain = AppDomain.CreateDomain("TestDomain", null, setupInfo); + var wrappedTestClass = (TypeForwardingActivatorTests)domain.CreateInstanceAndUnwrap(GetType().Assembly.FullName, typeof(TypeForwardingActivatorTests).FullName); + wrappedTestClass.CreateInstance_ForwardsAcrossVersionChangesImpl(version); +#elif NETCOREAPP2_0 + CreateInstance_ForwardsAcrossVersionChangesImpl(version); +#else +#error Target framework should be updated +#endif + } + + private void CreateInstance_ForwardsAcrossVersionChangesImpl(Version newVersion) + { + var activator = new TypeForwardingActivator(null); + + var typeInfo = typeof(ClassWithParameterlessCtor).GetTypeInfo(); + var typeName = typeInfo.FullName; + var assemblyName = typeInfo.Assembly.GetName(); + + assemblyName.Version = newVersion; + var newName = $"{typeName}, {assemblyName}"; + + Assert.NotEqual(typeInfo.AssemblyQualifiedName, newName); + Assert.IsType(activator.CreateInstance(typeof(object), newName, out var forwarded)); +#if NET46 + Assert.True(forwarded, "Should have forwarded this type to new version or namespace"); +#elif NETCOREAPP2_0 + Assert.False(forwarded, "Should not have forwarded this type to new version or namespace"); +#else +#error Target framework should be updated +#endif + } + + public static TheoryData AssemblyVersions + { + get + { + var current = typeof(ActivatorTests).Assembly.GetName().Version; + return new TheoryData + { + new Version(Math.Max(0, current.Major - 1), 0, 0, 0), + new Version(current.Major + 1, 0, 0, 0), + new Version(current.Major, current.Minor + 1, 0, 0), + new Version(current.Major, current.Minor, current.Revision + 1, 0), + }; + } + } + + private class MockTypeForwardingActivator : TypeForwardingActivator + { + public MockTypeForwardingActivator() : base(null) { } + public string Parse(string typeName) => RemoveVersionFromAssemblyName(typeName); + } + + private class ClassWithParameterlessCtor + { + } + + private class GenericType + { + } + + private class GenericType + { + } + } +} \ No newline at end of file