|
|
@@ -13,23 +13,13 @@ namespace Discord.ETF |
|
|
|
public unsafe class ETFWriter : IDisposable |
|
|
|
{ |
|
|
|
private readonly static byte[] _nilBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 3, (byte)'n', (byte)'i', (byte)'l' }; |
|
|
|
private readonly static byte[] _nilExtBytes = new byte[] { (byte)ETFType.NIL_EXT}; |
|
|
|
private readonly static byte[] _falseBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 5, (byte)'f', (byte)'a', (byte)'l', (byte)'s', (byte)'e' }; |
|
|
|
private readonly static byte[] _trueBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 4, (byte)'t', (byte)'r', (byte)'u', (byte)'e' }; |
|
|
|
|
|
|
|
private readonly static MethodInfo _writeTMethod = typeof(ETFWriter).GetTypeInfo() |
|
|
|
.GetDeclaredMethods(nameof(Write)) |
|
|
|
.Where(x => x.IsGenericMethodDefinition && x.GetParameters()[0].ParameterType == x.GetGenericArguments()[0]) |
|
|
|
.Single(); |
|
|
|
private readonly static MethodInfo _writeNullableTMethod = typeof(ETFWriter).GetTypeInfo() |
|
|
|
.GetDeclaredMethods(nameof(Write)) |
|
|
|
.Where(x => |
|
|
|
{ |
|
|
|
if (!x.IsGenericMethodDefinition) return false; |
|
|
|
var p = x.GetParameters()[0].ParameterType.GetTypeInfo(); |
|
|
|
return p.IsGenericType && p.GetGenericTypeDefinition() == typeof(Nullable<>); |
|
|
|
}) |
|
|
|
.Single(); |
|
|
|
private readonly static MethodInfo _writeTMethod = GetGenericWriteMethod(null); |
|
|
|
private readonly static MethodInfo _writeNullableTMethod = GetGenericWriteMethod(typeof(Nullable<>)); |
|
|
|
private readonly static MethodInfo _writeDictionaryTMethod = GetGenericWriteMethod(typeof(IDictionary<,>)); |
|
|
|
private readonly static MethodInfo _writeEnumerableTMethod = GetGenericWriteMethod(typeof(IEnumerable<>)); |
|
|
|
private readonly static DateTime _epochTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); |
|
|
|
|
|
|
|
private readonly Stream _stream; |
|
|
@@ -58,29 +48,6 @@ namespace Discord.ETF |
|
|
|
_serializers = new ConcurrentDictionary<Type, Delegate>(); |
|
|
|
_indirectSerializers = new ConcurrentDictionary<Type, Delegate>(); |
|
|
|
} |
|
|
|
|
|
|
|
enum EnumTest1 { A, B, C } |
|
|
|
public static byte[] Test() |
|
|
|
{ |
|
|
|
using (var stream = new MemoryStream()) |
|
|
|
{ |
|
|
|
using (var writer = new ETFWriter(stream)) |
|
|
|
{ |
|
|
|
var request = new API.Client.Rest.SendMessageRequest(109384029348) |
|
|
|
{ |
|
|
|
Content = "TestMsg", |
|
|
|
Nonce = null, |
|
|
|
IsTTS = false |
|
|
|
}; |
|
|
|
writer.Write(request); |
|
|
|
/*writer.Write<EnumTest1>((EnumTest1?)EnumTest1.C); |
|
|
|
writer.Write((object)(EnumTest1?)EnumTest1.C); |
|
|
|
writer.Write<EnumTest1>((EnumTest1?)null); |
|
|
|
writer.Write((object)(EnumTest1?)null);*/ |
|
|
|
} |
|
|
|
return stream.ToArray(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public void Write(bool value) |
|
|
|
{ |
|
|
@@ -177,18 +144,19 @@ namespace Discord.ETF |
|
|
|
_stream.Write(_buffer, 0, 9); |
|
|
|
} |
|
|
|
|
|
|
|
public void Write(DateTime value) => Write((ulong)((value.Ticks - _epochTime.Ticks) / TimeSpan.TicksPerMillisecond)); |
|
|
|
public void Write(DateTime value) => Write((ulong)((value.Ticks - _epochTime.Ticks) / TimeSpan.TicksPerSecond)); |
|
|
|
|
|
|
|
public void Write(byte? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); } |
|
|
|
public void Write(bool? value) { if (value.HasValue) Write((bool)value.Value); else WriteNil(); } |
|
|
|
public void Write(sbyte? value) { if (value.HasValue) Write((long)value.Value); else WriteNil(); } |
|
|
|
public void Write(ushort? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); } |
|
|
|
public void Write(byte? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); } |
|
|
|
public void Write(short? value) { if (value.HasValue) Write((long)value.Value); else WriteNil(); } |
|
|
|
public void Write(uint? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); } |
|
|
|
public void Write(ushort? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); } |
|
|
|
public void Write(int? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } |
|
|
|
public void Write(ulong? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } |
|
|
|
public void Write(uint? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); } |
|
|
|
public void Write(long? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } |
|
|
|
public void Write(float? value) { if (value.HasValue) Write((double)value.Value); else WriteNil(); } |
|
|
|
public void Write(ulong? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } |
|
|
|
public void Write(double? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } |
|
|
|
public void Write(float? value) { if (value.HasValue) Write((double)value.Value); else WriteNil(); } |
|
|
|
public void Write(DateTime? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } |
|
|
|
|
|
|
|
public void Write(byte[] value) |
|
|
@@ -251,10 +219,13 @@ namespace Discord.ETF |
|
|
|
_buffer[2] = (byte)((length >> 16) & 0xFF); |
|
|
|
_buffer[3] = (byte)((length >> 8) & 0xFF); |
|
|
|
_buffer[4] = (byte)(length & 0xFF); |
|
|
|
_stream.Write(_buffer, 0, 5); |
|
|
|
|
|
|
|
for (int i = 0; i < array.Length; i++) |
|
|
|
Write(array[i]); |
|
|
|
WriteNilExt(); |
|
|
|
_stream.Write(_buffer, 0, 5); |
|
|
|
|
|
|
|
_buffer[0] = (byte)ETFType.NIL_EXT; |
|
|
|
_stream.Write(_buffer, 0, 1); |
|
|
|
} |
|
|
|
else |
|
|
|
WriteNil(); |
|
|
@@ -269,12 +240,13 @@ namespace Discord.ETF |
|
|
|
_buffer[2] = (byte)((length >> 16) & 0xFF); |
|
|
|
_buffer[3] = (byte)((length >> 8) & 0xFF); |
|
|
|
_buffer[4] = (byte)(length & 0xFF); |
|
|
|
_stream.Write(_buffer, 0, 5); |
|
|
|
|
|
|
|
foreach (var pair in obj) |
|
|
|
{ |
|
|
|
Write(pair.Key); |
|
|
|
Write(pair.Value); |
|
|
|
} |
|
|
|
_stream.Write(_buffer, 0, 5); |
|
|
|
} |
|
|
|
else |
|
|
|
WriteNil(); |
|
|
@@ -292,147 +264,137 @@ namespace Discord.ETF |
|
|
|
WriteNil(); |
|
|
|
} |
|
|
|
|
|
|
|
private void WriteNil() => _stream.Write(_nilBytes, 0, _nilBytes.Length); |
|
|
|
|
|
|
|
public virtual void Flush() => _stream.Flush(); |
|
|
|
public virtual long Seek(int offset, SeekOrigin origin) => _stream.Seek(offset, origin); |
|
|
|
|
|
|
|
private Action<ETFWriter, T> CreateSerializer<T>(Type type, TypeInfo typeInfo, bool isObject) |
|
|
|
|
|
|
|
#region Emit |
|
|
|
private Action<ETFWriter, T> CreateSerializer<T>(Type type, TypeInfo typeInfo, bool isDirect) |
|
|
|
{ |
|
|
|
var method = new DynamicMethod(!isObject ? "SerializeETF" : "SerializeIndirectETF", |
|
|
|
null, new[] { typeof(ETFWriter), isObject? typeof(object) : type }, true); |
|
|
|
var method = new DynamicMethod(isDirect ? "SerializeETF" : "SerializeIndirectETF", |
|
|
|
null, new[] { typeof(ETFWriter), isDirect ? type : typeof(object) }, true); |
|
|
|
var generator = method.GetILGenerator(); |
|
|
|
|
|
|
|
if (typeInfo.IsPrimitive || typeInfo.IsEnum) |
|
|
|
|
|
|
|
generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) |
|
|
|
generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), value |
|
|
|
if (!isDirect) |
|
|
|
{ |
|
|
|
generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) |
|
|
|
generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), value |
|
|
|
if (isObject && typeInfo.IsValueType) |
|
|
|
generator.Emit(OpCodes.Unbox_Any, type); //ETFWriter(this), value |
|
|
|
EmitETFWriteValue(generator, type, typeInfo); |
|
|
|
if (typeInfo.IsValueType) //Unbox value types |
|
|
|
generator.Emit(OpCodes.Unbox_Any, type); //ETFWriter(this), real_value |
|
|
|
else //Cast reference types |
|
|
|
generator.Emit(OpCodes.Castclass, type); //ETFWriter(this), real_value |
|
|
|
generator.EmitCall(OpCodes.Call, _writeTMethod.MakeGenericMethod(type), null); //Call generic version |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
//Scan for certain interfaces |
|
|
|
Type dictionaryI = null, enumerableI = null; |
|
|
|
foreach (var i in typeInfo.ImplementedInterfaces |
|
|
|
.Where(x => x.IsConstructedGenericType)) |
|
|
|
{ |
|
|
|
if (i.GetGenericTypeDefinition() == typeof(IDictionary<,>)) |
|
|
|
{ |
|
|
|
//TODO: Emit null check |
|
|
|
dictionaryI = i; |
|
|
|
break; |
|
|
|
} |
|
|
|
else if (i.GetGenericTypeDefinition() == typeof(IEnumerable<>)) |
|
|
|
{ |
|
|
|
//TODO: Emit null check |
|
|
|
enumerableI = i; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (dictionaryI != null) |
|
|
|
{ |
|
|
|
throw new NotImplementedException(); |
|
|
|
} |
|
|
|
else if (enumerableI != null) |
|
|
|
{ |
|
|
|
throw new NotImplementedException(); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
//TODO: Add field/property names |
|
|
|
typeInfo.ForEachField(f => |
|
|
|
{ |
|
|
|
string name; |
|
|
|
if (!f.IsPublic || !IsETFProperty(f, out name)) return; |
|
|
|
|
|
|
|
generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) |
|
|
|
generator.Emit(OpCodes.Ldstr, name); //ETFWriter(this), name |
|
|
|
generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null); |
|
|
|
generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) |
|
|
|
generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), obj |
|
|
|
generator.Emit(OpCodes.Ldfld, f); //ETFWriter(this), obj.fieldValue |
|
|
|
if (isObject && typeInfo.IsValueType) |
|
|
|
generator.Emit(OpCodes.Unbox_Any, type); //ETFWriter(this), obj.fieldValue |
|
|
|
EmitETFWriteValue(generator, f.FieldType); |
|
|
|
}); |
|
|
|
|
|
|
|
typeInfo.ForEachProperty(p => |
|
|
|
{ |
|
|
|
string name; |
|
|
|
if (!p.CanRead || !p.GetMethod.IsPublic || !IsETFProperty(p, out name)) return; |
|
|
|
|
|
|
|
generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) |
|
|
|
generator.Emit(OpCodes.Ldstr, name); //ETFWriter(this), name |
|
|
|
generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null); |
|
|
|
generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) |
|
|
|
generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), obj |
|
|
|
generator.EmitCall(OpCodes.Callvirt, p.GetMethod, null); //ETFWriter(this), obj.propValue |
|
|
|
if (isObject && typeInfo.IsValueType) |
|
|
|
generator.Emit(OpCodes.Unbox_Any, type); //ETFWriter(this), obj.propValue |
|
|
|
EmitETFWriteValue(generator, p.PropertyType); |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
EmitWriteValue(generator, type, typeInfo, true); |
|
|
|
|
|
|
|
generator.Emit(OpCodes.Ret); |
|
|
|
return method.CreateDelegate(typeof(Action<ETFWriter, T>)) as Action<ETFWriter, T>; |
|
|
|
} |
|
|
|
|
|
|
|
private void EmitETFWriteValue(ILGenerator generator, Type type, TypeInfo typeInfo = null) |
|
|
|
{ |
|
|
|
if (typeInfo == null) |
|
|
|
typeInfo = type.GetTypeInfo(); |
|
|
|
|
|
|
|
private void EmitWriteValue(ILGenerator generator, Type type, TypeInfo typeInfo, bool isTop) |
|
|
|
{ |
|
|
|
//Convert enum types to their base type |
|
|
|
if (typeInfo.IsEnum) |
|
|
|
{ |
|
|
|
type = Enum.GetUnderlyingType(type); |
|
|
|
typeInfo = type.GetTypeInfo(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
//Check if this type already has a direct call or simple conversion |
|
|
|
//Primitives/Enums |
|
|
|
Type targetType = null; |
|
|
|
if (!typeInfo.IsEnum && IsType(type, typeof(long), typeof(ulong), typeof(double), typeof(bool), typeof(string), |
|
|
|
typeof(sbyte?), typeof(byte?), typeof(short?), typeof(ushort?), |
|
|
|
typeof(int?), typeof(uint?), typeof(long?), typeof(ulong?), |
|
|
|
typeof(bool?), typeof(float?), typeof(double?), |
|
|
|
typeof(object), typeof(DateTime))) |
|
|
|
targetType = type; //Direct |
|
|
|
{ |
|
|
|
//No conversion needed |
|
|
|
targetType = type; |
|
|
|
} |
|
|
|
else if (IsType(type, typeof(sbyte), typeof(short), typeof(int))) |
|
|
|
{ |
|
|
|
//Convert to long |
|
|
|
generator.Emit(OpCodes.Conv_I8); |
|
|
|
targetType = typeof(long); |
|
|
|
} |
|
|
|
else if (IsType(type, typeof(byte), typeof(ushort), typeof(uint))) |
|
|
|
{ |
|
|
|
//Convert to ulong |
|
|
|
generator.Emit(OpCodes.Conv_U8); |
|
|
|
targetType = typeof(ulong); |
|
|
|
} |
|
|
|
else if (IsType(type, typeof(float))) |
|
|
|
{ |
|
|
|
//Convert to double |
|
|
|
generator.Emit(OpCodes.Conv_R8); |
|
|
|
targetType = typeof(double); |
|
|
|
} |
|
|
|
|
|
|
|
//Primitives/Enums |
|
|
|
if (targetType != null) |
|
|
|
generator.EmitCall(OpCodes.Call, GetWriteMethod(targetType), null); |
|
|
|
|
|
|
|
//Nullable Non-Primitives |
|
|
|
else if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>)) |
|
|
|
//Dictionaries |
|
|
|
else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces |
|
|
|
.Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IDictionary<,>))) |
|
|
|
generator.EmitCall(OpCodes.Call, _writeDictionaryTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); |
|
|
|
|
|
|
|
//Enumerable |
|
|
|
else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces |
|
|
|
.Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>))) |
|
|
|
generator.EmitCall(OpCodes.Call, _writeEnumerableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); |
|
|
|
|
|
|
|
//Nullable Structs |
|
|
|
else if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>) && |
|
|
|
typeInfo.GenericTypeParameters[0].GetTypeInfo().IsValueType) |
|
|
|
generator.EmitCall(OpCodes.Call, _writeNullableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); |
|
|
|
|
|
|
|
//Structs/Classes |
|
|
|
else if (typeInfo.IsClass || (typeInfo.IsValueType && !typeInfo.IsPrimitive)) |
|
|
|
generator.EmitCall(OpCodes.Call, _writeTMethod.MakeGenericMethod(type), null); |
|
|
|
{ |
|
|
|
if (isTop) |
|
|
|
{ |
|
|
|
typeInfo.ForEachField(f => |
|
|
|
{ |
|
|
|
string name; |
|
|
|
if (!f.IsPublic || !IsETFProperty(f, out name)) return; |
|
|
|
|
|
|
|
generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) |
|
|
|
generator.Emit(OpCodes.Ldstr, name); //ETFWriter(this), name |
|
|
|
generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null); |
|
|
|
generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) |
|
|
|
generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), obj |
|
|
|
generator.Emit(OpCodes.Ldfld, f); //ETFWriter(this), obj.fieldValue |
|
|
|
EmitWriteValue(generator, f.FieldType, f.FieldType.GetTypeInfo(), false); |
|
|
|
}); |
|
|
|
|
|
|
|
typeInfo.ForEachProperty(p => |
|
|
|
{ |
|
|
|
string name; |
|
|
|
if (!p.CanRead || !p.GetMethod.IsPublic || !IsETFProperty(p, out name)) return; |
|
|
|
|
|
|
|
generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) |
|
|
|
generator.Emit(OpCodes.Ldstr, name); //ETFWriter(this), name |
|
|
|
generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null); |
|
|
|
generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) |
|
|
|
generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), obj |
|
|
|
generator.EmitCall(OpCodes.Callvirt, p.GetMethod, null); //ETFWriter(this), obj.propValue |
|
|
|
EmitWriteValue(generator, p.PropertyType, p.PropertyType.GetTypeInfo(), false); |
|
|
|
}); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
//While we could drill deeper and make a large serializer that also serializes all subclasses, |
|
|
|
//it's more efficient to serialize on a per-type basis via another Write<T> call. |
|
|
|
generator.EmitCall(OpCodes.Call, _writeTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
//Unsupported (decimal, char) |
|
|
|
else |
|
|
|
throw new InvalidOperationException($"Serializing {type.Name} is not supported."); |
|
|
|
} |
|
|
|
|
|
|
|
private bool IsType(Type type, params Type[] types) |
|
|
|
private static bool IsType(Type type, params Type[] types) |
|
|
|
{ |
|
|
|
for (int i = 0; i < types.Length; i++) |
|
|
|
{ |
|
|
@@ -441,17 +403,7 @@ namespace Discord.ETF |
|
|
|
} |
|
|
|
return false; |
|
|
|
} |
|
|
|
private MethodInfo GetWriteMethod(Type paramType) |
|
|
|
{ |
|
|
|
return typeof(ETFWriter).GetTypeInfo().GetDeclaredMethods(nameof(Write)) |
|
|
|
.Where(x => x.GetParameters()[0].ParameterType == paramType) |
|
|
|
.FirstOrDefault(); |
|
|
|
} |
|
|
|
|
|
|
|
private void WriteNil() => _stream.Write(_nilBytes, 0, _nilBytes.Length); |
|
|
|
private void WriteNilExt() => _stream.Write(_nilExtBytes, 0, _nilExtBytes.Length); |
|
|
|
|
|
|
|
private bool IsETFProperty(FieldInfo f, out string name) |
|
|
|
private static bool IsETFProperty(FieldInfo f, out string name) |
|
|
|
{ |
|
|
|
var attrib = f.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault(); |
|
|
|
if (attrib != null) |
|
|
@@ -462,7 +414,7 @@ namespace Discord.ETF |
|
|
|
name = null; |
|
|
|
return false; |
|
|
|
} |
|
|
|
private bool IsETFProperty(PropertyInfo p, out string name) |
|
|
|
private static bool IsETFProperty(PropertyInfo p, out string name) |
|
|
|
{ |
|
|
|
var attrib = p.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault(); |
|
|
|
if (attrib != null) |
|
|
@@ -474,6 +426,36 @@ namespace Discord.ETF |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
private static MethodInfo GetWriteMethod(Type paramType) |
|
|
|
{ |
|
|
|
return typeof(ETFWriter).GetTypeInfo().GetDeclaredMethods(nameof(Write)) |
|
|
|
.Where(x => x.GetParameters()[0].ParameterType == paramType) |
|
|
|
.FirstOrDefault(); |
|
|
|
} |
|
|
|
private static MethodInfo GetGenericWriteMethod(Type genericType) |
|
|
|
{ |
|
|
|
if (genericType == null) |
|
|
|
{ |
|
|
|
return typeof(ETFWriter).GetTypeInfo() |
|
|
|
.GetDeclaredMethods(nameof(Write)) |
|
|
|
.Where(x => x.IsGenericMethodDefinition && x.GetParameters()[0].ParameterType == x.GetGenericArguments()[0]) |
|
|
|
.Single(); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
return typeof(ETFWriter).GetTypeInfo() |
|
|
|
.GetDeclaredMethods(nameof(Write)) |
|
|
|
.Where(x => |
|
|
|
{ |
|
|
|
if (!x.IsGenericMethodDefinition) return false; |
|
|
|
var p = x.GetParameters()[0].ParameterType.GetTypeInfo(); |
|
|
|
return p.IsGenericType && p.GetGenericTypeDefinition() == genericType; |
|
|
|
}) |
|
|
|
.Single(); |
|
|
|
} |
|
|
|
} |
|
|
|
#endregion |
|
|
|
|
|
|
|
#region IDisposable |
|
|
|
private bool _isDisposed = false; |
|
|
|
|
|
|
|