Reorganize source code
In preparation for merging many other projects into this repo, this establishes a new source code organization which groups projects together based on subject matter.
\n\nCommit migrated from 7ce647cfa3
This commit is contained in:
commit
af034a9c5e
|
|
@ -0,0 +1,108 @@
|
||||||
|
// 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.Diagnostics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.ObjectPool
|
||||||
|
{
|
||||||
|
public class DefaultObjectPool<T> : ObjectPool<T> where T : class
|
||||||
|
{
|
||||||
|
private readonly ObjectWrapper[] _items;
|
||||||
|
private readonly IPooledObjectPolicy<T> _policy;
|
||||||
|
private readonly bool _isDefaultPolicy;
|
||||||
|
private T _firstItem;
|
||||||
|
|
||||||
|
// This class was introduced in 2.1 to avoid the interface call where possible
|
||||||
|
private readonly PooledObjectPolicy<T> _fastPolicy;
|
||||||
|
|
||||||
|
public DefaultObjectPool(IPooledObjectPolicy<T> policy)
|
||||||
|
: this(policy, Environment.ProcessorCount * 2)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DefaultObjectPool(IPooledObjectPolicy<T> policy, int maximumRetained)
|
||||||
|
{
|
||||||
|
_policy = policy ?? throw new ArgumentNullException(nameof(policy));
|
||||||
|
_fastPolicy = policy as PooledObjectPolicy<T>;
|
||||||
|
_isDefaultPolicy = IsDefaultPolicy();
|
||||||
|
|
||||||
|
// -1 due to _firstItem
|
||||||
|
_items = new ObjectWrapper[maximumRetained - 1];
|
||||||
|
|
||||||
|
bool IsDefaultPolicy()
|
||||||
|
{
|
||||||
|
var type = policy.GetType();
|
||||||
|
|
||||||
|
return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(DefaultPooledObjectPolicy<>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override T Get()
|
||||||
|
{
|
||||||
|
T item = _firstItem;
|
||||||
|
|
||||||
|
if (item == null || Interlocked.CompareExchange(ref _firstItem, null, item) != item)
|
||||||
|
{
|
||||||
|
item = GetViaScan();
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private T GetViaScan()
|
||||||
|
{
|
||||||
|
ObjectWrapper[] items = _items;
|
||||||
|
|
||||||
|
for (var i = 0; i < items.Length; i++)
|
||||||
|
{
|
||||||
|
T item = items[i];
|
||||||
|
|
||||||
|
if (item != null && Interlocked.CompareExchange(ref items[i].Element, null, item) == item)
|
||||||
|
{
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Create();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-inline to improve its code quality as uncommon path
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
private T Create() => _fastPolicy?.Create() ?? _policy.Create();
|
||||||
|
|
||||||
|
public override void Return(T obj)
|
||||||
|
{
|
||||||
|
if (_isDefaultPolicy || (_fastPolicy?.Return(obj) ?? _policy.Return(obj)))
|
||||||
|
{
|
||||||
|
if (_firstItem != null || Interlocked.CompareExchange(ref _firstItem, obj, null) != null)
|
||||||
|
{
|
||||||
|
ReturnViaScan(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private void ReturnViaScan(T obj)
|
||||||
|
{
|
||||||
|
ObjectWrapper[] items = _items;
|
||||||
|
|
||||||
|
for (var i = 0; i < items.Length && Interlocked.CompareExchange(ref items[i].Element, obj, null) != null; ++i)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebuggerDisplay("{Element}")]
|
||||||
|
private struct ObjectWrapper
|
||||||
|
{
|
||||||
|
public T Element;
|
||||||
|
|
||||||
|
public ObjectWrapper(T item) => Element = item;
|
||||||
|
|
||||||
|
public static implicit operator T(ObjectWrapper wrapper) => wrapper.Element;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
// 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.ObjectPool
|
||||||
|
{
|
||||||
|
public class DefaultObjectPoolProvider : ObjectPoolProvider
|
||||||
|
{
|
||||||
|
public int MaximumRetained { get; set; } = Environment.ProcessorCount * 2;
|
||||||
|
|
||||||
|
public override ObjectPool<T> Create<T>(IPooledObjectPolicy<T> policy)
|
||||||
|
{
|
||||||
|
return new DefaultObjectPool<T>(policy, MaximumRetained);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
// 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.Extensions.ObjectPool
|
||||||
|
{
|
||||||
|
public class DefaultPooledObjectPolicy<T> : PooledObjectPolicy<T> where T : class, new()
|
||||||
|
{
|
||||||
|
public override T Create()
|
||||||
|
{
|
||||||
|
return new T();
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultObjectPool<T> doesn't call 'Return' for the default policy.
|
||||||
|
// So take care adding any logic to this method, as it might require changes elsewhere.
|
||||||
|
public override bool Return(T obj)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// 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.Extensions.ObjectPool
|
||||||
|
{
|
||||||
|
public interface IPooledObjectPolicy<T>
|
||||||
|
{
|
||||||
|
T Create();
|
||||||
|
|
||||||
|
bool Return(T obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.Diagnostics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.ObjectPool
|
||||||
|
{
|
||||||
|
public class LeakTrackingObjectPool<T> : ObjectPool<T> where T : class
|
||||||
|
{
|
||||||
|
private readonly ConditionalWeakTable<T, Tracker> _trackers = new ConditionalWeakTable<T, Tracker>();
|
||||||
|
private readonly ObjectPool<T> _inner;
|
||||||
|
|
||||||
|
public LeakTrackingObjectPool(ObjectPool<T> inner)
|
||||||
|
{
|
||||||
|
if (inner == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(inner));
|
||||||
|
}
|
||||||
|
|
||||||
|
_inner = inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override T Get()
|
||||||
|
{
|
||||||
|
var value = _inner.Get();
|
||||||
|
_trackers.Add(value, new Tracker());
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Return(T obj)
|
||||||
|
{
|
||||||
|
Tracker tracker;
|
||||||
|
if (_trackers.TryGetValue(obj, out tracker))
|
||||||
|
{
|
||||||
|
_trackers.Remove(obj);
|
||||||
|
tracker.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_inner.Return(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Tracker : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _stack;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public Tracker()
|
||||||
|
{
|
||||||
|
_stack = Environment.StackTrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_disposed = true;
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
~Tracker()
|
||||||
|
{
|
||||||
|
if (!_disposed && !Environment.HasShutdownStarted)
|
||||||
|
{
|
||||||
|
Debug.Fail($"{typeof(T).Name} was leaked. Created at: {Environment.NewLine}{_stack}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
// 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.ObjectPool
|
||||||
|
{
|
||||||
|
public class LeakTrackingObjectPoolProvider : ObjectPoolProvider
|
||||||
|
{
|
||||||
|
private readonly ObjectPoolProvider _inner;
|
||||||
|
|
||||||
|
public LeakTrackingObjectPoolProvider(ObjectPoolProvider inner)
|
||||||
|
{
|
||||||
|
if (inner == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(inner));
|
||||||
|
}
|
||||||
|
|
||||||
|
_inner = inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ObjectPool<T> Create<T>(IPooledObjectPolicy<T> policy)
|
||||||
|
{
|
||||||
|
var inner = _inner.Create<T>(policy);
|
||||||
|
return new LeakTrackingObjectPool<T>(inner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<Description>A simple object pool implementation.</Description>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<PackageTags>pooling</PackageTags>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// 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.Extensions.ObjectPool
|
||||||
|
{
|
||||||
|
public abstract class ObjectPool<T> where T : class
|
||||||
|
{
|
||||||
|
public abstract T Get();
|
||||||
|
|
||||||
|
public abstract void Return(T obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
// 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.Extensions.ObjectPool
|
||||||
|
{
|
||||||
|
public abstract class ObjectPoolProvider
|
||||||
|
{
|
||||||
|
public ObjectPool<T> Create<T>() where T : class, new()
|
||||||
|
{
|
||||||
|
return Create<T>(new DefaultPooledObjectPolicy<T>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract ObjectPool<T> Create<T>(IPooledObjectPolicy<T> policy) where T : class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
// 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.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.ObjectPool
|
||||||
|
{
|
||||||
|
public static class ObjectPoolProviderExtensions
|
||||||
|
{
|
||||||
|
public static ObjectPool<StringBuilder> CreateStringBuilderPool(this ObjectPoolProvider provider)
|
||||||
|
{
|
||||||
|
return provider.Create<StringBuilder>(new StringBuilderPooledObjectPolicy());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ObjectPool<StringBuilder> CreateStringBuilderPool(
|
||||||
|
this ObjectPoolProvider provider,
|
||||||
|
int initialCapacity,
|
||||||
|
int maximumRetainedCapacity)
|
||||||
|
{
|
||||||
|
var policy = new StringBuilderPooledObjectPolicy()
|
||||||
|
{
|
||||||
|
InitialCapacity = initialCapacity,
|
||||||
|
MaximumRetainedCapacity = maximumRetainedCapacity,
|
||||||
|
};
|
||||||
|
|
||||||
|
return provider.Create<StringBuilder>(policy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// 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.Extensions.ObjectPool
|
||||||
|
{
|
||||||
|
public abstract class PooledObjectPolicy<T> : IPooledObjectPolicy<T>
|
||||||
|
{
|
||||||
|
public abstract T Create();
|
||||||
|
|
||||||
|
public abstract bool Return(T obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.ObjectPool
|
||||||
|
{
|
||||||
|
public class StringBuilderPooledObjectPolicy : PooledObjectPolicy<StringBuilder>
|
||||||
|
{
|
||||||
|
public int InitialCapacity { get; set; } = 100;
|
||||||
|
|
||||||
|
public int MaximumRetainedCapacity { get; set; } = 4 * 1024;
|
||||||
|
|
||||||
|
public override StringBuilder Create()
|
||||||
|
{
|
||||||
|
return new StringBuilder(InitialCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Return(StringBuilder obj)
|
||||||
|
{
|
||||||
|
if (obj.Capacity > MaximumRetainedCapacity)
|
||||||
|
{
|
||||||
|
// Too big. Discard this one.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.Clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,612 @@
|
||||||
|
{
|
||||||
|
"AssemblyIdentity": "Microsoft.Extensions.ObjectPool, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
|
||||||
|
"Types": [
|
||||||
|
{
|
||||||
|
"Name": "Microsoft.Extensions.ObjectPool.DefaultObjectPool<T0>",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"Kind": "Class",
|
||||||
|
"BaseType": "Microsoft.Extensions.ObjectPool.ObjectPool<T0>",
|
||||||
|
"ImplementedInterfaces": [],
|
||||||
|
"Members": [
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "Get",
|
||||||
|
"Parameters": [],
|
||||||
|
"ReturnType": "T0",
|
||||||
|
"Virtual": true,
|
||||||
|
"Override": true,
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "Return",
|
||||||
|
"Parameters": [
|
||||||
|
{
|
||||||
|
"Name": "obj",
|
||||||
|
"Type": "T0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ReturnType": "System.Void",
|
||||||
|
"Virtual": true,
|
||||||
|
"Override": true,
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Constructor",
|
||||||
|
"Name": ".ctor",
|
||||||
|
"Parameters": [
|
||||||
|
{
|
||||||
|
"Name": "policy",
|
||||||
|
"Type": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy<T0>"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Constructor",
|
||||||
|
"Name": ".ctor",
|
||||||
|
"Parameters": [
|
||||||
|
{
|
||||||
|
"Name": "policy",
|
||||||
|
"Type": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy<T0>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "maximumRetained",
|
||||||
|
"Type": "System.Int32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"GenericParameters": [
|
||||||
|
{
|
||||||
|
"ParameterName": "T",
|
||||||
|
"ParameterPosition": 0,
|
||||||
|
"Class": true,
|
||||||
|
"BaseTypeOrInterfaces": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Microsoft.Extensions.ObjectPool.DefaultObjectPoolProvider",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"Kind": "Class",
|
||||||
|
"BaseType": "Microsoft.Extensions.ObjectPool.ObjectPoolProvider",
|
||||||
|
"ImplementedInterfaces": [],
|
||||||
|
"Members": [
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "get_MaximumRetained",
|
||||||
|
"Parameters": [],
|
||||||
|
"ReturnType": "System.Int32",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "set_MaximumRetained",
|
||||||
|
"Parameters": [
|
||||||
|
{
|
||||||
|
"Name": "value",
|
||||||
|
"Type": "System.Int32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ReturnType": "System.Void",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "Create<T0>",
|
||||||
|
"Parameters": [
|
||||||
|
{
|
||||||
|
"Name": "policy",
|
||||||
|
"Type": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy<T0>"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ReturnType": "Microsoft.Extensions.ObjectPool.ObjectPool<T0>",
|
||||||
|
"Virtual": true,
|
||||||
|
"Override": true,
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": [
|
||||||
|
{
|
||||||
|
"ParameterName": "T",
|
||||||
|
"ParameterPosition": 0,
|
||||||
|
"Class": true,
|
||||||
|
"BaseTypeOrInterfaces": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Constructor",
|
||||||
|
"Name": ".ctor",
|
||||||
|
"Parameters": [],
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"GenericParameters": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Microsoft.Extensions.ObjectPool.DefaultPooledObjectPolicy<T0>",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"Kind": "Class",
|
||||||
|
"BaseType": "Microsoft.Extensions.ObjectPool.PooledObjectPolicy<T0>",
|
||||||
|
"ImplementedInterfaces": [],
|
||||||
|
"Members": [
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "Create",
|
||||||
|
"Parameters": [],
|
||||||
|
"ReturnType": "T0",
|
||||||
|
"Virtual": true,
|
||||||
|
"Override": true,
|
||||||
|
"ImplementedInterface": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy<T0>",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "Return",
|
||||||
|
"Parameters": [
|
||||||
|
{
|
||||||
|
"Name": "obj",
|
||||||
|
"Type": "T0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ReturnType": "System.Boolean",
|
||||||
|
"Virtual": true,
|
||||||
|
"Override": true,
|
||||||
|
"ImplementedInterface": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy<T0>",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Constructor",
|
||||||
|
"Name": ".ctor",
|
||||||
|
"Parameters": [],
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"GenericParameters": [
|
||||||
|
{
|
||||||
|
"ParameterName": "T",
|
||||||
|
"ParameterPosition": 0,
|
||||||
|
"New": true,
|
||||||
|
"Class": true,
|
||||||
|
"BaseTypeOrInterfaces": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy<T0>",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"Kind": "Interface",
|
||||||
|
"Abstract": true,
|
||||||
|
"ImplementedInterfaces": [],
|
||||||
|
"Members": [
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "Create",
|
||||||
|
"Parameters": [],
|
||||||
|
"ReturnType": "T0",
|
||||||
|
"GenericParameter": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "Return",
|
||||||
|
"Parameters": [
|
||||||
|
{
|
||||||
|
"Name": "obj",
|
||||||
|
"Type": "T0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ReturnType": "System.Boolean",
|
||||||
|
"GenericParameter": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"GenericParameters": [
|
||||||
|
{
|
||||||
|
"ParameterName": "T",
|
||||||
|
"ParameterPosition": 0,
|
||||||
|
"BaseTypeOrInterfaces": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Microsoft.Extensions.ObjectPool.LeakTrackingObjectPool<T0>",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"Kind": "Class",
|
||||||
|
"BaseType": "Microsoft.Extensions.ObjectPool.ObjectPool<T0>",
|
||||||
|
"ImplementedInterfaces": [],
|
||||||
|
"Members": [
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "Get",
|
||||||
|
"Parameters": [],
|
||||||
|
"ReturnType": "T0",
|
||||||
|
"Virtual": true,
|
||||||
|
"Override": true,
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "Return",
|
||||||
|
"Parameters": [
|
||||||
|
{
|
||||||
|
"Name": "obj",
|
||||||
|
"Type": "T0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ReturnType": "System.Void",
|
||||||
|
"Virtual": true,
|
||||||
|
"Override": true,
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Constructor",
|
||||||
|
"Name": ".ctor",
|
||||||
|
"Parameters": [
|
||||||
|
{
|
||||||
|
"Name": "inner",
|
||||||
|
"Type": "Microsoft.Extensions.ObjectPool.ObjectPool<T0>"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"GenericParameters": [
|
||||||
|
{
|
||||||
|
"ParameterName": "T",
|
||||||
|
"ParameterPosition": 0,
|
||||||
|
"Class": true,
|
||||||
|
"BaseTypeOrInterfaces": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Microsoft.Extensions.ObjectPool.LeakTrackingObjectPoolProvider",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"Kind": "Class",
|
||||||
|
"BaseType": "Microsoft.Extensions.ObjectPool.ObjectPoolProvider",
|
||||||
|
"ImplementedInterfaces": [],
|
||||||
|
"Members": [
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "Create<T0>",
|
||||||
|
"Parameters": [
|
||||||
|
{
|
||||||
|
"Name": "policy",
|
||||||
|
"Type": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy<T0>"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ReturnType": "Microsoft.Extensions.ObjectPool.ObjectPool<T0>",
|
||||||
|
"Virtual": true,
|
||||||
|
"Override": true,
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": [
|
||||||
|
{
|
||||||
|
"ParameterName": "T",
|
||||||
|
"ParameterPosition": 0,
|
||||||
|
"Class": true,
|
||||||
|
"BaseTypeOrInterfaces": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Constructor",
|
||||||
|
"Name": ".ctor",
|
||||||
|
"Parameters": [
|
||||||
|
{
|
||||||
|
"Name": "inner",
|
||||||
|
"Type": "Microsoft.Extensions.ObjectPool.ObjectPoolProvider"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"GenericParameters": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Microsoft.Extensions.ObjectPool.ObjectPool<T0>",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"Kind": "Class",
|
||||||
|
"Abstract": true,
|
||||||
|
"ImplementedInterfaces": [],
|
||||||
|
"Members": [
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "Get",
|
||||||
|
"Parameters": [],
|
||||||
|
"ReturnType": "T0",
|
||||||
|
"Virtual": true,
|
||||||
|
"Abstract": true,
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "Return",
|
||||||
|
"Parameters": [
|
||||||
|
{
|
||||||
|
"Name": "obj",
|
||||||
|
"Type": "T0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ReturnType": "System.Void",
|
||||||
|
"Virtual": true,
|
||||||
|
"Abstract": true,
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Constructor",
|
||||||
|
"Name": ".ctor",
|
||||||
|
"Parameters": [],
|
||||||
|
"Visibility": "Protected",
|
||||||
|
"GenericParameter": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"GenericParameters": [
|
||||||
|
{
|
||||||
|
"ParameterName": "T",
|
||||||
|
"ParameterPosition": 0,
|
||||||
|
"Class": true,
|
||||||
|
"BaseTypeOrInterfaces": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Microsoft.Extensions.ObjectPool.ObjectPoolProvider",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"Kind": "Class",
|
||||||
|
"Abstract": true,
|
||||||
|
"ImplementedInterfaces": [],
|
||||||
|
"Members": [
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "Create<T0>",
|
||||||
|
"Parameters": [
|
||||||
|
{
|
||||||
|
"Name": "policy",
|
||||||
|
"Type": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy<T0>"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ReturnType": "Microsoft.Extensions.ObjectPool.ObjectPool<T0>",
|
||||||
|
"Virtual": true,
|
||||||
|
"Abstract": true,
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": [
|
||||||
|
{
|
||||||
|
"ParameterName": "T",
|
||||||
|
"ParameterPosition": 0,
|
||||||
|
"Class": true,
|
||||||
|
"BaseTypeOrInterfaces": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "Create<T0>",
|
||||||
|
"Parameters": [],
|
||||||
|
"ReturnType": "Microsoft.Extensions.ObjectPool.ObjectPool<T0>",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": [
|
||||||
|
{
|
||||||
|
"ParameterName": "T",
|
||||||
|
"ParameterPosition": 0,
|
||||||
|
"New": true,
|
||||||
|
"Class": true,
|
||||||
|
"BaseTypeOrInterfaces": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Constructor",
|
||||||
|
"Name": ".ctor",
|
||||||
|
"Parameters": [],
|
||||||
|
"Visibility": "Protected",
|
||||||
|
"GenericParameter": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"GenericParameters": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Microsoft.Extensions.ObjectPool.ObjectPoolProviderExtensions",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"Kind": "Class",
|
||||||
|
"Abstract": true,
|
||||||
|
"Static": true,
|
||||||
|
"Sealed": true,
|
||||||
|
"ImplementedInterfaces": [],
|
||||||
|
"Members": [
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "CreateStringBuilderPool",
|
||||||
|
"Parameters": [
|
||||||
|
{
|
||||||
|
"Name": "provider",
|
||||||
|
"Type": "Microsoft.Extensions.ObjectPool.ObjectPoolProvider"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ReturnType": "Microsoft.Extensions.ObjectPool.ObjectPool<System.Text.StringBuilder>",
|
||||||
|
"Static": true,
|
||||||
|
"Extension": true,
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "CreateStringBuilderPool",
|
||||||
|
"Parameters": [
|
||||||
|
{
|
||||||
|
"Name": "provider",
|
||||||
|
"Type": "Microsoft.Extensions.ObjectPool.ObjectPoolProvider"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "initialCapacity",
|
||||||
|
"Type": "System.Int32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "maximumRetainedCapacity",
|
||||||
|
"Type": "System.Int32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ReturnType": "Microsoft.Extensions.ObjectPool.ObjectPool<System.Text.StringBuilder>",
|
||||||
|
"Static": true,
|
||||||
|
"Extension": true,
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"GenericParameters": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Microsoft.Extensions.ObjectPool.PooledObjectPolicy<T0>",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"Kind": "Class",
|
||||||
|
"Abstract": true,
|
||||||
|
"ImplementedInterfaces": [
|
||||||
|
"Microsoft.Extensions.ObjectPool.IPooledObjectPolicy<T0>"
|
||||||
|
],
|
||||||
|
"Members": [
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "Create",
|
||||||
|
"Parameters": [],
|
||||||
|
"ReturnType": "T0",
|
||||||
|
"Virtual": true,
|
||||||
|
"Abstract": true,
|
||||||
|
"ImplementedInterface": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy<T0>",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "Return",
|
||||||
|
"Parameters": [
|
||||||
|
{
|
||||||
|
"Name": "obj",
|
||||||
|
"Type": "T0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ReturnType": "System.Boolean",
|
||||||
|
"Virtual": true,
|
||||||
|
"Abstract": true,
|
||||||
|
"ImplementedInterface": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy<T0>",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Constructor",
|
||||||
|
"Name": ".ctor",
|
||||||
|
"Parameters": [],
|
||||||
|
"Visibility": "Protected",
|
||||||
|
"GenericParameter": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"GenericParameters": [
|
||||||
|
{
|
||||||
|
"ParameterName": "T",
|
||||||
|
"ParameterPosition": 0,
|
||||||
|
"BaseTypeOrInterfaces": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Microsoft.Extensions.ObjectPool.StringBuilderPooledObjectPolicy",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"Kind": "Class",
|
||||||
|
"BaseType": "Microsoft.Extensions.ObjectPool.PooledObjectPolicy<System.Text.StringBuilder>",
|
||||||
|
"ImplementedInterfaces": [],
|
||||||
|
"Members": [
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "Create",
|
||||||
|
"Parameters": [],
|
||||||
|
"ReturnType": "System.Text.StringBuilder",
|
||||||
|
"Virtual": true,
|
||||||
|
"Override": true,
|
||||||
|
"ImplementedInterface": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy<System.Text.StringBuilder>",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "Return",
|
||||||
|
"Parameters": [
|
||||||
|
{
|
||||||
|
"Name": "obj",
|
||||||
|
"Type": "System.Text.StringBuilder"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ReturnType": "System.Boolean",
|
||||||
|
"Virtual": true,
|
||||||
|
"Override": true,
|
||||||
|
"ImplementedInterface": "Microsoft.Extensions.ObjectPool.IPooledObjectPolicy<System.Text.StringBuilder>",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "get_InitialCapacity",
|
||||||
|
"Parameters": [],
|
||||||
|
"ReturnType": "System.Int32",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "set_InitialCapacity",
|
||||||
|
"Parameters": [
|
||||||
|
{
|
||||||
|
"Name": "value",
|
||||||
|
"Type": "System.Int32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ReturnType": "System.Void",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "get_MaximumRetainedCapacity",
|
||||||
|
"Parameters": [],
|
||||||
|
"ReturnType": "System.Int32",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "set_MaximumRetainedCapacity",
|
||||||
|
"Parameters": [
|
||||||
|
{
|
||||||
|
"Name": "value",
|
||||||
|
"Type": "System.Int32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ReturnType": "System.Void",
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Kind": "Constructor",
|
||||||
|
"Name": ".ctor",
|
||||||
|
"Parameters": [],
|
||||||
|
"Visibility": "Public",
|
||||||
|
"GenericParameter": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"GenericParameters": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
// 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.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.ObjectPool.Test
|
||||||
|
{
|
||||||
|
public class DefaultObjectPoolTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void DefaultObjectPoolWithDefaultPolicy_GetAnd_ReturnObject_SameInstance()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var pool = new DefaultObjectPool<object>(new DefaultPooledObjectPolicy<object>());
|
||||||
|
|
||||||
|
var obj1 = pool.Get();
|
||||||
|
pool.Return(obj1);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var obj2 = pool.Get();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Same(obj1, obj2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DefaultObjectPool_GetAndReturnObject_SameInstance()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var pool = new DefaultObjectPool<List<int>>(new ListPolicy());
|
||||||
|
|
||||||
|
var list1 = pool.Get();
|
||||||
|
pool.Return(list1);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var list2 = pool.Get();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Same(list1, list2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DefaultObjectPool_CreatedByPolicy()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var pool = new DefaultObjectPool<List<int>>(new ListPolicy());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var list = pool.Get();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(17, list.Capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DefaultObjectPool_Return_RejectedByPolicy()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var pool = new DefaultObjectPool<List<int>>(new ListPolicy());
|
||||||
|
var list1 = pool.Get();
|
||||||
|
list1.Capacity = 20;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
pool.Return(list1);
|
||||||
|
var list2 = pool.Get();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotSame(list1, list2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ListPolicy : IPooledObjectPolicy<List<int>>
|
||||||
|
{
|
||||||
|
public List<int> Create()
|
||||||
|
{
|
||||||
|
return new List<int>(17);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Return(List<int> obj)
|
||||||
|
{
|
||||||
|
return obj.Capacity == 17;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\src\Microsoft.Extensions.ObjectPool.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -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 System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using BenchmarkDotNet.Running;
|
||||||
|
using BenchmarkDotNet.Configs;
|
||||||
|
using BenchmarkDotNet.Jobs;
|
||||||
|
using BenchmarkDotNet.Toolchains.InProcess;
|
||||||
|
|
||||||
|
namespace BenchmarkDotNet.Attributes
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly)]
|
||||||
|
internal class AspNetCoreBenchmarkAttribute : Attribute, IConfigSource
|
||||||
|
{
|
||||||
|
public static bool UseValidationConfig { get; set; }
|
||||||
|
|
||||||
|
public Type ConfigType { get; }
|
||||||
|
public Type ValidationConfigType { get; }
|
||||||
|
|
||||||
|
public AspNetCoreBenchmarkAttribute() : this(typeof(DefaultCoreConfig))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public AspNetCoreBenchmarkAttribute(Type configType) : this(configType, typeof(DefaultCoreValidationConfig))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public AspNetCoreBenchmarkAttribute(Type configType, Type validationConfigType)
|
||||||
|
{
|
||||||
|
ConfigType = configType;
|
||||||
|
ValidationConfigType = validationConfigType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IConfig Config => (IConfig) Activator.CreateInstance(UseValidationConfig ? ValidationConfigType : ConfigType, Array.Empty<object>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
// 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 BenchmarkDotNet.Columns;
|
||||||
|
using BenchmarkDotNet.Configs;
|
||||||
|
using BenchmarkDotNet.Diagnosers;
|
||||||
|
using BenchmarkDotNet.Engines;
|
||||||
|
using BenchmarkDotNet.Exporters;
|
||||||
|
using BenchmarkDotNet.Jobs;
|
||||||
|
using BenchmarkDotNet.Loggers;
|
||||||
|
using BenchmarkDotNet.Toolchains.CsProj;
|
||||||
|
using BenchmarkDotNet.Toolchains.DotNetCli;
|
||||||
|
using BenchmarkDotNet.Validators;
|
||||||
|
|
||||||
|
namespace BenchmarkDotNet.Attributes
|
||||||
|
{
|
||||||
|
internal class DefaultCoreConfig : ManualConfig
|
||||||
|
{
|
||||||
|
public DefaultCoreConfig()
|
||||||
|
{
|
||||||
|
Add(ConsoleLogger.Default);
|
||||||
|
Add(MarkdownExporter.GitHub);
|
||||||
|
|
||||||
|
Add(MemoryDiagnoser.Default);
|
||||||
|
Add(StatisticColumn.OperationsPerSecond);
|
||||||
|
Add(DefaultColumnProviders.Instance);
|
||||||
|
|
||||||
|
Add(JitOptimizationsValidator.FailOnError);
|
||||||
|
|
||||||
|
Add(Job.Core
|
||||||
|
.With(CsProjCoreToolchain.From(NetCoreAppSettings.NetCoreApp21))
|
||||||
|
.With(new GcMode { Server = true })
|
||||||
|
.With(RunStrategy.Throughput));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
// 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.Reflection;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using BenchmarkDotNet.Running;
|
||||||
|
using BenchmarkDotNet.Configs;
|
||||||
|
using BenchmarkDotNet.Jobs;
|
||||||
|
using BenchmarkDotNet.Loggers;
|
||||||
|
using BenchmarkDotNet.Toolchains.InProcess;
|
||||||
|
|
||||||
|
namespace BenchmarkDotNet.Attributes
|
||||||
|
{
|
||||||
|
internal class DefaultCoreValidationConfig : ManualConfig
|
||||||
|
{
|
||||||
|
public DefaultCoreValidationConfig()
|
||||||
|
{
|
||||||
|
Add(ConsoleLogger.Default);
|
||||||
|
|
||||||
|
Add(Job.Dry.With(InProcessToolchain.Instance));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<Project>
|
||||||
|
|
||||||
|
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<PackageId>Microsoft.Extensions.$(ProjectDirName).Sources</PackageId>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
// 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 BenchmarkDotNet.Attributes
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly)]
|
||||||
|
internal class ParameterizedJobConfigAttribute: AspNetCoreBenchmarkAttribute
|
||||||
|
{
|
||||||
|
public ParameterizedJobConfigAttribute(Type configType) : base(configType)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
// 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.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using BenchmarkDotNet.Running;
|
||||||
|
using BenchmarkDotNet.Configs;
|
||||||
|
using BenchmarkDotNet.Jobs;
|
||||||
|
using BenchmarkDotNet.Toolchains.InProcess;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.BenchmarkDotNet.Runner
|
||||||
|
{
|
||||||
|
partial class Program
|
||||||
|
{
|
||||||
|
private static TextWriter _standardOutput;
|
||||||
|
private static StringBuilder _standardOutputText;
|
||||||
|
|
||||||
|
static partial void BeforeMain(string[] args);
|
||||||
|
|
||||||
|
private static int Main(string[] args)
|
||||||
|
{
|
||||||
|
BeforeMain(args);
|
||||||
|
|
||||||
|
CheckValidate(ref args);
|
||||||
|
var summaries = BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly)
|
||||||
|
.Run(args, ManualConfig.CreateEmpty());
|
||||||
|
|
||||||
|
foreach (var summary in summaries)
|
||||||
|
{
|
||||||
|
if (summary.HasCriticalValidationErrors)
|
||||||
|
{
|
||||||
|
return Fail(summary, nameof(summary.HasCriticalValidationErrors));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var report in summary.Reports)
|
||||||
|
{
|
||||||
|
if (!report.BuildResult.IsGenerateSuccess)
|
||||||
|
{
|
||||||
|
return Fail(report, nameof(report.BuildResult.IsGenerateSuccess));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!report.BuildResult.IsBuildSuccess)
|
||||||
|
{
|
||||||
|
return Fail(report, nameof(report.BuildResult.IsBuildSuccess));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!report.AllMeasurements.Any())
|
||||||
|
{
|
||||||
|
return Fail(report, nameof(report.AllMeasurements));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int Fail(object o, string message)
|
||||||
|
{
|
||||||
|
_standardOutput?.WriteLine(_standardOutputText.ToString());
|
||||||
|
|
||||||
|
Console.Error.WriteLine("'{0}' failed, reason: '{1}'", o, message);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckValidate(ref string[] args)
|
||||||
|
{
|
||||||
|
var argsList = args.ToList();
|
||||||
|
if (argsList.Remove("--validate") || argsList.Remove("--validate-fast"))
|
||||||
|
{
|
||||||
|
SuppressConsole();
|
||||||
|
AspNetCoreBenchmarkAttribute.UseValidationConfig = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
args = argsList.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SuppressConsole()
|
||||||
|
{
|
||||||
|
_standardOutput = Console.Out;
|
||||||
|
_standardOutputText = new StringBuilder();
|
||||||
|
Console.SetOut(new StringWriter(_standardOutputText));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,720 @@
|
||||||
|
// 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.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Certificates.Generation
|
||||||
|
{
|
||||||
|
internal class CertificateManager
|
||||||
|
{
|
||||||
|
public const string AspNetHttpsOid = "1.3.6.1.4.1.311.84.1.1";
|
||||||
|
public const string AspNetHttpsOidFriendlyName = "ASP.NET Core HTTPS development certificate";
|
||||||
|
|
||||||
|
public const string AspNetIdentityOid = "1.3.6.1.4.1.311.84.1.2";
|
||||||
|
public const string AspNetIdentityOidFriendlyName = "ASP.NET Core Identity Json Web Token signing development certificate";
|
||||||
|
|
||||||
|
private const string ServerAuthenticationEnhancedKeyUsageOid = "1.3.6.1.5.5.7.3.1";
|
||||||
|
private const string ServerAuthenticationEnhancedKeyUsageOidFriendlyName = "Server Authentication";
|
||||||
|
|
||||||
|
private const string LocalhostHttpsDnsName = "localhost";
|
||||||
|
private const string LocalhostHttpsDistinguishedName = "CN=" + LocalhostHttpsDnsName;
|
||||||
|
|
||||||
|
private const string IdentityDistinguishedName = "CN=Microsoft.AspNetCore.Identity.Signing";
|
||||||
|
|
||||||
|
public const int RSAMinimumKeySizeInBits = 2048;
|
||||||
|
|
||||||
|
private static readonly TimeSpan MaxRegexTimeout = TimeSpan.FromMinutes(1);
|
||||||
|
private const string CertificateSubjectRegex = "CN=(.*[^,]+).*";
|
||||||
|
private const string MacOSSystemKeyChain = "/Library/Keychains/System.keychain";
|
||||||
|
private static readonly string MacOSUserKeyChain = Environment.GetEnvironmentVariable("HOME") + "/Library/Keychains/login.keychain-db";
|
||||||
|
private const string MacOSFindCertificateCommandLine = "security";
|
||||||
|
#if NETCOREAPP2_0 || NETCOREAPP2_1
|
||||||
|
private static readonly string MacOSFindCertificateCommandLineArgumentsFormat = "find-certificate -c {0} -a -Z -p " + MacOSSystemKeyChain;
|
||||||
|
#endif
|
||||||
|
private const string MacOSFindCertificateOutputRegex = "SHA-1 hash: ([0-9A-Z]+)";
|
||||||
|
private const string MacOSRemoveCertificateTrustCommandLine = "sudo";
|
||||||
|
private const string MacOSRemoveCertificateTrustCommandLineArgumentsFormat = "security remove-trusted-cert -d {0}";
|
||||||
|
private const string MacOSDeleteCertificateCommandLine = "sudo";
|
||||||
|
private const string MacOSDeleteCertificateCommandLineArgumentsFormat = "security delete-certificate -Z {0} {1}";
|
||||||
|
private const string MacOSTrustCertificateCommandLine = "sudo";
|
||||||
|
#if NETCOREAPP2_0 || NETCOREAPP2_1
|
||||||
|
private static readonly string MacOSTrustCertificateCommandLineArguments = "security add-trusted-cert -d -r trustRoot -k " + MacOSSystemKeyChain + " ";
|
||||||
|
#endif
|
||||||
|
private const int UserCancelledErrorCode = 1223;
|
||||||
|
|
||||||
|
public IList<X509Certificate2> ListCertificates(
|
||||||
|
CertificatePurpose purpose,
|
||||||
|
StoreName storeName,
|
||||||
|
StoreLocation location,
|
||||||
|
bool isValid,
|
||||||
|
bool requireExportable = true)
|
||||||
|
{
|
||||||
|
var certificates = new List<X509Certificate2>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var store = new X509Store(storeName, location))
|
||||||
|
{
|
||||||
|
store.Open(OpenFlags.ReadOnly);
|
||||||
|
certificates.AddRange(store.Certificates.OfType<X509Certificate2>());
|
||||||
|
IEnumerable<X509Certificate2> matchingCertificates = certificates;
|
||||||
|
switch (purpose)
|
||||||
|
{
|
||||||
|
case CertificatePurpose.All:
|
||||||
|
matchingCertificates = matchingCertificates
|
||||||
|
.Where(c => HasOid(c, AspNetHttpsOid) || HasOid(c, AspNetIdentityOid));
|
||||||
|
break;
|
||||||
|
case CertificatePurpose.HTTPS:
|
||||||
|
matchingCertificates = matchingCertificates
|
||||||
|
.Where(c => HasOid(c, AspNetHttpsOid));
|
||||||
|
break;
|
||||||
|
case CertificatePurpose.Signing:
|
||||||
|
matchingCertificates = matchingCertificates
|
||||||
|
.Where(c => HasOid(c, AspNetIdentityOid));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (isValid)
|
||||||
|
{
|
||||||
|
// Ensure the certificate hasn't expired, has a private key and its exportable
|
||||||
|
// (for container/unix scenarios).
|
||||||
|
var now = DateTimeOffset.Now;
|
||||||
|
matchingCertificates = matchingCertificates
|
||||||
|
.Where(c => c.NotBefore <= now &&
|
||||||
|
now <= c.NotAfter &&
|
||||||
|
(!requireExportable || !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || IsExportable(c)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to enumerate the certificates early to prevent dispoisng issues.
|
||||||
|
matchingCertificates = matchingCertificates.ToList();
|
||||||
|
|
||||||
|
var certificatesToDispose = certificates.Except(matchingCertificates);
|
||||||
|
DisposeCertificates(certificatesToDispose);
|
||||||
|
|
||||||
|
store.Close();
|
||||||
|
|
||||||
|
return (IList<X509Certificate2>)matchingCertificates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
DisposeCertificates(certificates);
|
||||||
|
certificates.Clear();
|
||||||
|
return certificates;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasOid(X509Certificate2 certificate, string oid) =>
|
||||||
|
certificate.Extensions.OfType<X509Extension>()
|
||||||
|
.Any(e => string.Equals(oid, e.Oid.Value, StringComparison.Ordinal));
|
||||||
|
#if !XPLAT
|
||||||
|
bool IsExportable(X509Certificate2 c) =>
|
||||||
|
((c.GetRSAPrivateKey() is RSACryptoServiceProvider rsaPrivateKey &&
|
||||||
|
rsaPrivateKey.CspKeyContainerInfo.Exportable) ||
|
||||||
|
(c.GetRSAPrivateKey() is RSACng cngPrivateKey &&
|
||||||
|
cngPrivateKey.Key.ExportPolicy == CngExportPolicies.AllowExport));
|
||||||
|
#else
|
||||||
|
// Only check for RSA CryptoServiceProvider and do not fail in XPlat tooling as
|
||||||
|
// System.Security.Cryptography.Cng is not pat of the shared framework and we don't
|
||||||
|
// want to bring the dependency in on CLI scenarios. This functionality will be used
|
||||||
|
// on CLI scenarios as part of the first run experience, so checking the exportability
|
||||||
|
// of the certificate is not important.
|
||||||
|
bool IsExportable(X509Certificate2 c) =>
|
||||||
|
((c.GetRSAPrivateKey() is RSACryptoServiceProvider rsaPrivateKey &&
|
||||||
|
rsaPrivateKey.CspKeyContainerInfo.Exportable) || !(c.GetRSAPrivateKey() is RSACryptoServiceProvider));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DisposeCertificates(IEnumerable<X509Certificate2> disposables)
|
||||||
|
{
|
||||||
|
foreach (var disposable in disposables)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
disposable.Dispose();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if NETCOREAPP2_0 || NETCOREAPP2_1
|
||||||
|
|
||||||
|
public X509Certificate2 CreateAspNetCoreHttpsDevelopmentCertificate(DateTimeOffset notBefore, DateTimeOffset notAfter, string subjectOverride)
|
||||||
|
{
|
||||||
|
var subject = new X500DistinguishedName(subjectOverride ?? LocalhostHttpsDistinguishedName);
|
||||||
|
var extensions = new List<X509Extension>();
|
||||||
|
var sanBuilder = new SubjectAlternativeNameBuilder();
|
||||||
|
sanBuilder.AddDnsName(LocalhostHttpsDnsName);
|
||||||
|
|
||||||
|
var keyUsage = new X509KeyUsageExtension(X509KeyUsageFlags.KeyEncipherment, critical: true);
|
||||||
|
var enhancedKeyUsage = new X509EnhancedKeyUsageExtension(
|
||||||
|
new OidCollection() {
|
||||||
|
new Oid(
|
||||||
|
ServerAuthenticationEnhancedKeyUsageOid,
|
||||||
|
ServerAuthenticationEnhancedKeyUsageOidFriendlyName)
|
||||||
|
},
|
||||||
|
critical: true);
|
||||||
|
|
||||||
|
var basicConstraints = new X509BasicConstraintsExtension(
|
||||||
|
certificateAuthority: false,
|
||||||
|
hasPathLengthConstraint: false,
|
||||||
|
pathLengthConstraint: 0,
|
||||||
|
critical: true);
|
||||||
|
|
||||||
|
var aspNetHttpsExtension = new X509Extension(
|
||||||
|
new AsnEncodedData(
|
||||||
|
new Oid(AspNetHttpsOid, AspNetHttpsOidFriendlyName),
|
||||||
|
Encoding.ASCII.GetBytes(AspNetHttpsOidFriendlyName)),
|
||||||
|
critical: false);
|
||||||
|
|
||||||
|
extensions.Add(basicConstraints);
|
||||||
|
extensions.Add(keyUsage);
|
||||||
|
extensions.Add(enhancedKeyUsage);
|
||||||
|
extensions.Add(sanBuilder.Build(critical: true));
|
||||||
|
extensions.Add(aspNetHttpsExtension);
|
||||||
|
|
||||||
|
var certificate = CreateSelfSignedCertificate(subject, extensions, notBefore, notAfter);
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
certificate.FriendlyName = AspNetHttpsOidFriendlyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate2 CreateApplicationTokenSigningDevelopmentCertificate(DateTimeOffset notBefore, DateTimeOffset notAfter, string subjectOverride)
|
||||||
|
{
|
||||||
|
var subject = new X500DistinguishedName(subjectOverride ?? IdentityDistinguishedName);
|
||||||
|
var extensions = new List<X509Extension>();
|
||||||
|
|
||||||
|
var keyUsage = new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, critical: true);
|
||||||
|
var enhancedKeyUsage = new X509EnhancedKeyUsageExtension(
|
||||||
|
new OidCollection() {
|
||||||
|
new Oid(
|
||||||
|
ServerAuthenticationEnhancedKeyUsageOid,
|
||||||
|
ServerAuthenticationEnhancedKeyUsageOidFriendlyName)
|
||||||
|
},
|
||||||
|
critical: true);
|
||||||
|
|
||||||
|
var basicConstraints = new X509BasicConstraintsExtension(
|
||||||
|
certificateAuthority: false,
|
||||||
|
hasPathLengthConstraint: false,
|
||||||
|
pathLengthConstraint: 0,
|
||||||
|
critical: true);
|
||||||
|
|
||||||
|
var aspNetIdentityExtension = new X509Extension(
|
||||||
|
new AsnEncodedData(
|
||||||
|
new Oid(AspNetIdentityOid, AspNetIdentityOidFriendlyName),
|
||||||
|
Encoding.ASCII.GetBytes(AspNetIdentityOidFriendlyName)),
|
||||||
|
critical: false);
|
||||||
|
|
||||||
|
extensions.Add(basicConstraints);
|
||||||
|
extensions.Add(keyUsage);
|
||||||
|
extensions.Add(enhancedKeyUsage);
|
||||||
|
extensions.Add(aspNetIdentityExtension);
|
||||||
|
|
||||||
|
var certificate = CreateSelfSignedCertificate(subject, extensions, notBefore, notAfter);
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
certificate.FriendlyName = AspNetIdentityOidFriendlyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate2 CreateSelfSignedCertificate(
|
||||||
|
X500DistinguishedName subject,
|
||||||
|
IEnumerable<X509Extension> extensions,
|
||||||
|
DateTimeOffset notBefore,
|
||||||
|
DateTimeOffset notAfter)
|
||||||
|
{
|
||||||
|
var key = CreateKeyMaterial(RSAMinimumKeySizeInBits);
|
||||||
|
|
||||||
|
var request = new CertificateRequest(subject, key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||||
|
foreach (var extension in extensions)
|
||||||
|
{
|
||||||
|
request.CertificateExtensions.Add(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
return request.CreateSelfSigned(notBefore, notAfter);
|
||||||
|
|
||||||
|
RSA CreateKeyMaterial(int minimumKeySize)
|
||||||
|
{
|
||||||
|
var rsa = RSA.Create(minimumKeySize);
|
||||||
|
if (rsa.KeySize < minimumKeySize)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Failed to create a key with a size of {minimumKeySize} bits");
|
||||||
|
}
|
||||||
|
|
||||||
|
return rsa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate2 SaveCertificateInStore(X509Certificate2 certificate, StoreName name, StoreLocation location)
|
||||||
|
{
|
||||||
|
var imported = certificate;
|
||||||
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
|
{
|
||||||
|
// On non OSX systems we need to export the certificate and import it so that the transient
|
||||||
|
// key that we generated gets persisted.
|
||||||
|
var export = certificate.Export(X509ContentType.Pkcs12, "");
|
||||||
|
imported = new X509Certificate2(export, "", X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
|
||||||
|
Array.Clear(export, 0, export.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
imported.FriendlyName = certificate.FriendlyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var store = new X509Store(name, location))
|
||||||
|
{
|
||||||
|
store.Open(OpenFlags.ReadWrite);
|
||||||
|
store.Add(imported);
|
||||||
|
store.Close();
|
||||||
|
};
|
||||||
|
|
||||||
|
return imported;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ExportCertificate(X509Certificate2 certificate, string path, bool includePrivateKey, string password)
|
||||||
|
{
|
||||||
|
if (Path.GetDirectoryName(path) != "")
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includePrivateKey)
|
||||||
|
{
|
||||||
|
var bytes = certificate.Export(X509ContentType.Pkcs12, password);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllBytes(path, bytes);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Array.Clear(bytes, 0, bytes.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var bytes = certificate.Export(X509ContentType.Cert);
|
||||||
|
File.WriteAllBytes(path, bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TrustCertificate(X509Certificate2 certificate)
|
||||||
|
{
|
||||||
|
// Strip certificate of the private key if any.
|
||||||
|
var publicCertificate = new X509Certificate2(certificate.Export(X509ContentType.Cert));
|
||||||
|
|
||||||
|
if (!IsTrusted(publicCertificate))
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
TrustCertificateOnWindows(certificate, publicCertificate);
|
||||||
|
}
|
||||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
|
{
|
||||||
|
TrustCertificateOnMac(publicCertificate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TrustCertificateOnMac(X509Certificate2 publicCertificate)
|
||||||
|
{
|
||||||
|
var tmpFile = Path.GetTempFileName();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ExportCertificate(publicCertificate, tmpFile, includePrivateKey: false, password: null);
|
||||||
|
using (var process = Process.Start(MacOSTrustCertificateCommandLine, MacOSTrustCertificateCommandLineArguments + tmpFile))
|
||||||
|
{
|
||||||
|
process.WaitForExit();
|
||||||
|
if (process.ExitCode != 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("There was an error trusting the certificate.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(tmpFile))
|
||||||
|
{
|
||||||
|
File.Delete(tmpFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// We don't care if we can't delete the temp file.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TrustCertificateOnWindows(X509Certificate2 certificate, X509Certificate2 publicCertificate)
|
||||||
|
{
|
||||||
|
publicCertificate.FriendlyName = certificate.FriendlyName;
|
||||||
|
|
||||||
|
using (var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser))
|
||||||
|
{
|
||||||
|
store.Open(OpenFlags.ReadWrite);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
store.Add(publicCertificate);
|
||||||
|
}
|
||||||
|
catch (CryptographicException exception) when (exception.HResult == UserCancelledErrorCode)
|
||||||
|
{
|
||||||
|
throw new UserCancelledTrustException();
|
||||||
|
}
|
||||||
|
store.Close();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsTrusted(X509Certificate2 certificate)
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
return ListCertificates(CertificatePurpose.HTTPS, StoreName.Root, StoreLocation.CurrentUser, isValid: true, requireExportable: false)
|
||||||
|
.Any(c => c.Thumbprint == certificate.Thumbprint);
|
||||||
|
}
|
||||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
|
{
|
||||||
|
var subjectMatch = Regex.Match(certificate.Subject, CertificateSubjectRegex, RegexOptions.Singleline, MaxRegexTimeout);
|
||||||
|
if (!subjectMatch.Success)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Can't determine the subject for the certificate with subject '{certificate.Subject}'.");
|
||||||
|
}
|
||||||
|
var subject = subjectMatch.Groups[1].Value;
|
||||||
|
using (var checkTrustProcess = Process.Start(new ProcessStartInfo(
|
||||||
|
MacOSFindCertificateCommandLine,
|
||||||
|
string.Format(MacOSFindCertificateCommandLineArgumentsFormat, subject))
|
||||||
|
{
|
||||||
|
RedirectStandardOutput = true
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
var output = checkTrustProcess.StandardOutput.ReadToEnd();
|
||||||
|
checkTrustProcess.WaitForExit();
|
||||||
|
var matches = Regex.Matches(output, MacOSFindCertificateOutputRegex, RegexOptions.Multiline, MaxRegexTimeout);
|
||||||
|
var hashes = matches.OfType<Match>().Select(m => m.Groups[1].Value).ToList();
|
||||||
|
return hashes.Any(h => string.Equals(h, certificate.Thumbprint, StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CleanupHttpsCertificates(string subject = LocalhostHttpsDistinguishedName)
|
||||||
|
{
|
||||||
|
CleanupCertificates(CertificatePurpose.HTTPS, subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CleanupCertificates(CertificatePurpose purpose, string subject)
|
||||||
|
{
|
||||||
|
// On OS X we don't have a good way to manage trusted certificates in the system keychain
|
||||||
|
// so we do everything by invoking the native toolchain.
|
||||||
|
// This has some limitations, like for example not being able to identify our custom OID extension. For that
|
||||||
|
// matter, when we are cleaning up certificates on the machine, we start by removing the trusted certificates.
|
||||||
|
// To do this, we list the certificates that we can identify on the current user personal store and we invoke
|
||||||
|
// the native toolchain to remove them from the sytem keychain. Once we have removed the trusted certificates,
|
||||||
|
// we remove the certificates from the local user store to finish up the cleanup.
|
||||||
|
var certificates = ListCertificates(purpose, StoreName.My, StoreLocation.CurrentUser, isValid: false);
|
||||||
|
foreach (var certificate in certificates)
|
||||||
|
{
|
||||||
|
RemoveCertificate(certificate, RemoveLocations.All);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveAllCertificates(CertificatePurpose purpose, StoreName storeName, StoreLocation storeLocation, string subject = null)
|
||||||
|
{
|
||||||
|
var certificates = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ?
|
||||||
|
ListCertificates(purpose, StoreName.My, StoreLocation.CurrentUser, isValid: false) :
|
||||||
|
ListCertificates(purpose, storeName, storeLocation, isValid: false);
|
||||||
|
var certificatesWithName = subject == null ? certificates : certificates.Where(c => c.Subject == subject);
|
||||||
|
|
||||||
|
var removeLocation = storeName == StoreName.My ? RemoveLocations.Local : RemoveLocations.Trusted;
|
||||||
|
|
||||||
|
foreach (var certificate in certificates)
|
||||||
|
{
|
||||||
|
RemoveCertificate(certificate, removeLocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposeCertificates(certificates);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveCertificate(X509Certificate2 certificate, RemoveLocations locations)
|
||||||
|
{
|
||||||
|
switch (locations)
|
||||||
|
{
|
||||||
|
case RemoveLocations.Undefined:
|
||||||
|
throw new InvalidOperationException($"'{nameof(RemoveLocations.Undefined)}' is not a valid location.");
|
||||||
|
case RemoveLocations.Local:
|
||||||
|
RemoveCertificateFromUserStore(certificate);
|
||||||
|
break;
|
||||||
|
case RemoveLocations.Trusted when !RuntimeInformation.IsOSPlatform(OSPlatform.Linux):
|
||||||
|
RemoveCertificateFromTrustedRoots(certificate);
|
||||||
|
break;
|
||||||
|
case RemoveLocations.All:
|
||||||
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
{
|
||||||
|
RemoveCertificateFromTrustedRoots(certificate);
|
||||||
|
}
|
||||||
|
RemoveCertificateFromUserStore(certificate);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException("Invalid location.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RemoveCertificateFromUserStore(X509Certificate2 certificate)
|
||||||
|
{
|
||||||
|
using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
|
||||||
|
{
|
||||||
|
store.Open(OpenFlags.ReadWrite);
|
||||||
|
var matching = store.Certificates
|
||||||
|
.OfType<X509Certificate2>()
|
||||||
|
.Single(c => c.SerialNumber == certificate.SerialNumber);
|
||||||
|
|
||||||
|
store.Remove(matching);
|
||||||
|
store.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate)
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
using (var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser))
|
||||||
|
{
|
||||||
|
store.Open(OpenFlags.ReadWrite);
|
||||||
|
var matching = store.Certificates
|
||||||
|
.OfType<X509Certificate2>()
|
||||||
|
.Single(c => c.SerialNumber == certificate.SerialNumber);
|
||||||
|
|
||||||
|
store.Remove(matching);
|
||||||
|
store.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (IsTrusted(certificate)) // On OSX this check just ensures its on the system keychain
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RemoveCertificateTrustRule(certificate);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// We don't care if we fail to remove the trust rule if
|
||||||
|
// for some reason the certificate became untrusted.
|
||||||
|
// The delete command will fail if the certificate is
|
||||||
|
// trusted.
|
||||||
|
}
|
||||||
|
RemoveCertificateFromKeyChain(MacOSSystemKeyChain, certificate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RemoveCertificateTrustRule(X509Certificate2 certificate)
|
||||||
|
{
|
||||||
|
var certificatePath = Path.GetTempFileName();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var certBytes = certificate.Export(X509ContentType.Cert);
|
||||||
|
File.WriteAllBytes(certificatePath, certBytes);
|
||||||
|
var processInfo = new ProcessStartInfo(
|
||||||
|
MacOSRemoveCertificateTrustCommandLine,
|
||||||
|
string.Format(
|
||||||
|
MacOSRemoveCertificateTrustCommandLineArgumentsFormat,
|
||||||
|
certificatePath
|
||||||
|
));
|
||||||
|
using (var process = Process.Start(processInfo))
|
||||||
|
{
|
||||||
|
process.WaitForExit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(certificatePath))
|
||||||
|
{
|
||||||
|
File.Delete(certificatePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// We don't care about failing to do clean-up on a temp file.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RemoveCertificateFromKeyChain(string keyChain, X509Certificate2 certificate)
|
||||||
|
{
|
||||||
|
var processInfo = new ProcessStartInfo(
|
||||||
|
MacOSDeleteCertificateCommandLine,
|
||||||
|
string.Format(
|
||||||
|
MacOSDeleteCertificateCommandLineArgumentsFormat,
|
||||||
|
certificate.Thumbprint.ToUpperInvariant(),
|
||||||
|
keyChain
|
||||||
|
))
|
||||||
|
{
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true
|
||||||
|
};
|
||||||
|
|
||||||
|
using (var process = Process.Start(processInfo))
|
||||||
|
{
|
||||||
|
var output = process.StandardOutput.ReadToEnd() + process.StandardError.ReadToEnd();
|
||||||
|
process.WaitForExit();
|
||||||
|
|
||||||
|
if (process.ExitCode != 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($@"There was an error removing the certificate with thumbprint '{certificate.Thumbprint}'.
|
||||||
|
|
||||||
|
{output}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate(
|
||||||
|
DateTimeOffset notBefore,
|
||||||
|
DateTimeOffset notAfter,
|
||||||
|
string path = null,
|
||||||
|
bool trust = false,
|
||||||
|
bool includePrivateKey = false,
|
||||||
|
string password = null,
|
||||||
|
string subject = LocalhostHttpsDistinguishedName)
|
||||||
|
{
|
||||||
|
return EnsureValidCertificateExists(notBefore, notAfter, CertificatePurpose.HTTPS, path, trust, includePrivateKey, password, subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EnsureCertificateResult EnsureAspNetCoreApplicationTokensDevelopmentCertificate(
|
||||||
|
DateTimeOffset notBefore,
|
||||||
|
DateTimeOffset notAfter,
|
||||||
|
string path = null,
|
||||||
|
bool trust = false,
|
||||||
|
bool includePrivateKey = false,
|
||||||
|
string password = null,
|
||||||
|
string subject = IdentityDistinguishedName)
|
||||||
|
{
|
||||||
|
return EnsureValidCertificateExists(notBefore, notAfter, CertificatePurpose.Signing, path, trust, includePrivateKey, password, subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EnsureCertificateResult EnsureValidCertificateExists(
|
||||||
|
DateTimeOffset notBefore,
|
||||||
|
DateTimeOffset notAfter,
|
||||||
|
CertificatePurpose purpose,
|
||||||
|
string path = null,
|
||||||
|
bool trust = false,
|
||||||
|
bool includePrivateKey = false,
|
||||||
|
string password = null,
|
||||||
|
string subjectOverride = null)
|
||||||
|
{
|
||||||
|
if (purpose == CertificatePurpose.All)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("The certificate must have a specific purpose.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var certificates = ListCertificates(purpose, StoreName.My, StoreLocation.CurrentUser, isValid: true).Concat(
|
||||||
|
ListCertificates(purpose, StoreName.My, StoreLocation.LocalMachine, isValid: true));
|
||||||
|
|
||||||
|
certificates = subjectOverride == null ? certificates : certificates.Where(c => c.Subject == subjectOverride);
|
||||||
|
|
||||||
|
var result = EnsureCertificateResult.Succeeded;
|
||||||
|
|
||||||
|
X509Certificate2 certificate = null;
|
||||||
|
if (certificates.Count() > 0)
|
||||||
|
{
|
||||||
|
certificate = certificates.FirstOrDefault();
|
||||||
|
result = EnsureCertificateResult.ValidCertificatePresent;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch (purpose)
|
||||||
|
{
|
||||||
|
case CertificatePurpose.All:
|
||||||
|
throw new InvalidOperationException("The certificate must have a specific purpose.");
|
||||||
|
case CertificatePurpose.HTTPS:
|
||||||
|
certificate = CreateAspNetCoreHttpsDevelopmentCertificate(notBefore, notAfter, subjectOverride);
|
||||||
|
break;
|
||||||
|
case CertificatePurpose.Signing:
|
||||||
|
certificate = CreateApplicationTokenSigningDevelopmentCertificate(notBefore, notAfter, subjectOverride);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException("The certificate must have a purpose.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return EnsureCertificateResult.ErrorCreatingTheCertificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
certificate = SaveCertificateInStore(certificate, StoreName.My, StoreLocation.CurrentUser);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return EnsureCertificateResult.ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (path != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ExportCertificate(certificate, path, includePrivateKey, password);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return EnsureCertificateResult.ErrorExportingTheCertificate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) && trust)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TrustCertificate(certificate);
|
||||||
|
}
|
||||||
|
catch (UserCancelledTrustException)
|
||||||
|
{
|
||||||
|
return EnsureCertificateResult.UserCancelledTrustStep;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return EnsureCertificateResult.FailedToTrustTheCertificate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class UserCancelledTrustException : Exception
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum RemoveLocations
|
||||||
|
{
|
||||||
|
Undefined,
|
||||||
|
Local,
|
||||||
|
Trusted,
|
||||||
|
All
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// 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.Certificates.Generation
|
||||||
|
{
|
||||||
|
internal enum CertificatePurpose
|
||||||
|
{
|
||||||
|
All,
|
||||||
|
HTTPS,
|
||||||
|
Signing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<Project>
|
||||||
|
|
||||||
|
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<PackageId>Microsoft.AspNetCore.Certificates.Generation.Sources</PackageId>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#if NETCOREAPP2_0 || NETCOREAPP2_1
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Certificates.Generation
|
||||||
|
{
|
||||||
|
internal enum EnsureCertificateResult
|
||||||
|
{
|
||||||
|
Succeeded = 1,
|
||||||
|
ValidCertificatePresent,
|
||||||
|
ErrorCreatingTheCertificate,
|
||||||
|
ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore,
|
||||||
|
ErrorExportingTheCertificate,
|
||||||
|
FailedToTrustTheCertificate,
|
||||||
|
UserCancelledTrustStep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
// 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.Reflection;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Helper related to generic interface definitions and implementing classes.
|
||||||
|
/// </summary>
|
||||||
|
internal static class ClosedGenericMatcher
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Determine whether <paramref name="queryType"/> is or implements a closed generic <see cref="Type"/>
|
||||||
|
/// created from <paramref name="interfaceType"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="queryType">The <see cref="Type"/> of interest.</param>
|
||||||
|
/// <param name="interfaceType">The open generic <see cref="Type"/> to match. Usually an interface.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The closed generic <see cref="Type"/> created from <paramref name="interfaceType"/> that
|
||||||
|
/// <paramref name="queryType"/> is or implements. <c>null</c> if the two <see cref="Type"/>s have no such
|
||||||
|
/// relationship.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method will return <paramref name="queryType"/> if <paramref name="interfaceType"/> is
|
||||||
|
/// <c>typeof(KeyValuePair{,})</c>, and <paramref name="queryType"/> is
|
||||||
|
/// <c>typeof(KeyValuePair{string, object})</c>.
|
||||||
|
/// </remarks>
|
||||||
|
public static Type ExtractGenericInterface(Type queryType, Type interfaceType)
|
||||||
|
{
|
||||||
|
if (queryType == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(queryType));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interfaceType == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(interfaceType));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsGenericInstantiation(queryType, interfaceType))
|
||||||
|
{
|
||||||
|
// queryType matches (i.e. is a closed generic type created from) the open generic type.
|
||||||
|
return queryType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise check all interfaces the type implements for a match.
|
||||||
|
// - If multiple different generic instantiations exists, we want the most derived one.
|
||||||
|
// - If that doesn't break the tie, then we sort alphabetically so that it's deterministic.
|
||||||
|
//
|
||||||
|
// We do this by looking at interfaces on the type, and recursing to the base type
|
||||||
|
// if we don't find any matches.
|
||||||
|
return GetGenericInstantiation(queryType, interfaceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsGenericInstantiation(Type candidate, Type interfaceType)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
candidate.GetTypeInfo().IsGenericType &&
|
||||||
|
candidate.GetGenericTypeDefinition() == interfaceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Type GetGenericInstantiation(Type queryType, Type interfaceType)
|
||||||
|
{
|
||||||
|
Type bestMatch = null;
|
||||||
|
var interfaces = queryType.GetInterfaces();
|
||||||
|
foreach (var @interface in interfaces)
|
||||||
|
{
|
||||||
|
if (IsGenericInstantiation(@interface, interfaceType))
|
||||||
|
{
|
||||||
|
if (bestMatch == null)
|
||||||
|
{
|
||||||
|
bestMatch = @interface;
|
||||||
|
}
|
||||||
|
else if (StringComparer.Ordinal.Compare(@interface.FullName, bestMatch.FullName) < 0)
|
||||||
|
{
|
||||||
|
bestMatch = @interface;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// There are two matches at this level of the class hierarchy, but @interface is after
|
||||||
|
// bestMatch in the sort order.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestMatch != null)
|
||||||
|
{
|
||||||
|
return bestMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BaseType will be null for object and interfaces, which means we've reached 'bottom'.
|
||||||
|
var baseType = queryType?.GetTypeInfo().BaseType;
|
||||||
|
if (baseType == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return GetGenericInstantiation(baseType, interfaceType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
// 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.IO;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.CommandLineUtils
|
||||||
|
{
|
||||||
|
internal class AnsiConsole
|
||||||
|
{
|
||||||
|
private AnsiConsole(TextWriter writer, bool useConsoleColor)
|
||||||
|
{
|
||||||
|
Writer = writer;
|
||||||
|
|
||||||
|
_useConsoleColor = useConsoleColor;
|
||||||
|
if (_useConsoleColor)
|
||||||
|
{
|
||||||
|
OriginalForegroundColor = Console.ForegroundColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _boldRecursion;
|
||||||
|
private bool _useConsoleColor;
|
||||||
|
|
||||||
|
public static AnsiConsole GetOutput(bool useConsoleColor)
|
||||||
|
{
|
||||||
|
return new AnsiConsole(Console.Out, useConsoleColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AnsiConsole GetError(bool useConsoleColor)
|
||||||
|
{
|
||||||
|
return new AnsiConsole(Console.Error, useConsoleColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextWriter Writer { get; }
|
||||||
|
|
||||||
|
public ConsoleColor OriginalForegroundColor { get; }
|
||||||
|
|
||||||
|
private void SetColor(ConsoleColor color)
|
||||||
|
{
|
||||||
|
Console.ForegroundColor = (ConsoleColor)(((int)Console.ForegroundColor & 0x08) | ((int)color & 0x07));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetBold(bool bold)
|
||||||
|
{
|
||||||
|
_boldRecursion += bold ? 1 : -1;
|
||||||
|
if (_boldRecursion > 1 || (_boldRecursion == 1 && !bold))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.ForegroundColor = (ConsoleColor)((int)Console.ForegroundColor ^ 0x08);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteLine(string message)
|
||||||
|
{
|
||||||
|
if (!_useConsoleColor)
|
||||||
|
{
|
||||||
|
Writer.WriteLine(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var escapeScan = 0;
|
||||||
|
for (; ;)
|
||||||
|
{
|
||||||
|
var escapeIndex = message.IndexOf("\x1b[", escapeScan);
|
||||||
|
if (escapeIndex == -1)
|
||||||
|
{
|
||||||
|
var text = message.Substring(escapeScan);
|
||||||
|
Writer.Write(text);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var startIndex = escapeIndex + 2;
|
||||||
|
var endIndex = startIndex;
|
||||||
|
while (endIndex != message.Length &&
|
||||||
|
message[endIndex] >= 0x20 &&
|
||||||
|
message[endIndex] <= 0x3f)
|
||||||
|
{
|
||||||
|
endIndex += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = message.Substring(escapeScan, escapeIndex - escapeScan);
|
||||||
|
Writer.Write(text);
|
||||||
|
if (endIndex == message.Length)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (message[endIndex])
|
||||||
|
{
|
||||||
|
case 'm':
|
||||||
|
int value;
|
||||||
|
if (int.TryParse(message.Substring(startIndex, endIndex - startIndex), out value))
|
||||||
|
{
|
||||||
|
switch (value)
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
SetBold(true);
|
||||||
|
break;
|
||||||
|
case 22:
|
||||||
|
SetBold(false);
|
||||||
|
break;
|
||||||
|
case 30:
|
||||||
|
SetColor(ConsoleColor.Black);
|
||||||
|
break;
|
||||||
|
case 31:
|
||||||
|
SetColor(ConsoleColor.Red);
|
||||||
|
break;
|
||||||
|
case 32:
|
||||||
|
SetColor(ConsoleColor.Green);
|
||||||
|
break;
|
||||||
|
case 33:
|
||||||
|
SetColor(ConsoleColor.Yellow);
|
||||||
|
break;
|
||||||
|
case 34:
|
||||||
|
SetColor(ConsoleColor.Blue);
|
||||||
|
break;
|
||||||
|
case 35:
|
||||||
|
SetColor(ConsoleColor.Magenta);
|
||||||
|
break;
|
||||||
|
case 36:
|
||||||
|
SetColor(ConsoleColor.Cyan);
|
||||||
|
break;
|
||||||
|
case 37:
|
||||||
|
SetColor(ConsoleColor.Gray);
|
||||||
|
break;
|
||||||
|
case 39:
|
||||||
|
SetColor(OriginalForegroundColor);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeScan = endIndex + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Writer.WriteLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
// 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.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.CommandLineUtils
|
||||||
|
{
|
||||||
|
internal class CommandArgument
|
||||||
|
{
|
||||||
|
public CommandArgument()
|
||||||
|
{
|
||||||
|
Values = new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { get; set; }
|
||||||
|
public bool ShowInHelpText { get; set; } = true;
|
||||||
|
public string Description { get; set; }
|
||||||
|
public List<string> Values { get; private set; }
|
||||||
|
public bool MultipleValues { get; set; }
|
||||||
|
public string Value
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return Values.FirstOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,555 @@
|
||||||
|
// 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.CommandLineUtils
|
||||||
|
{
|
||||||
|
internal class CommandLineApplication
|
||||||
|
{
|
||||||
|
// Indicates whether the parser should throw an exception when it runs into an unexpected argument.
|
||||||
|
// If this field is set to false, the parser will stop parsing when it sees an unexpected argument, and all
|
||||||
|
// remaining arguments, including the first unexpected argument, will be stored in RemainingArguments property.
|
||||||
|
private readonly bool _throwOnUnexpectedArg;
|
||||||
|
|
||||||
|
public CommandLineApplication(bool throwOnUnexpectedArg = true)
|
||||||
|
{
|
||||||
|
_throwOnUnexpectedArg = throwOnUnexpectedArg;
|
||||||
|
Options = new List<CommandOption>();
|
||||||
|
Arguments = new List<CommandArgument>();
|
||||||
|
Commands = new List<CommandLineApplication>();
|
||||||
|
RemainingArguments = new List<string>();
|
||||||
|
Invoke = () => 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandLineApplication Parent { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string FullName { get; set; }
|
||||||
|
public string Syntax { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public bool ShowInHelpText { get; set; } = true;
|
||||||
|
public string ExtendedHelpText { get; set; }
|
||||||
|
public readonly List<CommandOption> Options;
|
||||||
|
public CommandOption OptionHelp { get; private set; }
|
||||||
|
public CommandOption OptionVersion { get; private set; }
|
||||||
|
public readonly List<CommandArgument> Arguments;
|
||||||
|
public readonly List<string> RemainingArguments;
|
||||||
|
public bool IsShowingInformation { get; protected set; } // Is showing help or version?
|
||||||
|
public Func<int> Invoke { get; set; }
|
||||||
|
public Func<string> LongVersionGetter { get; set; }
|
||||||
|
public Func<string> ShortVersionGetter { get; set; }
|
||||||
|
public readonly List<CommandLineApplication> Commands;
|
||||||
|
public bool AllowArgumentSeparator { get; set; }
|
||||||
|
public TextWriter Out { get; set; } = Console.Out;
|
||||||
|
public TextWriter Error { get; set; } = Console.Error;
|
||||||
|
|
||||||
|
public IEnumerable<CommandOption> GetOptions()
|
||||||
|
{
|
||||||
|
var expr = Options.AsEnumerable();
|
||||||
|
var rootNode = this;
|
||||||
|
while (rootNode.Parent != null)
|
||||||
|
{
|
||||||
|
rootNode = rootNode.Parent;
|
||||||
|
expr = expr.Concat(rootNode.Options.Where(o => o.Inherited));
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandLineApplication Command(string name, Action<CommandLineApplication> configuration,
|
||||||
|
bool throwOnUnexpectedArg = true)
|
||||||
|
{
|
||||||
|
var command = new CommandLineApplication(throwOnUnexpectedArg) { Name = name, Parent = this };
|
||||||
|
Commands.Add(command);
|
||||||
|
configuration(command);
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandOption Option(string template, string description, CommandOptionType optionType)
|
||||||
|
=> Option(template, description, optionType, _ => { }, inherited: false);
|
||||||
|
|
||||||
|
public CommandOption Option(string template, string description, CommandOptionType optionType, bool inherited)
|
||||||
|
=> Option(template, description, optionType, _ => { }, inherited);
|
||||||
|
|
||||||
|
public CommandOption Option(string template, string description, CommandOptionType optionType, Action<CommandOption> configuration)
|
||||||
|
=> Option(template, description, optionType, configuration, inherited: false);
|
||||||
|
|
||||||
|
public CommandOption Option(string template, string description, CommandOptionType optionType, Action<CommandOption> configuration, bool inherited)
|
||||||
|
{
|
||||||
|
var option = new CommandOption(template, optionType)
|
||||||
|
{
|
||||||
|
Description = description,
|
||||||
|
Inherited = inherited
|
||||||
|
};
|
||||||
|
Options.Add(option);
|
||||||
|
configuration(option);
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandArgument Argument(string name, string description, bool multipleValues = false)
|
||||||
|
{
|
||||||
|
return Argument(name, description, _ => { }, multipleValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandArgument Argument(string name, string description, Action<CommandArgument> configuration, bool multipleValues = false)
|
||||||
|
{
|
||||||
|
var lastArg = Arguments.LastOrDefault();
|
||||||
|
if (lastArg != null && lastArg.MultipleValues)
|
||||||
|
{
|
||||||
|
var message = string.Format("The last argument '{0}' accepts multiple values. No more argument can be added.",
|
||||||
|
lastArg.Name);
|
||||||
|
throw new InvalidOperationException(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
var argument = new CommandArgument { Name = name, Description = description, MultipleValues = multipleValues };
|
||||||
|
Arguments.Add(argument);
|
||||||
|
configuration(argument);
|
||||||
|
return argument;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnExecute(Func<int> invoke)
|
||||||
|
{
|
||||||
|
Invoke = invoke;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnExecute(Func<Task<int>> invoke)
|
||||||
|
{
|
||||||
|
Invoke = () => invoke().Result;
|
||||||
|
}
|
||||||
|
public int Execute(params string[] args)
|
||||||
|
{
|
||||||
|
CommandLineApplication command = this;
|
||||||
|
CommandOption option = null;
|
||||||
|
IEnumerator<CommandArgument> arguments = null;
|
||||||
|
|
||||||
|
for (var index = 0; index < args.Length; index++)
|
||||||
|
{
|
||||||
|
var arg = args[index];
|
||||||
|
var processed = false;
|
||||||
|
if (!processed && option == null)
|
||||||
|
{
|
||||||
|
string[] longOption = null;
|
||||||
|
string[] shortOption = null;
|
||||||
|
|
||||||
|
if (arg.StartsWith("--"))
|
||||||
|
{
|
||||||
|
longOption = arg.Substring(2).Split(new[] { ':', '=' }, 2);
|
||||||
|
}
|
||||||
|
else if (arg.StartsWith("-"))
|
||||||
|
{
|
||||||
|
shortOption = arg.Substring(1).Split(new[] { ':', '=' }, 2);
|
||||||
|
}
|
||||||
|
if (longOption != null)
|
||||||
|
{
|
||||||
|
processed = true;
|
||||||
|
var longOptionName = longOption[0];
|
||||||
|
option = command.GetOptions().SingleOrDefault(opt => string.Equals(opt.LongName, longOptionName, StringComparison.Ordinal));
|
||||||
|
|
||||||
|
if (option == null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(longOptionName) && !command._throwOnUnexpectedArg && AllowArgumentSeparator)
|
||||||
|
{
|
||||||
|
// skip over the '--' argument separator
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleUnexpectedArg(command, args, index, argTypeName: "option");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we find a help/version option, show information and stop parsing
|
||||||
|
if (command.OptionHelp == option)
|
||||||
|
{
|
||||||
|
command.ShowHelp();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
else if (command.OptionVersion == option)
|
||||||
|
{
|
||||||
|
command.ShowVersion();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (longOption.Length == 2)
|
||||||
|
{
|
||||||
|
if (!option.TryParse(longOption[1]))
|
||||||
|
{
|
||||||
|
command.ShowHint();
|
||||||
|
throw new CommandParsingException(command, $"Unexpected value '{longOption[1]}' for option '{option.LongName}'");
|
||||||
|
}
|
||||||
|
option = null;
|
||||||
|
}
|
||||||
|
else if (option.OptionType == CommandOptionType.NoValue)
|
||||||
|
{
|
||||||
|
// No value is needed for this option
|
||||||
|
option.TryParse(null);
|
||||||
|
option = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shortOption != null)
|
||||||
|
{
|
||||||
|
processed = true;
|
||||||
|
option = command.GetOptions().SingleOrDefault(opt => string.Equals(opt.ShortName, shortOption[0], StringComparison.Ordinal));
|
||||||
|
|
||||||
|
// If not a short option, try symbol option
|
||||||
|
if (option == null)
|
||||||
|
{
|
||||||
|
option = command.GetOptions().SingleOrDefault(opt => string.Equals(opt.SymbolName, shortOption[0], StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option == null)
|
||||||
|
{
|
||||||
|
HandleUnexpectedArg(command, args, index, argTypeName: "option");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we find a help/version option, show information and stop parsing
|
||||||
|
if (command.OptionHelp == option)
|
||||||
|
{
|
||||||
|
command.ShowHelp();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
else if (command.OptionVersion == option)
|
||||||
|
{
|
||||||
|
command.ShowVersion();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shortOption.Length == 2)
|
||||||
|
{
|
||||||
|
if (!option.TryParse(shortOption[1]))
|
||||||
|
{
|
||||||
|
command.ShowHint();
|
||||||
|
throw new CommandParsingException(command, $"Unexpected value '{shortOption[1]}' for option '{option.LongName}'");
|
||||||
|
}
|
||||||
|
option = null;
|
||||||
|
}
|
||||||
|
else if (option.OptionType == CommandOptionType.NoValue)
|
||||||
|
{
|
||||||
|
// No value is needed for this option
|
||||||
|
option.TryParse(null);
|
||||||
|
option = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!processed && option != null)
|
||||||
|
{
|
||||||
|
processed = true;
|
||||||
|
if (!option.TryParse(arg))
|
||||||
|
{
|
||||||
|
command.ShowHint();
|
||||||
|
throw new CommandParsingException(command, $"Unexpected value '{arg}' for option '{option.LongName}'");
|
||||||
|
}
|
||||||
|
option = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!processed && arguments == null)
|
||||||
|
{
|
||||||
|
var currentCommand = command;
|
||||||
|
foreach (var subcommand in command.Commands)
|
||||||
|
{
|
||||||
|
if (string.Equals(subcommand.Name, arg, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
processed = true;
|
||||||
|
command = subcommand;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we detect a subcommand
|
||||||
|
if (command != currentCommand)
|
||||||
|
{
|
||||||
|
processed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!processed)
|
||||||
|
{
|
||||||
|
if (arguments == null)
|
||||||
|
{
|
||||||
|
arguments = new CommandArgumentEnumerator(command.Arguments.GetEnumerator());
|
||||||
|
}
|
||||||
|
if (arguments.MoveNext())
|
||||||
|
{
|
||||||
|
processed = true;
|
||||||
|
arguments.Current.Values.Add(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!processed)
|
||||||
|
{
|
||||||
|
HandleUnexpectedArg(command, args, index, argTypeName: "command or argument");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option != null)
|
||||||
|
{
|
||||||
|
command.ShowHint();
|
||||||
|
throw new CommandParsingException(command, $"Missing value for option '{option.LongName}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
return command.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method that adds a help option
|
||||||
|
public CommandOption HelpOption(string template)
|
||||||
|
{
|
||||||
|
// Help option is special because we stop parsing once we see it
|
||||||
|
// So we store it separately for further use
|
||||||
|
OptionHelp = Option(template, "Show help information", CommandOptionType.NoValue);
|
||||||
|
|
||||||
|
return OptionHelp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandOption VersionOption(string template,
|
||||||
|
string shortFormVersion,
|
||||||
|
string longFormVersion = null)
|
||||||
|
{
|
||||||
|
if (longFormVersion == null)
|
||||||
|
{
|
||||||
|
return VersionOption(template, () => shortFormVersion);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return VersionOption(template, () => shortFormVersion, () => longFormVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method that adds a version option
|
||||||
|
public CommandOption VersionOption(string template,
|
||||||
|
Func<string> shortFormVersionGetter,
|
||||||
|
Func<string> longFormVersionGetter = null)
|
||||||
|
{
|
||||||
|
// Version option is special because we stop parsing once we see it
|
||||||
|
// So we store it separately for further use
|
||||||
|
OptionVersion = Option(template, "Show version information", CommandOptionType.NoValue);
|
||||||
|
ShortVersionGetter = shortFormVersionGetter;
|
||||||
|
LongVersionGetter = longFormVersionGetter ?? shortFormVersionGetter;
|
||||||
|
|
||||||
|
return OptionVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show short hint that reminds users to use help option
|
||||||
|
public void ShowHint()
|
||||||
|
{
|
||||||
|
if (OptionHelp != null)
|
||||||
|
{
|
||||||
|
Out.WriteLine(string.Format("Specify --{0} for a list of available options and commands.", OptionHelp.LongName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show full help
|
||||||
|
public void ShowHelp(string commandName = null)
|
||||||
|
{
|
||||||
|
for (var cmd = this; cmd != null; cmd = cmd.Parent)
|
||||||
|
{
|
||||||
|
cmd.IsShowingInformation = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Out.WriteLine(GetHelpText(commandName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual string GetHelpText(string commandName = null)
|
||||||
|
{
|
||||||
|
var headerBuilder = new StringBuilder("Usage:");
|
||||||
|
for (var cmd = this; cmd != null; cmd = cmd.Parent)
|
||||||
|
{
|
||||||
|
headerBuilder.Insert(6, string.Format(" {0}", cmd.Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandLineApplication target;
|
||||||
|
|
||||||
|
if (commandName == null || string.Equals(Name, commandName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
target = this;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
target = Commands.SingleOrDefault(cmd => string.Equals(cmd.Name, commandName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (target != null)
|
||||||
|
{
|
||||||
|
headerBuilder.AppendFormat(" {0}", commandName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// The command name is invalid so don't try to show help for something that doesn't exist
|
||||||
|
target = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var optionsBuilder = new StringBuilder();
|
||||||
|
var commandsBuilder = new StringBuilder();
|
||||||
|
var argumentsBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
var arguments = target.Arguments.Where(a => a.ShowInHelpText).ToList();
|
||||||
|
if (arguments.Any())
|
||||||
|
{
|
||||||
|
headerBuilder.Append(" [arguments]");
|
||||||
|
|
||||||
|
argumentsBuilder.AppendLine();
|
||||||
|
argumentsBuilder.AppendLine("Arguments:");
|
||||||
|
var maxArgLen = arguments.Max(a => a.Name.Length);
|
||||||
|
var outputFormat = string.Format(" {{0, -{0}}}{{1}}", maxArgLen + 2);
|
||||||
|
foreach (var arg in arguments)
|
||||||
|
{
|
||||||
|
argumentsBuilder.AppendFormat(outputFormat, arg.Name, arg.Description);
|
||||||
|
argumentsBuilder.AppendLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = target.GetOptions().Where(o => o.ShowInHelpText).ToList();
|
||||||
|
if (options.Any())
|
||||||
|
{
|
||||||
|
headerBuilder.Append(" [options]");
|
||||||
|
|
||||||
|
optionsBuilder.AppendLine();
|
||||||
|
optionsBuilder.AppendLine("Options:");
|
||||||
|
var maxOptLen = options.Max(o => o.Template.Length);
|
||||||
|
var outputFormat = string.Format(" {{0, -{0}}}{{1}}", maxOptLen + 2);
|
||||||
|
foreach (var opt in options)
|
||||||
|
{
|
||||||
|
optionsBuilder.AppendFormat(outputFormat, opt.Template, opt.Description);
|
||||||
|
optionsBuilder.AppendLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var commands = target.Commands.Where(c => c.ShowInHelpText).ToList();
|
||||||
|
if (commands.Any())
|
||||||
|
{
|
||||||
|
headerBuilder.Append(" [command]");
|
||||||
|
|
||||||
|
commandsBuilder.AppendLine();
|
||||||
|
commandsBuilder.AppendLine("Commands:");
|
||||||
|
var maxCmdLen = commands.Max(c => c.Name.Length);
|
||||||
|
var outputFormat = string.Format(" {{0, -{0}}}{{1}}", maxCmdLen + 2);
|
||||||
|
foreach (var cmd in commands.OrderBy(c => c.Name))
|
||||||
|
{
|
||||||
|
commandsBuilder.AppendFormat(outputFormat, cmd.Name, cmd.Description);
|
||||||
|
commandsBuilder.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OptionHelp != null)
|
||||||
|
{
|
||||||
|
commandsBuilder.AppendLine();
|
||||||
|
commandsBuilder.AppendFormat($"Use \"{target.Name} [command] --{OptionHelp.LongName}\" for more information about a command.");
|
||||||
|
commandsBuilder.AppendLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.AllowArgumentSeparator)
|
||||||
|
{
|
||||||
|
headerBuilder.Append(" [[--] <arg>...]");
|
||||||
|
}
|
||||||
|
|
||||||
|
headerBuilder.AppendLine();
|
||||||
|
|
||||||
|
var nameAndVersion = new StringBuilder();
|
||||||
|
nameAndVersion.AppendLine(GetFullNameAndVersion());
|
||||||
|
nameAndVersion.AppendLine();
|
||||||
|
|
||||||
|
return nameAndVersion.ToString()
|
||||||
|
+ headerBuilder.ToString()
|
||||||
|
+ argumentsBuilder.ToString()
|
||||||
|
+ optionsBuilder.ToString()
|
||||||
|
+ commandsBuilder.ToString()
|
||||||
|
+ target.ExtendedHelpText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowVersion()
|
||||||
|
{
|
||||||
|
for (var cmd = this; cmd != null; cmd = cmd.Parent)
|
||||||
|
{
|
||||||
|
cmd.IsShowingInformation = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Out.WriteLine(FullName);
|
||||||
|
Out.WriteLine(LongVersionGetter());
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetFullNameAndVersion()
|
||||||
|
{
|
||||||
|
return ShortVersionGetter == null ? FullName : string.Format("{0} {1}", FullName, ShortVersionGetter());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowRootCommandFullNameAndVersion()
|
||||||
|
{
|
||||||
|
var rootCmd = this;
|
||||||
|
while (rootCmd.Parent != null)
|
||||||
|
{
|
||||||
|
rootCmd = rootCmd.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
Out.WriteLine(rootCmd.GetFullNameAndVersion());
|
||||||
|
Out.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleUnexpectedArg(CommandLineApplication command, string[] args, int index, string argTypeName)
|
||||||
|
{
|
||||||
|
if (command._throwOnUnexpectedArg)
|
||||||
|
{
|
||||||
|
command.ShowHint();
|
||||||
|
throw new CommandParsingException(command, $"Unrecognized {argTypeName} '{args[index]}'");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// All remaining arguments are stored for further use
|
||||||
|
command.RemainingArguments.AddRange(new ArraySegment<string>(args, index, args.Length - index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CommandArgumentEnumerator : IEnumerator<CommandArgument>
|
||||||
|
{
|
||||||
|
private readonly IEnumerator<CommandArgument> _enumerator;
|
||||||
|
|
||||||
|
public CommandArgumentEnumerator(IEnumerator<CommandArgument> enumerator)
|
||||||
|
{
|
||||||
|
_enumerator = enumerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandArgument Current
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _enumerator.Current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object IEnumerator.Current
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return Current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_enumerator.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MoveNext()
|
||||||
|
{
|
||||||
|
if (Current == null || !Current.MultipleValues)
|
||||||
|
{
|
||||||
|
return _enumerator.MoveNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If current argument allows multiple values, we don't move forward and
|
||||||
|
// all later values will be added to current CommandArgument.Values
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
_enumerator.Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.CommandLineUtils
|
||||||
|
{
|
||||||
|
internal class CommandOption
|
||||||
|
{
|
||||||
|
public CommandOption(string template, CommandOptionType optionType)
|
||||||
|
{
|
||||||
|
Template = template;
|
||||||
|
OptionType = optionType;
|
||||||
|
Values = new List<string>();
|
||||||
|
|
||||||
|
foreach (var part in Template.Split(new[] { ' ', '|' }, StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
{
|
||||||
|
if (part.StartsWith("--"))
|
||||||
|
{
|
||||||
|
LongName = part.Substring(2);
|
||||||
|
}
|
||||||
|
else if (part.StartsWith("-"))
|
||||||
|
{
|
||||||
|
var optName = part.Substring(1);
|
||||||
|
|
||||||
|
// If there is only one char and it is not an English letter, it is a symbol option (e.g. "-?")
|
||||||
|
if (optName.Length == 1 && !IsEnglishLetter(optName[0]))
|
||||||
|
{
|
||||||
|
SymbolName = optName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ShortName = optName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (part.StartsWith("<") && part.EndsWith(">"))
|
||||||
|
{
|
||||||
|
ValueName = part.Substring(1, part.Length - 2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Invalid template pattern '{template}'", nameof(template));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(LongName) && string.IsNullOrEmpty(ShortName) && string.IsNullOrEmpty(SymbolName))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Invalid template pattern '{template}'", nameof(template));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Template { get; set; }
|
||||||
|
public string ShortName { get; set; }
|
||||||
|
public string LongName { get; set; }
|
||||||
|
public string SymbolName { get; set; }
|
||||||
|
public string ValueName { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public List<string> Values { get; private set; }
|
||||||
|
public CommandOptionType OptionType { get; private set; }
|
||||||
|
public bool ShowInHelpText { get; set; } = true;
|
||||||
|
public bool Inherited { get; set; }
|
||||||
|
|
||||||
|
public bool TryParse(string value)
|
||||||
|
{
|
||||||
|
switch (OptionType)
|
||||||
|
{
|
||||||
|
case CommandOptionType.MultipleValue:
|
||||||
|
Values.Add(value);
|
||||||
|
break;
|
||||||
|
case CommandOptionType.SingleValue:
|
||||||
|
if (Values.Any())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Values.Add(value);
|
||||||
|
break;
|
||||||
|
case CommandOptionType.NoValue:
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Add a value to indicate that this option was specified
|
||||||
|
Values.Add("on");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasValue()
|
||||||
|
{
|
||||||
|
return Values.Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Value()
|
||||||
|
{
|
||||||
|
return HasValue() ? Values[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsEnglishLetter(char c)
|
||||||
|
{
|
||||||
|
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
// 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.Extensions.CommandLineUtils
|
||||||
|
{
|
||||||
|
internal enum CommandOptionType
|
||||||
|
{
|
||||||
|
MultipleValue,
|
||||||
|
SingleValue,
|
||||||
|
NoValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 System;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.CommandLineUtils
|
||||||
|
{
|
||||||
|
internal class CommandParsingException : Exception
|
||||||
|
{
|
||||||
|
public CommandParsingException(CommandLineApplication command, string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
Command = command;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandLineApplication Command { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup Label="Globals">
|
||||||
|
<ProjectGuid>00947d4a-c20e-46e3-90c3-6cd6bc87ee72</ProjectGuid>
|
||||||
|
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
|
||||||
|
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
|
||||||
|
<PropertyGroup />
|
||||||
|
<Import Project="Microsoft.Extensions.CommandLineUtils.Sources.projitems" Label="Shared" />
|
||||||
|
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
// 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.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.CommandLineUtils
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A utility for escaping arguments for new processes.
|
||||||
|
/// </summary>
|
||||||
|
internal static class ArgumentEscaper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Undo the processing which took place to create string[] args in Main, so that the next process will
|
||||||
|
/// receive the same string[] args.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// See https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="args"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EscapeAndConcatenate(IEnumerable<string> args)
|
||||||
|
=> string.Join(" ", args.Select(EscapeSingleArg));
|
||||||
|
|
||||||
|
private static string EscapeSingleArg(string arg)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
var needsQuotes = ShouldSurroundWithQuotes(arg);
|
||||||
|
var isQuoted = needsQuotes || IsSurroundedWithQuotes(arg);
|
||||||
|
|
||||||
|
if (needsQuotes)
|
||||||
|
{
|
||||||
|
sb.Append('"');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < arg.Length; ++i)
|
||||||
|
{
|
||||||
|
var backslashes = 0;
|
||||||
|
|
||||||
|
// Consume all backslashes
|
||||||
|
while (i < arg.Length && arg[i] == '\\')
|
||||||
|
{
|
||||||
|
backslashes++;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i == arg.Length && isQuoted)
|
||||||
|
{
|
||||||
|
// Escape any backslashes at the end of the arg when the argument is also quoted.
|
||||||
|
// This ensures the outside quote is interpreted as an argument delimiter
|
||||||
|
sb.Append('\\', 2 * backslashes);
|
||||||
|
}
|
||||||
|
else if (i == arg.Length)
|
||||||
|
{
|
||||||
|
// At then end of the arg, which isn't quoted,
|
||||||
|
// just add the backslashes, no need to escape
|
||||||
|
sb.Append('\\', backslashes);
|
||||||
|
}
|
||||||
|
else if (arg[i] == '"')
|
||||||
|
{
|
||||||
|
// Escape any preceding backslashes and the quote
|
||||||
|
sb.Append('\\', (2 * backslashes) + 1);
|
||||||
|
sb.Append('"');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Output any consumed backslashes and the character
|
||||||
|
sb.Append('\\', backslashes);
|
||||||
|
sb.Append(arg[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsQuotes)
|
||||||
|
{
|
||||||
|
sb.Append('"');
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldSurroundWithQuotes(string argument)
|
||||||
|
{
|
||||||
|
// Don't quote already quoted strings
|
||||||
|
if (IsSurroundedWithQuotes(argument))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only quote if whitespace exists in the string
|
||||||
|
return ContainsWhitespace(argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSurroundedWithQuotes(string argument)
|
||||||
|
{
|
||||||
|
if (argument.Length <= 1)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return argument[0] == '"' && argument[argument.Length - 1] == '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ContainsWhitespace(string argument)
|
||||||
|
=> argument.IndexOfAny(new [] { ' ', '\t', '\n' }) >= 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// System.AppContext.GetData is not available in these frameworks
|
||||||
|
#if !NET451 && !NET452 && !NET46 && !NET461
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.CommandLineUtils
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Utilities for finding the "dotnet.exe" file from the currently running .NET Core application
|
||||||
|
/// </summary>
|
||||||
|
internal static class DotNetMuxer
|
||||||
|
{
|
||||||
|
private const string MuxerName = "dotnet";
|
||||||
|
|
||||||
|
static DotNetMuxer()
|
||||||
|
{
|
||||||
|
MuxerPath = TryFindMuxerPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The full filepath to the .NET Core muxer.
|
||||||
|
/// </summary>
|
||||||
|
public static string MuxerPath { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds the full filepath to the .NET Core muxer,
|
||||||
|
/// or returns a string containing the default name of the .NET Core muxer ('dotnet').
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The path or a string named 'dotnet'.</returns>
|
||||||
|
public static string MuxerPathOrDefault()
|
||||||
|
=> MuxerPath ?? MuxerName;
|
||||||
|
|
||||||
|
private static string TryFindMuxerPath()
|
||||||
|
{
|
||||||
|
var fileName = MuxerName;
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
fileName += ".exe";
|
||||||
|
}
|
||||||
|
|
||||||
|
var mainModule = Process.GetCurrentProcess().MainModule;
|
||||||
|
if (!string.IsNullOrEmpty(mainModule?.FileName)
|
||||||
|
&& Path.GetFileName(mainModule.FileName).Equals(fileName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return mainModule.FileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
// 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
internal class CopyOnWriteDictionary<TKey, TValue> : IDictionary<TKey, TValue>
|
||||||
|
{
|
||||||
|
private readonly IDictionary<TKey, TValue> _sourceDictionary;
|
||||||
|
private readonly IEqualityComparer<TKey> _comparer;
|
||||||
|
private IDictionary<TKey, TValue> _innerDictionary;
|
||||||
|
|
||||||
|
public CopyOnWriteDictionary(
|
||||||
|
IDictionary<TKey, TValue> sourceDictionary,
|
||||||
|
IEqualityComparer<TKey> comparer)
|
||||||
|
{
|
||||||
|
if (sourceDictionary == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(sourceDictionary));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comparer == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(comparer));
|
||||||
|
}
|
||||||
|
|
||||||
|
_sourceDictionary = sourceDictionary;
|
||||||
|
_comparer = comparer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IDictionary<TKey, TValue> ReadDictionary
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _innerDictionary ?? _sourceDictionary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IDictionary<TKey, TValue> WriteDictionary
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_innerDictionary == null)
|
||||||
|
{
|
||||||
|
_innerDictionary = new Dictionary<TKey, TValue>(_sourceDictionary,
|
||||||
|
_comparer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _innerDictionary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual ICollection<TKey> Keys
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return ReadDictionary.Keys;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual ICollection<TValue> Values
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return ReadDictionary.Values;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual int Count
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return ReadDictionary.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool IsReadOnly
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual TValue this[TKey key]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return ReadDictionary[key];
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
WriteDictionary[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool ContainsKey(TKey key)
|
||||||
|
{
|
||||||
|
return ReadDictionary.ContainsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Add(TKey key, TValue value)
|
||||||
|
{
|
||||||
|
WriteDictionary.Add(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool Remove(TKey key)
|
||||||
|
{
|
||||||
|
return WriteDictionary.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool TryGetValue(TKey key, out TValue value)
|
||||||
|
{
|
||||||
|
return ReadDictionary.TryGetValue(key, out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Add(KeyValuePair<TKey, TValue> item)
|
||||||
|
{
|
||||||
|
WriteDictionary.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Clear()
|
||||||
|
{
|
||||||
|
WriteDictionary.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool Contains(KeyValuePair<TKey, TValue> item)
|
||||||
|
{
|
||||||
|
return ReadDictionary.Contains(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
|
||||||
|
{
|
||||||
|
ReadDictionary.CopyTo(array, arrayIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Remove(KeyValuePair<TKey, TValue> item)
|
||||||
|
{
|
||||||
|
return WriteDictionary.Remove(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
|
||||||
|
{
|
||||||
|
return ReadDictionary.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
internal struct CopyOnWriteDictionaryHolder<TKey, TValue>
|
||||||
|
{
|
||||||
|
private readonly Dictionary<TKey, TValue> _source;
|
||||||
|
private Dictionary<TKey, TValue> _copy;
|
||||||
|
|
||||||
|
public CopyOnWriteDictionaryHolder(Dictionary<TKey, TValue> source)
|
||||||
|
{
|
||||||
|
if (source == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
_source = source;
|
||||||
|
_copy = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CopyOnWriteDictionaryHolder(CopyOnWriteDictionaryHolder<TKey, TValue> source)
|
||||||
|
{
|
||||||
|
_source = source._copy ?? source._source;
|
||||||
|
_copy = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasBeenCopied => _copy != null;
|
||||||
|
|
||||||
|
public Dictionary<TKey, TValue> ReadDictionary
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_copy != null)
|
||||||
|
{
|
||||||
|
return _copy;
|
||||||
|
}
|
||||||
|
else if (_source != null)
|
||||||
|
{
|
||||||
|
return _source;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Default-Constructor case
|
||||||
|
_copy = new Dictionary<TKey, TValue>();
|
||||||
|
return _copy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<TKey, TValue> WriteDictionary
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_copy == null && _source == null)
|
||||||
|
{
|
||||||
|
// Default-Constructor case
|
||||||
|
_copy = new Dictionary<TKey, TValue>();
|
||||||
|
}
|
||||||
|
else if (_copy == null)
|
||||||
|
{
|
||||||
|
_copy = new Dictionary<TKey, TValue>(_source, _source.Comparer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _copy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<TKey, TValue>.KeyCollection Keys
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return ReadDictionary.Keys;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<TKey, TValue>.ValueCollection Values
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return ReadDictionary.Values;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return ReadDictionary.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsReadOnly
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TValue this[TKey key]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return ReadDictionary[key];
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
WriteDictionary[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ContainsKey(TKey key)
|
||||||
|
{
|
||||||
|
return ReadDictionary.ContainsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(TKey key, TValue value)
|
||||||
|
{
|
||||||
|
WriteDictionary.Add(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Remove(TKey key)
|
||||||
|
{
|
||||||
|
return WriteDictionary.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetValue(TKey key, out TValue value)
|
||||||
|
{
|
||||||
|
return ReadDictionary.TryGetValue(key, out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(KeyValuePair<TKey, TValue> item)
|
||||||
|
{
|
||||||
|
((ICollection<KeyValuePair<TKey, TValue>>)WriteDictionary).Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
WriteDictionary.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Contains(KeyValuePair<TKey, TValue> item)
|
||||||
|
{
|
||||||
|
return ((ICollection<KeyValuePair<TKey, TValue>>)ReadDictionary).Contains(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
|
||||||
|
{
|
||||||
|
((ICollection<KeyValuePair<TKey, TValue>>)ReadDictionary).CopyTo(array, arrayIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Remove(KeyValuePair<TKey, TValue> item)
|
||||||
|
{
|
||||||
|
return ((ICollection<KeyValuePair<TKey, TValue>>)WriteDictionary).Remove(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<TKey, TValue>.Enumerator GetEnumerator()
|
||||||
|
{
|
||||||
|
return ReadDictionary.GetEnumerator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
// 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.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
internal struct HashCodeCombiner
|
||||||
|
{
|
||||||
|
private long _combinedHash64;
|
||||||
|
|
||||||
|
public int CombinedHash
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
get { return _combinedHash64.GetHashCode(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private HashCodeCombiner(long seed)
|
||||||
|
{
|
||||||
|
_combinedHash64 = seed;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void Add(IEnumerable e)
|
||||||
|
{
|
||||||
|
if (e == null)
|
||||||
|
{
|
||||||
|
Add(0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var count = 0;
|
||||||
|
foreach (object o in e)
|
||||||
|
{
|
||||||
|
Add(o);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
Add(count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static implicit operator int(HashCodeCombiner self)
|
||||||
|
{
|
||||||
|
return self.CombinedHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void Add(int i)
|
||||||
|
{
|
||||||
|
_combinedHash64 = ((_combinedHash64 << 5) + _combinedHash64) ^ i;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void Add(string s)
|
||||||
|
{
|
||||||
|
var hashCode = (s != null) ? s.GetHashCode() : 0;
|
||||||
|
Add(hashCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void Add(object o)
|
||||||
|
{
|
||||||
|
var hashCode = (o != null) ? o.GetHashCode() : 0;
|
||||||
|
Add(hashCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void Add<TValue>(TValue value, IEqualityComparer<TValue> comparer)
|
||||||
|
{
|
||||||
|
var hashCode = value != null ? comparer.GetHashCode(value) : 0;
|
||||||
|
Add(hashCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static HashCodeCombiner Start()
|
||||||
|
{
|
||||||
|
return new HashCodeCombiner(0x1505L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
// 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.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
internal struct AwaitableInfo
|
||||||
|
{
|
||||||
|
public Type AwaiterType { get; }
|
||||||
|
public PropertyInfo AwaiterIsCompletedProperty { get; }
|
||||||
|
public MethodInfo AwaiterGetResultMethod { get; }
|
||||||
|
public MethodInfo AwaiterOnCompletedMethod { get; }
|
||||||
|
public MethodInfo AwaiterUnsafeOnCompletedMethod { get; }
|
||||||
|
public Type ResultType { get; }
|
||||||
|
public MethodInfo GetAwaiterMethod { get; }
|
||||||
|
|
||||||
|
public AwaitableInfo(
|
||||||
|
Type awaiterType,
|
||||||
|
PropertyInfo awaiterIsCompletedProperty,
|
||||||
|
MethodInfo awaiterGetResultMethod,
|
||||||
|
MethodInfo awaiterOnCompletedMethod,
|
||||||
|
MethodInfo awaiterUnsafeOnCompletedMethod,
|
||||||
|
Type resultType,
|
||||||
|
MethodInfo getAwaiterMethod)
|
||||||
|
{
|
||||||
|
AwaiterType = awaiterType;
|
||||||
|
AwaiterIsCompletedProperty = awaiterIsCompletedProperty;
|
||||||
|
AwaiterGetResultMethod = awaiterGetResultMethod;
|
||||||
|
AwaiterOnCompletedMethod = awaiterOnCompletedMethod;
|
||||||
|
AwaiterUnsafeOnCompletedMethod = awaiterUnsafeOnCompletedMethod;
|
||||||
|
ResultType = resultType;
|
||||||
|
GetAwaiterMethod = getAwaiterMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsTypeAwaitable(Type type, out AwaitableInfo awaitableInfo)
|
||||||
|
{
|
||||||
|
// Based on Roslyn code: http://source.roslyn.io/#Microsoft.CodeAnalysis.Workspaces/Shared/Extensions/ISymbolExtensions.cs,db4d48ba694b9347
|
||||||
|
|
||||||
|
// Awaitable must have method matching "object GetAwaiter()"
|
||||||
|
var getAwaiterMethod = type.GetRuntimeMethods().FirstOrDefault(m =>
|
||||||
|
m.Name.Equals("GetAwaiter", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& m.GetParameters().Length == 0
|
||||||
|
&& m.ReturnType != null);
|
||||||
|
if (getAwaiterMethod == null)
|
||||||
|
{
|
||||||
|
awaitableInfo = default(AwaitableInfo);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var awaiterType = getAwaiterMethod.ReturnType;
|
||||||
|
|
||||||
|
// Awaiter must have property matching "bool IsCompleted { get; }"
|
||||||
|
var isCompletedProperty = awaiterType.GetRuntimeProperties().FirstOrDefault(p =>
|
||||||
|
p.Name.Equals("IsCompleted", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& p.PropertyType == typeof(bool)
|
||||||
|
&& p.GetMethod != null);
|
||||||
|
if (isCompletedProperty == null)
|
||||||
|
{
|
||||||
|
awaitableInfo = default(AwaitableInfo);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Awaiter must implement INotifyCompletion
|
||||||
|
var awaiterInterfaces = awaiterType.GetInterfaces();
|
||||||
|
var implementsINotifyCompletion = awaiterInterfaces.Any(t => t == typeof(INotifyCompletion));
|
||||||
|
if (!implementsINotifyCompletion)
|
||||||
|
{
|
||||||
|
awaitableInfo = default(AwaitableInfo);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INotifyCompletion supplies a method matching "void OnCompleted(Action action)"
|
||||||
|
var iNotifyCompletionMap = awaiterType
|
||||||
|
.GetTypeInfo()
|
||||||
|
.GetRuntimeInterfaceMap(typeof(INotifyCompletion));
|
||||||
|
var onCompletedMethod = iNotifyCompletionMap.InterfaceMethods.Single(m =>
|
||||||
|
m.Name.Equals("OnCompleted", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& m.ReturnType == typeof(void)
|
||||||
|
&& m.GetParameters().Length == 1
|
||||||
|
&& m.GetParameters()[0].ParameterType == typeof(Action));
|
||||||
|
|
||||||
|
// Awaiter optionally implements ICriticalNotifyCompletion
|
||||||
|
var implementsICriticalNotifyCompletion = awaiterInterfaces.Any(t => t == typeof(ICriticalNotifyCompletion));
|
||||||
|
MethodInfo unsafeOnCompletedMethod;
|
||||||
|
if (implementsICriticalNotifyCompletion)
|
||||||
|
{
|
||||||
|
// ICriticalNotifyCompletion supplies a method matching "void UnsafeOnCompleted(Action action)"
|
||||||
|
var iCriticalNotifyCompletionMap = awaiterType
|
||||||
|
.GetTypeInfo()
|
||||||
|
.GetRuntimeInterfaceMap(typeof(ICriticalNotifyCompletion));
|
||||||
|
unsafeOnCompletedMethod = iCriticalNotifyCompletionMap.InterfaceMethods.Single(m =>
|
||||||
|
m.Name.Equals("UnsafeOnCompleted", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& m.ReturnType == typeof(void)
|
||||||
|
&& m.GetParameters().Length == 1
|
||||||
|
&& m.GetParameters()[0].ParameterType == typeof(Action));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
unsafeOnCompletedMethod = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Awaiter must have method matching "void GetResult" or "T GetResult()"
|
||||||
|
var getResultMethod = awaiterType.GetRuntimeMethods().FirstOrDefault(m =>
|
||||||
|
m.Name.Equals("GetResult")
|
||||||
|
&& m.GetParameters().Length == 0);
|
||||||
|
if (getResultMethod == null)
|
||||||
|
{
|
||||||
|
awaitableInfo = default(AwaitableInfo);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
awaitableInfo = new AwaitableInfo(
|
||||||
|
awaiterType,
|
||||||
|
isCompletedProperty,
|
||||||
|
getResultMethod,
|
||||||
|
onCompletedMethod,
|
||||||
|
unsafeOnCompletedMethod,
|
||||||
|
getResultMethod.ReturnType,
|
||||||
|
getAwaiterMethod);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
// 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.Expressions;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
internal struct CoercedAwaitableInfo
|
||||||
|
{
|
||||||
|
public AwaitableInfo AwaitableInfo { get; }
|
||||||
|
public Expression CoercerExpression { get; }
|
||||||
|
public Type CoercerResultType { get; }
|
||||||
|
public bool RequiresCoercion => CoercerExpression != null;
|
||||||
|
|
||||||
|
public CoercedAwaitableInfo(AwaitableInfo awaitableInfo)
|
||||||
|
{
|
||||||
|
AwaitableInfo = awaitableInfo;
|
||||||
|
CoercerExpression = null;
|
||||||
|
CoercerResultType = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CoercedAwaitableInfo(Expression coercerExpression, Type coercerResultType, AwaitableInfo coercedAwaitableInfo)
|
||||||
|
{
|
||||||
|
CoercerExpression = coercerExpression;
|
||||||
|
CoercerResultType = coercerResultType;
|
||||||
|
AwaitableInfo = coercedAwaitableInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsTypeAwaitable(Type type, out CoercedAwaitableInfo info)
|
||||||
|
{
|
||||||
|
if (AwaitableInfo.IsTypeAwaitable(type, out var directlyAwaitableInfo))
|
||||||
|
{
|
||||||
|
info = new CoercedAwaitableInfo(directlyAwaitableInfo);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's not directly awaitable, but maybe we can coerce it.
|
||||||
|
// Currently we support coercing FSharpAsync<T>.
|
||||||
|
if (ObjectMethodExecutorFSharpSupport.TryBuildCoercerFromFSharpAsyncToAwaitable(type,
|
||||||
|
out var coercerExpression,
|
||||||
|
out var coercerResultType))
|
||||||
|
{
|
||||||
|
if (AwaitableInfo.IsTypeAwaitable(coercerResultType, out var coercedAwaitableInfo))
|
||||||
|
{
|
||||||
|
info = new CoercedAwaitableInfo(coercerExpression, coercerResultType, coercedAwaitableInfo);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info = default(CoercedAwaitableInfo);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,340 @@
|
||||||
|
// 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.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
internal class ObjectMethodExecutor
|
||||||
|
{
|
||||||
|
private readonly object[] _parameterDefaultValues;
|
||||||
|
private readonly MethodExecutorAsync _executorAsync;
|
||||||
|
private readonly MethodExecutor _executor;
|
||||||
|
|
||||||
|
private static readonly ConstructorInfo _objectMethodExecutorAwaitableConstructor =
|
||||||
|
typeof(ObjectMethodExecutorAwaitable).GetConstructor(new[] {
|
||||||
|
typeof(object), // customAwaitable
|
||||||
|
typeof(Func<object, object>), // getAwaiterMethod
|
||||||
|
typeof(Func<object, bool>), // isCompletedMethod
|
||||||
|
typeof(Func<object, object>), // getResultMethod
|
||||||
|
typeof(Action<object, Action>), // onCompletedMethod
|
||||||
|
typeof(Action<object, Action>) // unsafeOnCompletedMethod
|
||||||
|
});
|
||||||
|
|
||||||
|
private ObjectMethodExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo, object[] parameterDefaultValues)
|
||||||
|
{
|
||||||
|
if (methodInfo == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(methodInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
MethodInfo = methodInfo;
|
||||||
|
MethodParameters = methodInfo.GetParameters();
|
||||||
|
TargetTypeInfo = targetTypeInfo;
|
||||||
|
MethodReturnType = methodInfo.ReturnType;
|
||||||
|
|
||||||
|
var isAwaitable = CoercedAwaitableInfo.IsTypeAwaitable(MethodReturnType, out var coercedAwaitableInfo);
|
||||||
|
|
||||||
|
IsMethodAsync = isAwaitable;
|
||||||
|
AsyncResultType = isAwaitable ? coercedAwaitableInfo.AwaitableInfo.ResultType : null;
|
||||||
|
|
||||||
|
// Upstream code may prefer to use the sync-executor even for async methods, because if it knows
|
||||||
|
// that the result is a specific Task<T> where T is known, then it can directly cast to that type
|
||||||
|
// and await it without the extra heap allocations involved in the _executorAsync code path.
|
||||||
|
_executor = GetExecutor(methodInfo, targetTypeInfo);
|
||||||
|
|
||||||
|
if (IsMethodAsync)
|
||||||
|
{
|
||||||
|
_executorAsync = GetExecutorAsync(methodInfo, targetTypeInfo, coercedAwaitableInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
_parameterDefaultValues = parameterDefaultValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
private delegate ObjectMethodExecutorAwaitable MethodExecutorAsync(object target, object[] parameters);
|
||||||
|
|
||||||
|
private delegate object MethodExecutor(object target, object[] parameters);
|
||||||
|
|
||||||
|
private delegate void VoidMethodExecutor(object target, object[] parameters);
|
||||||
|
|
||||||
|
public MethodInfo MethodInfo { get; }
|
||||||
|
|
||||||
|
public ParameterInfo[] MethodParameters { get; }
|
||||||
|
|
||||||
|
public TypeInfo TargetTypeInfo { get; }
|
||||||
|
|
||||||
|
public Type AsyncResultType { get; }
|
||||||
|
|
||||||
|
// This field is made internal set because it is set in unit tests.
|
||||||
|
public Type MethodReturnType { get; internal set; }
|
||||||
|
|
||||||
|
public bool IsMethodAsync { get; }
|
||||||
|
|
||||||
|
public static ObjectMethodExecutor Create(MethodInfo methodInfo, TypeInfo targetTypeInfo)
|
||||||
|
{
|
||||||
|
return new ObjectMethodExecutor(methodInfo, targetTypeInfo, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ObjectMethodExecutor Create(MethodInfo methodInfo, TypeInfo targetTypeInfo, object[] parameterDefaultValues)
|
||||||
|
{
|
||||||
|
if (parameterDefaultValues == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(parameterDefaultValues));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ObjectMethodExecutor(methodInfo, targetTypeInfo, parameterDefaultValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the configured method on <paramref name="target"/>. This can be used whether or not
|
||||||
|
/// the configured method is asynchronous.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Even if the target method is asynchronous, it's desirable to invoke it using Execute rather than
|
||||||
|
/// ExecuteAsync if you know at compile time what the return type is, because then you can directly
|
||||||
|
/// "await" that value (via a cast), and then the generated code will be able to reference the
|
||||||
|
/// resulting awaitable as a value-typed variable. If you use ExecuteAsync instead, the generated
|
||||||
|
/// code will have to treat the resulting awaitable as a boxed object, because it doesn't know at
|
||||||
|
/// compile time what type it would be.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="target">The object whose method is to be executed.</param>
|
||||||
|
/// <param name="parameters">Parameters to pass to the method.</param>
|
||||||
|
/// <returns>The method return value.</returns>
|
||||||
|
public object Execute(object target, object[] parameters)
|
||||||
|
{
|
||||||
|
return _executor(target, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the configured method on <paramref name="target"/>. This can only be used if the configured
|
||||||
|
/// method is asynchronous.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// If you don't know at compile time the type of the method's returned awaitable, you can use ExecuteAsync,
|
||||||
|
/// which supplies an awaitable-of-object. This always works, but can incur several extra heap allocations
|
||||||
|
/// as compared with using Execute and then using "await" on the result value typecasted to the known
|
||||||
|
/// awaitable type. The possible extra heap allocations are for:
|
||||||
|
///
|
||||||
|
/// 1. The custom awaitable (though usually there's a heap allocation for this anyway, since normally
|
||||||
|
/// it's a reference type, and you normally create a new instance per call).
|
||||||
|
/// 2. The custom awaiter (whether or not it's a value type, since if it's not, you need a new instance
|
||||||
|
/// of it, and if it is, it will have to be boxed so the calling code can reference it as an object).
|
||||||
|
/// 3. The async result value, if it's a value type (it has to be boxed as an object, since the calling
|
||||||
|
/// code doesn't know what type it's going to be).
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="target">The object whose method is to be executed.</param>
|
||||||
|
/// <param name="parameters">Parameters to pass to the method.</param>
|
||||||
|
/// <returns>An object that you can "await" to get the method return value.</returns>
|
||||||
|
public ObjectMethodExecutorAwaitable ExecuteAsync(object target, object[] parameters)
|
||||||
|
{
|
||||||
|
return _executorAsync(target, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object GetDefaultValueForParameter(int index)
|
||||||
|
{
|
||||||
|
if (_parameterDefaultValues == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Cannot call {nameof(GetDefaultValueForParameter)}, because no parameter default values were supplied.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index > MethodParameters.Length - 1)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
return _parameterDefaultValues[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodExecutor GetExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo)
|
||||||
|
{
|
||||||
|
// Parameters to executor
|
||||||
|
var targetParameter = Expression.Parameter(typeof(object), "target");
|
||||||
|
var parametersParameter = Expression.Parameter(typeof(object[]), "parameters");
|
||||||
|
|
||||||
|
// Build parameter list
|
||||||
|
var parameters = new List<Expression>();
|
||||||
|
var paramInfos = methodInfo.GetParameters();
|
||||||
|
for (int i = 0; i < paramInfos.Length; i++)
|
||||||
|
{
|
||||||
|
var paramInfo = paramInfos[i];
|
||||||
|
var valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i));
|
||||||
|
var valueCast = Expression.Convert(valueObj, paramInfo.ParameterType);
|
||||||
|
|
||||||
|
// valueCast is "(Ti) parameters[i]"
|
||||||
|
parameters.Add(valueCast);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call method
|
||||||
|
var instanceCast = Expression.Convert(targetParameter, targetTypeInfo.AsType());
|
||||||
|
var methodCall = Expression.Call(instanceCast, methodInfo, parameters);
|
||||||
|
|
||||||
|
// methodCall is "((Ttarget) target) method((T0) parameters[0], (T1) parameters[1], ...)"
|
||||||
|
// Create function
|
||||||
|
if (methodCall.Type == typeof(void))
|
||||||
|
{
|
||||||
|
var lambda = Expression.Lambda<VoidMethodExecutor>(methodCall, targetParameter, parametersParameter);
|
||||||
|
var voidExecutor = lambda.Compile();
|
||||||
|
return WrapVoidMethod(voidExecutor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// must coerce methodCall to match ActionExecutor signature
|
||||||
|
var castMethodCall = Expression.Convert(methodCall, typeof(object));
|
||||||
|
var lambda = Expression.Lambda<MethodExecutor>(castMethodCall, targetParameter, parametersParameter);
|
||||||
|
return lambda.Compile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodExecutor WrapVoidMethod(VoidMethodExecutor executor)
|
||||||
|
{
|
||||||
|
return delegate (object target, object[] parameters)
|
||||||
|
{
|
||||||
|
executor(target, parameters);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodExecutorAsync GetExecutorAsync(
|
||||||
|
MethodInfo methodInfo,
|
||||||
|
TypeInfo targetTypeInfo,
|
||||||
|
CoercedAwaitableInfo coercedAwaitableInfo)
|
||||||
|
{
|
||||||
|
// Parameters to executor
|
||||||
|
var targetParameter = Expression.Parameter(typeof(object), "target");
|
||||||
|
var parametersParameter = Expression.Parameter(typeof(object[]), "parameters");
|
||||||
|
|
||||||
|
// Build parameter list
|
||||||
|
var parameters = new List<Expression>();
|
||||||
|
var paramInfos = methodInfo.GetParameters();
|
||||||
|
for (int i = 0; i < paramInfos.Length; i++)
|
||||||
|
{
|
||||||
|
var paramInfo = paramInfos[i];
|
||||||
|
var valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i));
|
||||||
|
var valueCast = Expression.Convert(valueObj, paramInfo.ParameterType);
|
||||||
|
|
||||||
|
// valueCast is "(Ti) parameters[i]"
|
||||||
|
parameters.Add(valueCast);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call method
|
||||||
|
var instanceCast = Expression.Convert(targetParameter, targetTypeInfo.AsType());
|
||||||
|
var methodCall = Expression.Call(instanceCast, methodInfo, parameters);
|
||||||
|
|
||||||
|
// Using the method return value, construct an ObjectMethodExecutorAwaitable based on
|
||||||
|
// the info we have about its implementation of the awaitable pattern. Note that all
|
||||||
|
// the funcs/actions we construct here are precompiled, so that only one instance of
|
||||||
|
// each is preserved throughout the lifetime of the ObjectMethodExecutor.
|
||||||
|
|
||||||
|
// var getAwaiterFunc = (object awaitable) =>
|
||||||
|
// (object)((CustomAwaitableType)awaitable).GetAwaiter();
|
||||||
|
var customAwaitableParam = Expression.Parameter(typeof(object), "awaitable");
|
||||||
|
var awaitableInfo = coercedAwaitableInfo.AwaitableInfo;
|
||||||
|
var postCoercionMethodReturnType = coercedAwaitableInfo.CoercerResultType ?? methodInfo.ReturnType;
|
||||||
|
var getAwaiterFunc = Expression.Lambda<Func<object, object>>(
|
||||||
|
Expression.Convert(
|
||||||
|
Expression.Call(
|
||||||
|
Expression.Convert(customAwaitableParam, postCoercionMethodReturnType),
|
||||||
|
awaitableInfo.GetAwaiterMethod),
|
||||||
|
typeof(object)),
|
||||||
|
customAwaitableParam).Compile();
|
||||||
|
|
||||||
|
// var isCompletedFunc = (object awaiter) =>
|
||||||
|
// ((CustomAwaiterType)awaiter).IsCompleted;
|
||||||
|
var isCompletedParam = Expression.Parameter(typeof(object), "awaiter");
|
||||||
|
var isCompletedFunc = Expression.Lambda<Func<object, bool>>(
|
||||||
|
Expression.MakeMemberAccess(
|
||||||
|
Expression.Convert(isCompletedParam, awaitableInfo.AwaiterType),
|
||||||
|
awaitableInfo.AwaiterIsCompletedProperty),
|
||||||
|
isCompletedParam).Compile();
|
||||||
|
|
||||||
|
var getResultParam = Expression.Parameter(typeof(object), "awaiter");
|
||||||
|
Func<object, object> getResultFunc;
|
||||||
|
if (awaitableInfo.ResultType == typeof(void))
|
||||||
|
{
|
||||||
|
// var getResultFunc = (object awaiter) =>
|
||||||
|
// {
|
||||||
|
// ((CustomAwaiterType)awaiter).GetResult(); // We need to invoke this to surface any exceptions
|
||||||
|
// return (object)null;
|
||||||
|
// };
|
||||||
|
getResultFunc = Expression.Lambda<Func<object, object>>(
|
||||||
|
Expression.Block(
|
||||||
|
Expression.Call(
|
||||||
|
Expression.Convert(getResultParam, awaitableInfo.AwaiterType),
|
||||||
|
awaitableInfo.AwaiterGetResultMethod),
|
||||||
|
Expression.Constant(null)
|
||||||
|
),
|
||||||
|
getResultParam).Compile();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// var getResultFunc = (object awaiter) =>
|
||||||
|
// (object)((CustomAwaiterType)awaiter).GetResult();
|
||||||
|
getResultFunc = Expression.Lambda<Func<object, object>>(
|
||||||
|
Expression.Convert(
|
||||||
|
Expression.Call(
|
||||||
|
Expression.Convert(getResultParam, awaitableInfo.AwaiterType),
|
||||||
|
awaitableInfo.AwaiterGetResultMethod),
|
||||||
|
typeof(object)),
|
||||||
|
getResultParam).Compile();
|
||||||
|
}
|
||||||
|
|
||||||
|
// var onCompletedFunc = (object awaiter, Action continuation) => {
|
||||||
|
// ((CustomAwaiterType)awaiter).OnCompleted(continuation);
|
||||||
|
// };
|
||||||
|
var onCompletedParam1 = Expression.Parameter(typeof(object), "awaiter");
|
||||||
|
var onCompletedParam2 = Expression.Parameter(typeof(Action), "continuation");
|
||||||
|
var onCompletedFunc = Expression.Lambda<Action<object, Action>>(
|
||||||
|
Expression.Call(
|
||||||
|
Expression.Convert(onCompletedParam1, awaitableInfo.AwaiterType),
|
||||||
|
awaitableInfo.AwaiterOnCompletedMethod,
|
||||||
|
onCompletedParam2),
|
||||||
|
onCompletedParam1,
|
||||||
|
onCompletedParam2).Compile();
|
||||||
|
|
||||||
|
Action<object, Action> unsafeOnCompletedFunc = null;
|
||||||
|
if (awaitableInfo.AwaiterUnsafeOnCompletedMethod != null)
|
||||||
|
{
|
||||||
|
// var unsafeOnCompletedFunc = (object awaiter, Action continuation) => {
|
||||||
|
// ((CustomAwaiterType)awaiter).UnsafeOnCompleted(continuation);
|
||||||
|
// };
|
||||||
|
var unsafeOnCompletedParam1 = Expression.Parameter(typeof(object), "awaiter");
|
||||||
|
var unsafeOnCompletedParam2 = Expression.Parameter(typeof(Action), "continuation");
|
||||||
|
unsafeOnCompletedFunc = Expression.Lambda<Action<object, Action>>(
|
||||||
|
Expression.Call(
|
||||||
|
Expression.Convert(unsafeOnCompletedParam1, awaitableInfo.AwaiterType),
|
||||||
|
awaitableInfo.AwaiterUnsafeOnCompletedMethod,
|
||||||
|
unsafeOnCompletedParam2),
|
||||||
|
unsafeOnCompletedParam1,
|
||||||
|
unsafeOnCompletedParam2).Compile();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we need to pass the method call result through a coercer function to get an
|
||||||
|
// awaitable, then do so.
|
||||||
|
var coercedMethodCall = coercedAwaitableInfo.RequiresCoercion
|
||||||
|
? Expression.Invoke(coercedAwaitableInfo.CoercerExpression, methodCall)
|
||||||
|
: (Expression)methodCall;
|
||||||
|
|
||||||
|
// return new ObjectMethodExecutorAwaitable(
|
||||||
|
// (object)coercedMethodCall,
|
||||||
|
// getAwaiterFunc,
|
||||||
|
// isCompletedFunc,
|
||||||
|
// getResultFunc,
|
||||||
|
// onCompletedFunc,
|
||||||
|
// unsafeOnCompletedFunc);
|
||||||
|
var returnValueExpression = Expression.New(
|
||||||
|
_objectMethodExecutorAwaitableConstructor,
|
||||||
|
Expression.Convert(coercedMethodCall, typeof(object)),
|
||||||
|
Expression.Constant(getAwaiterFunc),
|
||||||
|
Expression.Constant(isCompletedFunc),
|
||||||
|
Expression.Constant(getResultFunc),
|
||||||
|
Expression.Constant(onCompletedFunc),
|
||||||
|
Expression.Constant(unsafeOnCompletedFunc, typeof(Action<object, Action>)));
|
||||||
|
|
||||||
|
var lambda = Expression.Lambda<MethodExecutorAsync>(returnValueExpression, targetParameter, parametersParameter);
|
||||||
|
return lambda.Compile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
// 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.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a common awaitable structure that <see cref="ObjectMethodExecutor.ExecuteAsync"/> can
|
||||||
|
/// return, regardless of whether the underlying value is a System.Task, an FSharpAsync, or an
|
||||||
|
/// application-defined custom awaitable.
|
||||||
|
/// </summary>
|
||||||
|
internal struct ObjectMethodExecutorAwaitable
|
||||||
|
{
|
||||||
|
private readonly object _customAwaitable;
|
||||||
|
private readonly Func<object, object> _getAwaiterMethod;
|
||||||
|
private readonly Func<object, bool> _isCompletedMethod;
|
||||||
|
private readonly Func<object, object> _getResultMethod;
|
||||||
|
private readonly Action<object, Action> _onCompletedMethod;
|
||||||
|
private readonly Action<object, Action> _unsafeOnCompletedMethod;
|
||||||
|
|
||||||
|
// Perf note: since we're requiring the customAwaitable to be supplied here as an object,
|
||||||
|
// this will trigger a further allocation if it was a value type (i.e., to box it). We can't
|
||||||
|
// fix this by making the customAwaitable type generic, because the calling code typically
|
||||||
|
// does not know the type of the awaitable/awaiter at compile-time anyway.
|
||||||
|
//
|
||||||
|
// However, we could fix it by not passing the customAwaitable here at all, and instead
|
||||||
|
// passing a func that maps directly from the target object (e.g., controller instance),
|
||||||
|
// target method (e.g., action method info), and params array to the custom awaiter in the
|
||||||
|
// GetAwaiter() method below. In effect, by delaying the actual method call until the
|
||||||
|
// upstream code calls GetAwaiter on this ObjectMethodExecutorAwaitable instance.
|
||||||
|
// This optimization is not currently implemented because:
|
||||||
|
// [1] It would make no difference when the awaitable was an object type, which is
|
||||||
|
// by far the most common scenario (e.g., System.Task<T>).
|
||||||
|
// [2] It would be complex - we'd need some kind of object pool to track all the parameter
|
||||||
|
// arrays until we needed to use them in GetAwaiter().
|
||||||
|
// We can reconsider this in the future if there's a need to optimize for ValueTask<T>
|
||||||
|
// or other value-typed awaitables.
|
||||||
|
|
||||||
|
public ObjectMethodExecutorAwaitable(
|
||||||
|
object customAwaitable,
|
||||||
|
Func<object, object> getAwaiterMethod,
|
||||||
|
Func<object, bool> isCompletedMethod,
|
||||||
|
Func<object, object> getResultMethod,
|
||||||
|
Action<object, Action> onCompletedMethod,
|
||||||
|
Action<object, Action> unsafeOnCompletedMethod)
|
||||||
|
{
|
||||||
|
_customAwaitable = customAwaitable;
|
||||||
|
_getAwaiterMethod = getAwaiterMethod;
|
||||||
|
_isCompletedMethod = isCompletedMethod;
|
||||||
|
_getResultMethod = getResultMethod;
|
||||||
|
_onCompletedMethod = onCompletedMethod;
|
||||||
|
_unsafeOnCompletedMethod = unsafeOnCompletedMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Awaiter GetAwaiter()
|
||||||
|
{
|
||||||
|
var customAwaiter = _getAwaiterMethod(_customAwaitable);
|
||||||
|
return new Awaiter(customAwaiter, _isCompletedMethod, _getResultMethod, _onCompletedMethod, _unsafeOnCompletedMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Awaiter : ICriticalNotifyCompletion
|
||||||
|
{
|
||||||
|
private readonly object _customAwaiter;
|
||||||
|
private readonly Func<object, bool> _isCompletedMethod;
|
||||||
|
private readonly Func<object, object> _getResultMethod;
|
||||||
|
private readonly Action<object, Action> _onCompletedMethod;
|
||||||
|
private readonly Action<object, Action> _unsafeOnCompletedMethod;
|
||||||
|
|
||||||
|
public Awaiter(
|
||||||
|
object customAwaiter,
|
||||||
|
Func<object, bool> isCompletedMethod,
|
||||||
|
Func<object, object> getResultMethod,
|
||||||
|
Action<object, Action> onCompletedMethod,
|
||||||
|
Action<object, Action> unsafeOnCompletedMethod)
|
||||||
|
{
|
||||||
|
_customAwaiter = customAwaiter;
|
||||||
|
_isCompletedMethod = isCompletedMethod;
|
||||||
|
_getResultMethod = getResultMethod;
|
||||||
|
_onCompletedMethod = onCompletedMethod;
|
||||||
|
_unsafeOnCompletedMethod = unsafeOnCompletedMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsCompleted => _isCompletedMethod(_customAwaiter);
|
||||||
|
|
||||||
|
public object GetResult() => _getResultMethod(_customAwaiter);
|
||||||
|
|
||||||
|
public void OnCompleted(Action continuation)
|
||||||
|
{
|
||||||
|
_onCompletedMethod(_customAwaiter, continuation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UnsafeOnCompleted(Action continuation)
|
||||||
|
{
|
||||||
|
// If the underlying awaitable implements ICriticalNotifyCompletion, use its UnsafeOnCompleted.
|
||||||
|
// If not, fall back on using its OnCompleted.
|
||||||
|
//
|
||||||
|
// Why this is safe:
|
||||||
|
// - Implementing ICriticalNotifyCompletion is a way of saying the caller can choose whether it
|
||||||
|
// needs the execution context to be preserved (which it signals by calling OnCompleted), or
|
||||||
|
// that it doesn't (which it signals by calling UnsafeOnCompleted). Obviously it's faster *not*
|
||||||
|
// to preserve and restore the context, so we prefer that where possible.
|
||||||
|
// - If a caller doesn't need the execution context to be preserved and hence calls UnsafeOnCompleted,
|
||||||
|
// there's no harm in preserving it anyway - it's just a bit of wasted cost. That's what will happen
|
||||||
|
// if a caller sees that the proxy implements ICriticalNotifyCompletion but the proxy chooses to
|
||||||
|
// pass the call on to the underlying awaitable's OnCompleted method.
|
||||||
|
|
||||||
|
var underlyingMethodToUse = _unsafeOnCompletedMethod ?? _onCompletedMethod;
|
||||||
|
underlyingMethodToUse(_customAwaiter, continuation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
// 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.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Helper for detecting whether a given type is FSharpAsync`1, and if so, supplying
|
||||||
|
/// an <see cref="Expression"/> for mapping instances of that type to a C# awaitable.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The main design goal here is to avoid taking a compile-time dependency on
|
||||||
|
/// FSharp.Core.dll, because non-F# applications wouldn't use it. So all the references
|
||||||
|
/// to FSharp types have to be constructed dynamically at runtime.
|
||||||
|
/// </remarks>
|
||||||
|
internal static class ObjectMethodExecutorFSharpSupport
|
||||||
|
{
|
||||||
|
private static object _fsharpValuesCacheLock = new object();
|
||||||
|
private static Assembly _fsharpCoreAssembly;
|
||||||
|
private static MethodInfo _fsharpAsyncStartAsTaskGenericMethod;
|
||||||
|
private static PropertyInfo _fsharpOptionOfTaskCreationOptionsNoneProperty;
|
||||||
|
private static PropertyInfo _fsharpOptionOfCancellationTokenNoneProperty;
|
||||||
|
|
||||||
|
public static bool TryBuildCoercerFromFSharpAsyncToAwaitable(
|
||||||
|
Type possibleFSharpAsyncType,
|
||||||
|
out Expression coerceToAwaitableExpression,
|
||||||
|
out Type awaitableType)
|
||||||
|
{
|
||||||
|
var methodReturnGenericType = possibleFSharpAsyncType.IsGenericType
|
||||||
|
? possibleFSharpAsyncType.GetGenericTypeDefinition()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!IsFSharpAsyncOpenGenericType(methodReturnGenericType))
|
||||||
|
{
|
||||||
|
coerceToAwaitableExpression = null;
|
||||||
|
awaitableType = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var awaiterResultType = possibleFSharpAsyncType.GetGenericArguments().Single();
|
||||||
|
awaitableType = typeof(Task<>).MakeGenericType(awaiterResultType);
|
||||||
|
|
||||||
|
// coerceToAwaitableExpression = (object fsharpAsync) =>
|
||||||
|
// {
|
||||||
|
// return (object)FSharpAsync.StartAsTask<TResult>(
|
||||||
|
// (Microsoft.FSharp.Control.FSharpAsync<TResult>)fsharpAsync,
|
||||||
|
// FSharpOption<TaskCreationOptions>.None,
|
||||||
|
// FSharpOption<CancellationToken>.None);
|
||||||
|
// };
|
||||||
|
var startAsTaskClosedMethod = _fsharpAsyncStartAsTaskGenericMethod
|
||||||
|
.MakeGenericMethod(awaiterResultType);
|
||||||
|
var coerceToAwaitableParam = Expression.Parameter(typeof(object));
|
||||||
|
coerceToAwaitableExpression = Expression.Lambda(
|
||||||
|
Expression.Convert(
|
||||||
|
Expression.Call(
|
||||||
|
startAsTaskClosedMethod,
|
||||||
|
Expression.Convert(coerceToAwaitableParam, possibleFSharpAsyncType),
|
||||||
|
Expression.MakeMemberAccess(null, _fsharpOptionOfTaskCreationOptionsNoneProperty),
|
||||||
|
Expression.MakeMemberAccess(null, _fsharpOptionOfCancellationTokenNoneProperty)),
|
||||||
|
typeof(object)),
|
||||||
|
coerceToAwaitableParam);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsFSharpAsyncOpenGenericType(Type possibleFSharpAsyncGenericType)
|
||||||
|
{
|
||||||
|
var typeFullName = possibleFSharpAsyncGenericType?.FullName;
|
||||||
|
if (!string.Equals(typeFullName, "Microsoft.FSharp.Control.FSharpAsync`1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_fsharpValuesCacheLock)
|
||||||
|
{
|
||||||
|
if (_fsharpCoreAssembly != null)
|
||||||
|
{
|
||||||
|
// Since we've already found the real FSharpAsync.Core assembly, we just have
|
||||||
|
// to check that the supplied FSharpAsync`1 type is the one from that assembly.
|
||||||
|
return possibleFSharpAsyncGenericType.Assembly == _fsharpCoreAssembly;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We'll keep trying to find the FSharp types/values each time any type called
|
||||||
|
// FSharpAsync`1 is supplied.
|
||||||
|
return TryPopulateFSharpValueCaches(possibleFSharpAsyncGenericType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryPopulateFSharpValueCaches(Type possibleFSharpAsyncGenericType)
|
||||||
|
{
|
||||||
|
var assembly = possibleFSharpAsyncGenericType.Assembly;
|
||||||
|
var fsharpOptionType = assembly.GetType("Microsoft.FSharp.Core.FSharpOption`1");
|
||||||
|
var fsharpAsyncType = assembly.GetType("Microsoft.FSharp.Control.FSharpAsync");
|
||||||
|
|
||||||
|
if (fsharpOptionType == null || fsharpAsyncType == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a reference to FSharpOption<TaskCreationOptions>.None
|
||||||
|
var fsharpOptionOfTaskCreationOptionsType = fsharpOptionType
|
||||||
|
.MakeGenericType(typeof(TaskCreationOptions));
|
||||||
|
_fsharpOptionOfTaskCreationOptionsNoneProperty = fsharpOptionOfTaskCreationOptionsType
|
||||||
|
.GetTypeInfo()
|
||||||
|
.GetRuntimeProperty("None");
|
||||||
|
|
||||||
|
// Get a reference to FSharpOption<CancellationToken>.None
|
||||||
|
var fsharpOptionOfCancellationTokenType = fsharpOptionType
|
||||||
|
.MakeGenericType(typeof(CancellationToken));
|
||||||
|
_fsharpOptionOfCancellationTokenNoneProperty = fsharpOptionOfCancellationTokenType
|
||||||
|
.GetTypeInfo()
|
||||||
|
.GetRuntimeProperty("None");
|
||||||
|
|
||||||
|
// Get a reference to FSharpAsync.StartAsTask<>
|
||||||
|
var fsharpAsyncMethods = fsharpAsyncType
|
||||||
|
.GetRuntimeMethods()
|
||||||
|
.Where(m => m.Name.Equals("StartAsTask", StringComparison.Ordinal));
|
||||||
|
foreach (var candidateMethodInfo in fsharpAsyncMethods)
|
||||||
|
{
|
||||||
|
var parameters = candidateMethodInfo.GetParameters();
|
||||||
|
if (parameters.Length == 3
|
||||||
|
&& TypesHaveSameIdentity(parameters[0].ParameterType, possibleFSharpAsyncGenericType)
|
||||||
|
&& parameters[1].ParameterType == fsharpOptionOfTaskCreationOptionsType
|
||||||
|
&& parameters[2].ParameterType == fsharpOptionOfCancellationTokenType)
|
||||||
|
{
|
||||||
|
// This really does look like the correct method (and hence assembly).
|
||||||
|
_fsharpAsyncStartAsTaskGenericMethod = candidateMethodInfo;
|
||||||
|
_fsharpCoreAssembly = assembly;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _fsharpCoreAssembly != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TypesHaveSameIdentity(Type type1, Type type2)
|
||||||
|
{
|
||||||
|
return type1.Assembly == type2.Assembly
|
||||||
|
&& string.Equals(type1.Namespace, type2.Namespace, StringComparison.Ordinal)
|
||||||
|
&& string.Equals(type1.Name, type2.Name, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
// 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.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
internal static class ProcessExtensions
|
||||||
|
{
|
||||||
|
private static readonly bool _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||||
|
private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
|
public static void KillTree(this Process process)
|
||||||
|
{
|
||||||
|
process.KillTree(_defaultTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void KillTree(this Process process, TimeSpan timeout)
|
||||||
|
{
|
||||||
|
string stdout;
|
||||||
|
if (_isWindows)
|
||||||
|
{
|
||||||
|
RunProcessAndWaitForExit(
|
||||||
|
"taskkill",
|
||||||
|
$"/T /F /PID {process.Id}",
|
||||||
|
timeout,
|
||||||
|
out stdout);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var children = new HashSet<int>();
|
||||||
|
GetAllChildIdsUnix(process.Id, children, timeout);
|
||||||
|
foreach (var childId in children)
|
||||||
|
{
|
||||||
|
KillProcessUnix(childId, timeout);
|
||||||
|
}
|
||||||
|
KillProcessUnix(process.Id, timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void GetAllChildIdsUnix(int parentId, ISet<int> children, TimeSpan timeout)
|
||||||
|
{
|
||||||
|
string stdout;
|
||||||
|
var exitCode = RunProcessAndWaitForExit(
|
||||||
|
"pgrep",
|
||||||
|
$"-P {parentId}",
|
||||||
|
timeout,
|
||||||
|
out stdout);
|
||||||
|
|
||||||
|
if (exitCode == 0 && !string.IsNullOrEmpty(stdout))
|
||||||
|
{
|
||||||
|
using (var reader = new StringReader(stdout))
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var text = reader.ReadLine();
|
||||||
|
if (text == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int id;
|
||||||
|
if (int.TryParse(text, out id))
|
||||||
|
{
|
||||||
|
children.Add(id);
|
||||||
|
// Recursively get the children
|
||||||
|
GetAllChildIdsUnix(id, children, timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void KillProcessUnix(int processId, TimeSpan timeout)
|
||||||
|
{
|
||||||
|
string stdout;
|
||||||
|
RunProcessAndWaitForExit(
|
||||||
|
"kill",
|
||||||
|
$"-TERM {processId}",
|
||||||
|
timeout,
|
||||||
|
out stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string stdout)
|
||||||
|
{
|
||||||
|
var startInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = fileName,
|
||||||
|
Arguments = arguments,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
UseShellExecute = false
|
||||||
|
};
|
||||||
|
|
||||||
|
var process = Process.Start(startInfo);
|
||||||
|
|
||||||
|
stdout = null;
|
||||||
|
if (process.WaitForExit((int)timeout.TotalMilliseconds))
|
||||||
|
{
|
||||||
|
stdout = process.StandardOutput.ReadToEnd();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
process.Kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
return process.ExitCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
// 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.Reflection;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
internal class PropertyActivator<TContext>
|
||||||
|
{
|
||||||
|
private readonly Func<TContext, object> _valueAccessor;
|
||||||
|
private readonly Action<object, object> _fastPropertySetter;
|
||||||
|
|
||||||
|
public PropertyActivator(
|
||||||
|
PropertyInfo propertyInfo,
|
||||||
|
Func<TContext, object> valueAccessor)
|
||||||
|
{
|
||||||
|
if (propertyInfo == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(propertyInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueAccessor == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(valueAccessor));
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyInfo = propertyInfo;
|
||||||
|
_valueAccessor = valueAccessor;
|
||||||
|
_fastPropertySetter = PropertyHelper.MakeFastPropertySetter(propertyInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PropertyInfo PropertyInfo { get; private set; }
|
||||||
|
|
||||||
|
public object Activate(object instance, TContext context)
|
||||||
|
{
|
||||||
|
if (instance == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(instance));
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = _valueAccessor(context);
|
||||||
|
_fastPropertySetter(instance, value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PropertyActivator<TContext>[] GetPropertiesToActivate(
|
||||||
|
Type type,
|
||||||
|
Type activateAttributeType,
|
||||||
|
Func<PropertyInfo, PropertyActivator<TContext>> createActivateInfo)
|
||||||
|
{
|
||||||
|
if (type == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activateAttributeType == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(activateAttributeType));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createActivateInfo == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(createActivateInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetPropertiesToActivate(type, activateAttributeType, createActivateInfo, includeNonPublic: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PropertyActivator<TContext>[] GetPropertiesToActivate(
|
||||||
|
Type type,
|
||||||
|
Type activateAttributeType,
|
||||||
|
Func<PropertyInfo, PropertyActivator<TContext>> createActivateInfo,
|
||||||
|
bool includeNonPublic)
|
||||||
|
{
|
||||||
|
if (type == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activateAttributeType == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(activateAttributeType));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createActivateInfo == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(createActivateInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
var properties = type.GetRuntimeProperties()
|
||||||
|
.Where((property) =>
|
||||||
|
{
|
||||||
|
return
|
||||||
|
property.IsDefined(activateAttributeType) &&
|
||||||
|
property.GetIndexParameters().Length == 0 &&
|
||||||
|
property.SetMethod != null &&
|
||||||
|
!property.SetMethod.IsStatic;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!includeNonPublic)
|
||||||
|
{
|
||||||
|
properties = properties.Where(property => property.SetMethod.IsPublic);
|
||||||
|
}
|
||||||
|
|
||||||
|
return properties.Select(createActivateInfo).ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,526 @@
|
||||||
|
// 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.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
internal class PropertyHelper
|
||||||
|
{
|
||||||
|
// Delegate type for a by-ref property getter
|
||||||
|
private delegate TValue ByRefFunc<TDeclaringType, TValue>(ref TDeclaringType arg);
|
||||||
|
|
||||||
|
private static readonly MethodInfo CallPropertyGetterOpenGenericMethod =
|
||||||
|
typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallPropertyGetter));
|
||||||
|
|
||||||
|
private static readonly MethodInfo CallPropertyGetterByReferenceOpenGenericMethod =
|
||||||
|
typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallPropertyGetterByReference));
|
||||||
|
|
||||||
|
private static readonly MethodInfo CallNullSafePropertyGetterOpenGenericMethod =
|
||||||
|
typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallNullSafePropertyGetter));
|
||||||
|
|
||||||
|
private static readonly MethodInfo CallNullSafePropertyGetterByReferenceOpenGenericMethod =
|
||||||
|
typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallNullSafePropertyGetterByReference));
|
||||||
|
|
||||||
|
private static readonly MethodInfo CallPropertySetterOpenGenericMethod =
|
||||||
|
typeof(PropertyHelper).GetTypeInfo().GetDeclaredMethod(nameof(CallPropertySetter));
|
||||||
|
|
||||||
|
// Using an array rather than IEnumerable, as target will be called on the hot path numerous times.
|
||||||
|
private static readonly ConcurrentDictionary<Type, PropertyHelper[]> PropertiesCache =
|
||||||
|
new ConcurrentDictionary<Type, PropertyHelper[]>();
|
||||||
|
|
||||||
|
private static readonly ConcurrentDictionary<Type, PropertyHelper[]> VisiblePropertiesCache =
|
||||||
|
new ConcurrentDictionary<Type, PropertyHelper[]>();
|
||||||
|
|
||||||
|
private Action<object, object> _valueSetter;
|
||||||
|
private Func<object, object> _valueGetter;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a fast <see cref="PropertyHelper"/>.
|
||||||
|
/// This constructor does not cache the helper. For caching, use <see cref="GetProperties(Type)"/>.
|
||||||
|
/// </summary>
|
||||||
|
public PropertyHelper(PropertyInfo property)
|
||||||
|
{
|
||||||
|
if (property == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(property));
|
||||||
|
}
|
||||||
|
|
||||||
|
Property = property;
|
||||||
|
Name = property.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the backing <see cref="PropertyInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
public PropertyInfo Property { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets (or sets in derived types) the property name.
|
||||||
|
/// </summary>
|
||||||
|
public virtual string Name { get; protected set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the property value getter.
|
||||||
|
/// </summary>
|
||||||
|
public Func<object, object> ValueGetter
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_valueGetter == null)
|
||||||
|
{
|
||||||
|
_valueGetter = MakeFastPropertyGetter(Property);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _valueGetter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the property value setter.
|
||||||
|
/// </summary>
|
||||||
|
public Action<object, object> ValueSetter
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_valueSetter == null)
|
||||||
|
{
|
||||||
|
_valueSetter = MakeFastPropertySetter(Property);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _valueSetter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the property value for the specified <paramref name="instance"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The object whose property value will be returned.</param>
|
||||||
|
/// <returns>The property value.</returns>
|
||||||
|
public object GetValue(object instance)
|
||||||
|
{
|
||||||
|
return ValueGetter(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the property value for the specified <paramref name="instance" />.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The object whose property value will be set.</param>
|
||||||
|
/// <param name="value">The property value.</param>
|
||||||
|
public void SetValue(object instance, object value)
|
||||||
|
{
|
||||||
|
ValueSetter(instance, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates and caches fast property helpers that expose getters for every public get property on the
|
||||||
|
/// underlying type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="typeInfo">The type info to extract property accessors for.</param>
|
||||||
|
/// <returns>A cached array of all public properties of the specified type.
|
||||||
|
/// </returns>
|
||||||
|
public static PropertyHelper[] GetProperties(TypeInfo typeInfo)
|
||||||
|
{
|
||||||
|
return GetProperties(typeInfo.AsType());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates and caches fast property helpers that expose getters for every public get property on the
|
||||||
|
/// specified type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The type to extract property accessors for.</param>
|
||||||
|
/// <returns>A cached array of all public properties of the specified type.
|
||||||
|
/// </returns>
|
||||||
|
public static PropertyHelper[] GetProperties(Type type)
|
||||||
|
{
|
||||||
|
return GetProperties(type, CreateInstance, PropertiesCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <para>
|
||||||
|
/// Creates and caches fast property helpers that expose getters for every non-hidden get property
|
||||||
|
/// on the specified type.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="M:GetVisibleProperties"/> excludes properties defined on base types that have been
|
||||||
|
/// hidden by definitions using the <c>new</c> keyword.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="typeInfo">The type info to extract property accessors for.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A cached array of all public properties of the specified type.
|
||||||
|
/// </returns>
|
||||||
|
public static PropertyHelper[] GetVisibleProperties(TypeInfo typeInfo)
|
||||||
|
{
|
||||||
|
return GetVisibleProperties(typeInfo.AsType(), CreateInstance, PropertiesCache, VisiblePropertiesCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <para>
|
||||||
|
/// Creates and caches fast property helpers that expose getters for every non-hidden get property
|
||||||
|
/// on the specified type.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="M:GetVisibleProperties"/> excludes properties defined on base types that have been
|
||||||
|
/// hidden by definitions using the <c>new</c> keyword.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The type to extract property accessors for.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A cached array of all public properties of the specified type.
|
||||||
|
/// </returns>
|
||||||
|
public static PropertyHelper[] GetVisibleProperties(Type type)
|
||||||
|
{
|
||||||
|
return GetVisibleProperties(type, CreateInstance, PropertiesCache, VisiblePropertiesCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a single fast property getter. The result is not cached.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="propertyInfo">propertyInfo to extract the getter for.</param>
|
||||||
|
/// <returns>a fast getter.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method is more memory efficient than a dynamically compiled lambda, and about the
|
||||||
|
/// same speed.
|
||||||
|
/// </remarks>
|
||||||
|
public static Func<object, object> MakeFastPropertyGetter(PropertyInfo propertyInfo)
|
||||||
|
{
|
||||||
|
Debug.Assert(propertyInfo != null);
|
||||||
|
|
||||||
|
return MakeFastPropertyGetter(
|
||||||
|
propertyInfo,
|
||||||
|
CallPropertyGetterOpenGenericMethod,
|
||||||
|
CallPropertyGetterByReferenceOpenGenericMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a single fast property getter which is safe for a null input object. The result is not cached.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="propertyInfo">propertyInfo to extract the getter for.</param>
|
||||||
|
/// <returns>a fast getter.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method is more memory efficient than a dynamically compiled lambda, and about the
|
||||||
|
/// same speed.
|
||||||
|
/// </remarks>
|
||||||
|
public static Func<object, object> MakeNullSafeFastPropertyGetter(PropertyInfo propertyInfo)
|
||||||
|
{
|
||||||
|
Debug.Assert(propertyInfo != null);
|
||||||
|
|
||||||
|
return MakeFastPropertyGetter(
|
||||||
|
propertyInfo,
|
||||||
|
CallNullSafePropertyGetterOpenGenericMethod,
|
||||||
|
CallNullSafePropertyGetterByReferenceOpenGenericMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Func<object, object> MakeFastPropertyGetter(
|
||||||
|
PropertyInfo propertyInfo,
|
||||||
|
MethodInfo propertyGetterWrapperMethod,
|
||||||
|
MethodInfo propertyGetterByRefWrapperMethod)
|
||||||
|
{
|
||||||
|
Debug.Assert(propertyInfo != null);
|
||||||
|
|
||||||
|
// Must be a generic method with a Func<,> parameter
|
||||||
|
Debug.Assert(propertyGetterWrapperMethod != null);
|
||||||
|
Debug.Assert(propertyGetterWrapperMethod.IsGenericMethodDefinition);
|
||||||
|
Debug.Assert(propertyGetterWrapperMethod.GetParameters().Length == 2);
|
||||||
|
|
||||||
|
// Must be a generic method with a ByRefFunc<,> parameter
|
||||||
|
Debug.Assert(propertyGetterByRefWrapperMethod != null);
|
||||||
|
Debug.Assert(propertyGetterByRefWrapperMethod.IsGenericMethodDefinition);
|
||||||
|
Debug.Assert(propertyGetterByRefWrapperMethod.GetParameters().Length == 2);
|
||||||
|
|
||||||
|
var getMethod = propertyInfo.GetMethod;
|
||||||
|
Debug.Assert(getMethod != null);
|
||||||
|
Debug.Assert(!getMethod.IsStatic);
|
||||||
|
Debug.Assert(getMethod.GetParameters().Length == 0);
|
||||||
|
|
||||||
|
// Instance methods in the CLR can be turned into static methods where the first parameter
|
||||||
|
// is open over "target". This parameter is always passed by reference, so we have a code
|
||||||
|
// path for value types and a code path for reference types.
|
||||||
|
if (getMethod.DeclaringType.GetTypeInfo().IsValueType)
|
||||||
|
{
|
||||||
|
// Create a delegate (ref TDeclaringType) -> TValue
|
||||||
|
return MakeFastPropertyGetter(
|
||||||
|
typeof(ByRefFunc<,>),
|
||||||
|
getMethod,
|
||||||
|
propertyGetterByRefWrapperMethod);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Create a delegate TDeclaringType -> TValue
|
||||||
|
return MakeFastPropertyGetter(
|
||||||
|
typeof(Func<,>),
|
||||||
|
getMethod,
|
||||||
|
propertyGetterWrapperMethod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Func<object, object> MakeFastPropertyGetter(
|
||||||
|
Type openGenericDelegateType,
|
||||||
|
MethodInfo propertyGetMethod,
|
||||||
|
MethodInfo openGenericWrapperMethod)
|
||||||
|
{
|
||||||
|
var typeInput = propertyGetMethod.DeclaringType;
|
||||||
|
var typeOutput = propertyGetMethod.ReturnType;
|
||||||
|
|
||||||
|
var delegateType = openGenericDelegateType.MakeGenericType(typeInput, typeOutput);
|
||||||
|
var propertyGetterDelegate = propertyGetMethod.CreateDelegate(delegateType);
|
||||||
|
|
||||||
|
var wrapperDelegateMethod = openGenericWrapperMethod.MakeGenericMethod(typeInput, typeOutput);
|
||||||
|
var accessorDelegate = wrapperDelegateMethod.CreateDelegate(
|
||||||
|
typeof(Func<object, object>),
|
||||||
|
propertyGetterDelegate);
|
||||||
|
|
||||||
|
return (Func<object, object>)accessorDelegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a single fast property setter for reference types. The result is not cached.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="propertyInfo">propertyInfo to extract the setter for.</param>
|
||||||
|
/// <returns>a fast getter.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method is more memory efficient than a dynamically compiled lambda, and about the
|
||||||
|
/// same speed. This only works for reference types.
|
||||||
|
/// </remarks>
|
||||||
|
public static Action<object, object> MakeFastPropertySetter(PropertyInfo propertyInfo)
|
||||||
|
{
|
||||||
|
Debug.Assert(propertyInfo != null);
|
||||||
|
Debug.Assert(!propertyInfo.DeclaringType.GetTypeInfo().IsValueType);
|
||||||
|
|
||||||
|
var setMethod = propertyInfo.SetMethod;
|
||||||
|
Debug.Assert(setMethod != null);
|
||||||
|
Debug.Assert(!setMethod.IsStatic);
|
||||||
|
Debug.Assert(setMethod.ReturnType == typeof(void));
|
||||||
|
var parameters = setMethod.GetParameters();
|
||||||
|
Debug.Assert(parameters.Length == 1);
|
||||||
|
|
||||||
|
// Instance methods in the CLR can be turned into static methods where the first parameter
|
||||||
|
// is open over "target". This parameter is always passed by reference, so we have a code
|
||||||
|
// path for value types and a code path for reference types.
|
||||||
|
var typeInput = setMethod.DeclaringType;
|
||||||
|
var parameterType = parameters[0].ParameterType;
|
||||||
|
|
||||||
|
// Create a delegate TDeclaringType -> { TDeclaringType.Property = TValue; }
|
||||||
|
var propertySetterAsAction =
|
||||||
|
setMethod.CreateDelegate(typeof(Action<,>).MakeGenericType(typeInput, parameterType));
|
||||||
|
var callPropertySetterClosedGenericMethod =
|
||||||
|
CallPropertySetterOpenGenericMethod.MakeGenericMethod(typeInput, parameterType);
|
||||||
|
var callPropertySetterDelegate =
|
||||||
|
callPropertySetterClosedGenericMethod.CreateDelegate(
|
||||||
|
typeof(Action<object, object>), propertySetterAsAction);
|
||||||
|
|
||||||
|
return (Action<object, object>)callPropertySetterDelegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Given an object, adds each instance property with a public get method as a key and its
|
||||||
|
/// associated value to a dictionary.
|
||||||
|
///
|
||||||
|
/// If the object is already an <see cref="IDictionary{String, Object}"/> instance, then a copy
|
||||||
|
/// is returned.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The implementation of PropertyHelper will cache the property accessors per-type. This is
|
||||||
|
/// faster when the same type is used multiple times with ObjectToDictionary.
|
||||||
|
/// </remarks>
|
||||||
|
public static IDictionary<string, object> ObjectToDictionary(object value)
|
||||||
|
{
|
||||||
|
var dictionary = value as IDictionary<string, object>;
|
||||||
|
if (dictionary != null)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, object>(dictionary, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
dictionary = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
foreach (var helper in GetProperties(value.GetType()))
|
||||||
|
{
|
||||||
|
dictionary[helper.Name] = helper.GetValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dictionary;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PropertyHelper CreateInstance(PropertyInfo property)
|
||||||
|
{
|
||||||
|
return new PropertyHelper(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called via reflection
|
||||||
|
private static object CallPropertyGetter<TDeclaringType, TValue>(
|
||||||
|
Func<TDeclaringType, TValue> getter,
|
||||||
|
object target)
|
||||||
|
{
|
||||||
|
return getter((TDeclaringType)target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called via reflection
|
||||||
|
private static object CallPropertyGetterByReference<TDeclaringType, TValue>(
|
||||||
|
ByRefFunc<TDeclaringType, TValue> getter,
|
||||||
|
object target)
|
||||||
|
{
|
||||||
|
var unboxed = (TDeclaringType)target;
|
||||||
|
return getter(ref unboxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called via reflection
|
||||||
|
private static object CallNullSafePropertyGetter<TDeclaringType, TValue>(
|
||||||
|
Func<TDeclaringType, TValue> getter,
|
||||||
|
object target)
|
||||||
|
{
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getter((TDeclaringType)target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called via reflection
|
||||||
|
private static object CallNullSafePropertyGetterByReference<TDeclaringType, TValue>(
|
||||||
|
ByRefFunc<TDeclaringType, TValue> getter,
|
||||||
|
object target)
|
||||||
|
{
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var unboxed = (TDeclaringType)target;
|
||||||
|
return getter(ref unboxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CallPropertySetter<TDeclaringType, TValue>(
|
||||||
|
Action<TDeclaringType, TValue> setter,
|
||||||
|
object target,
|
||||||
|
object value)
|
||||||
|
{
|
||||||
|
setter((TDeclaringType)target, (TValue)value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static PropertyHelper[] GetVisibleProperties(
|
||||||
|
Type type,
|
||||||
|
Func<PropertyInfo, PropertyHelper> createPropertyHelper,
|
||||||
|
ConcurrentDictionary<Type, PropertyHelper[]> allPropertiesCache,
|
||||||
|
ConcurrentDictionary<Type, PropertyHelper[]> visiblePropertiesCache)
|
||||||
|
{
|
||||||
|
PropertyHelper[] result;
|
||||||
|
if (visiblePropertiesCache.TryGetValue(type, out result))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The simple and common case, this is normal POCO object - no need to allocate.
|
||||||
|
var allPropertiesDefinedOnType = true;
|
||||||
|
var allProperties = GetProperties(type, createPropertyHelper, allPropertiesCache);
|
||||||
|
foreach (var propertyHelper in allProperties)
|
||||||
|
{
|
||||||
|
if (propertyHelper.Property.DeclaringType != type)
|
||||||
|
{
|
||||||
|
allPropertiesDefinedOnType = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allPropertiesDefinedOnType)
|
||||||
|
{
|
||||||
|
result = allProperties;
|
||||||
|
visiblePropertiesCache.TryAdd(type, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// There's some inherited properties here, so we need to check for hiding via 'new'.
|
||||||
|
var filteredProperties = new List<PropertyHelper>(allProperties.Length);
|
||||||
|
foreach (var propertyHelper in allProperties)
|
||||||
|
{
|
||||||
|
var declaringType = propertyHelper.Property.DeclaringType;
|
||||||
|
if (declaringType == type)
|
||||||
|
{
|
||||||
|
filteredProperties.Add(propertyHelper);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this property was declared on a base type then look for the definition closest to the
|
||||||
|
// the type to see if we should include it.
|
||||||
|
var ignoreProperty = false;
|
||||||
|
|
||||||
|
// Walk up the hierarchy until we find the type that actually declares this
|
||||||
|
// PropertyInfo.
|
||||||
|
var currentTypeInfo = type.GetTypeInfo();
|
||||||
|
var declaringTypeInfo = declaringType.GetTypeInfo();
|
||||||
|
while (currentTypeInfo != null && currentTypeInfo != declaringTypeInfo)
|
||||||
|
{
|
||||||
|
// We've found a 'more proximal' public definition
|
||||||
|
var declaredProperty = currentTypeInfo.GetDeclaredProperty(propertyHelper.Name);
|
||||||
|
if (declaredProperty != null)
|
||||||
|
{
|
||||||
|
ignoreProperty = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTypeInfo = currentTypeInfo.BaseType?.GetTypeInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ignoreProperty)
|
||||||
|
{
|
||||||
|
filteredProperties.Add(propertyHelper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = filteredProperties.ToArray();
|
||||||
|
visiblePropertiesCache.TryAdd(type, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static PropertyHelper[] GetProperties(
|
||||||
|
Type type,
|
||||||
|
Func<PropertyInfo, PropertyHelper> createPropertyHelper,
|
||||||
|
ConcurrentDictionary<Type, PropertyHelper[]> cache)
|
||||||
|
{
|
||||||
|
// Unwrap nullable types. This means Nullable<T>.Value and Nullable<T>.HasValue will not be
|
||||||
|
// part of the sequence of properties returned by this method.
|
||||||
|
type = Nullable.GetUnderlyingType(type) ?? type;
|
||||||
|
|
||||||
|
PropertyHelper[] helpers;
|
||||||
|
if (!cache.TryGetValue(type, out helpers))
|
||||||
|
{
|
||||||
|
// We avoid loading indexed properties using the Where statement.
|
||||||
|
var properties = type.GetRuntimeProperties().Where(IsInterestingProperty);
|
||||||
|
|
||||||
|
var typeInfo = type.GetTypeInfo();
|
||||||
|
if (typeInfo.IsInterface)
|
||||||
|
{
|
||||||
|
// Reflection does not return information about inherited properties on the interface itself.
|
||||||
|
properties = properties.Concat(typeInfo.ImplementedInterfaces.SelectMany(
|
||||||
|
interfaceType => interfaceType.GetRuntimeProperties().Where(IsInterestingProperty)));
|
||||||
|
}
|
||||||
|
|
||||||
|
helpers = properties.Select(p => createPropertyHelper(p)).ToArray();
|
||||||
|
cache.TryAdd(type, helpers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return helpers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indexed properties are not useful (or valid) for grabbing properties off an object.
|
||||||
|
private static bool IsInterestingProperty(PropertyInfo property)
|
||||||
|
{
|
||||||
|
// For improving application startup time, do not use GetIndexParameters() api early in this check as it
|
||||||
|
// creates a copy of parameter array and also we would like to check for the presence of a get method
|
||||||
|
// and short circuit asap.
|
||||||
|
return property.GetMethod != null &&
|
||||||
|
property.GetMethod.IsPublic &&
|
||||||
|
!property.GetMethod.IsStatic &&
|
||||||
|
property.GetMethod.GetParameters().Length == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
// 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.RazorViews
|
||||||
|
{
|
||||||
|
internal class AttributeValue
|
||||||
|
{
|
||||||
|
public AttributeValue(string prefix, object value, bool literal)
|
||||||
|
{
|
||||||
|
Prefix = prefix;
|
||||||
|
Value = value;
|
||||||
|
Literal = literal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Prefix { get; }
|
||||||
|
|
||||||
|
public object Value { get; }
|
||||||
|
|
||||||
|
public bool Literal { get; }
|
||||||
|
|
||||||
|
public static AttributeValue FromTuple(Tuple<string, object, bool> value)
|
||||||
|
{
|
||||||
|
return new AttributeValue(value.Item1, value.Item2, value.Item3);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AttributeValue FromTuple(Tuple<string, string, bool> value)
|
||||||
|
{
|
||||||
|
return new AttributeValue(value.Item1, value.Item2, value.Item3);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static implicit operator AttributeValue(Tuple<string, object, bool> value)
|
||||||
|
{
|
||||||
|
return FromTuple(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,279 @@
|
||||||
|
// 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.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.RazorViews
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Infrastructure
|
||||||
|
/// </summary>
|
||||||
|
internal abstract class BaseView
|
||||||
|
{
|
||||||
|
private static readonly Encoding UTF8NoBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
|
||||||
|
private readonly Stack<TextWriter> _textWriterStack = new Stack<TextWriter>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The request context
|
||||||
|
/// </summary>
|
||||||
|
protected HttpContext Context { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The request
|
||||||
|
/// </summary>
|
||||||
|
protected HttpRequest Request { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The response
|
||||||
|
/// </summary>
|
||||||
|
protected HttpResponse Response { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The output stream
|
||||||
|
/// </summary>
|
||||||
|
protected TextWriter Output { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Html encoder used to encode content.
|
||||||
|
/// </summary>
|
||||||
|
protected HtmlEncoder HtmlEncoder { get; set; } = HtmlEncoder.Default;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Url encoder used to encode content.
|
||||||
|
/// </summary>
|
||||||
|
protected UrlEncoder UrlEncoder { get; set; } = UrlEncoder.Default;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JavaScript encoder used to encode content.
|
||||||
|
/// </summary>
|
||||||
|
protected JavaScriptEncoder JavaScriptEncoder { get; set; } = JavaScriptEncoder.Default;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute an individual request
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
public async Task ExecuteAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
Context = context;
|
||||||
|
Request = Context.Request;
|
||||||
|
Response = Context.Response;
|
||||||
|
Output = new StreamWriter(Response.Body, UTF8NoBOM, 4096, leaveOpen: true);
|
||||||
|
await ExecuteAsync();
|
||||||
|
Output.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute an individual request
|
||||||
|
/// </summary>
|
||||||
|
public abstract Task ExecuteAsync();
|
||||||
|
|
||||||
|
protected virtual void PushWriter(TextWriter writer)
|
||||||
|
{
|
||||||
|
if (writer == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(writer));
|
||||||
|
}
|
||||||
|
|
||||||
|
_textWriterStack.Push(Output);
|
||||||
|
Output = writer;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual TextWriter PopWriter()
|
||||||
|
{
|
||||||
|
Output = _textWriterStack.Pop();
|
||||||
|
return Output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Write the given value without HTML encoding directly to <see cref="Output"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The <see cref="object"/> to write.</param>
|
||||||
|
protected void WriteLiteral(object value)
|
||||||
|
{
|
||||||
|
WriteLiteral(Convert.ToString(value, CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Write the given value without HTML encoding directly to <see cref="Output"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The <see cref="string"/> to write.</param>
|
||||||
|
protected void WriteLiteral(string value)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
Output.Write(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> AttributeValues { get; set; }
|
||||||
|
|
||||||
|
protected void WriteAttributeValue(string thingy, int startPostion, object value, int endValue, int dealyo, bool yesno)
|
||||||
|
{
|
||||||
|
if (AttributeValues == null)
|
||||||
|
{
|
||||||
|
AttributeValues = new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
AttributeValues.Add(value.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private string AttributeEnding { get; set; }
|
||||||
|
|
||||||
|
protected void BeginWriteAttribute(string name, string begining, int startPosition, string ending, int endPosition, int thingy)
|
||||||
|
{
|
||||||
|
Debug.Assert(string.IsNullOrEmpty(AttributeEnding));
|
||||||
|
|
||||||
|
Output.Write(begining);
|
||||||
|
AttributeEnding = ending;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void EndWriteAttribute()
|
||||||
|
{
|
||||||
|
Debug.Assert(!string.IsNullOrEmpty(AttributeEnding));
|
||||||
|
|
||||||
|
var attributes = string.Join(" ", AttributeValues);
|
||||||
|
Output.Write(attributes);
|
||||||
|
AttributeValues = null;
|
||||||
|
|
||||||
|
Output.Write(AttributeEnding);
|
||||||
|
AttributeEnding = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes the given attribute to the given writer
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The name of the attribute to write</param>
|
||||||
|
/// <param name="leader">The value of the prefix</param>
|
||||||
|
/// <param name="trailer">The value of the suffix</param>
|
||||||
|
/// <param name="values">The <see cref="AttributeValue"/>s to write.</param>
|
||||||
|
protected void WriteAttribute(
|
||||||
|
string name,
|
||||||
|
string leader,
|
||||||
|
string trailer,
|
||||||
|
params AttributeValue[] values)
|
||||||
|
{
|
||||||
|
if (name == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (leader == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(leader));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trailer == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(trailer));
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteLiteral(leader);
|
||||||
|
foreach (var value in values)
|
||||||
|
{
|
||||||
|
WriteLiteral(value.Prefix);
|
||||||
|
|
||||||
|
// The special cases here are that the value we're writing might already be a string, or that the
|
||||||
|
// value might be a bool. If the value is the bool 'true' we want to write the attribute name
|
||||||
|
// instead of the string 'true'. If the value is the bool 'false' we don't want to write anything.
|
||||||
|
// Otherwise the value is another object (perhaps an HtmlString) and we'll ask it to format itself.
|
||||||
|
string stringValue;
|
||||||
|
if (value.Value is bool)
|
||||||
|
{
|
||||||
|
if ((bool)value.Value)
|
||||||
|
{
|
||||||
|
stringValue = name;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
stringValue = value.Value as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the WriteTo(string) overload when possible
|
||||||
|
if (value.Literal && stringValue != null)
|
||||||
|
{
|
||||||
|
WriteLiteral(stringValue);
|
||||||
|
}
|
||||||
|
else if (value.Literal)
|
||||||
|
{
|
||||||
|
WriteLiteral(value.Value);
|
||||||
|
}
|
||||||
|
else if (stringValue != null)
|
||||||
|
{
|
||||||
|
Write(stringValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Write(value.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WriteLiteral(trailer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="HelperResult.WriteTo(TextWriter)"/> is invoked
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="result">The <see cref="HelperResult"/> to invoke</param>
|
||||||
|
protected void Write(HelperResult result)
|
||||||
|
{
|
||||||
|
Write(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes the specified <paramref name="value"/> to <see cref="Output"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The <see cref="object"/> to write.</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// <see cref="HelperResult.WriteTo(TextWriter)"/> is invoked for <see cref="HelperResult"/> types.
|
||||||
|
/// For all other types, the encoded result of <see cref="object.ToString"/> is written to
|
||||||
|
/// <see cref="Output"/>.
|
||||||
|
/// </remarks>
|
||||||
|
protected void Write(object value)
|
||||||
|
{
|
||||||
|
if (value is HelperResult helperResult)
|
||||||
|
{
|
||||||
|
helperResult.WriteTo(Output);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Write(Convert.ToString(value, CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes the specified <paramref name="value"/> with HTML encoding to <see cref="Output"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The <see cref="string"/> to write.</param>
|
||||||
|
protected void Write(string value)
|
||||||
|
{
|
||||||
|
WriteLiteral(HtmlEncoder.Encode(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected string HtmlEncodeAndReplaceLineBreaks(string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split on line breaks before passing it through the encoder.
|
||||||
|
return string.Join("<br />" + Environment.NewLine,
|
||||||
|
input.Split(new[] { "\r\n" }, StringSplitOptions.None)
|
||||||
|
.SelectMany(s => s.Split(new[] { '\r', '\n' }, StringSplitOptions.None))
|
||||||
|
.Select(HtmlEncoder.Encode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
// 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.IO;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.RazorViews
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a deferred write operation in a <see cref="BaseView"/>.
|
||||||
|
/// </summary>
|
||||||
|
internal class HelperResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of <see cref="HelperResult"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="action">The delegate to invoke when <see cref="WriteTo(TextWriter)"/> is called.</param>
|
||||||
|
public HelperResult(Action<TextWriter> action)
|
||||||
|
{
|
||||||
|
WriteAction = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Action<TextWriter> WriteAction { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Method invoked to produce content from the <see cref="HelperResult"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="writer">The <see cref="TextWriter"/> instance to write to.</param>
|
||||||
|
public void WriteTo(TextWriter writer)
|
||||||
|
{
|
||||||
|
WriteAction(writer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
// 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.Security.Claims;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Helper code used when implementing authentication middleware
|
||||||
|
/// </summary>
|
||||||
|
internal static class SecurityHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Add all ClaimsIdentities from an additional ClaimPrincipal to the ClaimsPrincipal
|
||||||
|
/// Merges a new claims principal, placing all new identities first, and eliminating
|
||||||
|
/// any empty unauthenticated identities from context.User
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="existingPrincipal">The <see cref="ClaimsPrincipal"/> containing existing <see cref="ClaimsIdentity"/>.</param>
|
||||||
|
/// <param name="additionalPrincipal">The <see cref="ClaimsPrincipal"/> containing <see cref="ClaimsIdentity"/> to be added.</param>
|
||||||
|
public static ClaimsPrincipal MergeUserPrincipal(ClaimsPrincipal existingPrincipal, ClaimsPrincipal additionalPrincipal)
|
||||||
|
{
|
||||||
|
var newPrincipal = new ClaimsPrincipal();
|
||||||
|
|
||||||
|
// New principal identities go first
|
||||||
|
if (additionalPrincipal != null)
|
||||||
|
{
|
||||||
|
newPrincipal.AddIdentities(additionalPrincipal.Identities);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then add any existing non empty or authenticated identities
|
||||||
|
if (existingPrincipal != null)
|
||||||
|
{
|
||||||
|
newPrincipal.AddIdentities(existingPrincipal.Identities.Where(i => i.IsAuthenticated || i.Claims.Any()));
|
||||||
|
}
|
||||||
|
return newPrincipal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.StackTrace.Sources
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains details for individual exception messages.
|
||||||
|
/// </summary>
|
||||||
|
internal class ExceptionDetails
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An individual exception
|
||||||
|
/// </summary>
|
||||||
|
public Exception Error { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The generated stack frames
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<StackFrameSourceCodeInfo> StackFrames { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the summary message.
|
||||||
|
/// </summary>
|
||||||
|
public string ErrorMessage { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
// 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.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.Extensions.FileProviders;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.StackTrace.Sources
|
||||||
|
{
|
||||||
|
internal class ExceptionDetailsProvider
|
||||||
|
{
|
||||||
|
private readonly IFileProvider _fileProvider;
|
||||||
|
private readonly int _sourceCodeLineCount;
|
||||||
|
|
||||||
|
public ExceptionDetailsProvider(IFileProvider fileProvider, int sourceCodeLineCount)
|
||||||
|
{
|
||||||
|
_fileProvider = fileProvider;
|
||||||
|
_sourceCodeLineCount = sourceCodeLineCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ExceptionDetails> GetDetails(Exception exception)
|
||||||
|
{
|
||||||
|
var exceptions = FlattenAndReverseExceptionTree(exception);
|
||||||
|
|
||||||
|
foreach (var ex in exceptions)
|
||||||
|
{
|
||||||
|
yield return new ExceptionDetails
|
||||||
|
{
|
||||||
|
Error = ex,
|
||||||
|
StackFrames = StackTraceHelper.GetFrames(ex)
|
||||||
|
.Select(frame => GetStackFrameSourceCodeInfo(
|
||||||
|
frame.MethodDisplayInfo.ToString(),
|
||||||
|
frame.FilePath,
|
||||||
|
frame.LineNumber))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<Exception> FlattenAndReverseExceptionTree(Exception ex)
|
||||||
|
{
|
||||||
|
// ReflectionTypeLoadException is special because the details are in
|
||||||
|
// the LoaderExceptions property
|
||||||
|
var typeLoadException = ex as ReflectionTypeLoadException;
|
||||||
|
if (typeLoadException != null)
|
||||||
|
{
|
||||||
|
var typeLoadExceptions = new List<Exception>();
|
||||||
|
foreach (var loadException in typeLoadException.LoaderExceptions)
|
||||||
|
{
|
||||||
|
typeLoadExceptions.AddRange(FlattenAndReverseExceptionTree(loadException));
|
||||||
|
}
|
||||||
|
|
||||||
|
typeLoadExceptions.Add(ex);
|
||||||
|
return typeLoadExceptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
var list = new List<Exception>();
|
||||||
|
if (ex is AggregateException aggregateException)
|
||||||
|
{
|
||||||
|
list.Add(ex);
|
||||||
|
foreach (var innerException in aggregateException.Flatten().InnerExceptions)
|
||||||
|
{
|
||||||
|
list.Add(innerException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
while (ex != null)
|
||||||
|
{
|
||||||
|
list.Add(ex);
|
||||||
|
ex = ex.InnerException;
|
||||||
|
}
|
||||||
|
list.Reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make it internal to enable unit testing
|
||||||
|
internal StackFrameSourceCodeInfo GetStackFrameSourceCodeInfo(string method, string filePath, int lineNumber)
|
||||||
|
{
|
||||||
|
var stackFrame = new StackFrameSourceCodeInfo
|
||||||
|
{
|
||||||
|
Function = method,
|
||||||
|
File = filePath,
|
||||||
|
Line = lineNumber
|
||||||
|
};
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(stackFrame.File))
|
||||||
|
{
|
||||||
|
return stackFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerable<string> lines = null;
|
||||||
|
if (File.Exists(stackFrame.File))
|
||||||
|
{
|
||||||
|
lines = File.ReadLines(stackFrame.File);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Handle relative paths and embedded files
|
||||||
|
var fileInfo = _fileProvider.GetFileInfo(stackFrame.File);
|
||||||
|
if (fileInfo.Exists)
|
||||||
|
{
|
||||||
|
// ReadLines doesn't accept a stream. Use ReadLines as its more efficient
|
||||||
|
// relative to reading lines via stream reader
|
||||||
|
if (!string.IsNullOrEmpty(fileInfo.PhysicalPath))
|
||||||
|
{
|
||||||
|
lines = File.ReadLines(fileInfo.PhysicalPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lines = ReadLines(fileInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lines != null)
|
||||||
|
{
|
||||||
|
ReadFrameContent(stackFrame, lines, stackFrame.Line, stackFrame.Line);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stackFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make it internal to enable unit testing
|
||||||
|
internal void ReadFrameContent(
|
||||||
|
StackFrameSourceCodeInfo frame,
|
||||||
|
IEnumerable<string> allLines,
|
||||||
|
int errorStartLineNumberInFile,
|
||||||
|
int errorEndLineNumberInFile)
|
||||||
|
{
|
||||||
|
// Get the line boundaries in the file to be read and read all these lines at once into an array.
|
||||||
|
var preErrorLineNumberInFile = Math.Max(errorStartLineNumberInFile - _sourceCodeLineCount, 1);
|
||||||
|
var postErrorLineNumberInFile = errorEndLineNumberInFile + _sourceCodeLineCount;
|
||||||
|
var codeBlock = allLines
|
||||||
|
.Skip(preErrorLineNumberInFile - 1)
|
||||||
|
.Take(postErrorLineNumberInFile - preErrorLineNumberInFile + 1)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var numOfErrorLines = (errorEndLineNumberInFile - errorStartLineNumberInFile) + 1;
|
||||||
|
var errorStartLineNumberInArray = errorStartLineNumberInFile - preErrorLineNumberInFile;
|
||||||
|
|
||||||
|
frame.PreContextLine = preErrorLineNumberInFile;
|
||||||
|
frame.PreContextCode = codeBlock.Take(errorStartLineNumberInArray).ToArray();
|
||||||
|
frame.ContextCode = codeBlock
|
||||||
|
.Skip(errorStartLineNumberInArray)
|
||||||
|
.Take(numOfErrorLines)
|
||||||
|
.ToArray();
|
||||||
|
frame.PostContextCode = codeBlock
|
||||||
|
.Skip(errorStartLineNumberInArray + numOfErrorLines)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> ReadLines(IFileInfo fileInfo)
|
||||||
|
{
|
||||||
|
using (var reader = new StreamReader(fileInfo.CreateReadStream()))
|
||||||
|
{
|
||||||
|
string line;
|
||||||
|
while ((line = reader.ReadLine()) != null)
|
||||||
|
{
|
||||||
|
yield return line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
// 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.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.StackTrace.Sources
|
||||||
|
{
|
||||||
|
internal class MethodDisplayInfo
|
||||||
|
{
|
||||||
|
public string DeclaringTypeName { get; set; }
|
||||||
|
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public string GenericArguments { get; set; }
|
||||||
|
|
||||||
|
public string SubMethod { get; set; }
|
||||||
|
|
||||||
|
public IEnumerable<ParameterDisplayInfo> Parameters { get; set; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
if (!string.IsNullOrEmpty(DeclaringTypeName))
|
||||||
|
{
|
||||||
|
builder
|
||||||
|
.Append(DeclaringTypeName)
|
||||||
|
.Append(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(Name);
|
||||||
|
builder.Append(GenericArguments);
|
||||||
|
|
||||||
|
builder.Append("(");
|
||||||
|
builder.Append(string.Join(", ", Parameters.Select(p => p.ToString())));
|
||||||
|
builder.Append(")");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(SubMethod))
|
||||||
|
{
|
||||||
|
builder.Append("+");
|
||||||
|
builder.Append(SubMethod);
|
||||||
|
builder.Append("()");
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// 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.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.StackTrace.Sources
|
||||||
|
{
|
||||||
|
internal class ParameterDisplayInfo
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
public string Prefix { get; set; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
if (!string.IsNullOrEmpty(Prefix))
|
||||||
|
{
|
||||||
|
builder
|
||||||
|
.Append(Prefix)
|
||||||
|
.Append(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(Type);
|
||||||
|
builder.Append(" ");
|
||||||
|
builder.Append(Name);
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
// 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.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Reflection.Metadata;
|
||||||
|
using System.Reflection.Metadata.Ecma335;
|
||||||
|
using System.Reflection.PortableExecutable;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.StackTrace.Sources
|
||||||
|
{
|
||||||
|
internal class PortablePdbReader : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, MetadataReaderProvider> _cache =
|
||||||
|
new Dictionary<string, MetadataReaderProvider>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
public void PopulateStackFrame(StackFrameInfo frameInfo, MethodBase method, int IlOffset)
|
||||||
|
{
|
||||||
|
if (method.Module.Assembly.IsDynamic)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadataReader = GetMetadataReader(method.Module.Assembly.Location);
|
||||||
|
|
||||||
|
if (metadataReader == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var methodToken = MetadataTokens.Handle(method.MetadataToken);
|
||||||
|
|
||||||
|
Debug.Assert(methodToken.Kind == HandleKind.MethodDefinition);
|
||||||
|
|
||||||
|
var handle = ((MethodDefinitionHandle)methodToken).ToDebugInformationHandle();
|
||||||
|
|
||||||
|
if (!handle.IsNil)
|
||||||
|
{
|
||||||
|
var methodDebugInfo = metadataReader.GetMethodDebugInformation(handle);
|
||||||
|
var sequencePoints = methodDebugInfo.GetSequencePoints();
|
||||||
|
SequencePoint? bestPointSoFar = null;
|
||||||
|
|
||||||
|
foreach (var point in sequencePoints)
|
||||||
|
{
|
||||||
|
if (point.Offset > IlOffset)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (point.StartLine != SequencePoint.HiddenLine)
|
||||||
|
{
|
||||||
|
bestPointSoFar = point;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestPointSoFar.HasValue)
|
||||||
|
{
|
||||||
|
frameInfo.LineNumber = bestPointSoFar.Value.StartLine;
|
||||||
|
frameInfo.FilePath = metadataReader.GetString(metadataReader.GetDocument(bestPointSoFar.Value.Document).Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MetadataReader GetMetadataReader(string assemblyPath)
|
||||||
|
{
|
||||||
|
MetadataReaderProvider provider = null;
|
||||||
|
if (!_cache.TryGetValue(assemblyPath, out provider))
|
||||||
|
{
|
||||||
|
var pdbPath = GetPdbPath(assemblyPath);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(pdbPath) && File.Exists(pdbPath) && IsPortable(pdbPath))
|
||||||
|
{
|
||||||
|
var pdbStream = File.OpenRead(pdbPath);
|
||||||
|
provider = MetadataReaderProvider.FromPortablePdbStream(pdbStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
_cache[assemblyPath] = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider?.GetMetadataReader();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetPdbPath(string assemblyPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(assemblyPath))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(assemblyPath))
|
||||||
|
{
|
||||||
|
var peStream = File.OpenRead(assemblyPath);
|
||||||
|
|
||||||
|
using (var peReader = new PEReader(peStream))
|
||||||
|
{
|
||||||
|
foreach (var entry in peReader.ReadDebugDirectory())
|
||||||
|
{
|
||||||
|
if (entry.Type == DebugDirectoryEntryType.CodeView)
|
||||||
|
{
|
||||||
|
var codeViewData = peReader.ReadCodeViewDebugDirectoryData(entry);
|
||||||
|
var peDirectory = Path.GetDirectoryName(assemblyPath);
|
||||||
|
return Path.Combine(peDirectory, Path.GetFileName(codeViewData.Path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsPortable(string pdbPath)
|
||||||
|
{
|
||||||
|
using (var pdbStream = File.OpenRead(pdbPath))
|
||||||
|
{
|
||||||
|
return pdbStream.ReadByte() == 'B' &&
|
||||||
|
pdbStream.ReadByte() == 'S' &&
|
||||||
|
pdbStream.ReadByte() == 'J' &&
|
||||||
|
pdbStream.ReadByte() == 'B';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var entry in _cache)
|
||||||
|
{
|
||||||
|
entry.Value?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_cache.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.StackTrace.Sources
|
||||||
|
{
|
||||||
|
internal class StackFrameInfo
|
||||||
|
{
|
||||||
|
public int LineNumber { get; set; }
|
||||||
|
|
||||||
|
public string FilePath { get; set; }
|
||||||
|
|
||||||
|
public StackFrame StackFrame { get; set; }
|
||||||
|
|
||||||
|
public MethodDisplayInfo MethodDisplayInfo { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
// 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.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.StackTrace.Sources
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains the source code where the exception occurred.
|
||||||
|
/// </summary>
|
||||||
|
internal class StackFrameSourceCodeInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Function containing instruction
|
||||||
|
/// </summary>
|
||||||
|
public string Function { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// File containing the instruction
|
||||||
|
/// </summary>
|
||||||
|
public string File { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The line number of the instruction
|
||||||
|
/// </summary>
|
||||||
|
public int Line { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The line preceding the frame line
|
||||||
|
/// </summary>
|
||||||
|
public int PreContextLine { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lines of code before the actual error line(s).
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<string> PreContextCode { get; set; } = Enumerable.Empty<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Line(s) of code responsible for the error.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<string> ContextCode { get; set; } = Enumerable.Empty<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lines of code after the actual error line(s).
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<string> PostContextCode { get; set; } = Enumerable.Empty<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specific error details for this stack frame.
|
||||||
|
/// </summary>
|
||||||
|
public string ErrorDetails { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,261 @@
|
||||||
|
// 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.ExceptionServices;
|
||||||
|
using Microsoft.Extensions.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.StackTrace.Sources
|
||||||
|
{
|
||||||
|
internal class StackTraceHelper
|
||||||
|
{
|
||||||
|
public static IList<StackFrameInfo> GetFrames(Exception exception)
|
||||||
|
{
|
||||||
|
var frames = new List<StackFrameInfo>();
|
||||||
|
|
||||||
|
if (exception == null)
|
||||||
|
{
|
||||||
|
return frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var portablePdbReader = new PortablePdbReader())
|
||||||
|
{
|
||||||
|
var needFileInfo = true;
|
||||||
|
var stackTrace = new System.Diagnostics.StackTrace(exception, needFileInfo);
|
||||||
|
var stackFrames = stackTrace.GetFrames();
|
||||||
|
|
||||||
|
if (stackFrames == null)
|
||||||
|
{
|
||||||
|
return frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < stackFrames.Length; i++)
|
||||||
|
{
|
||||||
|
var frame = stackFrames[i];
|
||||||
|
var method = frame.GetMethod();
|
||||||
|
|
||||||
|
// Always show last stackFrame
|
||||||
|
if (!ShowInStackTrace(method) && i < stackFrames.Length - 1)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stackFrame = new StackFrameInfo
|
||||||
|
{
|
||||||
|
StackFrame = frame,
|
||||||
|
FilePath = frame.GetFileName(),
|
||||||
|
LineNumber = frame.GetFileLineNumber(),
|
||||||
|
MethodDisplayInfo = GetMethodDisplayString(frame.GetMethod()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(stackFrame.FilePath))
|
||||||
|
{
|
||||||
|
// .NET Framework and older versions of mono don't support portable PDBs
|
||||||
|
// so we read it manually to get file name and line information
|
||||||
|
portablePdbReader.PopulateStackFrame(stackFrame, method, frame.GetILOffset());
|
||||||
|
}
|
||||||
|
|
||||||
|
frames.Add(stackFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static MethodDisplayInfo GetMethodDisplayString(MethodBase method)
|
||||||
|
{
|
||||||
|
// Special case: no method available
|
||||||
|
if (method == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var methodDisplayInfo = new MethodDisplayInfo();
|
||||||
|
|
||||||
|
// Type name
|
||||||
|
var type = method.DeclaringType;
|
||||||
|
|
||||||
|
var methodName = method.Name;
|
||||||
|
|
||||||
|
if (type != null && type.IsDefined(typeof(CompilerGeneratedAttribute)) &&
|
||||||
|
(typeof(IAsyncStateMachine).IsAssignableFrom(type) || typeof(IEnumerator).IsAssignableFrom(type)))
|
||||||
|
{
|
||||||
|
// Convert StateMachine methods to correct overload +MoveNext()
|
||||||
|
if (TryResolveStateMachineMethod(ref method, out type))
|
||||||
|
{
|
||||||
|
methodDisplayInfo.SubMethod = methodName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ResolveStateMachineMethod may have set declaringType to null
|
||||||
|
if (type != null)
|
||||||
|
{
|
||||||
|
methodDisplayInfo.DeclaringTypeName = TypeNameHelper.GetTypeDisplayName(type, includeGenericParameterNames: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method name
|
||||||
|
methodDisplayInfo.Name = method.Name;
|
||||||
|
if (method.IsGenericMethod)
|
||||||
|
{
|
||||||
|
var genericArguments = string.Join(", ", method.GetGenericArguments()
|
||||||
|
.Select(arg => TypeNameHelper.GetTypeDisplayName(arg, fullName: false, includeGenericParameterNames: true)));
|
||||||
|
methodDisplayInfo.GenericArguments += "<" + genericArguments + ">";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method parameters
|
||||||
|
methodDisplayInfo.Parameters = method.GetParameters().Select(parameter =>
|
||||||
|
{
|
||||||
|
var parameterType = parameter.ParameterType;
|
||||||
|
|
||||||
|
var prefix = string.Empty;
|
||||||
|
if (parameter.IsOut)
|
||||||
|
{
|
||||||
|
prefix = "out";
|
||||||
|
}
|
||||||
|
else if (parameterType != null && parameterType.IsByRef)
|
||||||
|
{
|
||||||
|
prefix = "ref";
|
||||||
|
}
|
||||||
|
|
||||||
|
var parameterTypeString = "?";
|
||||||
|
if (parameterType != null)
|
||||||
|
{
|
||||||
|
if (parameterType.IsByRef)
|
||||||
|
{
|
||||||
|
parameterType = parameterType.GetElementType();
|
||||||
|
}
|
||||||
|
|
||||||
|
parameterTypeString = TypeNameHelper.GetTypeDisplayName(parameterType, fullName: false, includeGenericParameterNames: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ParameterDisplayInfo
|
||||||
|
{
|
||||||
|
Prefix = prefix,
|
||||||
|
Name = parameter.Name,
|
||||||
|
Type = parameterTypeString,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return methodDisplayInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShowInStackTrace(MethodBase method)
|
||||||
|
{
|
||||||
|
Debug.Assert(method != null);
|
||||||
|
|
||||||
|
// Don't show any methods marked with the StackTraceHiddenAttribute
|
||||||
|
// https://github.com/dotnet/coreclr/pull/14652
|
||||||
|
if (HasStackTraceHiddenAttribute(method))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var type = method.DeclaringType;
|
||||||
|
if (type == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HasStackTraceHiddenAttribute(type))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallbacks for runtime pre-StackTraceHiddenAttribute
|
||||||
|
if (type == typeof(ExceptionDispatchInfo) && method.Name == "Throw")
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (type == typeof(TaskAwaiter) ||
|
||||||
|
type == typeof(TaskAwaiter<>) ||
|
||||||
|
type == typeof(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter) ||
|
||||||
|
type == typeof(ConfiguredTaskAwaitable<>.ConfiguredTaskAwaiter))
|
||||||
|
{
|
||||||
|
switch (method.Name)
|
||||||
|
{
|
||||||
|
case "HandleNonSuccessAndDebuggerNotification":
|
||||||
|
case "ThrowForNonSuccess":
|
||||||
|
case "ValidateEnd":
|
||||||
|
case "GetResult":
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryResolveStateMachineMethod(ref MethodBase method, out Type declaringType)
|
||||||
|
{
|
||||||
|
Debug.Assert(method != null);
|
||||||
|
Debug.Assert(method.DeclaringType != null);
|
||||||
|
|
||||||
|
declaringType = method.DeclaringType;
|
||||||
|
|
||||||
|
var parentType = declaringType.DeclaringType;
|
||||||
|
if (parentType == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var methods = parentType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly);
|
||||||
|
if (methods == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var candidateMethod in methods)
|
||||||
|
{
|
||||||
|
var attributes = candidateMethod.GetCustomAttributes<StateMachineAttribute>();
|
||||||
|
if (attributes == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var asma in attributes)
|
||||||
|
{
|
||||||
|
if (asma.StateMachineType == declaringType)
|
||||||
|
{
|
||||||
|
method = candidateMethod;
|
||||||
|
declaringType = candidateMethod.DeclaringType;
|
||||||
|
// Mark the iterator as changed; so it gets the + annotation of the original method
|
||||||
|
// async statemachines resolve directly to their builder methods so aren't marked as changed
|
||||||
|
return asma is IteratorStateMachineAttribute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasStackTraceHiddenAttribute(MemberInfo memberInfo)
|
||||||
|
{
|
||||||
|
IList<CustomAttributeData> attributes;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Accessing MembmerInfo.GetCustomAttributesData throws for some types (such as types in dynamically generated assemblies).
|
||||||
|
// We'll skip looking up StackTraceHiddenAttributes on such types.
|
||||||
|
attributes = memberInfo.GetCustomAttributesData();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < attributes.Count; i++)
|
||||||
|
{
|
||||||
|
if (attributes[i].AttributeType.Name == "StackTraceHiddenAttribute")
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
// 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
internal class TypeNameHelper
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<Type, string> _builtInTypeNames = new Dictionary<Type, string>
|
||||||
|
{
|
||||||
|
{ typeof(void), "void" },
|
||||||
|
{ typeof(bool), "bool" },
|
||||||
|
{ typeof(byte), "byte" },
|
||||||
|
{ typeof(char), "char" },
|
||||||
|
{ typeof(decimal), "decimal" },
|
||||||
|
{ typeof(double), "double" },
|
||||||
|
{ typeof(float), "float" },
|
||||||
|
{ typeof(int), "int" },
|
||||||
|
{ typeof(long), "long" },
|
||||||
|
{ typeof(object), "object" },
|
||||||
|
{ typeof(sbyte), "sbyte" },
|
||||||
|
{ typeof(short), "short" },
|
||||||
|
{ typeof(string), "string" },
|
||||||
|
{ typeof(uint), "uint" },
|
||||||
|
{ typeof(ulong), "ulong" },
|
||||||
|
{ typeof(ushort), "ushort" }
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string GetTypeDisplayName(object item, bool fullName = true)
|
||||||
|
{
|
||||||
|
return item == null ? null : GetTypeDisplayName(item.GetType(), fullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pretty print a type name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The <see cref="Type"/>.</param>
|
||||||
|
/// <param name="fullName"><c>true</c> to print a fully qualified name.</param>
|
||||||
|
/// <param name="includeGenericParameterNames"><c>true</c> to include generic parameter names.</param>
|
||||||
|
/// <returns>The pretty printed type name.</returns>
|
||||||
|
public static string GetTypeDisplayName(Type type, bool fullName = true, bool includeGenericParameterNames = false)
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
ProcessType(builder, type, new DisplayNameOptions(fullName, includeGenericParameterNames));
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ProcessType(StringBuilder builder, Type type, DisplayNameOptions options)
|
||||||
|
{
|
||||||
|
if (type.IsGenericType)
|
||||||
|
{
|
||||||
|
var genericArguments = type.GetGenericArguments();
|
||||||
|
ProcessGenericType(builder, type, genericArguments, genericArguments.Length, options);
|
||||||
|
}
|
||||||
|
else if (type.IsArray)
|
||||||
|
{
|
||||||
|
ProcessArrayType(builder, type, options);
|
||||||
|
}
|
||||||
|
else if (_builtInTypeNames.TryGetValue(type, out var builtInName))
|
||||||
|
{
|
||||||
|
builder.Append(builtInName);
|
||||||
|
}
|
||||||
|
else if (type.IsGenericParameter)
|
||||||
|
{
|
||||||
|
if (options.IncludeGenericParameterNames)
|
||||||
|
{
|
||||||
|
builder.Append(type.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
builder.Append(options.FullName ? type.FullName : type.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ProcessArrayType(StringBuilder builder, Type type, DisplayNameOptions options)
|
||||||
|
{
|
||||||
|
var innerType = type;
|
||||||
|
while (innerType.IsArray)
|
||||||
|
{
|
||||||
|
innerType = innerType.GetElementType();
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessType(builder, innerType, options);
|
||||||
|
|
||||||
|
while (type.IsArray)
|
||||||
|
{
|
||||||
|
builder.Append('[');
|
||||||
|
builder.Append(',', type.GetArrayRank() - 1);
|
||||||
|
builder.Append(']');
|
||||||
|
type = type.GetElementType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ProcessGenericType(StringBuilder builder, Type type, Type[] genericArguments, int length, DisplayNameOptions options)
|
||||||
|
{
|
||||||
|
var offset = 0;
|
||||||
|
if (type.IsNested)
|
||||||
|
{
|
||||||
|
offset = type.DeclaringType.GetGenericArguments().Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.FullName)
|
||||||
|
{
|
||||||
|
if (type.IsNested)
|
||||||
|
{
|
||||||
|
ProcessGenericType(builder, type.DeclaringType, genericArguments, offset, options);
|
||||||
|
builder.Append('+');
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(type.Namespace))
|
||||||
|
{
|
||||||
|
builder.Append(type.Namespace);
|
||||||
|
builder.Append('.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var genericPartIndex = type.Name.IndexOf('`');
|
||||||
|
if (genericPartIndex <= 0)
|
||||||
|
{
|
||||||
|
builder.Append(type.Name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(type.Name, 0, genericPartIndex);
|
||||||
|
|
||||||
|
builder.Append('<');
|
||||||
|
for (var i = offset; i < length; i++)
|
||||||
|
{
|
||||||
|
ProcessType(builder, genericArguments[i], options);
|
||||||
|
if (i + 1 == length)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(',');
|
||||||
|
if (options.IncludeGenericParameterNames || !genericArguments[i + 1].IsGenericParameter)
|
||||||
|
{
|
||||||
|
builder.Append(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.Append('>');
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct DisplayNameOptions
|
||||||
|
{
|
||||||
|
public DisplayNameOptions(bool fullName, bool includeGenericParameterNames)
|
||||||
|
{
|
||||||
|
FullName = fullName;
|
||||||
|
IncludeGenericParameterNames = includeGenericParameterNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool FullName { get; }
|
||||||
|
|
||||||
|
public bool IncludeGenericParameterNames { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
internal struct ValueStopwatch
|
||||||
|
{
|
||||||
|
private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency;
|
||||||
|
|
||||||
|
private long _startTimestamp;
|
||||||
|
|
||||||
|
public bool IsActive => _startTimestamp != 0;
|
||||||
|
|
||||||
|
private ValueStopwatch(long startTimestamp)
|
||||||
|
{
|
||||||
|
_startTimestamp = startTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ValueStopwatch StartNew() => new ValueStopwatch(Stopwatch.GetTimestamp());
|
||||||
|
|
||||||
|
public TimeSpan GetElapsedTime()
|
||||||
|
{
|
||||||
|
// Start timestamp can't be zero in an initialized ValueStopwatch. It would have to be literally the first thing executed when the machine boots to be 0.
|
||||||
|
// So it being 0 is a clear indication of default(ValueStopwatch)
|
||||||
|
if (!IsActive)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("An uninitialized, or 'default', ValueStopwatch cannot be used to get elapsed time.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var end = Stopwatch.GetTimestamp();
|
||||||
|
var timestampDelta = end - _startTimestamp;
|
||||||
|
var ticks = (long)(TimestampToTicks * timestampDelta);
|
||||||
|
return new TimeSpan(ticks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
// 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.Globalization;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.WebEncoders.Sources
|
||||||
|
{
|
||||||
|
// TODO using a resx file. project.json, unfortunately, fails to embed resx files when there are also compile items
|
||||||
|
// in the contentFiles section. Revisit once we convert repos to MSBuild
|
||||||
|
internal static class EncoderResources
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Invalid {0}, {1} or {2} length.
|
||||||
|
/// </summary>
|
||||||
|
internal static readonly string WebEncoders_InvalidCountOffsetOrLength = "Invalid {0}, {1} or {2} length.";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Malformed input: {0} is an invalid input length.
|
||||||
|
/// </summary>
|
||||||
|
internal static readonly string WebEncoders_MalformedInput = "Malformed input: {0} is an invalid input length.";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invalid {0}, {1} or {2} length.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatWebEncoders_InvalidCountOffsetOrLength(object p0, object p1, object p2)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, WebEncoders_InvalidCountOffsetOrLength, p0, p1, p2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Malformed input: {0} is an invalid input length.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatWebEncoders_MalformedInput(object p0)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, WebEncoders_MalformedInput, p0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,388 @@
|
||||||
|
// 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.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using Microsoft.Extensions.WebEncoders.Sources;
|
||||||
|
|
||||||
|
#if WebEncoders_In_WebUtilities
|
||||||
|
namespace Microsoft.AspNetCore.WebUtilities
|
||||||
|
#else
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains utility APIs to assist with common encoding and decoding operations.
|
||||||
|
/// </summary>
|
||||||
|
#if WebEncoders_In_WebUtilities
|
||||||
|
public
|
||||||
|
#else
|
||||||
|
internal
|
||||||
|
#endif
|
||||||
|
static class WebEncoders
|
||||||
|
{
|
||||||
|
private static readonly byte[] EmptyBytes = new byte[0];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decodes a base64url-encoded string.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">The base64url-encoded input to decode.</param>
|
||||||
|
/// <returns>The base64url-decoded form of the input.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// The input must not contain any whitespace or padding characters.
|
||||||
|
/// Throws <see cref="FormatException"/> if the input is malformed.
|
||||||
|
/// </remarks>
|
||||||
|
public static byte[] Base64UrlDecode(string input)
|
||||||
|
{
|
||||||
|
if (input == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Base64UrlDecode(input, offset: 0, count: input.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decodes a base64url-encoded substring of a given string.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">A string containing the base64url-encoded input to decode.</param>
|
||||||
|
/// <param name="offset">The position in <paramref name="input"/> at which decoding should begin.</param>
|
||||||
|
/// <param name="count">The number of characters in <paramref name="input"/> to decode.</param>
|
||||||
|
/// <returns>The base64url-decoded form of the input.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// The input must not contain any whitespace or padding characters.
|
||||||
|
/// Throws <see cref="FormatException"/> if the input is malformed.
|
||||||
|
/// </remarks>
|
||||||
|
public static byte[] Base64UrlDecode(string input, int offset, int count)
|
||||||
|
{
|
||||||
|
if (input == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateParameters(input.Length, nameof(input), offset, count);
|
||||||
|
|
||||||
|
// Special-case empty input
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
return EmptyBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create array large enough for the Base64 characters, not just shorter Base64-URL-encoded form.
|
||||||
|
var buffer = new char[GetArraySizeRequiredToDecode(count)];
|
||||||
|
|
||||||
|
return Base64UrlDecode(input, offset, buffer, bufferOffset: 0, count: count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decodes a base64url-encoded <paramref name="input"/> into a <c>byte[]</c>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">A string containing the base64url-encoded input to decode.</param>
|
||||||
|
/// <param name="offset">The position in <paramref name="input"/> at which decoding should begin.</param>
|
||||||
|
/// <param name="buffer">
|
||||||
|
/// Scratch buffer to hold the <see cref="char"/>s to decode. Array must be large enough to hold
|
||||||
|
/// <paramref name="bufferOffset"/> and <paramref name="count"/> characters as well as Base64 padding
|
||||||
|
/// characters. Content is not preserved.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="bufferOffset">
|
||||||
|
/// The offset into <paramref name="buffer"/> at which to begin writing the <see cref="char"/>s to decode.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="count">The number of characters in <paramref name="input"/> to decode.</param>
|
||||||
|
/// <returns>The base64url-decoded form of the <paramref name="input"/>.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// The input must not contain any whitespace or padding characters.
|
||||||
|
/// Throws <see cref="FormatException"/> if the input is malformed.
|
||||||
|
/// </remarks>
|
||||||
|
public static byte[] Base64UrlDecode(string input, int offset, char[] buffer, int bufferOffset, int count)
|
||||||
|
{
|
||||||
|
if (input == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(input));
|
||||||
|
}
|
||||||
|
if (buffer == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateParameters(input.Length, nameof(input), offset, count);
|
||||||
|
if (bufferOffset < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(bufferOffset));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
return EmptyBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumption: input is base64url encoded without padding and contains no whitespace.
|
||||||
|
|
||||||
|
var paddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(count);
|
||||||
|
var arraySizeRequired = checked(count + paddingCharsToAdd);
|
||||||
|
Debug.Assert(arraySizeRequired % 4 == 0, "Invariant: Array length must be a multiple of 4.");
|
||||||
|
|
||||||
|
if (buffer.Length - bufferOffset < arraySizeRequired)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
string.Format(
|
||||||
|
CultureInfo.CurrentCulture,
|
||||||
|
EncoderResources.WebEncoders_InvalidCountOffsetOrLength,
|
||||||
|
nameof(count),
|
||||||
|
nameof(bufferOffset),
|
||||||
|
nameof(input)),
|
||||||
|
nameof(count));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy input into buffer, fixing up '-' -> '+' and '_' -> '/'.
|
||||||
|
var i = bufferOffset;
|
||||||
|
for (var j = offset; i - bufferOffset < count; i++, j++)
|
||||||
|
{
|
||||||
|
var ch = input[j];
|
||||||
|
if (ch == '-')
|
||||||
|
{
|
||||||
|
buffer[i] = '+';
|
||||||
|
}
|
||||||
|
else if (ch == '_')
|
||||||
|
{
|
||||||
|
buffer[i] = '/';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
buffer[i] = ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the padding characters back.
|
||||||
|
for (; paddingCharsToAdd > 0; i++, paddingCharsToAdd--)
|
||||||
|
{
|
||||||
|
buffer[i] = '=';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode.
|
||||||
|
// If the caller provided invalid base64 chars, they'll be caught here.
|
||||||
|
return Convert.FromBase64CharArray(buffer, bufferOffset, arraySizeRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the minimum <c>char[]</c> size required for decoding of <paramref name="count"/> characters
|
||||||
|
/// with the <see cref="Base64UrlDecode(string, int, char[], int, int)"/> method.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="count">The number of characters to decode.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The minimum <c>char[]</c> size required for decoding of <paramref name="count"/> characters.
|
||||||
|
/// </returns>
|
||||||
|
public static int GetArraySizeRequiredToDecode(int count)
|
||||||
|
{
|
||||||
|
if (count < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(count));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var numPaddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(count);
|
||||||
|
|
||||||
|
return checked(count + numPaddingCharsToAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encodes <paramref name="input"/> using base64url encoding.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">The binary input to encode.</param>
|
||||||
|
/// <returns>The base64url-encoded form of <paramref name="input"/>.</returns>
|
||||||
|
public static string Base64UrlEncode(byte[] input)
|
||||||
|
{
|
||||||
|
if (input == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Base64UrlEncode(input, offset: 0, count: input.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encodes <paramref name="input"/> using base64url encoding.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">The binary input to encode.</param>
|
||||||
|
/// <param name="offset">The offset into <paramref name="input"/> at which to begin encoding.</param>
|
||||||
|
/// <param name="count">The number of bytes from <paramref name="input"/> to encode.</param>
|
||||||
|
/// <returns>The base64url-encoded form of <paramref name="input"/>.</returns>
|
||||||
|
public static string Base64UrlEncode(byte[] input, int offset, int count)
|
||||||
|
{
|
||||||
|
if (input == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateParameters(input.Length, nameof(input), offset, count);
|
||||||
|
|
||||||
|
// Special-case empty input
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = new char[GetArraySizeRequiredToEncode(count)];
|
||||||
|
var numBase64Chars = Base64UrlEncode(input, offset, buffer, outputOffset: 0, count: count);
|
||||||
|
|
||||||
|
return new String(buffer, startIndex: 0, length: numBase64Chars);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encodes <paramref name="input"/> using base64url encoding.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">The binary input to encode.</param>
|
||||||
|
/// <param name="offset">The offset into <paramref name="input"/> at which to begin encoding.</param>
|
||||||
|
/// <param name="output">
|
||||||
|
/// Buffer to receive the base64url-encoded form of <paramref name="input"/>. Array must be large enough to
|
||||||
|
/// hold <paramref name="outputOffset"/> characters and the full base64-encoded form of
|
||||||
|
/// <paramref name="input"/>, including padding characters.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="outputOffset">
|
||||||
|
/// The offset into <paramref name="output"/> at which to begin writing the base64url-encoded form of
|
||||||
|
/// <paramref name="input"/>.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="count">The number of <c>byte</c>s from <paramref name="input"/> to encode.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The number of characters written to <paramref name="output"/>, less any padding characters.
|
||||||
|
/// </returns>
|
||||||
|
public static int Base64UrlEncode(byte[] input, int offset, char[] output, int outputOffset, int count)
|
||||||
|
{
|
||||||
|
if (input == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(input));
|
||||||
|
}
|
||||||
|
if (output == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(output));
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateParameters(input.Length, nameof(input), offset, count);
|
||||||
|
if (outputOffset < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(outputOffset));
|
||||||
|
}
|
||||||
|
|
||||||
|
var arraySizeRequired = GetArraySizeRequiredToEncode(count);
|
||||||
|
if (output.Length - outputOffset < arraySizeRequired)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
string.Format(
|
||||||
|
CultureInfo.CurrentCulture,
|
||||||
|
EncoderResources.WebEncoders_InvalidCountOffsetOrLength,
|
||||||
|
nameof(count),
|
||||||
|
nameof(outputOffset),
|
||||||
|
nameof(output)),
|
||||||
|
nameof(count));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special-case empty input.
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use base64url encoding with no padding characters. See RFC 4648, Sec. 5.
|
||||||
|
|
||||||
|
// Start with default Base64 encoding.
|
||||||
|
var numBase64Chars = Convert.ToBase64CharArray(input, offset, count, output, outputOffset);
|
||||||
|
|
||||||
|
// Fix up '+' -> '-' and '/' -> '_'. Drop padding characters.
|
||||||
|
for (var i = outputOffset; i - outputOffset < numBase64Chars; i++)
|
||||||
|
{
|
||||||
|
var ch = output[i];
|
||||||
|
if (ch == '+')
|
||||||
|
{
|
||||||
|
output[i] = '-';
|
||||||
|
}
|
||||||
|
else if (ch == '/')
|
||||||
|
{
|
||||||
|
output[i] = '_';
|
||||||
|
}
|
||||||
|
else if (ch == '=')
|
||||||
|
{
|
||||||
|
// We've reached a padding character; truncate the remainder.
|
||||||
|
return i - outputOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return numBase64Chars;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the minimum output <c>char[]</c> size required for encoding <paramref name="count"/>
|
||||||
|
/// <see cref="byte"/>s with the <see cref="Base64UrlEncode(byte[], int, char[], int, int)"/> method.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="count">The number of characters to encode.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The minimum output <c>char[]</c> size required for encoding <paramref name="count"/> <see cref="byte"/>s.
|
||||||
|
/// </returns>
|
||||||
|
public static int GetArraySizeRequiredToEncode(int count)
|
||||||
|
{
|
||||||
|
var numWholeOrPartialInputBlocks = checked(count + 2) / 3;
|
||||||
|
return checked(numWholeOrPartialInputBlocks * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetNumBase64PaddingCharsInString(string str)
|
||||||
|
{
|
||||||
|
// Assumption: input contains a well-formed base64 string with no whitespace.
|
||||||
|
|
||||||
|
// base64 guaranteed have 0 - 2 padding characters.
|
||||||
|
if (str[str.Length - 1] == '=')
|
||||||
|
{
|
||||||
|
if (str[str.Length - 2] == '=')
|
||||||
|
{
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetNumBase64PaddingCharsToAddForDecode(int inputLength)
|
||||||
|
{
|
||||||
|
switch (inputLength % 4)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
return 0;
|
||||||
|
case 2:
|
||||||
|
return 2;
|
||||||
|
case 3:
|
||||||
|
return 1;
|
||||||
|
default:
|
||||||
|
throw new FormatException(
|
||||||
|
string.Format(
|
||||||
|
CultureInfo.CurrentCulture,
|
||||||
|
EncoderResources.WebEncoders_MalformedInput,
|
||||||
|
inputLength));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateParameters(int bufferLength, string inputName, int offset, int count)
|
||||||
|
{
|
||||||
|
if (offset < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(offset));
|
||||||
|
}
|
||||||
|
if (count < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(count));
|
||||||
|
}
|
||||||
|
if (bufferLength - offset < count)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
string.Format(
|
||||||
|
CultureInfo.CurrentCulture,
|
||||||
|
EncoderResources.WebEncoders_InvalidCountOffsetOrLength,
|
||||||
|
nameof(count),
|
||||||
|
nameof(offset),
|
||||||
|
inputName),
|
||||||
|
nameof(count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
// 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 Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.CommandLineUtils
|
||||||
|
{
|
||||||
|
public class ArgumentEscaperTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(new[] { "one", "two", "three" }, "one two three")]
|
||||||
|
[InlineData(new[] { "line1\nline2", "word1\tword2" }, "\"line1\nline2\" \"word1\tword2\"")]
|
||||||
|
[InlineData(new[] { "with spaces" }, "\"with spaces\"")]
|
||||||
|
[InlineData(new[] { @"with\backslash" }, @"with\backslash")]
|
||||||
|
[InlineData(new[] { @"""quotedwith\backslash""" }, @"\""quotedwith\backslash\""")]
|
||||||
|
[InlineData(new[] { @"C:\Users\" }, @"C:\Users\")]
|
||||||
|
[InlineData(new[] { @"C:\Program Files\dotnet\" }, @"""C:\Program Files\dotnet\\""")]
|
||||||
|
[InlineData(new[] { @"backslash\""preceedingquote" }, @"backslash\\\""preceedingquote")]
|
||||||
|
public void EscapesArguments(string[] args, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, ArgumentEscaper.EscapeAndConcatenate(args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,304 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#if NETCOREAPP2_0 || NETCOREAPP2_1
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Text;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Certificates.Generation.Tests
|
||||||
|
{
|
||||||
|
public class CertificateManagerTests
|
||||||
|
{
|
||||||
|
public CertificateManagerTests(ITestOutputHelper output)
|
||||||
|
{
|
||||||
|
Output = output;
|
||||||
|
}
|
||||||
|
|
||||||
|
public const string TestCertificateSubject = "CN=aspnet.test";
|
||||||
|
|
||||||
|
public ITestOutputHelper Output { get; }
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EnsureCreateHttpsCertificate_CreatesACertificate_WhenThereAreNoHttpsCertificates()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string CertificateName = nameof(EnsureCreateHttpsCertificate_CreatesACertificate_WhenThereAreNoHttpsCertificates) + ".cer";
|
||||||
|
var manager = new CertificateManager();
|
||||||
|
|
||||||
|
manager.RemoveAllCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, TestCertificateSubject);
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
manager.RemoveAllCertificates(CertificatePurpose.HTTPS, StoreName.Root, StoreLocation.CurrentUser, TestCertificateSubject);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||||
|
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
|
||||||
|
var result = manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, subject: TestCertificateSubject);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(EnsureCertificateResult.Succeeded, result);
|
||||||
|
Assert.True(File.Exists(CertificateName));
|
||||||
|
|
||||||
|
var exportedCertificate = new X509Certificate2(File.ReadAllBytes(CertificateName));
|
||||||
|
Assert.NotNull(exportedCertificate);
|
||||||
|
Assert.False(exportedCertificate.HasPrivateKey);
|
||||||
|
|
||||||
|
var httpsCertificates = manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false);
|
||||||
|
var httpsCertificate = Assert.Single(httpsCertificates, c => c.Subject == TestCertificateSubject);
|
||||||
|
Assert.True(httpsCertificate.HasPrivateKey);
|
||||||
|
Assert.Equal(TestCertificateSubject, httpsCertificate.Subject);
|
||||||
|
Assert.Equal(TestCertificateSubject, httpsCertificate.Issuer);
|
||||||
|
Assert.Equal("sha256RSA", httpsCertificate.SignatureAlgorithm.FriendlyName);
|
||||||
|
Assert.Equal("1.2.840.113549.1.1.11", httpsCertificate.SignatureAlgorithm.Value);
|
||||||
|
|
||||||
|
Assert.Equal(now.LocalDateTime, httpsCertificate.NotBefore);
|
||||||
|
Assert.Equal(now.AddYears(1).LocalDateTime, httpsCertificate.NotAfter);
|
||||||
|
Assert.Contains(
|
||||||
|
httpsCertificate.Extensions.OfType<X509Extension>(),
|
||||||
|
e => e is X509BasicConstraintsExtension basicConstraints &&
|
||||||
|
basicConstraints.Critical == true &&
|
||||||
|
basicConstraints.CertificateAuthority == false &&
|
||||||
|
basicConstraints.HasPathLengthConstraint == false &&
|
||||||
|
basicConstraints.PathLengthConstraint == 0);
|
||||||
|
|
||||||
|
Assert.Contains(
|
||||||
|
httpsCertificate.Extensions.OfType<X509Extension>(),
|
||||||
|
e => e is X509KeyUsageExtension keyUsage &&
|
||||||
|
keyUsage.Critical == true &&
|
||||||
|
keyUsage.KeyUsages == X509KeyUsageFlags.KeyEncipherment);
|
||||||
|
|
||||||
|
Assert.Contains(
|
||||||
|
httpsCertificate.Extensions.OfType<X509Extension>(),
|
||||||
|
e => e is X509EnhancedKeyUsageExtension enhancedKeyUsage &&
|
||||||
|
enhancedKeyUsage.Critical == true &&
|
||||||
|
enhancedKeyUsage.EnhancedKeyUsages.OfType<Oid>().Single() is Oid keyUsage &&
|
||||||
|
keyUsage.Value == "1.3.6.1.5.5.7.3.1");
|
||||||
|
|
||||||
|
// Subject alternative name
|
||||||
|
Assert.Contains(
|
||||||
|
httpsCertificate.Extensions.OfType<X509Extension>(),
|
||||||
|
e => e.Critical == true &&
|
||||||
|
e.Oid.Value == "2.5.29.17");
|
||||||
|
|
||||||
|
// ASP.NET HTTPS Development certificate extension
|
||||||
|
Assert.Contains(
|
||||||
|
httpsCertificate.Extensions.OfType<X509Extension>(),
|
||||||
|
e => e.Critical == false &&
|
||||||
|
e.Oid.Value == "1.3.6.1.4.1.311.84.1.1" &&
|
||||||
|
Encoding.ASCII.GetString(e.RawData) == "ASP.NET Core HTTPS development certificate");
|
||||||
|
|
||||||
|
Assert.Equal(httpsCertificate.GetCertHashString(), exportedCertificate.GetCertHashString());
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Output.WriteLine(e.Message);
|
||||||
|
ListCertificates(Output);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ListCertificates(ITestOutputHelper output)
|
||||||
|
{
|
||||||
|
using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
|
||||||
|
{
|
||||||
|
store.Open(OpenFlags.ReadOnly);
|
||||||
|
var certificates = store.Certificates;
|
||||||
|
foreach (var certificate in certificates)
|
||||||
|
{
|
||||||
|
Output.WriteLine($"Certificate: '{Convert.ToBase64String(certificate.Export(X509ContentType.Cert))}'.");
|
||||||
|
certificate.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
store.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EnsureCreateHttpsCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string CertificateName = nameof(EnsureCreateHttpsCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates) + ".pfx";
|
||||||
|
var certificatePassword = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
var manager = new CertificateManager();
|
||||||
|
|
||||||
|
manager.RemoveAllCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, TestCertificateSubject);
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
manager.RemoveAllCertificates(CertificatePurpose.HTTPS, StoreName.Root, StoreLocation.CurrentUser, TestCertificateSubject);
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||||
|
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
|
||||||
|
manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject);
|
||||||
|
|
||||||
|
var httpsCertificate = manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false).Single(c => c.Subject == TestCertificateSubject);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, includePrivateKey: true, password: certificatePassword, subject: TestCertificateSubject);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(EnsureCertificateResult.ValidCertificatePresent, result);
|
||||||
|
Assert.True(File.Exists(CertificateName));
|
||||||
|
|
||||||
|
var exportedCertificate = new X509Certificate2(File.ReadAllBytes(CertificateName), certificatePassword);
|
||||||
|
Assert.NotNull(exportedCertificate);
|
||||||
|
Assert.True(exportedCertificate.HasPrivateKey);
|
||||||
|
|
||||||
|
|
||||||
|
Assert.Equal(httpsCertificate.GetCertHashString(), exportedCertificate.GetCertHashString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact(Skip = "Requires user interaction")]
|
||||||
|
public void EnsureAspNetCoreHttpsDevelopmentCertificate_ReturnsCorrectResult_WhenUserCancelsTrustStepOnWindows()
|
||||||
|
{
|
||||||
|
var manager = new CertificateManager();
|
||||||
|
|
||||||
|
manager.RemoveAllCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, TestCertificateSubject);
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
manager.RemoveAllCertificates(CertificatePurpose.HTTPS, StoreName.Root, StoreLocation.CurrentUser, TestCertificateSubject);
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||||
|
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
|
||||||
|
var trustFailed = manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: true, subject: TestCertificateSubject);
|
||||||
|
|
||||||
|
Assert.Equal(EnsureCertificateResult.UserCancelledTrustStep, trustFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact(Skip = "Requires user interaction")]
|
||||||
|
public void EnsureAspNetCoreHttpsDevelopmentCertificate_CanRemoveCertificates()
|
||||||
|
{
|
||||||
|
var manager = new CertificateManager();
|
||||||
|
|
||||||
|
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||||
|
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
|
||||||
|
manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), path: null, trust: true, subject: TestCertificateSubject);
|
||||||
|
|
||||||
|
manager.CleanupHttpsCertificates(TestCertificateSubject);
|
||||||
|
|
||||||
|
Assert.Empty(manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: false).Where(c => c.Subject == TestCertificateSubject));
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
Assert.Empty(manager.ListCertificates(CertificatePurpose.HTTPS, StoreName.Root, StoreLocation.CurrentUser, isValid: false).Where(c => c.Subject == TestCertificateSubject));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EnsureCreateIdentityTokenSigningCertificate_CreatesACertificate_WhenThereAreNoHttpsCertificates()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string CertificateName = nameof(EnsureCreateIdentityTokenSigningCertificate_CreatesACertificate_WhenThereAreNoHttpsCertificates) + ".cer";
|
||||||
|
var manager = new CertificateManager();
|
||||||
|
|
||||||
|
manager.RemoveAllCertificates(CertificatePurpose.Signing, StoreName.My, StoreLocation.CurrentUser, TestCertificateSubject);
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
manager.RemoveAllCertificates(CertificatePurpose.Signing, StoreName.Root, StoreLocation.CurrentUser, TestCertificateSubject);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||||
|
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
|
||||||
|
var result = manager.EnsureAspNetCoreApplicationTokensDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, subject: TestCertificateSubject);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(EnsureCertificateResult.Succeeded, result);
|
||||||
|
Assert.True(File.Exists(CertificateName));
|
||||||
|
|
||||||
|
var exportedCertificate = new X509Certificate2(File.ReadAllBytes(CertificateName));
|
||||||
|
Assert.NotNull(exportedCertificate);
|
||||||
|
Assert.False(exportedCertificate.HasPrivateKey);
|
||||||
|
|
||||||
|
var identityCertificates = manager.ListCertificates(CertificatePurpose.Signing, StoreName.My, StoreLocation.CurrentUser, isValid: false);
|
||||||
|
var identityCertificate = Assert.Single(identityCertificates, i => i.Subject == TestCertificateSubject);
|
||||||
|
Assert.True(identityCertificate.HasPrivateKey);
|
||||||
|
Assert.Equal(TestCertificateSubject, identityCertificate.Subject);
|
||||||
|
Assert.Equal(TestCertificateSubject, identityCertificate.Issuer);
|
||||||
|
Assert.Equal("sha256RSA", identityCertificate.SignatureAlgorithm.FriendlyName);
|
||||||
|
Assert.Equal("1.2.840.113549.1.1.11", identityCertificate.SignatureAlgorithm.Value);
|
||||||
|
|
||||||
|
Assert.Equal(now.LocalDateTime, identityCertificate.NotBefore);
|
||||||
|
Assert.Equal(now.AddYears(1).LocalDateTime, identityCertificate.NotAfter);
|
||||||
|
Assert.Contains(
|
||||||
|
identityCertificate.Extensions.OfType<X509Extension>(),
|
||||||
|
e => e is X509BasicConstraintsExtension basicConstraints &&
|
||||||
|
basicConstraints.Critical == true &&
|
||||||
|
basicConstraints.CertificateAuthority == false &&
|
||||||
|
basicConstraints.HasPathLengthConstraint == false &&
|
||||||
|
basicConstraints.PathLengthConstraint == 0);
|
||||||
|
|
||||||
|
Assert.Contains(
|
||||||
|
identityCertificate.Extensions.OfType<X509Extension>(),
|
||||||
|
e => e is X509KeyUsageExtension keyUsage &&
|
||||||
|
keyUsage.Critical == true &&
|
||||||
|
keyUsage.KeyUsages == X509KeyUsageFlags.DigitalSignature);
|
||||||
|
|
||||||
|
Assert.Contains(
|
||||||
|
identityCertificate.Extensions.OfType<X509Extension>(),
|
||||||
|
e => e is X509EnhancedKeyUsageExtension enhancedKeyUsage &&
|
||||||
|
enhancedKeyUsage.Critical == true &&
|
||||||
|
enhancedKeyUsage.EnhancedKeyUsages.OfType<Oid>().Single() is Oid keyUsage &&
|
||||||
|
keyUsage.Value == "1.3.6.1.5.5.7.3.1");
|
||||||
|
|
||||||
|
// ASP.NET Core Identity Json Web Token signing development certificate
|
||||||
|
Assert.Contains(
|
||||||
|
identityCertificate.Extensions.OfType<X509Extension>(),
|
||||||
|
e => e.Critical == false &&
|
||||||
|
e.Oid.Value == "1.3.6.1.4.1.311.84.1.2" &&
|
||||||
|
Encoding.ASCII.GetString(e.RawData) == "ASP.NET Core Identity Json Web Token signing development certificate");
|
||||||
|
|
||||||
|
Assert.Equal(identityCertificate.GetCertHashString(), exportedCertificate.GetCertHashString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EnsureCreateIdentityTokenSigningCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string CertificateName = nameof(EnsureCreateIdentityTokenSigningCertificate_DoesNotCreateACertificate_WhenThereIsAnExistingHttpsCertificates) + ".pfx";
|
||||||
|
var certificatePassword = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
var manager = new CertificateManager();
|
||||||
|
|
||||||
|
manager.RemoveAllCertificates(CertificatePurpose.Signing, StoreName.My, StoreLocation.CurrentUser, TestCertificateSubject);
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
manager.RemoveAllCertificates(CertificatePurpose.Signing, StoreName.Root, StoreLocation.CurrentUser, TestCertificateSubject);
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||||
|
now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, 0, now.Offset);
|
||||||
|
manager.EnsureAspNetCoreApplicationTokensDevelopmentCertificate(now, now.AddYears(1), path: null, trust: false, subject: TestCertificateSubject);
|
||||||
|
|
||||||
|
var identityTokenSigningCertificates = manager.ListCertificates(CertificatePurpose.Signing, StoreName.My, StoreLocation.CurrentUser, isValid: false).Single(c => c.Subject == TestCertificateSubject);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = manager.EnsureAspNetCoreApplicationTokensDevelopmentCertificate(now, now.AddYears(1), CertificateName, trust: false, includePrivateKey: true, password: certificatePassword, subject: TestCertificateSubject);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(EnsureCertificateResult.ValidCertificatePresent, result);
|
||||||
|
Assert.True(File.Exists(CertificateName));
|
||||||
|
|
||||||
|
var exportedCertificate = new X509Certificate2(File.ReadAllBytes(CertificateName), certificatePassword);
|
||||||
|
Assert.NotNull(exportedCertificate);
|
||||||
|
Assert.True(exportedCertificate.HasPrivateKey);
|
||||||
|
|
||||||
|
Assert.Equal(identityTokenSigningCertificates.GetCertHashString(), exportedCertificate.GetCertHashString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,360 @@
|
||||||
|
// 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
public class ClosedGenericMatcherTest
|
||||||
|
{
|
||||||
|
// queryType, interfaceType, expectedResult
|
||||||
|
public static TheoryData<Type, Type, Type> ExtractGenericInterfaceDataSet
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return new TheoryData<Type, Type, Type>
|
||||||
|
{
|
||||||
|
// Closed generic types that match given open generic type.
|
||||||
|
{
|
||||||
|
typeof(IEnumerable<BaseClass>),
|
||||||
|
typeof(IEnumerable<>),
|
||||||
|
typeof(IEnumerable<BaseClass>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(IReadOnlyList<int>),
|
||||||
|
typeof(IReadOnlyList<>),
|
||||||
|
typeof(IReadOnlyList<int>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(KeyValuePair<string, object>),
|
||||||
|
typeof(KeyValuePair<,>),
|
||||||
|
typeof(KeyValuePair<string, object>)
|
||||||
|
},
|
||||||
|
// Closed generic interfaces that implement sub-interface of given open generic type.
|
||||||
|
{
|
||||||
|
typeof(ICollection<BaseClass>),
|
||||||
|
typeof(IEnumerable<>),
|
||||||
|
typeof(IEnumerable<BaseClass>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(IReadOnlyList<int>),
|
||||||
|
typeof(IEnumerable<>),
|
||||||
|
typeof(IEnumerable<int>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(IDictionary<string, object>),
|
||||||
|
typeof(IEnumerable<>),
|
||||||
|
typeof(IEnumerable<KeyValuePair<string, object>>)
|
||||||
|
},
|
||||||
|
// Class that implements closed generic based on given open generic interface.
|
||||||
|
{
|
||||||
|
typeof(BaseClass),
|
||||||
|
typeof(IDictionary<,>),
|
||||||
|
typeof(IDictionary<string, object>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(BaseClass),
|
||||||
|
typeof(IEquatable<>),
|
||||||
|
typeof(IEquatable<BaseClass>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(BaseClass),
|
||||||
|
typeof(ICollection<>),
|
||||||
|
typeof(ICollection<KeyValuePair<string, object>>)
|
||||||
|
},
|
||||||
|
// Derived class that implements closed generic based on given open generic interface.
|
||||||
|
{
|
||||||
|
typeof(DerivedClass),
|
||||||
|
typeof(IDictionary<,>),
|
||||||
|
typeof(IDictionary<string, object>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(DerivedClass),
|
||||||
|
typeof(IEquatable<>),
|
||||||
|
typeof(IEquatable<BaseClass>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(DerivedClass),
|
||||||
|
typeof(ICollection<>),
|
||||||
|
typeof(ICollection<KeyValuePair<string, object>>)
|
||||||
|
},
|
||||||
|
// Derived class that also implements another interface.
|
||||||
|
{
|
||||||
|
typeof(DerivedClassWithComparable),
|
||||||
|
typeof(IDictionary<,>),
|
||||||
|
typeof(IDictionary<string, object>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(DerivedClassWithComparable),
|
||||||
|
typeof(IEquatable<>),
|
||||||
|
typeof(IEquatable<BaseClass>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(DerivedClassWithComparable),
|
||||||
|
typeof(ICollection<>),
|
||||||
|
typeof(ICollection<KeyValuePair<string, object>>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(DerivedClassWithComparable),
|
||||||
|
typeof(IComparable<>),
|
||||||
|
typeof(IComparable<DerivedClassWithComparable>)
|
||||||
|
},
|
||||||
|
// Derived class using system implementation.
|
||||||
|
{
|
||||||
|
typeof(DerivedClassFromSystemImplementation),
|
||||||
|
typeof(ICollection<>),
|
||||||
|
typeof(ICollection<BaseClass>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(DerivedClassFromSystemImplementation),
|
||||||
|
typeof(IReadOnlyList<>),
|
||||||
|
typeof(IReadOnlyList<BaseClass>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(DerivedClassFromSystemImplementation),
|
||||||
|
typeof(IEnumerable<>),
|
||||||
|
typeof(IEnumerable<BaseClass>)
|
||||||
|
},
|
||||||
|
// Not given an open generic type.
|
||||||
|
{
|
||||||
|
typeof(IEnumerable<BaseClass>),
|
||||||
|
typeof(IEnumerable<BaseClass>),
|
||||||
|
null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(IEnumerable<BaseClass>),
|
||||||
|
typeof(IEnumerable),
|
||||||
|
null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(IReadOnlyList<int>),
|
||||||
|
typeof(BaseClass),
|
||||||
|
null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(KeyValuePair<,>),
|
||||||
|
typeof(KeyValuePair<string, object>),
|
||||||
|
null
|
||||||
|
},
|
||||||
|
// Not a match.
|
||||||
|
{
|
||||||
|
typeof(IEnumerable<BaseClass>),
|
||||||
|
typeof(IReadOnlyList<>),
|
||||||
|
null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(IList<int>),
|
||||||
|
typeof(IReadOnlyList<>),
|
||||||
|
null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(IDictionary<string, object>),
|
||||||
|
typeof(KeyValuePair<,>),
|
||||||
|
null
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(ExtractGenericInterfaceDataSet))]
|
||||||
|
public void ExtractGenericInterface_ReturnsExpectedType(
|
||||||
|
Type queryType,
|
||||||
|
Type interfaceType,
|
||||||
|
Type expectedResult)
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var result = ClosedGenericMatcher.ExtractGenericInterface(queryType, interfaceType);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedResult, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// IEnumerable<int> is preferred because it is defined on the more-derived type.
|
||||||
|
[Fact]
|
||||||
|
public void ExtractGenericInterface_MultipleDefinitionsInherited()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var type = typeof(TwoIEnumerableImplementationsInherited);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IEnumerable<>));
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
Assert.Equal(typeof(IEnumerable<int>), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// IEnumerable<int> is preferred because we sort by Ordinal on the full name.
|
||||||
|
[Fact]
|
||||||
|
public void ExtractGenericInterface_MultipleDefinitionsOnSameType()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var type = typeof(TwoIEnumerableImplementationsOnSameClass);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IEnumerable<>));
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
Assert.Equal(typeof(IEnumerable<int>), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TwoIEnumerableImplementationsOnSameClass : IEnumerable<string>, IEnumerable<int>
|
||||||
|
{
|
||||||
|
IEnumerator<int> IEnumerable<int>.GetEnumerator()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator<string> IEnumerable<string>.GetEnumerator()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TwoIEnumerableImplementationsInherited : List<int>, IEnumerable<string>
|
||||||
|
{
|
||||||
|
IEnumerator<string> IEnumerable<string>.GetEnumerator()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BaseClass : IDictionary<string, object>, IEquatable<BaseClass>
|
||||||
|
{
|
||||||
|
object IDictionary<string, object>.this[string key]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
set
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int ICollection<KeyValuePair<string, object>>.Count
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ICollection<KeyValuePair<string, object>>.IsReadOnly
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ICollection<string> IDictionary<string, object>.Keys
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ICollection<object> IDictionary<string, object>.Values
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(BaseClass other)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
void IDictionary<string, object>.Add(string key, object value)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ICollection<KeyValuePair<string, object>>.Clear()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IDictionary<string, object>.ContainsKey(string key)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IDictionary<string, object>.Remove(string key)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IDictionary<string, object>.TryGetValue(string key, out object value)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DerivedClass : BaseClass
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DerivedClassWithComparable : DerivedClass, IComparable<DerivedClassWithComparable>
|
||||||
|
{
|
||||||
|
public int CompareTo(DerivedClassWithComparable other)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DerivedClassFromSystemImplementation : Collection<BaseClass>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,693 @@
|
||||||
|
// 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.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
public class CommandLineApplicationTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CommandNameCanBeMatched()
|
||||||
|
{
|
||||||
|
var called = false;
|
||||||
|
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
app.Command("test", c =>
|
||||||
|
{
|
||||||
|
c.OnExecute(() =>
|
||||||
|
{
|
||||||
|
called = true;
|
||||||
|
return 5;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = app.Execute("test");
|
||||||
|
Assert.Equal(5, result);
|
||||||
|
Assert.True(called);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemainingArgsArePassed()
|
||||||
|
{
|
||||||
|
CommandArgument first = null;
|
||||||
|
CommandArgument second = null;
|
||||||
|
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
|
||||||
|
app.Command("test", c =>
|
||||||
|
{
|
||||||
|
first = c.Argument("first", "First argument");
|
||||||
|
second = c.Argument("second", "Second argument");
|
||||||
|
c.OnExecute(() => 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.Execute("test", "one", "two");
|
||||||
|
|
||||||
|
Assert.Equal("one", first.Value);
|
||||||
|
Assert.Equal("two", second.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExtraArgumentCausesException()
|
||||||
|
{
|
||||||
|
CommandArgument first = null;
|
||||||
|
CommandArgument second = null;
|
||||||
|
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
|
||||||
|
app.Command("test", c =>
|
||||||
|
{
|
||||||
|
first = c.Argument("first", "First argument");
|
||||||
|
second = c.Argument("second", "Second argument");
|
||||||
|
c.OnExecute(() => 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
var ex = Assert.Throws<CommandParsingException>(() => app.Execute("test", "one", "two", "three"));
|
||||||
|
|
||||||
|
Assert.Contains("three", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UnknownCommandCausesException()
|
||||||
|
{
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
|
||||||
|
app.Command("test", c =>
|
||||||
|
{
|
||||||
|
c.Argument("first", "First argument");
|
||||||
|
c.Argument("second", "Second argument");
|
||||||
|
c.OnExecute(() => 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
var ex = Assert.Throws<CommandParsingException>(() => app.Execute("test2", "one", "two", "three"));
|
||||||
|
|
||||||
|
Assert.Contains("test2", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MultipleValuesArgumentConsumesAllArgumentValues()
|
||||||
|
{
|
||||||
|
CommandArgument argument = null;
|
||||||
|
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
|
||||||
|
app.Command("test", c =>
|
||||||
|
{
|
||||||
|
argument = c.Argument("arg", "Argument that allows multiple values", multipleValues: true);
|
||||||
|
c.OnExecute(() => 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.Execute("test", "one", "two", "three", "four", "five");
|
||||||
|
|
||||||
|
Assert.Equal(new[] { "one", "two", "three", "four", "five" }, argument.Values);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MultipleValuesArgumentConsumesAllRemainingArgumentValues()
|
||||||
|
{
|
||||||
|
CommandArgument first = null;
|
||||||
|
CommandArgument second = null;
|
||||||
|
CommandArgument third = null;
|
||||||
|
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
|
||||||
|
app.Command("test", c =>
|
||||||
|
{
|
||||||
|
first = c.Argument("first", "First argument");
|
||||||
|
second = c.Argument("second", "Second argument");
|
||||||
|
third = c.Argument("third", "Third argument that allows multiple values", multipleValues: true);
|
||||||
|
c.OnExecute(() => 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.Execute("test", "one", "two", "three", "four", "five");
|
||||||
|
|
||||||
|
Assert.Equal("one", first.Value);
|
||||||
|
Assert.Equal("two", second.Value);
|
||||||
|
Assert.Equal(new[] { "three", "four", "five" }, third.Values);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MultipleValuesArgumentMustBeTheLastArgument()
|
||||||
|
{
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
app.Argument("first", "First argument", multipleValues: true);
|
||||||
|
var ex = Assert.Throws<InvalidOperationException>(() => app.Argument("second", "Second argument"));
|
||||||
|
|
||||||
|
Assert.Contains($"The last argument 'first' accepts multiple values. No more argument can be added.",
|
||||||
|
ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OptionSwitchMayBeProvided()
|
||||||
|
{
|
||||||
|
CommandOption first = null;
|
||||||
|
CommandOption second = null;
|
||||||
|
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
|
||||||
|
app.Command("test", c =>
|
||||||
|
{
|
||||||
|
first = c.Option("--first <NAME>", "First argument", CommandOptionType.SingleValue);
|
||||||
|
second = c.Option("--second <NAME>", "Second argument", CommandOptionType.SingleValue);
|
||||||
|
c.OnExecute(() => 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.Execute("test", "--first", "one", "--second", "two");
|
||||||
|
|
||||||
|
Assert.Equal("one", first.Values[0]);
|
||||||
|
Assert.Equal("two", second.Values[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OptionValueMustBeProvided()
|
||||||
|
{
|
||||||
|
CommandOption first = null;
|
||||||
|
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
|
||||||
|
app.Command("test", c =>
|
||||||
|
{
|
||||||
|
first = c.Option("--first <NAME>", "First argument", CommandOptionType.SingleValue);
|
||||||
|
c.OnExecute(() => 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
var ex = Assert.Throws<CommandParsingException>(() => app.Execute("test", "--first"));
|
||||||
|
|
||||||
|
Assert.Contains($"Missing value for option '{first.LongName}'", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValuesMayBeAttachedToSwitch()
|
||||||
|
{
|
||||||
|
CommandOption first = null;
|
||||||
|
CommandOption second = null;
|
||||||
|
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
|
||||||
|
app.Command("test", c =>
|
||||||
|
{
|
||||||
|
first = c.Option("--first <NAME>", "First argument", CommandOptionType.SingleValue);
|
||||||
|
second = c.Option("--second <NAME>", "Second argument", CommandOptionType.SingleValue);
|
||||||
|
c.OnExecute(() => 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.Execute("test", "--first=one", "--second:two");
|
||||||
|
|
||||||
|
Assert.Equal("one", first.Values[0]);
|
||||||
|
Assert.Equal("two", second.Values[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShortNamesMayBeDefined()
|
||||||
|
{
|
||||||
|
CommandOption first = null;
|
||||||
|
CommandOption second = null;
|
||||||
|
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
|
||||||
|
app.Command("test", c =>
|
||||||
|
{
|
||||||
|
first = c.Option("-1 --first <NAME>", "First argument", CommandOptionType.SingleValue);
|
||||||
|
second = c.Option("-2 --second <NAME>", "Second argument", CommandOptionType.SingleValue);
|
||||||
|
c.OnExecute(() => 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.Execute("test", "-1=one", "-2", "two");
|
||||||
|
|
||||||
|
Assert.Equal("one", first.Values[0]);
|
||||||
|
Assert.Equal("two", second.Values[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ThrowsExceptionOnUnexpectedCommandOrArgumentByDefault()
|
||||||
|
{
|
||||||
|
var unexpectedArg = "UnexpectedArg";
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
|
||||||
|
app.Command("test", c =>
|
||||||
|
{
|
||||||
|
c.OnExecute(() => 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
var exception = Assert.Throws<CommandParsingException>(() => app.Execute("test", unexpectedArg));
|
||||||
|
Assert.Equal($"Unrecognized command or argument '{unexpectedArg}'", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AllowNoThrowBehaviorOnUnexpectedArgument()
|
||||||
|
{
|
||||||
|
var unexpectedArg = "UnexpectedArg";
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
|
||||||
|
var testCmd = app.Command("test", c =>
|
||||||
|
{
|
||||||
|
c.OnExecute(() => 0);
|
||||||
|
},
|
||||||
|
throwOnUnexpectedArg: false);
|
||||||
|
|
||||||
|
// (does not throw)
|
||||||
|
app.Execute("test", unexpectedArg);
|
||||||
|
var arg = Assert.Single(testCmd.RemainingArguments);
|
||||||
|
Assert.Equal(unexpectedArg, arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ThrowsExceptionOnUnexpectedLongOptionByDefault()
|
||||||
|
{
|
||||||
|
var unexpectedOption = "--UnexpectedOption";
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
|
||||||
|
app.Command("test", c =>
|
||||||
|
{
|
||||||
|
c.OnExecute(() => 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
var exception = Assert.Throws<CommandParsingException>(() => app.Execute("test", unexpectedOption));
|
||||||
|
Assert.Equal($"Unrecognized option '{unexpectedOption}'", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AllowNoThrowBehaviorOnUnexpectedLongOption()
|
||||||
|
{
|
||||||
|
var unexpectedOption = "--UnexpectedOption";
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
|
||||||
|
var testCmd = app.Command("test", c =>
|
||||||
|
{
|
||||||
|
c.OnExecute(() => 0);
|
||||||
|
},
|
||||||
|
throwOnUnexpectedArg: false);
|
||||||
|
|
||||||
|
// (does not throw)
|
||||||
|
app.Execute("test", unexpectedOption);
|
||||||
|
var arg = Assert.Single(testCmd.RemainingArguments);
|
||||||
|
Assert.Equal(unexpectedOption, arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ThrowsExceptionOnUnexpectedShortOptionByDefault()
|
||||||
|
{
|
||||||
|
var unexpectedOption = "-uexp";
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
|
||||||
|
app.Command("test", c =>
|
||||||
|
{
|
||||||
|
c.OnExecute(() => 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
var exception = Assert.Throws<CommandParsingException>(() => app.Execute("test", unexpectedOption));
|
||||||
|
Assert.Equal($"Unrecognized option '{unexpectedOption}'", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AllowNoThrowBehaviorOnUnexpectedShortOption()
|
||||||
|
{
|
||||||
|
var unexpectedOption = "-uexp";
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
|
||||||
|
var testCmd = app.Command("test", c =>
|
||||||
|
{
|
||||||
|
c.OnExecute(() => 0);
|
||||||
|
},
|
||||||
|
throwOnUnexpectedArg: false);
|
||||||
|
|
||||||
|
// (does not throw)
|
||||||
|
app.Execute("test", unexpectedOption);
|
||||||
|
var arg = Assert.Single(testCmd.RemainingArguments);
|
||||||
|
Assert.Equal(unexpectedOption, arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ThrowsExceptionOnUnexpectedSymbolOptionByDefault()
|
||||||
|
{
|
||||||
|
var unexpectedOption = "-?";
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
|
||||||
|
app.Command("test", c =>
|
||||||
|
{
|
||||||
|
c.OnExecute(() => 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
var exception = Assert.Throws<CommandParsingException>(() => app.Execute("test", unexpectedOption));
|
||||||
|
Assert.Equal($"Unrecognized option '{unexpectedOption}'", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AllowNoThrowBehaviorOnUnexpectedSymbolOption()
|
||||||
|
{
|
||||||
|
var unexpectedOption = "-?";
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
|
||||||
|
var testCmd = app.Command("test", c =>
|
||||||
|
{
|
||||||
|
c.OnExecute(() => 0);
|
||||||
|
},
|
||||||
|
throwOnUnexpectedArg: false);
|
||||||
|
|
||||||
|
// (does not throw)
|
||||||
|
app.Execute("test", unexpectedOption);
|
||||||
|
var arg = Assert.Single(testCmd.RemainingArguments);
|
||||||
|
Assert.Equal(unexpectedOption, arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ThrowsExceptionOnUnexpectedOptionBeforeValidSubcommandByDefault()
|
||||||
|
{
|
||||||
|
var unexpectedOption = "--unexpected";
|
||||||
|
CommandLineApplication subCmd = null;
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
|
||||||
|
app.Command("k", c =>
|
||||||
|
{
|
||||||
|
subCmd = c.Command("run", _ => { });
|
||||||
|
c.OnExecute(() => 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
var exception = Assert.Throws<CommandParsingException>(() => app.Execute("k", unexpectedOption, "run"));
|
||||||
|
Assert.Equal($"Unrecognized option '{unexpectedOption}'", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AllowNoThrowBehaviorOnUnexpectedOptionAfterSubcommand()
|
||||||
|
{
|
||||||
|
var unexpectedOption = "--unexpected";
|
||||||
|
CommandLineApplication subCmd = null;
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
|
||||||
|
var testCmd = app.Command("k", c =>
|
||||||
|
{
|
||||||
|
subCmd = c.Command("run", _ => { }, throwOnUnexpectedArg: false);
|
||||||
|
c.OnExecute(() => 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// (does not throw)
|
||||||
|
app.Execute("k", "run", unexpectedOption);
|
||||||
|
Assert.Empty(testCmd.RemainingArguments);
|
||||||
|
var arg = Assert.Single(subCmd.RemainingArguments);
|
||||||
|
Assert.Equal(unexpectedOption, arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OptionsCanBeInherited()
|
||||||
|
{
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
var optionA = app.Option("-a|--option-a", "", CommandOptionType.SingleValue, inherited: true);
|
||||||
|
string optionAValue = null;
|
||||||
|
|
||||||
|
var optionB = app.Option("-b", "", CommandOptionType.SingleValue, inherited: false);
|
||||||
|
|
||||||
|
var subcmd = app.Command("subcmd", c =>
|
||||||
|
{
|
||||||
|
c.OnExecute(() =>
|
||||||
|
{
|
||||||
|
optionAValue = optionA.Value();
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal(2, app.GetOptions().Count());
|
||||||
|
Assert.Single(subcmd.GetOptions());
|
||||||
|
|
||||||
|
app.Execute("-a", "A1", "subcmd");
|
||||||
|
Assert.Equal("A1", optionAValue);
|
||||||
|
|
||||||
|
Assert.Throws<CommandParsingException>(() => app.Execute("subcmd", "-b", "B"));
|
||||||
|
|
||||||
|
Assert.Contains("-a|--option-a", subcmd.GetHelpText());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NestedOptionConflictThrows()
|
||||||
|
{
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
app.Option("-a|--always", "Top-level", CommandOptionType.SingleValue, inherited: true);
|
||||||
|
app.Command("subcmd", c =>
|
||||||
|
{
|
||||||
|
c.Option("-a|--ask", "Nested", CommandOptionType.SingleValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => app.Execute("subcmd", "-a", "b"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OptionsWithSameName()
|
||||||
|
{
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
var top = app.Option("-a|--always", "Top-level", CommandOptionType.SingleValue, inherited: false);
|
||||||
|
CommandOption nested = null;
|
||||||
|
app.Command("subcmd", c =>
|
||||||
|
{
|
||||||
|
nested = c.Option("-a|--ask", "Nested", CommandOptionType.SingleValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.Execute("-a", "top");
|
||||||
|
Assert.Equal("top", top.Value());
|
||||||
|
Assert.Null(nested.Value());
|
||||||
|
|
||||||
|
top.Values.Clear();
|
||||||
|
|
||||||
|
app.Execute("subcmd", "-a", "nested");
|
||||||
|
Assert.Null(top.Value());
|
||||||
|
Assert.Equal("nested", nested.Value());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NestedInheritedOptions()
|
||||||
|
{
|
||||||
|
string globalOptionValue = null, nest1OptionValue = null, nest2OptionValue = null;
|
||||||
|
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
CommandLineApplication subcmd2 = null;
|
||||||
|
var g = app.Option("-g|--global", "Global option", CommandOptionType.SingleValue, inherited: true);
|
||||||
|
var subcmd1 = app.Command("lvl1", s1 =>
|
||||||
|
{
|
||||||
|
var n1 = s1.Option("--nest1", "Nested one level down", CommandOptionType.SingleValue, inherited: true);
|
||||||
|
subcmd2 = s1.Command("lvl2", s2 =>
|
||||||
|
{
|
||||||
|
var n2 = s2.Option("--nest2", "Nested one level down", CommandOptionType.SingleValue, inherited: true);
|
||||||
|
s2.HelpOption("-h|--help");
|
||||||
|
s2.OnExecute(() =>
|
||||||
|
{
|
||||||
|
globalOptionValue = g.Value();
|
||||||
|
nest1OptionValue = n1.Value();
|
||||||
|
nest2OptionValue = n2.Value();
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.DoesNotContain(app.GetOptions(), o => o.LongName == "nest2");
|
||||||
|
Assert.DoesNotContain(app.GetOptions(), o => o.LongName == "nest1");
|
||||||
|
Assert.Contains(app.GetOptions(), o => o.LongName == "global");
|
||||||
|
|
||||||
|
Assert.DoesNotContain(subcmd1.GetOptions(), o => o.LongName == "nest2");
|
||||||
|
Assert.Contains(subcmd1.GetOptions(), o => o.LongName == "nest1");
|
||||||
|
Assert.Contains(subcmd1.GetOptions(), o => o.LongName == "global");
|
||||||
|
|
||||||
|
Assert.Contains(subcmd2.GetOptions(), o => o.LongName == "nest2");
|
||||||
|
Assert.Contains(subcmd2.GetOptions(), o => o.LongName == "nest1");
|
||||||
|
Assert.Contains(subcmd2.GetOptions(), o => o.LongName == "global");
|
||||||
|
|
||||||
|
Assert.Throws<CommandParsingException>(() => app.Execute("--nest2", "N2", "--nest1", "N1", "-g", "G"));
|
||||||
|
Assert.Throws<CommandParsingException>(() => app.Execute("lvl1", "--nest2", "N2", "--nest1", "N1", "-g", "G"));
|
||||||
|
|
||||||
|
app.Execute("lvl1", "lvl2", "--nest2", "N2", "-g", "G", "--nest1", "N1");
|
||||||
|
Assert.Equal("G", globalOptionValue);
|
||||||
|
Assert.Equal("N1", nest1OptionValue);
|
||||||
|
Assert.Equal("N2", nest2OptionValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(new string[0], new string[0], null)]
|
||||||
|
[InlineData(new[] { "--" }, new string[0], null)]
|
||||||
|
[InlineData(new[] { "-t", "val" }, new string[0], "val")]
|
||||||
|
[InlineData(new[] { "-t", "val", "--" }, new string[0], "val")]
|
||||||
|
[InlineData(new[] { "--top", "val", "--", "a" }, new[] { "a" }, "val")]
|
||||||
|
[InlineData(new[] { "--", "a", "--top", "val" }, new[] { "a", "--top", "val" }, null)]
|
||||||
|
[InlineData(new[] { "-t", "val", "--", "a", "--", "b" }, new[] { "a", "--", "b" }, "val")]
|
||||||
|
[InlineData(new[] { "--", "--help" }, new[] { "--help" }, null)]
|
||||||
|
[InlineData(new[] { "--", "--version" }, new[] { "--version" }, null)]
|
||||||
|
public void ArgumentSeparator(string[] input, string[] expectedRemaining, string topLevelValue)
|
||||||
|
{
|
||||||
|
var app = new CommandLineApplication(throwOnUnexpectedArg: false)
|
||||||
|
{
|
||||||
|
AllowArgumentSeparator = true
|
||||||
|
};
|
||||||
|
var optHelp = app.HelpOption("--help");
|
||||||
|
var optVersion = app.VersionOption("--version", "1", "1.0");
|
||||||
|
var optTop = app.Option("-t|--top <TOP>", "arg for command", CommandOptionType.SingleValue);
|
||||||
|
app.Execute(input);
|
||||||
|
|
||||||
|
Assert.Equal(topLevelValue, optTop.Value());
|
||||||
|
Assert.False(optHelp.HasValue());
|
||||||
|
Assert.False(optVersion.HasValue());
|
||||||
|
Assert.Equal(expectedRemaining, app.RemainingArguments.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HelpTextIgnoresHiddenItems()
|
||||||
|
{
|
||||||
|
var app = new CommandLineApplication()
|
||||||
|
{
|
||||||
|
Name = "ninja-app",
|
||||||
|
Description = "You can't see it until it is too late"
|
||||||
|
};
|
||||||
|
|
||||||
|
app.Command("star", c =>
|
||||||
|
{
|
||||||
|
c.Option("--points <p>", "How many", CommandOptionType.MultipleValue);
|
||||||
|
c.ShowInHelpText = false;
|
||||||
|
});
|
||||||
|
app.Option("--smile", "Be a nice ninja", CommandOptionType.NoValue, o => { o.ShowInHelpText = false; });
|
||||||
|
|
||||||
|
var a = app.Argument("name", "Pseudonym, of course");
|
||||||
|
a.ShowInHelpText = false;
|
||||||
|
|
||||||
|
var help = app.GetHelpText();
|
||||||
|
|
||||||
|
Assert.Contains("ninja-app", help);
|
||||||
|
Assert.DoesNotContain("--points", help);
|
||||||
|
Assert.DoesNotContain("--smile", help);
|
||||||
|
Assert.DoesNotContain("name", help);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HelpTextUsesHelpOptionName()
|
||||||
|
{
|
||||||
|
var app = new CommandLineApplication
|
||||||
|
{
|
||||||
|
Name = "superhombre"
|
||||||
|
};
|
||||||
|
|
||||||
|
app.HelpOption("--ayuda-me");
|
||||||
|
var help = app.GetHelpText();
|
||||||
|
Assert.Contains("--ayuda-me", help);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HelpTextShowsArgSeparator()
|
||||||
|
{
|
||||||
|
var app = new CommandLineApplication(throwOnUnexpectedArg: false)
|
||||||
|
{
|
||||||
|
Name = "proxy-command",
|
||||||
|
AllowArgumentSeparator = true
|
||||||
|
};
|
||||||
|
app.HelpOption("-h|--help");
|
||||||
|
Assert.Contains("Usage: proxy-command [options] [[--] <arg>...]", app.GetHelpText());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HelpTextShowsExtendedHelp()
|
||||||
|
{
|
||||||
|
var app = new CommandLineApplication()
|
||||||
|
{
|
||||||
|
Name = "befuddle",
|
||||||
|
ExtendedHelpText = @"
|
||||||
|
Remarks:
|
||||||
|
This command is so confusing that I want to include examples in the help text.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
dotnet befuddle -- I Can Haz Confusion Arguments
|
||||||
|
"
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Contains(app.ExtendedHelpText, app.GetHelpText());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(new[] { "--version", "--flag" }, "1.0")]
|
||||||
|
[InlineData(new[] { "-V", "-f" }, "1.0")]
|
||||||
|
[InlineData(new[] { "--help", "--flag" }, "some flag")]
|
||||||
|
[InlineData(new[] { "-h", "-f" }, "some flag")]
|
||||||
|
public void HelpAndVersionOptionStopProcessing(string[] input, string expectedOutData)
|
||||||
|
{
|
||||||
|
using (var outWriter = new StringWriter())
|
||||||
|
{
|
||||||
|
var app = new CommandLineApplication { Out = outWriter };
|
||||||
|
app.HelpOption("-h --help");
|
||||||
|
app.VersionOption("-V --version", "1", "1.0");
|
||||||
|
var optFlag = app.Option("-f |--flag", "some flag", CommandOptionType.NoValue);
|
||||||
|
|
||||||
|
app.Execute(input);
|
||||||
|
|
||||||
|
outWriter.Flush();
|
||||||
|
var outData = outWriter.ToString();
|
||||||
|
Assert.Contains(expectedOutData, outData);
|
||||||
|
Assert.False(optFlag.HasValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// disable inaccurate analyzer error https://github.com/xunit/xunit/issues/1274
|
||||||
|
#pragma warning disable xUnit1010
|
||||||
|
#pragma warning disable xUnit1011
|
||||||
|
[Theory]
|
||||||
|
[InlineData("-f:File1", "-f:File2")]
|
||||||
|
[InlineData("--file:File1", "--file:File2")]
|
||||||
|
[InlineData("--file", "File1", "--file", "File2")]
|
||||||
|
#pragma warning restore xUnit1010
|
||||||
|
#pragma warning restore xUnit1011
|
||||||
|
public void ThrowsExceptionOnSingleValueOptionHavingTwoValues(params string[] inputOptions)
|
||||||
|
{
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
app.Option("-f |--file", "some file", CommandOptionType.SingleValue);
|
||||||
|
|
||||||
|
var exception = Assert.Throws<CommandParsingException>(() => app.Execute(inputOptions));
|
||||||
|
|
||||||
|
Assert.Equal("Unexpected value 'File2' for option 'file'", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("-v")]
|
||||||
|
[InlineData("--verbose")]
|
||||||
|
public void NoValueOptionCanBeSet(string input)
|
||||||
|
{
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
var optVerbose = app.Option("-v |--verbose", "be verbose", CommandOptionType.NoValue);
|
||||||
|
|
||||||
|
app.Execute(input);
|
||||||
|
|
||||||
|
Assert.True(optVerbose.HasValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("-v:true")]
|
||||||
|
[InlineData("--verbose:true")]
|
||||||
|
public void ThrowsExceptionOnNoValueOptionHavingValue(string inputOption)
|
||||||
|
{
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
app.Option("-v |--verbose", "be verbose", CommandOptionType.NoValue);
|
||||||
|
|
||||||
|
var exception = Assert.Throws<CommandParsingException>(() => app.Execute(inputOption));
|
||||||
|
|
||||||
|
Assert.Equal("Unexpected value 'true' for option 'verbose'", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ThrowsExceptionOnEmptyCommandOrArgument()
|
||||||
|
{
|
||||||
|
var inputOption = String.Empty;
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
|
||||||
|
var exception = Assert.Throws<CommandParsingException>(() => app.Execute(inputOption));
|
||||||
|
|
||||||
|
Assert.Equal($"Unrecognized command or argument '{inputOption}'", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ThrowsExceptionOnInvalidOption()
|
||||||
|
{
|
||||||
|
var inputOption = "-";
|
||||||
|
var app = new CommandLineApplication();
|
||||||
|
|
||||||
|
var exception = Assert.Throws<CommandParsingException>(() => app.Execute(inputOption));
|
||||||
|
|
||||||
|
Assert.Equal($"Unrecognized option '{inputOption}'", exception.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
// 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 Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
public class CopyOnWriteDictionaryHolderTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ReadOperation_DelegatesToSourceDictionary_IfNoMutationsArePerformed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var source = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ "test-key", "test-value" },
|
||||||
|
{ "key2", "key2-value" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var holder = new CopyOnWriteDictionaryHolder<string, object>(source);
|
||||||
|
|
||||||
|
// Act and Assert
|
||||||
|
Assert.Equal("key2-value", holder["key2"]);
|
||||||
|
Assert.Equal(2, holder.Count);
|
||||||
|
Assert.Equal(new string[] { "test-key", "key2" }, holder.Keys.ToArray());
|
||||||
|
Assert.Equal(new object[] { "test-value", "key2-value" }, holder.Values.ToArray());
|
||||||
|
Assert.True(holder.ContainsKey("test-key"));
|
||||||
|
|
||||||
|
object value;
|
||||||
|
Assert.False(holder.TryGetValue("different-key", out value));
|
||||||
|
|
||||||
|
Assert.False(holder.HasBeenCopied);
|
||||||
|
Assert.Same(source, holder.ReadDictionary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReadOperation_DoesNotDelegateToSourceDictionary_OnceAValueIsChanged()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var source = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ "key1", "value1" },
|
||||||
|
{ "key2", "value2" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var holder = new CopyOnWriteDictionaryHolder<string, object>(source);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
holder["key2"] = "value3";
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("value2", source["key2"]);
|
||||||
|
Assert.Equal(2, holder.Count);
|
||||||
|
Assert.Equal("value1", holder["key1"]);
|
||||||
|
Assert.Equal("value3", holder["key2"]);
|
||||||
|
|
||||||
|
Assert.True(holder.HasBeenCopied);
|
||||||
|
Assert.NotSame(source, holder.ReadDictionary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReadOperation_DoesNotDelegateToSourceDictionary_OnceValueIsAdded()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var source = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ "key1", "value1" },
|
||||||
|
{ "key2", "value2" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var holder = new CopyOnWriteDictionaryHolder<string, object>(source);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
holder.Add("key3", "value3");
|
||||||
|
holder.Remove("key1");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, source.Count);
|
||||||
|
Assert.Equal("value1", source["key1"]);
|
||||||
|
Assert.Equal(2, holder.Count);
|
||||||
|
Assert.Equal("value2", holder["KeY2"]);
|
||||||
|
Assert.Equal("value3", holder["key3"]);
|
||||||
|
|
||||||
|
Assert.True(holder.HasBeenCopied);
|
||||||
|
Assert.NotSame(source, holder.ReadDictionary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
// 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 Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
public class CopyOnWriteDictionaryTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ReadOperation_DelegatesToSourceDictionary_IfNoMutationsArePerformed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var values = new List<object>();
|
||||||
|
var enumerator = Mock.Of<IEnumerator<KeyValuePair<string, object>>>();
|
||||||
|
var sourceDictionary = new Mock<IDictionary<string, object>>(MockBehavior.Strict);
|
||||||
|
sourceDictionary
|
||||||
|
.SetupGet(d => d.Count)
|
||||||
|
.Returns(100)
|
||||||
|
.Verifiable();
|
||||||
|
sourceDictionary
|
||||||
|
.SetupGet(d => d.Values)
|
||||||
|
.Returns(values)
|
||||||
|
.Verifiable();
|
||||||
|
sourceDictionary
|
||||||
|
.Setup(d => d.ContainsKey("test-key"))
|
||||||
|
.Returns(value: true)
|
||||||
|
.Verifiable();
|
||||||
|
sourceDictionary
|
||||||
|
.Setup(d => d.GetEnumerator())
|
||||||
|
.Returns(enumerator)
|
||||||
|
.Verifiable();
|
||||||
|
sourceDictionary
|
||||||
|
.Setup(d => d["key2"])
|
||||||
|
.Returns("key2-value")
|
||||||
|
.Verifiable();
|
||||||
|
object value;
|
||||||
|
sourceDictionary.Setup(d => d.TryGetValue("different-key", out value))
|
||||||
|
.Returns(false)
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
var copyOnWriteDictionary = new CopyOnWriteDictionary<string, object>(sourceDictionary.Object,
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// Act and Assert
|
||||||
|
Assert.Equal("key2-value", copyOnWriteDictionary["key2"]);
|
||||||
|
Assert.Equal(100, copyOnWriteDictionary.Count);
|
||||||
|
Assert.Same(values, copyOnWriteDictionary.Values);
|
||||||
|
Assert.True(copyOnWriteDictionary.ContainsKey("test-key"));
|
||||||
|
Assert.Same(enumerator, copyOnWriteDictionary.GetEnumerator());
|
||||||
|
Assert.False(copyOnWriteDictionary.TryGetValue("different-key", out value));
|
||||||
|
sourceDictionary.Verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReadOperation_DoesNotDelegateToSourceDictionary_OnceAValueIsChanged()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var values = new List<object>();
|
||||||
|
var sourceDictionary = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "key1", "value1" },
|
||||||
|
{ "key2", "value2" }
|
||||||
|
};
|
||||||
|
var copyOnWriteDictionary = new CopyOnWriteDictionary<string, object>(
|
||||||
|
sourceDictionary,
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
copyOnWriteDictionary["key2"] = "value3";
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("value2", sourceDictionary["key2"]);
|
||||||
|
Assert.Equal(2, copyOnWriteDictionary.Count);
|
||||||
|
Assert.Equal("value1", copyOnWriteDictionary["key1"]);
|
||||||
|
Assert.Equal("value3", copyOnWriteDictionary["key2"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReadOperation_DoesNotDelegateToSourceDictionary_OnceDictionaryIsModified()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var values = new List<object>();
|
||||||
|
var sourceDictionary = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "key1", "value1" },
|
||||||
|
{ "key2", "value2" }
|
||||||
|
};
|
||||||
|
var copyOnWriteDictionary = new CopyOnWriteDictionary<string, object>(
|
||||||
|
sourceDictionary,
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
copyOnWriteDictionary.Add("key3", "value3");
|
||||||
|
copyOnWriteDictionary.Remove("key1");
|
||||||
|
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, sourceDictionary.Count);
|
||||||
|
Assert.Equal("value1", sourceDictionary["key1"]);
|
||||||
|
Assert.Equal(2, copyOnWriteDictionary.Count);
|
||||||
|
Assert.Equal("value2", copyOnWriteDictionary["KeY2"]);
|
||||||
|
Assert.Equal("value3", copyOnWriteDictionary["key3"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#if NETCOREAPP2_0 || NETCOREAPP2_1
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.CommandLineUtils
|
||||||
|
{
|
||||||
|
public class DotNetMuxerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void FindsTheMuxer()
|
||||||
|
{
|
||||||
|
var muxerPath = DotNetMuxer.MuxerPath;
|
||||||
|
Assert.NotNull(muxerPath);
|
||||||
|
Assert.True(File.Exists(muxerPath), "The file did not exist");
|
||||||
|
Assert.True(Path.IsPathRooted(muxerPath), "The path should be rooted");
|
||||||
|
Assert.Equal("dotnet", Path.GetFileNameWithoutExtension(muxerPath), ignoreCase: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
@ -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 Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
public class HashCodeCombinerTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GivenTheSameInputs_ItProducesTheSameOutput()
|
||||||
|
{
|
||||||
|
var hashCode1 = new HashCodeCombiner();
|
||||||
|
var hashCode2 = new HashCodeCombiner();
|
||||||
|
|
||||||
|
hashCode1.Add(42);
|
||||||
|
hashCode1.Add("foo");
|
||||||
|
hashCode2.Add(42);
|
||||||
|
hashCode2.Add("foo");
|
||||||
|
|
||||||
|
Assert.Equal(hashCode1.CombinedHash, hashCode2.CombinedHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HashCode_Is_OrderSensitive()
|
||||||
|
{
|
||||||
|
var hashCode1 = HashCodeCombiner.Start();
|
||||||
|
var hashCode2 = HashCodeCombiner.Start();
|
||||||
|
|
||||||
|
hashCode1.Add(42);
|
||||||
|
hashCode1.Add("foo");
|
||||||
|
|
||||||
|
hashCode2.Add("foo");
|
||||||
|
hashCode2.Add(42);
|
||||||
|
|
||||||
|
Assert.NotEqual(hashCode1.CombinedHash, hashCode2.CombinedHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
||||||
|
<DebugType>portable</DebugType>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\src\**\*.cs"
|
||||||
|
Exclude="
|
||||||
|
..\src\BenchmarkRunner\**\*.cs;
|
||||||
|
..\src\RazorViews\**\*.cs;
|
||||||
|
..\src\StackTrace\ExceptionDetails\**\*.cs;
|
||||||
|
" />
|
||||||
|
<EmbeddedResource Include="..\src\**\*.resx" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\testassets\ThrowingLibrary\ThrowingLibrary.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="FSharp.Core" />
|
||||||
|
<Reference Include="System.Reflection.Metadata" />
|
||||||
|
<Reference Include="System.Threading.Tasks.Extensions" />
|
||||||
|
<Reference Include="System.Security.Cryptography.Cng" />
|
||||||
|
<Reference Include="System.Runtime.CompilerServices.Unsafe" />
|
||||||
|
<Reference Include="System.IO.Pipelines" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,634 @@
|
||||||
|
// 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.FSharp.Control;
|
||||||
|
using Microsoft.FSharp.Core;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
public class ObjectMethodExecutorTest
|
||||||
|
{
|
||||||
|
private TestObject _targetObject = new TestObject();
|
||||||
|
private TypeInfo targetTypeInfo = typeof(TestObject).GetTypeInfo();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExecuteValueMethod()
|
||||||
|
{
|
||||||
|
var executor = GetExecutorForMethod("ValueMethod");
|
||||||
|
var result = executor.Execute(
|
||||||
|
_targetObject,
|
||||||
|
new object[] { 10, 20 });
|
||||||
|
Assert.False(executor.IsMethodAsync);
|
||||||
|
Assert.Equal(30, (int)result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExecuteVoidValueMethod()
|
||||||
|
{
|
||||||
|
var executor = GetExecutorForMethod("VoidValueMethod");
|
||||||
|
var result = executor.Execute(
|
||||||
|
_targetObject,
|
||||||
|
new object[] { 10 });
|
||||||
|
Assert.False(executor.IsMethodAsync);
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExecuteValueMethodWithReturnType()
|
||||||
|
{
|
||||||
|
var executor = GetExecutorForMethod("ValueMethodWithReturnType");
|
||||||
|
var result = executor.Execute(
|
||||||
|
_targetObject,
|
||||||
|
new object[] { 10 });
|
||||||
|
var resultObject = Assert.IsType<TestObject>(result);
|
||||||
|
Assert.False(executor.IsMethodAsync);
|
||||||
|
Assert.Equal("Hello", resultObject.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExecuteValueMethodUpdateValue()
|
||||||
|
{
|
||||||
|
var executor = GetExecutorForMethod("ValueMethodUpdateValue");
|
||||||
|
var parameter = new TestObject();
|
||||||
|
var result = executor.Execute(
|
||||||
|
_targetObject,
|
||||||
|
new object[] { parameter });
|
||||||
|
var resultObject = Assert.IsType<TestObject>(result);
|
||||||
|
Assert.False(executor.IsMethodAsync);
|
||||||
|
Assert.Equal("HelloWorld", resultObject.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExecuteValueMethodWithReturnTypeThrowsException()
|
||||||
|
{
|
||||||
|
var executor = GetExecutorForMethod("ValueMethodWithReturnTypeThrowsException");
|
||||||
|
var parameter = new TestObject();
|
||||||
|
Assert.False(executor.IsMethodAsync);
|
||||||
|
Assert.Throws<NotImplementedException>(
|
||||||
|
() => executor.Execute(
|
||||||
|
_targetObject,
|
||||||
|
new object[] { parameter }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExecuteValueMethodAsync()
|
||||||
|
{
|
||||||
|
var executor = GetExecutorForMethod("ValueMethodAsync");
|
||||||
|
var result = await executor.ExecuteAsync(
|
||||||
|
_targetObject,
|
||||||
|
new object[] { 10, 20 });
|
||||||
|
Assert.True(executor.IsMethodAsync);
|
||||||
|
Assert.Equal(30, (int)result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExecuteValueMethodWithReturnTypeAsync()
|
||||||
|
{
|
||||||
|
var executor = GetExecutorForMethod("ValueMethodWithReturnTypeAsync");
|
||||||
|
var result = await executor.ExecuteAsync(
|
||||||
|
_targetObject,
|
||||||
|
new object[] { 10 });
|
||||||
|
var resultObject = Assert.IsType<TestObject>(result);
|
||||||
|
Assert.True(executor.IsMethodAsync);
|
||||||
|
Assert.Equal("Hello", resultObject.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExecuteValueMethodUpdateValueAsync()
|
||||||
|
{
|
||||||
|
var executor = GetExecutorForMethod("ValueMethodUpdateValueAsync");
|
||||||
|
var parameter = new TestObject();
|
||||||
|
var result = await executor.ExecuteAsync(
|
||||||
|
_targetObject,
|
||||||
|
new object[] { parameter });
|
||||||
|
var resultObject = Assert.IsType<TestObject>(result);
|
||||||
|
Assert.True(executor.IsMethodAsync);
|
||||||
|
Assert.Equal("HelloWorld", resultObject.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExecuteValueMethodWithReturnTypeThrowsExceptionAsync()
|
||||||
|
{
|
||||||
|
var executor = GetExecutorForMethod("ValueMethodWithReturnTypeThrowsExceptionAsync");
|
||||||
|
var parameter = new TestObject();
|
||||||
|
Assert.True(executor.IsMethodAsync);
|
||||||
|
await Assert.ThrowsAsync<NotImplementedException>(
|
||||||
|
async () => await executor.ExecuteAsync(
|
||||||
|
_targetObject,
|
||||||
|
new object[] { parameter }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExecuteValueMethodWithReturnVoidThrowsExceptionAsync()
|
||||||
|
{
|
||||||
|
var executor = GetExecutorForMethod("ValueMethodWithReturnVoidThrowsExceptionAsync");
|
||||||
|
var parameter = new TestObject();
|
||||||
|
Assert.True(executor.IsMethodAsync);
|
||||||
|
await Assert.ThrowsAsync<NotImplementedException>(
|
||||||
|
async () => await executor.ExecuteAsync(
|
||||||
|
_targetObject,
|
||||||
|
new object[] { parameter }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDefaultValueForParameters_ReturnsSuppliedValues()
|
||||||
|
{
|
||||||
|
var suppliedDefaultValues = new object[] { 123, "test value" };
|
||||||
|
var executor = GetExecutorForMethod("MethodWithMultipleParameters", suppliedDefaultValues);
|
||||||
|
Assert.Equal(suppliedDefaultValues[0], executor.GetDefaultValueForParameter(0));
|
||||||
|
Assert.Equal(suppliedDefaultValues[1], executor.GetDefaultValueForParameter(1));
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => executor.GetDefaultValueForParameter(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDefaultValueForParameters_ThrowsIfNoneWereSupplied()
|
||||||
|
{
|
||||||
|
var executor = GetExecutorForMethod("MethodWithMultipleParameters");
|
||||||
|
Assert.Throws<InvalidOperationException>(() => executor.GetDefaultValueForParameter(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void TargetMethodReturningCustomAwaitableOfReferenceType_CanInvokeViaExecute()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var executor = GetExecutorForMethod("CustomAwaitableOfReferenceTypeAsync");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await (TestAwaitable<TestObject>)executor.Execute(_targetObject, new object[] { "Hello", 123 });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(executor.IsMethodAsync);
|
||||||
|
Assert.Same(typeof(TestObject), executor.AsyncResultType);
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal("Hello 123", result.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void TargetMethodReturningCustomAwaitableOfValueType_CanInvokeViaExecute()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var executor = GetExecutorForMethod("CustomAwaitableOfValueTypeAsync");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await (TestAwaitable<int>)executor.Execute(_targetObject, new object[] { 123, 456 });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(executor.IsMethodAsync);
|
||||||
|
Assert.Same(typeof(int), executor.AsyncResultType);
|
||||||
|
Assert.Equal(579, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void TargetMethodReturningCustomAwaitableOfReferenceType_CanInvokeViaExecuteAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var executor = GetExecutorForMethod("CustomAwaitableOfReferenceTypeAsync");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await executor.ExecuteAsync(_targetObject, new object[] { "Hello", 123 });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(executor.IsMethodAsync);
|
||||||
|
Assert.Same(typeof(TestObject), executor.AsyncResultType);
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.IsType<TestObject>(result);
|
||||||
|
Assert.Equal("Hello 123", ((TestObject)result).value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void TargetMethodReturningCustomAwaitableOfValueType_CanInvokeViaExecuteAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var executor = GetExecutorForMethod("CustomAwaitableOfValueTypeAsync");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await executor.ExecuteAsync(_targetObject, new object[] { 123, 456 });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(executor.IsMethodAsync);
|
||||||
|
Assert.Same(typeof(int), executor.AsyncResultType);
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.IsType<int>(result);
|
||||||
|
Assert.Equal(579, (int)result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void TargetMethodReturningAwaitableOfVoidType_CanInvokeViaExecuteAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var executor = GetExecutorForMethod("VoidValueMethodAsync");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await executor.ExecuteAsync(_targetObject, new object[] { 123 });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(executor.IsMethodAsync);
|
||||||
|
Assert.Same(typeof(void), executor.AsyncResultType);
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void TargetMethodReturningAwaitableWithICriticalNotifyCompletion_UsesUnsafeOnCompleted()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var executor = GetExecutorForMethod("CustomAwaitableWithICriticalNotifyCompletion");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await executor.ExecuteAsync(_targetObject, new object[0]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(executor.IsMethodAsync);
|
||||||
|
Assert.Same(typeof(string), executor.AsyncResultType);
|
||||||
|
Assert.Equal("Used UnsafeOnCompleted", (string)result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void TargetMethodReturningAwaitableWithoutICriticalNotifyCompletion_UsesOnCompleted()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var executor = GetExecutorForMethod("CustomAwaitableWithoutICriticalNotifyCompletion");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await executor.ExecuteAsync(_targetObject, new object[0]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(executor.IsMethodAsync);
|
||||||
|
Assert.Same(typeof(string), executor.AsyncResultType);
|
||||||
|
Assert.Equal("Used OnCompleted", (string)result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void TargetMethodReturningValueTaskOfValueType_CanBeInvokedViaExecute()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var executor = GetExecutorForMethod("ValueTaskOfValueType");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await (ValueTask<int>)executor.Execute(_targetObject, new object[] { 123 });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(executor.IsMethodAsync);
|
||||||
|
Assert.Same(typeof(int), executor.AsyncResultType);
|
||||||
|
Assert.Equal(123, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void TargetMethodReturningValueTaskOfReferenceType_CanBeInvokedViaExecute()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var executor = GetExecutorForMethod("ValueTaskOfReferenceType");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await (ValueTask<string>)executor.Execute(_targetObject, new object[] { "test result" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(executor.IsMethodAsync);
|
||||||
|
Assert.Same(typeof(string), executor.AsyncResultType);
|
||||||
|
Assert.Equal("test result", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void TargetMethodReturningValueTaskOfValueType_CanBeInvokedViaExecuteAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var executor = GetExecutorForMethod("ValueTaskOfValueType");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await executor.ExecuteAsync(_targetObject, new object[] { 123 });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(executor.IsMethodAsync);
|
||||||
|
Assert.Same(typeof(int), executor.AsyncResultType);
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(123, (int)result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void TargetMethodReturningValueTaskOfReferenceType_CanBeInvokedViaExecuteAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var executor = GetExecutorForMethod("ValueTaskOfReferenceType");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await executor.ExecuteAsync(_targetObject, new object[] { "test result" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(executor.IsMethodAsync);
|
||||||
|
Assert.Same(typeof(string), executor.AsyncResultType);
|
||||||
|
Assert.Equal("test result", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void TargetMethodReturningFSharpAsync_CanBeInvokedViaExecute()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var executor = GetExecutorForMethod("FSharpAsyncMethod");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var fsharpAsync = (FSharpAsync<string>)executor.Execute(_targetObject, new object[] { "test result" });
|
||||||
|
var result = await FSharpAsync.StartAsTask(fsharpAsync,
|
||||||
|
FSharpOption<TaskCreationOptions>.None,
|
||||||
|
FSharpOption<CancellationToken>.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(executor.IsMethodAsync);
|
||||||
|
Assert.Same(typeof(string), executor.AsyncResultType);
|
||||||
|
Assert.Equal("test result", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void TargetMethodReturningFailingFSharpAsync_CanBeInvokedViaExecute()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var executor = GetExecutorForMethod("FSharpAsyncFailureMethod");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var fsharpAsync = (FSharpAsync<string>)executor.Execute(_targetObject, new object[] { "test result" });
|
||||||
|
var resultTask = FSharpAsync.StartAsTask(fsharpAsync,
|
||||||
|
FSharpOption<TaskCreationOptions>.None,
|
||||||
|
FSharpOption<CancellationToken>.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(executor.IsMethodAsync);
|
||||||
|
Assert.Same(typeof(string), executor.AsyncResultType);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<AggregateException>(async () => await resultTask);
|
||||||
|
Assert.IsType<InvalidOperationException>(exception.InnerException);
|
||||||
|
Assert.Equal("Test exception", exception.InnerException.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void TargetMethodReturningFSharpAsync_CanBeInvokedViaExecuteAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var executor = GetExecutorForMethod("FSharpAsyncMethod");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await executor.ExecuteAsync(_targetObject, new object[] { "test result" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(executor.IsMethodAsync);
|
||||||
|
Assert.Same(typeof(string), executor.AsyncResultType);
|
||||||
|
Assert.Equal("test result", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void TargetMethodReturningFailingFSharpAsync_CanBeInvokedViaExecuteAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var executor = GetExecutorForMethod("FSharpAsyncFailureMethod");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var resultTask = executor.ExecuteAsync(_targetObject, new object[] { "test result" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(executor.IsMethodAsync);
|
||||||
|
Assert.Same(typeof(string), executor.AsyncResultType);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<AggregateException>(async () => await resultTask);
|
||||||
|
Assert.IsType<InvalidOperationException>(exception.InnerException);
|
||||||
|
Assert.Equal("Test exception", exception.InnerException.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObjectMethodExecutor GetExecutorForMethod(string methodName)
|
||||||
|
{
|
||||||
|
var method = typeof(TestObject).GetMethod(methodName);
|
||||||
|
return ObjectMethodExecutor.Create(method, targetTypeInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObjectMethodExecutor GetExecutorForMethod(string methodName, object[] parameterDefaultValues)
|
||||||
|
{
|
||||||
|
var method = typeof(TestObject).GetMethod(methodName);
|
||||||
|
return ObjectMethodExecutor.Create(method, targetTypeInfo, parameterDefaultValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestObject
|
||||||
|
{
|
||||||
|
public string value;
|
||||||
|
public int ValueMethod(int i, int j)
|
||||||
|
{
|
||||||
|
return i + j;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void VoidValueMethod(int i)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestObject ValueMethodWithReturnType(int i)
|
||||||
|
{
|
||||||
|
return new TestObject() { value = "Hello" }; ;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestObject ValueMethodWithReturnTypeThrowsException(TestObject i)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException("Not Implemented Exception");
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestObject ValueMethodUpdateValue(TestObject parameter)
|
||||||
|
{
|
||||||
|
parameter.value = "HelloWorld";
|
||||||
|
return parameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<int> ValueMethodAsync(int i, int j)
|
||||||
|
{
|
||||||
|
return Task.FromResult<int>(i + j);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task VoidValueMethodAsync(int i)
|
||||||
|
{
|
||||||
|
await ValueMethodAsync(3, 4);
|
||||||
|
}
|
||||||
|
public Task<TestObject> ValueMethodWithReturnTypeAsync(int i)
|
||||||
|
{
|
||||||
|
return Task.FromResult<TestObject>(new TestObject() { value = "Hello" });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ValueMethodWithReturnVoidThrowsExceptionAsync(TestObject i)
|
||||||
|
{
|
||||||
|
await Task.CompletedTask;
|
||||||
|
throw new NotImplementedException("Not Implemented Exception");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TestObject> ValueMethodWithReturnTypeThrowsExceptionAsync(TestObject i)
|
||||||
|
{
|
||||||
|
await Task.CompletedTask;
|
||||||
|
throw new NotImplementedException("Not Implemented Exception");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<TestObject> ValueMethodUpdateValueAsync(TestObject parameter)
|
||||||
|
{
|
||||||
|
parameter.value = "HelloWorld";
|
||||||
|
return Task.FromResult<TestObject>(parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestAwaitable<TestObject> CustomAwaitableOfReferenceTypeAsync(
|
||||||
|
string input1,
|
||||||
|
int input2)
|
||||||
|
{
|
||||||
|
return new TestAwaitable<TestObject>(new TestObject
|
||||||
|
{
|
||||||
|
value = $"{input1} {input2}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestAwaitable<int> CustomAwaitableOfValueTypeAsync(
|
||||||
|
int input1,
|
||||||
|
int input2)
|
||||||
|
{
|
||||||
|
return new TestAwaitable<int>(input1 + input2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestAwaitableWithICriticalNotifyCompletion CustomAwaitableWithICriticalNotifyCompletion()
|
||||||
|
{
|
||||||
|
return new TestAwaitableWithICriticalNotifyCompletion();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestAwaitableWithoutICriticalNotifyCompletion CustomAwaitableWithoutICriticalNotifyCompletion()
|
||||||
|
{
|
||||||
|
return new TestAwaitableWithoutICriticalNotifyCompletion();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<int> ValueTaskOfValueType(int result)
|
||||||
|
{
|
||||||
|
return new ValueTask<int>(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<string> ValueTaskOfReferenceType(string result)
|
||||||
|
{
|
||||||
|
return new ValueTask<string>(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MethodWithMultipleParameters(int valueTypeParam, string referenceTypeParam)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public FSharpAsync<string> FSharpAsyncMethod(string parameter)
|
||||||
|
{
|
||||||
|
return FSharpAsync.AwaitTask(Task.FromResult(parameter));
|
||||||
|
}
|
||||||
|
|
||||||
|
public FSharpAsync<string> FSharpAsyncFailureMethod(string parameter)
|
||||||
|
{
|
||||||
|
return FSharpAsync.AwaitTask(
|
||||||
|
Task.FromException<string>(new InvalidOperationException("Test exception")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestAwaitable<T>
|
||||||
|
{
|
||||||
|
private T _result;
|
||||||
|
private bool _isCompleted;
|
||||||
|
private List<Action> _onCompletedCallbacks = new List<Action>();
|
||||||
|
|
||||||
|
public TestAwaitable(T result)
|
||||||
|
{
|
||||||
|
_result = result;
|
||||||
|
|
||||||
|
// Simulate a brief delay before completion
|
||||||
|
ThreadPool.QueueUserWorkItem(_ =>
|
||||||
|
{
|
||||||
|
Thread.Sleep(100);
|
||||||
|
SetCompleted();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetCompleted()
|
||||||
|
{
|
||||||
|
_isCompleted = true;
|
||||||
|
|
||||||
|
foreach (var callback in _onCompletedCallbacks)
|
||||||
|
{
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestAwaiter GetAwaiter()
|
||||||
|
{
|
||||||
|
return new TestAwaiter(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct TestAwaiter : INotifyCompletion
|
||||||
|
{
|
||||||
|
private TestAwaitable<T> _owner;
|
||||||
|
|
||||||
|
public TestAwaiter(TestAwaitable<T> owner) : this()
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsCompleted => _owner._isCompleted;
|
||||||
|
|
||||||
|
public void OnCompleted(Action continuation)
|
||||||
|
{
|
||||||
|
if (_owner._isCompleted)
|
||||||
|
{
|
||||||
|
continuation();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_owner._onCompletedCallbacks.Add(continuation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public T GetResult()
|
||||||
|
{
|
||||||
|
return _owner._result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestAwaitableWithICriticalNotifyCompletion
|
||||||
|
{
|
||||||
|
public TestAwaiterWithICriticalNotifyCompletion GetAwaiter()
|
||||||
|
=> new TestAwaiterWithICriticalNotifyCompletion();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestAwaitableWithoutICriticalNotifyCompletion
|
||||||
|
{
|
||||||
|
public TestAwaiterWithoutICriticalNotifyCompletion GetAwaiter()
|
||||||
|
=> new TestAwaiterWithoutICriticalNotifyCompletion();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestAwaiterWithICriticalNotifyCompletion
|
||||||
|
: CompletionTrackingAwaiterBase, ICriticalNotifyCompletion
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestAwaiterWithoutICriticalNotifyCompletion
|
||||||
|
: CompletionTrackingAwaiterBase, INotifyCompletion
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CompletionTrackingAwaiterBase
|
||||||
|
{
|
||||||
|
private string _result;
|
||||||
|
|
||||||
|
public bool IsCompleted { get; private set; }
|
||||||
|
|
||||||
|
public string GetResult() => _result;
|
||||||
|
|
||||||
|
public void OnCompleted(Action continuation)
|
||||||
|
{
|
||||||
|
_result = "Used OnCompleted";
|
||||||
|
IsCompleted = true;
|
||||||
|
continuation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UnsafeOnCompleted(Action continuation)
|
||||||
|
{
|
||||||
|
_result = "Used UnsafeOnCompleted";
|
||||||
|
IsCompleted = true;
|
||||||
|
continuation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
// 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.Reflection;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
public class PropertyActivatorTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Activate_InvokesValueAccessorWithExpectedValue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var instance = new TestClass();
|
||||||
|
var typeInfo = instance.GetType().GetTypeInfo();
|
||||||
|
var property = typeInfo.GetDeclaredProperty("IntProperty");
|
||||||
|
var invokedWith = -1;
|
||||||
|
var activator = new PropertyActivator<int>(
|
||||||
|
property,
|
||||||
|
valueAccessor: (val) =>
|
||||||
|
{
|
||||||
|
invokedWith = val;
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
activator.Activate(instance, 123);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(123, invokedWith);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Activate_SetsPropertyValue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var instance = new TestClass();
|
||||||
|
var typeInfo = instance.GetType().GetTypeInfo();
|
||||||
|
var property = typeInfo.GetDeclaredProperty("IntProperty");
|
||||||
|
var activator = new PropertyActivator<int>(property, valueAccessor: (val) => val + 1);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
activator.Activate(instance, 123);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(124, instance.IntProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetPropertiesToActivate_RestrictsActivatableProperties()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var instance = new TestClass();
|
||||||
|
var typeInfo = instance.GetType().GetTypeInfo();
|
||||||
|
var expectedPropertyInfo = typeInfo.GetDeclaredProperty("ActivatableProperty");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var propertiesToActivate = PropertyActivator<int>.GetPropertiesToActivate(
|
||||||
|
type: typeof(TestClass),
|
||||||
|
activateAttributeType: typeof(TestActivateAttribute),
|
||||||
|
createActivateInfo:
|
||||||
|
(propertyInfo) => new PropertyActivator<int>(propertyInfo, valueAccessor: (val) => val + 1));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
propertiesToActivate,
|
||||||
|
(activator) =>
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedPropertyInfo, activator.PropertyInfo);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetPropertiesToActivate_CanCreateCustomPropertyActivators()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var instance = new TestClass();
|
||||||
|
var typeInfo = instance.GetType().GetTypeInfo();
|
||||||
|
var expectedPropertyInfo = typeInfo.GetDeclaredProperty("IntProperty");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var propertiesToActivate = PropertyActivator<int>.GetPropertiesToActivate(
|
||||||
|
type: typeof(TestClass),
|
||||||
|
activateAttributeType: typeof(TestActivateAttribute),
|
||||||
|
createActivateInfo:
|
||||||
|
(propertyInfo) => new PropertyActivator<int>(expectedPropertyInfo, valueAccessor: (val) => val + 1));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
propertiesToActivate,
|
||||||
|
(activator) =>
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedPropertyInfo, activator.PropertyInfo);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetPropertiesToActivate_ExcludesNonPublic()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var instance = new TestClassWithPropertyVisiblity();
|
||||||
|
var typeInfo = instance.GetType().GetTypeInfo();
|
||||||
|
var expectedPropertyInfo = typeInfo.GetDeclaredProperty("Public");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var propertiesToActivate = PropertyActivator<int>.GetPropertiesToActivate(
|
||||||
|
typeof(TestClassWithPropertyVisiblity),
|
||||||
|
typeof(TestActivateAttribute),
|
||||||
|
(propertyInfo) => new PropertyActivator<int>(propertyInfo, valueAccessor: (val) => val));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(propertiesToActivate);
|
||||||
|
Assert.Single(propertiesToActivate, p => p.PropertyInfo == expectedPropertyInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetPropertiesToActivate_IncludesNonPublic()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var instance = new TestClassWithPropertyVisiblity();
|
||||||
|
var typeInfo = instance.GetType().GetTypeInfo();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var propertiesToActivate = PropertyActivator<int>.GetPropertiesToActivate(
|
||||||
|
typeof(TestClassWithPropertyVisiblity),
|
||||||
|
typeof(TestActivateAttribute),
|
||||||
|
(propertyInfo) => new PropertyActivator<int>(propertyInfo, valueAccessor: (val) => val),
|
||||||
|
includeNonPublic: true);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(5, propertiesToActivate.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestClass
|
||||||
|
{
|
||||||
|
public int IntProperty { get; set; }
|
||||||
|
|
||||||
|
[TestActivate]
|
||||||
|
public int ActivatableProperty { get; set; }
|
||||||
|
|
||||||
|
[TestActivate]
|
||||||
|
public int NoSetterActivatableProperty { get; }
|
||||||
|
|
||||||
|
[TestActivate]
|
||||||
|
public int this[int something] // Not activatable
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestActivate]
|
||||||
|
public static int StaticActivatablProperty { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestClassWithPropertyVisiblity
|
||||||
|
{
|
||||||
|
[TestActivate]
|
||||||
|
public int Public { get; set; }
|
||||||
|
|
||||||
|
[TestActivate]
|
||||||
|
protected int Protected { get; set; }
|
||||||
|
|
||||||
|
[TestActivate]
|
||||||
|
internal int Internal { get; set; }
|
||||||
|
|
||||||
|
[TestActivate]
|
||||||
|
protected internal int ProtectedInternal {get; set; }
|
||||||
|
|
||||||
|
[TestActivate]
|
||||||
|
private int Private { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
private class TestActivateAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ActivationInfo
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,831 @@
|
||||||
|
// 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.Reflection;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
public class PropertyHelperTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void PropertyHelper_ReturnsNameCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var anonymous = new { foo = "bar" };
|
||||||
|
var property = PropertyHelper.GetProperties(anonymous.GetType()).First().Property;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var helper = new PropertyHelper(property);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("foo", property.Name);
|
||||||
|
Assert.Equal("foo", helper.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertyHelper_ReturnsValueCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var anonymous = new { bar = "baz" };
|
||||||
|
var property = PropertyHelper.GetProperties(anonymous.GetType()).First().Property;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var helper = new PropertyHelper(property);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("bar", helper.Name);
|
||||||
|
Assert.Equal("baz", helper.GetValue(anonymous));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertyHelper_ReturnsGetterDelegate()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var anonymous = new { bar = "baz" };
|
||||||
|
var property = PropertyHelper.GetProperties(anonymous.GetType()).First().Property;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var helper = new PropertyHelper(property);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(helper.ValueGetter);
|
||||||
|
Assert.Equal("baz", helper.ValueGetter(anonymous));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetValue_SetsPropertyValue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expected = "new value";
|
||||||
|
var instance = new BaseClass { PropA = "old value" };
|
||||||
|
var helper = PropertyHelper.GetProperties(
|
||||||
|
instance.GetType()).First(prop => prop.Name == "PropA");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
helper.SetValue(instance, expected);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, instance.PropA);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertyHelper_ReturnsSetterDelegate()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expected = "new value";
|
||||||
|
var instance = new BaseClass { PropA = "old value" };
|
||||||
|
var helper = PropertyHelper.GetProperties(
|
||||||
|
instance.GetType()).First(prop => prop.Name == "PropA");
|
||||||
|
|
||||||
|
// Act and Assert
|
||||||
|
Assert.NotNull(helper.ValueSetter);
|
||||||
|
helper.ValueSetter(instance, expected);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, instance.PropA);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertyHelper_ReturnsValueCorrectly_ForValueTypes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var anonymous = new { foo = 32 };
|
||||||
|
var property = PropertyHelper.GetProperties(anonymous.GetType()).First().Property;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var helper = new PropertyHelper(property);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("foo", helper.Name);
|
||||||
|
Assert.Equal(32, helper.GetValue(anonymous));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertyHelper_ReturnsCachedPropertyHelper()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var anonymous = new { foo = "bar" };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var helpers1 = PropertyHelper.GetProperties(anonymous.GetType().GetTypeInfo());
|
||||||
|
var helpers2 = PropertyHelper.GetProperties(anonymous.GetType().GetTypeInfo());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(helpers1);
|
||||||
|
Assert.Same(helpers1, helpers2);
|
||||||
|
Assert.Same(helpers1[0], helpers2[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertyHelper_DoesNotChangeUnderscores()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var anonymous = new { bar_baz2 = "foo" };
|
||||||
|
|
||||||
|
// Act + Assert
|
||||||
|
var helper = Assert.Single(PropertyHelper.GetProperties(anonymous.GetType().GetTypeInfo()));
|
||||||
|
Assert.Equal("bar_baz2", helper.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertyHelper_DoesNotFindPrivateProperties()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var anonymous = new PrivateProperties();
|
||||||
|
|
||||||
|
// Act + Assert
|
||||||
|
var helper = Assert.Single(PropertyHelper.GetProperties(anonymous.GetType().GetTypeInfo()));
|
||||||
|
Assert.Equal("Prop1", helper.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertyHelper_DoesNotFindStaticProperties()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var anonymous = new Static();
|
||||||
|
|
||||||
|
// Act + Assert
|
||||||
|
var helper = Assert.Single(PropertyHelper.GetProperties(anonymous.GetType().GetTypeInfo()));
|
||||||
|
Assert.Equal("Prop5", helper.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertyHelper_DoesNotFindSetOnlyProperties()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var anonymous = new SetOnly();
|
||||||
|
|
||||||
|
// Act + Assert
|
||||||
|
var helper = Assert.Single(PropertyHelper.GetProperties(anonymous.GetType().GetTypeInfo()));
|
||||||
|
Assert.Equal("Prop6", helper.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(int?))]
|
||||||
|
[InlineData(typeof(DayOfWeek?))]
|
||||||
|
public void PropertyHelper_WorksForNullablePrimitiveAndEnumTypes(Type nullableType)
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var properties = PropertyHelper.GetProperties(nullableType);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertyHelper_UnwrapsNullableTypes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var myType = typeof(MyStruct?);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var properties = PropertyHelper.GetProperties(myType);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var property = Assert.Single(properties);
|
||||||
|
Assert.Equal("Foo", property.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertyHelper_WorksForStruct()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var anonymous = new MyProperties();
|
||||||
|
|
||||||
|
anonymous.IntProp = 3;
|
||||||
|
anonymous.StringProp = "Five";
|
||||||
|
|
||||||
|
// Act + Assert
|
||||||
|
var helper1 = Assert.Single(PropertyHelper.GetProperties(anonymous.GetType().GetTypeInfo()).Where(prop => prop.Name == "IntProp"));
|
||||||
|
var helper2 = Assert.Single(PropertyHelper.GetProperties(anonymous.GetType().GetTypeInfo()).Where(prop => prop.Name == "StringProp"));
|
||||||
|
Assert.Equal(3, helper1.GetValue(anonymous));
|
||||||
|
Assert.Equal("Five", helper2.GetValue(anonymous));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertyHelper_ForDerivedClass()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var derived = new DerivedClass { PropA = "propAValue", PropB = "propBValue" };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var helpers = PropertyHelper.GetProperties(derived.GetType().GetTypeInfo()).ToArray();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(helpers);
|
||||||
|
Assert.Equal(2, helpers.Length);
|
||||||
|
|
||||||
|
var propAHelper = Assert.Single(helpers.Where(h => h.Name == "PropA"));
|
||||||
|
var propBHelper = Assert.Single(helpers.Where(h => h.Name == "PropB"));
|
||||||
|
|
||||||
|
Assert.Equal("propAValue", propAHelper.GetValue(derived));
|
||||||
|
Assert.Equal("propBValue", propBHelper.GetValue(derived));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertyHelper_ForDerivedClass_WithNew()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var derived = new DerivedClassWithNew { PropA = "propAValue" };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var helpers = PropertyHelper.GetProperties(derived.GetType().GetTypeInfo()).ToArray();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(helpers);
|
||||||
|
Assert.Equal(2, helpers.Length);
|
||||||
|
|
||||||
|
var propAHelper = Assert.Single(helpers.Where(h => h.Name == "PropA"));
|
||||||
|
var propBHelper = Assert.Single(helpers.Where(h => h.Name == "PropB"));
|
||||||
|
|
||||||
|
Assert.Equal("propAValue", propAHelper.GetValue(derived));
|
||||||
|
Assert.Equal("Newed", propBHelper.GetValue(derived));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertyHelper_ForDerived_WithVirtual()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var derived = new DerivedClassWithOverride { PropA = "propAValue", PropB = "propBValue" };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var helpers = PropertyHelper.GetProperties(derived.GetType().GetTypeInfo()).ToArray();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(helpers);
|
||||||
|
Assert.Equal(2, helpers.Length);
|
||||||
|
|
||||||
|
var propAHelper = Assert.Single(helpers.Where(h => h.Name == "PropA"));
|
||||||
|
var propBHelper = Assert.Single(helpers.Where(h => h.Name == "PropB"));
|
||||||
|
|
||||||
|
Assert.Equal("OverridenpropAValue", propAHelper.GetValue(derived));
|
||||||
|
Assert.Equal("propBValue", propBHelper.GetValue(derived));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertyHelper_ForInterface_ReturnsExpectedProperties()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedNames = new[] { "Count", "IsReadOnly" };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var helpers = PropertyHelper.GetProperties(typeof(ICollection<string>));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
helpers.OrderBy(helper => helper.Name, StringComparer.Ordinal),
|
||||||
|
helper => { Assert.Equal(expectedNames[0], helper.Name, StringComparer.Ordinal); },
|
||||||
|
helper => { Assert.Equal(expectedNames[1], helper.Name, StringComparer.Ordinal); });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertyHelper_ForDerivedInterface_ReturnsAllProperties()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedNames = new[] { "Count", "IsReadOnly", "Keys", "Values" };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var helpers = PropertyHelper.GetProperties(typeof(IDictionary<string, string>));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
helpers.OrderBy(helper => helper.Name, StringComparer.Ordinal),
|
||||||
|
helper => { Assert.Equal(expectedNames[0], helper.Name, StringComparer.Ordinal); },
|
||||||
|
helper => { Assert.Equal(expectedNames[1], helper.Name, StringComparer.Ordinal); },
|
||||||
|
helper => { Assert.Equal(expectedNames[2], helper.Name, StringComparer.Ordinal); },
|
||||||
|
helper => { Assert.Equal(expectedNames[3], helper.Name, StringComparer.Ordinal); });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetProperties_ExcludesIndexersAndPropertiesWithoutPublicGetters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var type = typeof(DerivedClassWithNonReadableProperties);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = PropertyHelper.GetProperties(type).ToArray();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(3, result.Length);
|
||||||
|
Assert.Equal("Visible", result[0].Name);
|
||||||
|
Assert.Equal("PropA", result[1].Name);
|
||||||
|
Assert.Equal("PropB", result[2].Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetVisibleProperties_NoHiddenProperty()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var type = typeof(string);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = PropertyHelper.GetVisibleProperties(type).ToArray();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var property = Assert.Single(result);
|
||||||
|
Assert.Equal("Length", property.Name);
|
||||||
|
Assert.Equal(typeof(int), property.Property.PropertyType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetVisibleProperties_HiddenProperty()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var type = typeof(DerivedHiddenProperty);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = PropertyHelper.GetVisibleProperties(type).ToArray();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result.Length);
|
||||||
|
Assert.Equal("Id", result[0].Name);
|
||||||
|
Assert.Equal(typeof(string), result[0].Property.PropertyType);
|
||||||
|
Assert.Equal("Name", result[1].Name);
|
||||||
|
Assert.Equal(typeof(string), result[1].Property.PropertyType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetVisibleProperties_HiddenProperty_TwoLevels()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var type = typeof(DerivedHiddenProperty2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = PropertyHelper.GetVisibleProperties(type).ToArray();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result.Length);
|
||||||
|
Assert.Equal("Id", result[0].Name);
|
||||||
|
Assert.Equal(typeof(Guid), result[0].Property.PropertyType);
|
||||||
|
Assert.Equal("Name", result[1].Name);
|
||||||
|
Assert.Equal(typeof(string), result[1].Property.PropertyType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetVisibleProperties_NoHiddenPropertyWithTypeInfoInput()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var type = typeof(string);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = PropertyHelper.GetVisibleProperties(type.GetTypeInfo()).ToArray();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var property = Assert.Single(result);
|
||||||
|
Assert.Equal("Length", property.Name);
|
||||||
|
Assert.Equal(typeof(int), property.Property.PropertyType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetVisibleProperties_HiddenPropertyWithTypeInfoInput()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var type = typeof(DerivedHiddenProperty);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = PropertyHelper.GetVisibleProperties(type.GetTypeInfo()).ToArray();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result.Length);
|
||||||
|
Assert.Equal("Id", result[0].Name);
|
||||||
|
Assert.Equal(typeof(string), result[0].Property.PropertyType);
|
||||||
|
Assert.Equal("Name", result[1].Name);
|
||||||
|
Assert.Equal(typeof(string), result[1].Property.PropertyType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetVisibleProperties_HiddenProperty_TwoLevelsWithTypeInfoInput()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var type = typeof(DerivedHiddenProperty2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = PropertyHelper.GetVisibleProperties(type.GetTypeInfo()).ToArray();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result.Length);
|
||||||
|
Assert.Equal("Id", result[0].Name);
|
||||||
|
Assert.Equal(typeof(Guid), result[0].Property.PropertyType);
|
||||||
|
Assert.Equal("Name", result[1].Name);
|
||||||
|
Assert.Equal(typeof(string), result[1].Property.PropertyType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MakeFastPropertySetter_SetsPropertyValues_ForPublicAndNobPublicProperties()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var instance = new BaseClass();
|
||||||
|
var typeInfo = instance.GetType().GetTypeInfo();
|
||||||
|
var publicProperty = typeInfo.GetDeclaredProperty("PropA");
|
||||||
|
var protectedProperty = typeInfo.GetDeclaredProperty("PropProtected");
|
||||||
|
var publicPropertySetter = PropertyHelper.MakeFastPropertySetter(publicProperty);
|
||||||
|
var protectedPropertySetter = PropertyHelper.MakeFastPropertySetter(protectedProperty);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
publicPropertySetter(instance, "TestPublic");
|
||||||
|
protectedPropertySetter(instance, "TestProtected");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("TestPublic", instance.PropA);
|
||||||
|
Assert.Equal("TestProtected", instance.GetPropProtected());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MakeFastPropertySetter_SetsPropertyValues_ForOverridenProperties()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var instance = new DerivedClassWithOverride();
|
||||||
|
var typeInfo = instance.GetType().GetTypeInfo();
|
||||||
|
var property = typeInfo.GetDeclaredProperty("PropA");
|
||||||
|
var propertySetter = PropertyHelper.MakeFastPropertySetter(property);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
propertySetter(instance, "Test value");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("OverridenTest value", instance.PropA);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MakeFastPropertySetter_SetsPropertyValues_ForNewedProperties()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var instance = new DerivedClassWithNew();
|
||||||
|
var typeInfo = instance.GetType().GetTypeInfo();
|
||||||
|
var property = typeInfo.GetDeclaredProperty("PropB");
|
||||||
|
var propertySetter = PropertyHelper.MakeFastPropertySetter(property);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
propertySetter(instance, "Test value");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("NewedTest value", instance.PropB);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MakeFastPropertyGetter_ReferenceType_ForNullObject_Throws()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var property = PropertyHelper
|
||||||
|
.GetProperties(typeof(BaseClass))
|
||||||
|
.Single(p => p.Name == nameof(BaseClass.PropA));
|
||||||
|
|
||||||
|
var accessor = PropertyHelper.MakeFastPropertyGetter(property.Property);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<NullReferenceException>(() => accessor(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MakeFastPropertyGetter_ValueType_ForNullObject_Throws()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var property = PropertyHelper
|
||||||
|
.GetProperties(typeof(MyProperties))
|
||||||
|
.Single(p => p.Name == nameof(MyProperties.StringProp));
|
||||||
|
|
||||||
|
var accessor = PropertyHelper.MakeFastPropertyGetter(property.Property);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<NullReferenceException>(() => accessor(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MakeNullSafeFastPropertyGetter_ReferenceType_Success()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var property = PropertyHelper
|
||||||
|
.GetProperties(typeof(BaseClass))
|
||||||
|
.Single(p => p.Name == nameof(BaseClass.PropA));
|
||||||
|
|
||||||
|
var accessor = PropertyHelper.MakeNullSafeFastPropertyGetter(property.Property);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var value = accessor(new BaseClass() { PropA = "Hi" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Hi", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MakeNullSafeFastPropertyGetter_ValueType_Success()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var property = PropertyHelper
|
||||||
|
.GetProperties(typeof(MyProperties))
|
||||||
|
.Single(p => p.Name == nameof(MyProperties.StringProp));
|
||||||
|
|
||||||
|
var accessor = PropertyHelper.MakeNullSafeFastPropertyGetter(property.Property);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var value = accessor(new MyProperties() { StringProp = "Hi" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Hi", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MakeNullSafeFastPropertyGetter_ReferenceType_ForNullObject_ReturnsNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var property = PropertyHelper
|
||||||
|
.GetProperties(typeof(BaseClass))
|
||||||
|
.Single(p => p.Name == nameof(BaseClass.PropA));
|
||||||
|
|
||||||
|
var accessor = PropertyHelper.MakeNullSafeFastPropertyGetter(property.Property);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var value = accessor(null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MakeNullSafeFastPropertyGetter_ValueType_ForNullObject_ReturnsNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var property = PropertyHelper
|
||||||
|
.GetProperties(typeof(MyProperties))
|
||||||
|
.Single(p => p.Name == nameof(MyProperties.StringProp));
|
||||||
|
|
||||||
|
var accessor = PropertyHelper.MakeNullSafeFastPropertyGetter(property.Property);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var value = accessor(null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TheoryData<object, KeyValuePair<string, object>> IgnoreCaseTestData
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return new TheoryData<object, KeyValuePair<string, object>>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
selected = true,
|
||||||
|
SeLeCtEd = false
|
||||||
|
},
|
||||||
|
new KeyValuePair<string, object>("selected", false)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
SeLeCtEd = false,
|
||||||
|
selected = true
|
||||||
|
},
|
||||||
|
new KeyValuePair<string, object>("SeLeCtEd", true)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
SelECTeD = false,
|
||||||
|
SeLECTED = true
|
||||||
|
},
|
||||||
|
new KeyValuePair<string, object>("SelECTeD", true)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(IgnoreCaseTestData))]
|
||||||
|
public void ObjectToDictionary_IgnoresPropertyCase(object testObject,
|
||||||
|
KeyValuePair<string, object> expectedEntry)
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = PropertyHelper.ObjectToDictionary(testObject);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var entry = Assert.Single(result);
|
||||||
|
Assert.Equal(expectedEntry, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ObjectToDictionary_WithNullObject_ReturnsEmptyDictionary()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
object value = null;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var dictValues = PropertyHelper.ObjectToDictionary(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(dictValues);
|
||||||
|
Assert.Equal(0, dictValues.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ObjectToDictionary_WithPlainObjectType_ReturnsEmptyDictionary()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var value = new object();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var dictValues = PropertyHelper.ObjectToDictionary(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(dictValues);
|
||||||
|
Assert.Equal(0, dictValues.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ObjectToDictionary_WithPrimitiveType_LooksUpPublicProperties()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var value = "test";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var dictValues = PropertyHelper.ObjectToDictionary(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(dictValues);
|
||||||
|
Assert.Equal(1, dictValues.Count);
|
||||||
|
Assert.Equal(4, dictValues["Length"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ObjectToDictionary_WithAnonymousType_LooksUpProperties()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var value = new { test = "value", other = 1 };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var dictValues = PropertyHelper.ObjectToDictionary(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(dictValues);
|
||||||
|
Assert.Equal(2, dictValues.Count);
|
||||||
|
Assert.Equal("value", dictValues["test"]);
|
||||||
|
Assert.Equal(1, dictValues["other"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ObjectToDictionary_ReturnsCaseInsensitiveDictionary()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var value = new { TEST = "value", oThEr = 1 };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var dictValues = PropertyHelper.ObjectToDictionary(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(dictValues);
|
||||||
|
Assert.Equal(2, dictValues.Count);
|
||||||
|
Assert.Equal("value", dictValues["test"]);
|
||||||
|
Assert.Equal(1, dictValues["other"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ObjectToDictionary_ReturnsInheritedProperties()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var value = new ThreeDPoint() { X = 5, Y = 10, Z = 17 };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var dictValues = PropertyHelper.ObjectToDictionary(value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(dictValues);
|
||||||
|
Assert.Equal(3, dictValues.Count);
|
||||||
|
Assert.Equal(5, dictValues["X"]);
|
||||||
|
Assert.Equal(10, dictValues["Y"]);
|
||||||
|
Assert.Equal(17, dictValues["Z"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Point
|
||||||
|
{
|
||||||
|
public int X { get; set; }
|
||||||
|
public int Y { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ThreeDPoint : Point
|
||||||
|
{
|
||||||
|
public int Z { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Static
|
||||||
|
{
|
||||||
|
public static int Prop2 { get; set; }
|
||||||
|
public int Prop5 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct MyProperties
|
||||||
|
{
|
||||||
|
public int IntProp { get; set; }
|
||||||
|
public string StringProp { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SetOnly
|
||||||
|
{
|
||||||
|
public int Prop2 { set { } }
|
||||||
|
public int Prop6 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PrivateProperties
|
||||||
|
{
|
||||||
|
public int Prop1 { get; set; }
|
||||||
|
protected int Prop2 { get; set; }
|
||||||
|
private int Prop3 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BaseClass
|
||||||
|
{
|
||||||
|
public string PropA { get; set; }
|
||||||
|
|
||||||
|
protected string PropProtected { get; set; }
|
||||||
|
|
||||||
|
public string GetPropProtected()
|
||||||
|
{
|
||||||
|
return PropProtected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DerivedClass : BaseClass
|
||||||
|
{
|
||||||
|
public string PropB { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BaseClassWithVirtual
|
||||||
|
{
|
||||||
|
public virtual string PropA { get; set; }
|
||||||
|
public string PropB { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DerivedClassWithNew : BaseClassWithVirtual
|
||||||
|
{
|
||||||
|
private string _value = "Newed";
|
||||||
|
|
||||||
|
public new string PropB
|
||||||
|
{
|
||||||
|
get { return _value; }
|
||||||
|
set { _value = "Newed" + value; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DerivedClassWithOverride : BaseClassWithVirtual
|
||||||
|
{
|
||||||
|
private string _value = "Overriden";
|
||||||
|
|
||||||
|
public override string PropA
|
||||||
|
{
|
||||||
|
get { return _value; }
|
||||||
|
set { _value = "Overriden" + value; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DerivedClassWithNonReadableProperties : BaseClassWithVirtual
|
||||||
|
{
|
||||||
|
public string this[int index]
|
||||||
|
{
|
||||||
|
get { return string.Empty; }
|
||||||
|
set { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Visible { get; set; }
|
||||||
|
|
||||||
|
private string NotVisible { get; set; }
|
||||||
|
|
||||||
|
public string NotVisible2 { private get; set; }
|
||||||
|
|
||||||
|
public string NotVisible3
|
||||||
|
{
|
||||||
|
set { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NotVisible4 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct MyStruct
|
||||||
|
{
|
||||||
|
public string Foo { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BaseHiddenProperty
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DerivedHiddenProperty : BaseHiddenProperty
|
||||||
|
{
|
||||||
|
public new string Id { get; set; }
|
||||||
|
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DerivedHiddenProperty2 : DerivedHiddenProperty
|
||||||
|
{
|
||||||
|
public new Guid Id { get; set; }
|
||||||
|
|
||||||
|
public new string Name { get; private set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
NOTE:
|
||||||
|
1. The tests for 'ExceptionDetailProvider' and 'StackTraceHelper' in project 'Microsoft.Extensions.StackTrace.Sources' are located in Diagnostics
|
||||||
|
repo. This is because they refer to some packages from FileSystem repo which causes a circular reference and breaks the
|
||||||
|
build.
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
// 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.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Security.Principal;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
public class SecurityHelperTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void AddingToAnonymousIdentityDoesNotKeepAnonymousIdentity()
|
||||||
|
{
|
||||||
|
var user = SecurityHelper.MergeUserPrincipal(new ClaimsPrincipal(), new GenericPrincipal(new GenericIdentity("Test1", "Alpha"), new string[0]));
|
||||||
|
|
||||||
|
Assert.NotNull(user);
|
||||||
|
Assert.Equal("Alpha", user.Identity.AuthenticationType);
|
||||||
|
Assert.Equal("Test1", user.Identity.Name);
|
||||||
|
Assert.IsAssignableFrom<ClaimsPrincipal>(user);
|
||||||
|
Assert.IsAssignableFrom<ClaimsIdentity>(user.Identity);
|
||||||
|
Assert.Single(user.Identities);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddingExistingIdentityChangesDefaultButPreservesPrior()
|
||||||
|
{
|
||||||
|
ClaimsPrincipal user = new GenericPrincipal(new GenericIdentity("Test1", "Alpha"), null);
|
||||||
|
|
||||||
|
Assert.Equal("Alpha", user.Identity.AuthenticationType);
|
||||||
|
Assert.Equal("Test1", user.Identity.Name);
|
||||||
|
|
||||||
|
user = SecurityHelper.MergeUserPrincipal(user, new GenericPrincipal(new GenericIdentity("Test2", "Beta"), new string[0]));
|
||||||
|
|
||||||
|
Assert.Equal("Beta", user.Identity.AuthenticationType);
|
||||||
|
Assert.Equal("Test2", user.Identity.Name);
|
||||||
|
|
||||||
|
user = SecurityHelper.MergeUserPrincipal(user, new GenericPrincipal(new GenericIdentity("Test3", "Gamma"), new string[0]));
|
||||||
|
|
||||||
|
Assert.Equal("Gamma", user.Identity.AuthenticationType);
|
||||||
|
Assert.Equal("Test3", user.Identity.Name);
|
||||||
|
|
||||||
|
Assert.Equal(3, user.Identities.Count());
|
||||||
|
Assert.Equal("Test3", user.Identities.Skip(0).First().Name);
|
||||||
|
Assert.Equal("Test2", user.Identities.Skip(1).First().Name);
|
||||||
|
Assert.Equal("Test1", user.Identities.Skip(2).First().Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddingPreservesNewIdentitiesAndDropsEmpty()
|
||||||
|
{
|
||||||
|
var existingPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
|
||||||
|
var identityNoAuthTypeWithClaim = new ClaimsIdentity();
|
||||||
|
identityNoAuthTypeWithClaim.AddClaim(new Claim("identityNoAuthTypeWithClaim", "yes"));
|
||||||
|
existingPrincipal.AddIdentity(identityNoAuthTypeWithClaim);
|
||||||
|
var identityEmptyWithAuthType = new ClaimsIdentity("empty");
|
||||||
|
existingPrincipal.AddIdentity(identityEmptyWithAuthType);
|
||||||
|
|
||||||
|
Assert.False(existingPrincipal.Identity.IsAuthenticated);
|
||||||
|
|
||||||
|
var newPrincipal = new ClaimsPrincipal();
|
||||||
|
var newEmptyIdentity = new ClaimsIdentity();
|
||||||
|
var identityTwo = new ClaimsIdentity("yep");
|
||||||
|
newPrincipal.AddIdentity(newEmptyIdentity);
|
||||||
|
newPrincipal.AddIdentity(identityTwo);
|
||||||
|
|
||||||
|
var user = SecurityHelper.MergeUserPrincipal(existingPrincipal, newPrincipal);
|
||||||
|
|
||||||
|
// Preserve newPrincipal order
|
||||||
|
Assert.False(user.Identity.IsAuthenticated);
|
||||||
|
Assert.Null(user.Identity.Name);
|
||||||
|
|
||||||
|
Assert.Equal(4, user.Identities.Count());
|
||||||
|
Assert.Equal(newEmptyIdentity, user.Identities.Skip(0).First());
|
||||||
|
Assert.Equal(identityTwo, user.Identities.Skip(1).First());
|
||||||
|
Assert.Equal(identityNoAuthTypeWithClaim, user.Identities.Skip(2).First());
|
||||||
|
Assert.Equal(identityEmptyWithAuthType, user.Identities.Skip(3).First());
|
||||||
|
|
||||||
|
// This merge should drop newEmptyIdentity since its empty
|
||||||
|
user = SecurityHelper.MergeUserPrincipal(user, new GenericPrincipal(new GenericIdentity("Test3", "Gamma"), new string[0]));
|
||||||
|
|
||||||
|
Assert.Equal("Gamma", user.Identity.AuthenticationType);
|
||||||
|
Assert.Equal("Test3", user.Identity.Name);
|
||||||
|
|
||||||
|
Assert.Equal(4, user.Identities.Count());
|
||||||
|
Assert.Equal("Test3", user.Identities.Skip(0).First().Name);
|
||||||
|
Assert.Equal(identityTwo, user.Identities.Skip(1).First());
|
||||||
|
Assert.Equal(identityNoAuthTypeWithClaim, user.Identities.Skip(2).First());
|
||||||
|
Assert.Equal(identityEmptyWithAuthType, user.Identities.Skip(3).First());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,345 @@
|
||||||
|
// 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.Linq.Expressions;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.StackTrace.Sources;
|
||||||
|
using ThrowingLibrary;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
public class StackTraceHelperTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void StackTraceHelper_IncludesLineNumbersForFiles()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
Exception exception = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Throwing an exception in the current assembly always seems to populate the full stack
|
||||||
|
// trace regardless of symbol type. Crossing assembly boundaries ensures PortablePdbReader gets used
|
||||||
|
// on desktop.
|
||||||
|
Thrower.Throw();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
exception = ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var stackFrames = StackTraceHelper.GetFrames(exception);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(stackFrames,
|
||||||
|
frame =>
|
||||||
|
{
|
||||||
|
Assert.Contains("Thrower.cs", frame.FilePath);
|
||||||
|
Assert.Equal(17, frame.LineNumber);
|
||||||
|
},
|
||||||
|
frame =>
|
||||||
|
{
|
||||||
|
Assert.Contains("StackTraceHelperTest.cs", frame.FilePath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StackTraceHelper_PrettyPrintsStackTraceForGenericMethods()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var exception = Record.Exception(() => GenericMethod<string>(null));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var stackFrames = StackTraceHelper.GetFrames(exception);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray();
|
||||||
|
Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.GenericMethod<T>(T val)", methods[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StackTraceHelper_PrettyPrintsStackTraceForMethodsWithOutParameters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var exception = Record.Exception(() => MethodWithOutParameter(out var value));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var stackFrames = StackTraceHelper.GetFrames(exception);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray();
|
||||||
|
Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.MethodWithOutParameter(out int value)", methods[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StackTraceHelper_PrettyPrintsStackTraceForMethodsWithGenericOutParameters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var exception = Record.Exception(() => MethodWithGenericOutParameter("Test", out int value));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var stackFrames = StackTraceHelper.GetFrames(exception);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray();
|
||||||
|
Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.MethodWithGenericOutParameter<TVal>(string a, out TVal value)", methods[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StackTraceHelper_PrettyPrintsStackTraceForMethodsWithRefParameters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var value = 0;
|
||||||
|
var exception = Record.Exception(() => MethodWithRefParameter(ref value));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var stackFrames = StackTraceHelper.GetFrames(exception);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray();
|
||||||
|
Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.MethodWithRefParameter(ref int value)", methods[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StackTraceHelper_PrettyPrintsStackTraceForMethodsWithGenericRefParameters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var value = 0;
|
||||||
|
var exception = Record.Exception(() => MethodWithGenericRefParameter(ref value));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var stackFrames = StackTraceHelper.GetFrames(exception);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray();
|
||||||
|
Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.MethodWithGenericRefParameter<TVal>(ref TVal value)", methods[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StackTraceHelper_PrettyPrintsStackTraceForMethodsWithNullableParameters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var value = 0;
|
||||||
|
var exception = Record.Exception(() => MethodWithNullableParameter(value));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var stackFrames = StackTraceHelper.GetFrames(exception);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray();
|
||||||
|
Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.MethodWithNullableParameter(Nullable<int> value)", methods[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StackTraceHelper_PrettyPrintsStackTraceForMethodsOnGenericTypes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var exception = Record.Exception(() => new GenericClass<int>().Throw(0));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var stackFrames = StackTraceHelper.GetFrames(exception);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray();
|
||||||
|
Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest+GenericClass<T>.Throw(T parameter)", methods[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StackTraceHelper_ProducesReadableOutput()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedCallStack = new List<string>()
|
||||||
|
{
|
||||||
|
"Microsoft.Extensions.Internal.StackTraceHelperTest.Iterator()+MoveNext()",
|
||||||
|
"string.Join(string separator, IEnumerable<string> values)",
|
||||||
|
"Microsoft.Extensions.Internal.StackTraceHelperTest+GenericClass<T>.GenericMethod<V>(ref V value)",
|
||||||
|
"Microsoft.Extensions.Internal.StackTraceHelperTest.MethodAsync(int value)",
|
||||||
|
"Microsoft.Extensions.Internal.StackTraceHelperTest.MethodAsync<TValue>(TValue value)",
|
||||||
|
"Microsoft.Extensions.Internal.StackTraceHelperTest.Method(string value)",
|
||||||
|
"Microsoft.Extensions.Internal.StackTraceHelperTest.StackTraceHelper_ProducesReadableOutput()",
|
||||||
|
};
|
||||||
|
|
||||||
|
Exception exception = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Method("test");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
exception = ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var stackFrames = StackTraceHelper.GetFrames(exception);
|
||||||
|
var methodNames = stackFrames.Select(stackFrame => stackFrame.MethodDisplayInfo.ToString()).ToArray();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedCallStack, methodNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StackTraceHelper_DoesNotIncludeInstanceMethodsOnTypesWithStackTraceHiddenAttribute()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var exception = Record.Exception(() => InvokeMethodOnTypeWithStackTraceHiddenAttribute());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var stackFrames = StackTraceHelper.GetFrames(exception);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray();
|
||||||
|
Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.ThrowCore()", methods[0]);
|
||||||
|
Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.InvokeMethodOnTypeWithStackTraceHiddenAttribute()", methods[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StackTraceHelper_DoesNotIncludeStaticMethodsOnTypesWithStackTraceHiddenAttribute()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var exception = Record.Exception(() => InvokeStaticMethodOnTypeWithStackTraceHiddenAttribute());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var stackFrames = StackTraceHelper.GetFrames(exception);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray();
|
||||||
|
Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.ThrowCore()", methods[0]);
|
||||||
|
Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.InvokeStaticMethodOnTypeWithStackTraceHiddenAttribute()", methods[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StackTraceHelper_DoesNotIncludeMethodsWithStackTraceHiddenAttribute()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var exception = Record.Exception(() => new TypeWithMethodWithStackTraceHiddenAttribute().Throw());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var stackFrames = StackTraceHelper.GetFrames(exception);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var methods = stackFrames.Select(frame => frame.MethodDisplayInfo.ToString()).ToArray();
|
||||||
|
Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest.ThrowCore()", methods[0]);
|
||||||
|
Assert.Equal("Microsoft.Extensions.Internal.StackTraceHelperTest+TypeWithMethodWithStackTraceHiddenAttribute.Throw()", methods[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetFrames_DoesNotFailForDynamicallyGeneratedAssemblies()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var action = (Action)Expression.Lambda(
|
||||||
|
Expression.Throw(
|
||||||
|
Expression.New(typeof(Exception)))).Compile();
|
||||||
|
var exception = Record.Exception(action);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var frames = StackTraceHelper.GetFrames(exception).ToArray();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var frame = frames[0];
|
||||||
|
Assert.Null(frame.FilePath);
|
||||||
|
Assert.Equal($"lambda_method(Closure )", frame.MethodDisplayInfo.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
|
||||||
|
async Task<string> MethodAsync(int value)
|
||||||
|
{
|
||||||
|
await Task.Delay(0);
|
||||||
|
return GenericClass<byte>.GenericMethod(ref value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
|
||||||
|
async Task<string> MethodAsync<TValue>(TValue value)
|
||||||
|
{
|
||||||
|
return await MethodAsync(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
|
||||||
|
string Method(string value)
|
||||||
|
{
|
||||||
|
return MethodAsync(value).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
|
||||||
|
static IEnumerable<string> Iterator()
|
||||||
|
{
|
||||||
|
yield return "Success";
|
||||||
|
throw new Exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
|
||||||
|
void MethodWithOutParameter(out int value) => throw new Exception();
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
|
||||||
|
void MethodWithGenericOutParameter<TVal>(string a, out TVal value) => throw new Exception();
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
|
||||||
|
void MethodWithRefParameter(ref int value) => throw new Exception();
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
|
||||||
|
void MethodWithGenericRefParameter<TVal>(ref TVal value) => throw new Exception();
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
|
||||||
|
void MethodWithNullableParameter(int? value) => throw new Exception();
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
|
||||||
|
void InvokeMethodOnTypeWithStackTraceHiddenAttribute() => new TypeWithStackTraceHiddenAttribute().Throw();
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
|
||||||
|
void InvokeStaticMethodOnTypeWithStackTraceHiddenAttribute() => TypeWithStackTraceHiddenAttribute.ThrowStatic();
|
||||||
|
|
||||||
|
class GenericClass<T>
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
|
||||||
|
public static string GenericMethod<V>(ref V value)
|
||||||
|
{
|
||||||
|
var returnVal = "";
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
returnVal += string.Join(", ", Iterator());
|
||||||
|
}
|
||||||
|
return returnVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
|
||||||
|
public void Throw(T parameter) => throw new Exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
|
||||||
|
private void GenericMethod<T>(T val) where T : class => throw new Exception();
|
||||||
|
|
||||||
|
private class StackTraceHiddenAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[StackTraceHidden]
|
||||||
|
private class TypeWithStackTraceHiddenAttribute
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
|
||||||
|
public void Throw() => ThrowCore();
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
|
||||||
|
public static void ThrowStatic() => ThrowCore();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TypeWithMethodWithStackTraceHiddenAttribute
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
|
||||||
|
[StackTraceHidden]
|
||||||
|
public void MethodWithStackTraceHiddenAttribute()
|
||||||
|
{
|
||||||
|
ThrowCore();
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
|
||||||
|
public void Throw() => MethodWithStackTraceHiddenAttribute();
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
|
||||||
|
private static void ThrowCore() => throw new Exception();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,263 @@
|
||||||
|
// 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 Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
public class TypeNameHelperTest
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
// Predefined Types
|
||||||
|
[InlineData(typeof(int), "int")]
|
||||||
|
[InlineData(typeof(List<int>), "System.Collections.Generic.List<int>")]
|
||||||
|
[InlineData(typeof(Dictionary<int, string>), "System.Collections.Generic.Dictionary<int, string>")]
|
||||||
|
[InlineData(typeof(Dictionary<int, List<string>>), "System.Collections.Generic.Dictionary<int, System.Collections.Generic.List<string>>")]
|
||||||
|
[InlineData(typeof(List<List<string>>), "System.Collections.Generic.List<System.Collections.Generic.List<string>>")]
|
||||||
|
// Classes inside NonGeneric class
|
||||||
|
[InlineData(typeof(A),
|
||||||
|
"Microsoft.Extensions.Internal.TypeNameHelperTest+A")]
|
||||||
|
[InlineData(typeof(B<int>),
|
||||||
|
"Microsoft.Extensions.Internal.TypeNameHelperTest+B<int>")]
|
||||||
|
[InlineData(typeof(C<int, string>),
|
||||||
|
"Microsoft.Extensions.Internal.TypeNameHelperTest+C<int, string>")]
|
||||||
|
[InlineData(typeof(B<B<string>>),
|
||||||
|
"Microsoft.Extensions.Internal.TypeNameHelperTest+B<Microsoft.Extensions.Internal.TypeNameHelperTest+B<string>>")]
|
||||||
|
[InlineData(typeof(C<int, B<string>>),
|
||||||
|
"Microsoft.Extensions.Internal.TypeNameHelperTest+C<int, Microsoft.Extensions.Internal.TypeNameHelperTest+B<string>>")]
|
||||||
|
// Classes inside Generic class
|
||||||
|
[InlineData(typeof(Outer<int>.D),
|
||||||
|
"Microsoft.Extensions.Internal.TypeNameHelperTest+Outer<int>+D")]
|
||||||
|
[InlineData(typeof(Outer<int>.E<int>),
|
||||||
|
"Microsoft.Extensions.Internal.TypeNameHelperTest+Outer<int>+E<int>")]
|
||||||
|
[InlineData(typeof(Outer<int>.F<int, string>),
|
||||||
|
"Microsoft.Extensions.Internal.TypeNameHelperTest+Outer<int>+F<int, string>")]
|
||||||
|
[InlineData(typeof(Level1<int>.Level2<bool>.Level3<int>),
|
||||||
|
"Microsoft.Extensions.Internal.TypeNameHelperTest+Level1<int>+Level2<bool>+Level3<int>")]
|
||||||
|
[InlineData(typeof(Outer<int>.E<Outer<int>.E<string>>),
|
||||||
|
"Microsoft.Extensions.Internal.TypeNameHelperTest+Outer<int>+E<Microsoft.Extensions.Internal.TypeNameHelperTest+Outer<int>+E<string>>")]
|
||||||
|
[InlineData(typeof(Outer<int>.F<int, Outer<int>.E<string>>),
|
||||||
|
"Microsoft.Extensions.Internal.TypeNameHelperTest+Outer<int>+F<int, Microsoft.Extensions.Internal.TypeNameHelperTest+Outer<int>+E<string>>")]
|
||||||
|
[InlineData(typeof(OuterGeneric<int>.InnerNonGeneric.InnerGeneric<int, string>.InnerGenericLeafNode<bool>),
|
||||||
|
"Microsoft.Extensions.Internal.TypeNameHelperTest+OuterGeneric<int>+InnerNonGeneric+InnerGeneric<int, string>+InnerGenericLeafNode<bool>")]
|
||||||
|
public void Can_pretty_print_CLR_full_name(Type type, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, TypeNameHelper.GetTypeDisplayName(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DoesNotPrintNamespace_ForGenericTypes_IfNullOrEmpty()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var type = typeof(ClassInGlobalNamespace<int>);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Equal("ClassInGlobalNamespace<int>", TypeNameHelper.GetTypeDisplayName(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
// Predefined Types
|
||||||
|
[InlineData(typeof(int), "int")]
|
||||||
|
[InlineData(typeof(List<int>), "List<int>")]
|
||||||
|
[InlineData(typeof(Dictionary<int, string>), "Dictionary<int, string>")]
|
||||||
|
[InlineData(typeof(Dictionary<int, List<string>>), "Dictionary<int, List<string>>")]
|
||||||
|
[InlineData(typeof(List<List<string>>), "List<List<string>>")]
|
||||||
|
// Classes inside NonGeneric class
|
||||||
|
[InlineData(typeof(A), "A")]
|
||||||
|
[InlineData(typeof(B<int>), "B<int>")]
|
||||||
|
[InlineData(typeof(C<int, string>), "C<int, string>")]
|
||||||
|
[InlineData(typeof(C<int, B<string>>), "C<int, B<string>>")]
|
||||||
|
[InlineData(typeof(B<B<string>>), "B<B<string>>")]
|
||||||
|
// Classes inside Generic class
|
||||||
|
[InlineData(typeof(Outer<int>.D), "D")]
|
||||||
|
[InlineData(typeof(Outer<int>.E<int>), "E<int>")]
|
||||||
|
[InlineData(typeof(Outer<int>.F<int, string>), "F<int, string>")]
|
||||||
|
[InlineData(typeof(Outer<int>.F<int, Outer<int>.E<string>>), "F<int, E<string>>")]
|
||||||
|
[InlineData(typeof(Outer<int>.E<Outer<int>.E<string>>), "E<E<string>>")]
|
||||||
|
[InlineData(typeof(OuterGeneric<int>.InnerNonGeneric.InnerGeneric<int, string>.InnerGenericLeafNode<bool>), "InnerGenericLeafNode<bool>")]
|
||||||
|
public void Can_pretty_print_CLR_name(Type type, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, TypeNameHelper.GetTypeDisplayName(type, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(void), "void")]
|
||||||
|
[InlineData(typeof(bool), "bool")]
|
||||||
|
[InlineData(typeof(byte), "byte")]
|
||||||
|
[InlineData(typeof(char), "char")]
|
||||||
|
[InlineData(typeof(decimal), "decimal")]
|
||||||
|
[InlineData(typeof(double), "double")]
|
||||||
|
[InlineData(typeof(float), "float")]
|
||||||
|
[InlineData(typeof(int), "int")]
|
||||||
|
[InlineData(typeof(long), "long")]
|
||||||
|
[InlineData(typeof(object), "object")]
|
||||||
|
[InlineData(typeof(sbyte), "sbyte")]
|
||||||
|
[InlineData(typeof(short), "short")]
|
||||||
|
[InlineData(typeof(string), "string")]
|
||||||
|
[InlineData(typeof(uint), "uint")]
|
||||||
|
[InlineData(typeof(ulong), "ulong")]
|
||||||
|
[InlineData(typeof(ushort), "ushort")]
|
||||||
|
public void Returns_common_name_for_built_in_types(Type type, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, TypeNameHelper.GetTypeDisplayName(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(int[]), true, "int[]")]
|
||||||
|
[InlineData(typeof(string[][]), true, "string[][]")]
|
||||||
|
[InlineData(typeof(int[,]), true, "int[,]")]
|
||||||
|
[InlineData(typeof(bool[,,,]), true, "bool[,,,]")]
|
||||||
|
[InlineData(typeof(A[,][,,]), true, "Microsoft.Extensions.Internal.TypeNameHelperTest+A[,][,,]")]
|
||||||
|
[InlineData(typeof(List<int[,][,,]>), true, "System.Collections.Generic.List<int[,][,,]>")]
|
||||||
|
[InlineData(typeof(List<int[,,][,]>[,][,,]), false, "List<int[,,][,]>[,][,,]")]
|
||||||
|
public void Can_pretty_print_array_name(Type type, bool fullName, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, TypeNameHelper.GetTypeDisplayName(type, fullName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TheoryData GetOpenGenericsTestData()
|
||||||
|
{
|
||||||
|
var openDictionaryType = typeof(Dictionary<,>);
|
||||||
|
var genArgsDictionary = openDictionaryType.GetGenericArguments();
|
||||||
|
genArgsDictionary[0] = typeof(B<>);
|
||||||
|
var closedDictionaryType = openDictionaryType.MakeGenericType(genArgsDictionary);
|
||||||
|
|
||||||
|
var openLevelType = typeof(Level1<>.Level2<>.Level3<>);
|
||||||
|
var genArgsLevel = openLevelType.GetGenericArguments();
|
||||||
|
genArgsLevel[1] = typeof(string);
|
||||||
|
var closedLevelType = openLevelType.MakeGenericType(genArgsLevel);
|
||||||
|
|
||||||
|
var openInnerType = typeof(OuterGeneric<>.InnerNonGeneric.InnerGeneric<,>.InnerGenericLeafNode<>);
|
||||||
|
var genArgsInnerType = openInnerType.GetGenericArguments();
|
||||||
|
genArgsInnerType[3] = typeof(bool);
|
||||||
|
var closedInnerType = openInnerType.MakeGenericType(genArgsInnerType);
|
||||||
|
|
||||||
|
return new TheoryData<Type, bool, string>
|
||||||
|
{
|
||||||
|
{ typeof(List<>), false, "List<>" },
|
||||||
|
{ typeof(Dictionary<,>), false , "Dictionary<,>" },
|
||||||
|
{ typeof(List<>), true , "System.Collections.Generic.List<>" },
|
||||||
|
{ typeof(Dictionary<,>), true , "System.Collections.Generic.Dictionary<,>" },
|
||||||
|
{ typeof(Level1<>.Level2<>.Level3<>), true, "Microsoft.Extensions.Internal.TypeNameHelperTest+Level1<>+Level2<>+Level3<>" },
|
||||||
|
{
|
||||||
|
typeof(PartiallyClosedGeneric<>).BaseType,
|
||||||
|
true,
|
||||||
|
"Microsoft.Extensions.Internal.TypeNameHelperTest+C<, int>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(OuterGeneric<>.InnerNonGeneric.InnerGeneric<,>.InnerGenericLeafNode<>),
|
||||||
|
true,
|
||||||
|
"Microsoft.Extensions.Internal.TypeNameHelperTest+OuterGeneric<>+InnerNonGeneric+InnerGeneric<,>+InnerGenericLeafNode<>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
closedDictionaryType,
|
||||||
|
true,
|
||||||
|
"System.Collections.Generic.Dictionary<Microsoft.Extensions.Internal.TypeNameHelperTest+B<>,>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
closedLevelType,
|
||||||
|
true,
|
||||||
|
"Microsoft.Extensions.Internal.TypeNameHelperTest+Level1<>+Level2<string>+Level3<>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
closedInnerType,
|
||||||
|
true,
|
||||||
|
"Microsoft.Extensions.Internal.TypeNameHelperTest+OuterGeneric<>+InnerNonGeneric+InnerGeneric<,>+InnerGenericLeafNode<bool>"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(GetOpenGenericsTestData))]
|
||||||
|
public void Can_pretty_print_open_generics(Type type, bool fullName, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, TypeNameHelper.GetTypeDisplayName(type, fullName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TheoryData GetTypeDisplayName_IncludesGenericParameterNamesWhenOptionIsSetData =>
|
||||||
|
new TheoryData<Type, string>
|
||||||
|
{
|
||||||
|
{ typeof(B<>),"Microsoft.Extensions.Internal.TypeNameHelperTest+B<T>" },
|
||||||
|
{ typeof(C<,>),"Microsoft.Extensions.Internal.TypeNameHelperTest+C<T1, T2>" },
|
||||||
|
{ typeof(PartiallyClosedGeneric<>).BaseType,"Microsoft.Extensions.Internal.TypeNameHelperTest+C<T, int>" },
|
||||||
|
{ typeof(Level1<>.Level2<>),"Microsoft.Extensions.Internal.TypeNameHelperTest+Level1<T1>+Level2<T2>" },
|
||||||
|
};
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(GetTypeDisplayName_IncludesGenericParameterNamesWhenOptionIsSetData))]
|
||||||
|
public void GetTypeDisplayName_IncludesGenericParameterNamesWhenOptionIsSet(Type type, string expected)
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var actual = TypeNameHelper.GetTypeDisplayName(type, fullName: true, includeGenericParameterNames: true);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TheoryData GetTypeDisplayName_WithoutFullName_IncludesGenericParameterNamesWhenOptionIsSetData =>
|
||||||
|
new TheoryData<Type, string>
|
||||||
|
{
|
||||||
|
{ typeof(B<>),"B<T>" },
|
||||||
|
{ typeof(C<,>),"C<T1, T2>" },
|
||||||
|
{ typeof(PartiallyClosedGeneric<>).BaseType,"C<T, int>" },
|
||||||
|
{ typeof(Level1<>.Level2<>),"Level2<T2>" },
|
||||||
|
};
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(GetTypeDisplayName_WithoutFullName_IncludesGenericParameterNamesWhenOptionIsSetData))]
|
||||||
|
public void GetTypeDisplayName_WithoutFullName_IncludesGenericParameterNamesWhenOptionIsSet(Type type, string expected)
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var actual = TypeNameHelper.GetTypeDisplayName(type, fullName: false, includeGenericParameterNames: true);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class A { }
|
||||||
|
|
||||||
|
private class B<T> { }
|
||||||
|
|
||||||
|
private class C<T1, T2> { }
|
||||||
|
|
||||||
|
private class PartiallyClosedGeneric<T> : C<T, int> { }
|
||||||
|
|
||||||
|
private class Outer<T>
|
||||||
|
{
|
||||||
|
public class D { }
|
||||||
|
|
||||||
|
public class E<T1> { }
|
||||||
|
|
||||||
|
public class F<T1, T2> { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OuterGeneric<T1>
|
||||||
|
{
|
||||||
|
public class InnerNonGeneric
|
||||||
|
{
|
||||||
|
public class InnerGeneric<T2, T3>
|
||||||
|
{
|
||||||
|
public class InnerGenericLeafNode<T4> { }
|
||||||
|
|
||||||
|
public class InnerLeafNode { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Level1<T1>
|
||||||
|
{
|
||||||
|
public class Level2<T2>
|
||||||
|
{
|
||||||
|
public class Level3<T3>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ClassInGlobalNamespace<T>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -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 System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal.Test
|
||||||
|
{
|
||||||
|
public class ValueStopwatchTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void IsActiveIsFalseForDefaultValueStopwatch()
|
||||||
|
{
|
||||||
|
Assert.False(default(ValueStopwatch).IsActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsActiveIsTrueWhenValueStopwatchStartedWithStartNew()
|
||||||
|
{
|
||||||
|
Assert.True(ValueStopwatch.StartNew().IsActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetElapsedTimeThrowsIfValueStopwatchIsDefaultValue()
|
||||||
|
{
|
||||||
|
var stopwatch = default(ValueStopwatch);
|
||||||
|
Assert.Throws<InvalidOperationException>(() => stopwatch.GetElapsedTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetElapsedTimeReturnsTimeElapsedSinceStart()
|
||||||
|
{
|
||||||
|
var stopwatch = ValueStopwatch.StartNew();
|
||||||
|
await Task.Delay(200);
|
||||||
|
Assert.True(stopwatch.GetElapsedTime().TotalMilliseconds > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
// 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 Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Internal
|
||||||
|
{
|
||||||
|
public class WebEncodersTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("", 1, 0)]
|
||||||
|
[InlineData("", 0, 1)]
|
||||||
|
[InlineData("0123456789", 9, 2)]
|
||||||
|
[InlineData("0123456789", Int32.MaxValue, 2)]
|
||||||
|
[InlineData("0123456789", 9, -1)]
|
||||||
|
public void Base64UrlDecode_BadOffsets(string input, int offset, int count)
|
||||||
|
{
|
||||||
|
// Act & assert
|
||||||
|
Assert.ThrowsAny<ArgumentException>(() =>
|
||||||
|
{
|
||||||
|
var retVal = WebEncoders.Base64UrlDecode(input, offset, count);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("x")]
|
||||||
|
[InlineData("(x)")]
|
||||||
|
public void Base64UrlDecode_MalformedInput(string input)
|
||||||
|
{
|
||||||
|
// Act & assert
|
||||||
|
Assert.Throws<FormatException>(() =>
|
||||||
|
{
|
||||||
|
var retVal = WebEncoders.Base64UrlDecode(input);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("", "")]
|
||||||
|
[InlineData("123456qwerty++//X+/x", "123456qwerty--__X-_x")]
|
||||||
|
[InlineData("123456qwerty++//X+/xxw==", "123456qwerty--__X-_xxw")]
|
||||||
|
[InlineData("123456qwerty++//X+/xxw0=", "123456qwerty--__X-_xxw0")]
|
||||||
|
public void Base64UrlEncode_And_Decode(string base64Input, string expectedBase64Url)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
byte[] input = new byte[3].Concat(Convert.FromBase64String(base64Input)).Concat(new byte[2]).ToArray();
|
||||||
|
|
||||||
|
// Act & assert - 1
|
||||||
|
string actualBase64Url = WebEncoders.Base64UrlEncode(input, 3, input.Length - 5); // also helps test offsets
|
||||||
|
Assert.Equal(expectedBase64Url, actualBase64Url);
|
||||||
|
|
||||||
|
// Act & assert - 2
|
||||||
|
// Verify that values round-trip
|
||||||
|
byte[] roundTripped = WebEncoders.Base64UrlDecode("xx" + actualBase64Url + "yyy", 2, actualBase64Url.Length); // also helps test offsets
|
||||||
|
string roundTrippedAsBase64 = Convert.ToBase64String(roundTripped);
|
||||||
|
Assert.Equal(roundTrippedAsBase64, base64Input);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("", "")]
|
||||||
|
[InlineData("123456qwerty++//X+/x", "123456qwerty--__X-_x")]
|
||||||
|
[InlineData("123456qwerty++//X+/xxw==", "123456qwerty--__X-_xxw")]
|
||||||
|
[InlineData("123456qwerty++//X+/xxw0=", "123456qwerty--__X-_xxw0")]
|
||||||
|
public void Base64UrlEncode_And_Decode_WithBufferOffsets(string base64Input, string expectedBase64Url)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var input = new byte[3].Concat(Convert.FromBase64String(base64Input)).Concat(new byte[2]).ToArray();
|
||||||
|
var buffer = new char[30];
|
||||||
|
var output = new char[30];
|
||||||
|
for (var i = 0; i < buffer.Length; i++)
|
||||||
|
{
|
||||||
|
buffer[i] = '^';
|
||||||
|
output[i] = '^';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act 1
|
||||||
|
var numEncodedChars =
|
||||||
|
WebEncoders.Base64UrlEncode(input, offset: 3, output: output, outputOffset: 4, count: input.Length - 5);
|
||||||
|
|
||||||
|
// Assert 1
|
||||||
|
var encodedString = new string(output, startIndex: 4, length: numEncodedChars);
|
||||||
|
Assert.Equal(expectedBase64Url, encodedString);
|
||||||
|
|
||||||
|
// Act 2
|
||||||
|
var roundTripInput = new string(output);
|
||||||
|
var roundTripped =
|
||||||
|
WebEncoders.Base64UrlDecode(roundTripInput, offset: 4, buffer: buffer, bufferOffset: 5, count: numEncodedChars);
|
||||||
|
|
||||||
|
// Assert 2, verify that values round-trip
|
||||||
|
var roundTrippedAsBase64 = Convert.ToBase64String(roundTripped);
|
||||||
|
Assert.Equal(roundTrippedAsBase64, base64Input);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0, 1, 0)]
|
||||||
|
[InlineData(0, 0, 1)]
|
||||||
|
[InlineData(10, 9, 2)]
|
||||||
|
[InlineData(10, Int32.MaxValue, 2)]
|
||||||
|
[InlineData(10, 9, -1)]
|
||||||
|
public void Base64UrlEncode_BadOffsets(int inputLength, int offset, int count)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
byte[] input = new byte[inputLength];
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
Assert.ThrowsAny<ArgumentException>(() =>
|
||||||
|
{
|
||||||
|
var retVal = WebEncoders.Base64UrlEncode(input, offset, count);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
// 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.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace ThrowingLibrary
|
||||||
|
{
|
||||||
|
// Throwing an exception in the current assembly always seems to populate the full stack
|
||||||
|
// trace regardless of symbol type. This type exists to simulate an exception thrown
|
||||||
|
// across assemblies which is the typical use case for StackTraceHelper.
|
||||||
|
public static class Thrower
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
public static void Throw()
|
||||||
|
{
|
||||||
|
throw new DivideByZeroException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<DebugType>portable</DebugType>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Loading…
Reference in New Issue