Ignore assembly version when activating DataProtection types from string name (#223)

This commit is contained in:
Nate McMaster 2017-04-25 14:29:26 -07:00 committed by GitHub
parent 5fe4807c1e
commit 4dad47eeab
6 changed files with 253 additions and 94 deletions

View File

@ -3,8 +3,8 @@
<Import Project="..\..\build\common.props" />
<PropertyGroup>
<Description>Redis storrage support as key store.</Description>
<VersionPrefix>0.1.0</VersionPrefix>
<Description>Redis storage support as key store.</Description>
<VersionPrefix>0.3.0</VersionPrefix>
<TargetFrameworks>net46;netstandard1.5</TargetFrameworks>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

View File

@ -30,7 +30,7 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentNullException(nameof(services));
}
services.AddSingleton<IActivator, RC1ForwardingActivator>();
services.TryAddSingleton<IActivator, TypeForwardingActivator>();
services.AddOptions();
AddDataProtectionServices(services);

View File

@ -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);
}
}
}

View File

@ -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, "");
}
}

View File

@ -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<object>(name);
// Assert
Assert.IsType<ClassWithParameterlessCtor>(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<Exception>(()=> activator.CreateInstance<object>(name));
Assert.Contains("Microsoft.AspNet.DataProtection.Test", exception.Message);
}
private class ClassWithParameterlessCtor
{
}
}
}

View File

@ -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<object>(name);
// Assert
Assert.IsType<ClassWithParameterlessCtor>(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<Exception>(() => activator.CreateInstance<object>(name));
Assert.Contains("Microsoft.AspNet.DataProtection.Test", exception.Message);
}
[Theory]
[InlineData(typeof(GenericType<GenericType<ClassWithParameterlessCtor>>))]
[InlineData(typeof(GenericType<ClassWithParameterlessCtor>))]
[InlineData(typeof(GenericType<GenericType<string>>))]
[InlineData(typeof(GenericType<GenericType<string, string>>))]
[InlineData(typeof(GenericType<string>))]
[InlineData(typeof(GenericType<int>))]
[InlineData(typeof(List<ClassWithParameterlessCtor>))]
public void CreateInstance_Generics(Type type)
{
// Arrange
var activator = new TypeForwardingActivator(null);
var name = type.AssemblyQualifiedName;
// Act & Assert
Assert.IsType(type, activator.CreateInstance<object>(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<ArgumentException>(() => activator.CreateInstance<object>(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<string>))]
[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<ClassWithParameterlessCtor>(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<Version> AssemblyVersions
{
get
{
var current = typeof(ActivatorTests).Assembly.GetName().Version;
return new TheoryData<Version>
{
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<T>
{
}
private class GenericType<T1, T2>
{
}
}
}