From 4cf5452c514acdf77d944dad9c97bad0506d8b4d Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 13 May 2015 16:39:00 -0700 Subject: [PATCH] Initial checkin of Notification service This implementation uses Reflection.Emit to generate proxies for duck-typing of event data on net45 and dnxcore50. For now, the code that does a 'splat' of the top-level event data is still using reflection instead of code-generation. The next update will address this and add more test coverage for this part of the code. --- .gitignore | 1 + EventNotification.sln | 36 ++ global.json | 3 + .../INotifier.cs | 14 + .../INotifierParameterAdapter.cs | 12 + .../Internal/CacheResult.cs | 46 +++ .../Internal/Converter.cs | 376 ++++++++++++++++++ .../Internal/ConverterCache.cs | 16 + .../Internal/ProxyBase.cs | 27 ++ .../Internal/ProxyBaseOfT.cs | 39 ++ .../Microsoft.Framework.Notification.xproj | 28 ++ .../NotificationNameAttribute.cs | 17 + .../Notifier.cs | 109 +++++ .../NotifierParameterAdapter.cs | 28 ++ .../Properties/Resources.Designer.cs | 94 +++++ .../Resources.resx | 132 ++++++ .../project.json | 57 +++ ...icrosoft.Framework.Notification.Test.xproj | 24 ++ .../NotifierParameterAdapterTest.cs | 291 ++++++++++++++ .../NotifierTest.cs | 206 ++++++++++ .../project.json | 14 + 21 files changed, 1570 insertions(+) create mode 100644 EventNotification.sln create mode 100644 global.json create mode 100644 src/Microsoft.Framework.Notification/INotifier.cs create mode 100644 src/Microsoft.Framework.Notification/INotifierParameterAdapter.cs create mode 100644 src/Microsoft.Framework.Notification/Internal/CacheResult.cs create mode 100644 src/Microsoft.Framework.Notification/Internal/Converter.cs create mode 100644 src/Microsoft.Framework.Notification/Internal/ConverterCache.cs create mode 100644 src/Microsoft.Framework.Notification/Internal/ProxyBase.cs create mode 100644 src/Microsoft.Framework.Notification/Internal/ProxyBaseOfT.cs create mode 100644 src/Microsoft.Framework.Notification/Microsoft.Framework.Notification.xproj create mode 100644 src/Microsoft.Framework.Notification/NotificationNameAttribute.cs create mode 100644 src/Microsoft.Framework.Notification/Notifier.cs create mode 100644 src/Microsoft.Framework.Notification/NotifierParameterAdapter.cs create mode 100644 src/Microsoft.Framework.Notification/Properties/Resources.Designer.cs create mode 100644 src/Microsoft.Framework.Notification/Resources.resx create mode 100644 src/Microsoft.Framework.Notification/project.json create mode 100644 test/Microsoft.Framework.Notification.Test/Microsoft.Framework.Notification.Test.xproj create mode 100644 test/Microsoft.Framework.Notification.Test/NotifierParameterAdapterTest.cs create mode 100644 test/Microsoft.Framework.Notification.Test/NotifierTest.cs create mode 100644 test/Microsoft.Framework.Notification.Test/project.json diff --git a/.gitignore b/.gitignore index 216e8d9..f2cf5e8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ _ReSharper.*/ packages/ artifacts/ PublishProfiles/ +project.lock.json *.user *.suo *.cache diff --git a/EventNotification.sln b/EventNotification.sln new file mode 100644 index 0000000..43f57a6 --- /dev/null +++ b/EventNotification.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.22808.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8B66E199-1AFE-4B68-AC71-4521C46EC4CD}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{D75011A4-DEEE-48DE-BB83-CE042F2AC05B}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Framework.Notification", "src\Microsoft.Framework.Notification\Microsoft.Framework.Notification.xproj", "{4C660D0B-32C5-43D0-899D-73FF60352172}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Framework.Notification.Test", "test\Microsoft.Framework.Notification.Test\Microsoft.Framework.Notification.Test.xproj", "{51E95E1C-FE88-4DAF-8E03-3FC3153A03AF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4C660D0B-32C5-43D0-899D-73FF60352172}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C660D0B-32C5-43D0-899D-73FF60352172}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C660D0B-32C5-43D0-899D-73FF60352172}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C660D0B-32C5-43D0-899D-73FF60352172}.Release|Any CPU.Build.0 = Release|Any CPU + {51E95E1C-FE88-4DAF-8E03-3FC3153A03AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {51E95E1C-FE88-4DAF-8E03-3FC3153A03AF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {51E95E1C-FE88-4DAF-8E03-3FC3153A03AF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {51E95E1C-FE88-4DAF-8E03-3FC3153A03AF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {4C660D0B-32C5-43D0-899D-73FF60352172} = {8B66E199-1AFE-4B68-AC71-4521C46EC4CD} + {51E95E1C-FE88-4DAF-8E03-3FC3153A03AF} = {D75011A4-DEEE-48DE-BB83-CE042F2AC05B} + EndGlobalSection +EndGlobal diff --git a/global.json b/global.json new file mode 100644 index 0000000..983ba04 --- /dev/null +++ b/global.json @@ -0,0 +1,3 @@ +{ + "projects": ["src"] +} diff --git a/src/Microsoft.Framework.Notification/INotifier.cs b/src/Microsoft.Framework.Notification/INotifier.cs new file mode 100644 index 0000000..527d281 --- /dev/null +++ b/src/Microsoft.Framework.Notification/INotifier.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.Framework.Notification +{ + public interface INotifier + { + void EnlistTarget(object target); + + bool ShouldNotify(string notificationName); + + void Notify(string notificationName, object parameters); + } +} diff --git a/src/Microsoft.Framework.Notification/INotifierParameterAdapter.cs b/src/Microsoft.Framework.Notification/INotifierParameterAdapter.cs new file mode 100644 index 0000000..9348e98 --- /dev/null +++ b/src/Microsoft.Framework.Notification/INotifierParameterAdapter.cs @@ -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. + +using System; + +namespace Microsoft.Framework.Notification +{ + public interface INotifyParameterAdapter + { + object Adapt(object inputParameter, Type outputType); + } +} diff --git a/src/Microsoft.Framework.Notification/Internal/CacheResult.cs b/src/Microsoft.Framework.Notification/Internal/CacheResult.cs new file mode 100644 index 0000000..5a9d2f2 --- /dev/null +++ b/src/Microsoft.Framework.Notification/Internal/CacheResult.cs @@ -0,0 +1,46 @@ +// 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 NET45 || DNX451 || DNXCORE50 + +using System; +using System.Reflection.Emit; + +namespace Microsoft.Framework.Notification.Internal +{ + public class CacheResult + { + public static CacheResult FromError(Tuple key, string error) + { + return new CacheResult() + { + Key = key, + Error = error, + }; + } + + public static CacheResult FromTypeBuilder( + Tuple key, + TypeBuilder typeBuilder, + ConstructorBuilder constructorBuilder) + { + return new CacheResult() + { + Key = key, + TypeBuilder = typeBuilder, + ConstructorBuilder = constructorBuilder, + }; + } + + public ConstructorBuilder ConstructorBuilder { get; private set; } + + public string Error { get; private set; } + + public bool IsError => Error != null; + + public Tuple Key { get; private set; } + + public TypeBuilder TypeBuilder { get; private set; } + } +} +#endif diff --git a/src/Microsoft.Framework.Notification/Internal/Converter.cs b/src/Microsoft.Framework.Notification/Internal/Converter.cs new file mode 100644 index 0000000..61aa23b --- /dev/null +++ b/src/Microsoft.Framework.Notification/Internal/Converter.cs @@ -0,0 +1,376 @@ +// 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 NET45 || DNX451 || DNXCORE50 + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace Microsoft.Framework.Notification.Internal +{ + public static class Converter + { + private static int _counter = 0; + + private static AssemblyBuilder AssemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("ProxyHolderAssembly"), AssemblyBuilderAccess.Run); + private static ModuleBuilder ModuleBuilder = AssemblyBuilder.DefineDynamicModule("Main Module"); + + public static object Convert(ConverterCache cache, Type outputType, Type inputType, object input) + { + if (input == null) + { + return null; + } + + if (inputType == outputType) + { + return input; + } + + if (outputType.IsAssignableFrom(inputType)) + { + return input; + } + + // If we get to this point, all of the trivial conversions have been tried. We don't attempt value + // conversions such as int -> double. The only thing left is proxy generation, which requires that + // the destination type be an interface. + // + // We should always end up with a proxy type or an exception + var proxyType = GetProxyType(cache, outputType, inputType); + Debug.Assert(proxyType != null); + + return Activator.CreateInstance(proxyType, input); + } + + private static Type GetProxyType(ConverterCache cache, Type tout, Type tin) + { + var key = new Tuple(tin, tout); + + CacheResult result; + if (!cache.TryGetValue(key, out result)) + { + var context = new ProxyBuilderContext(cache, tout, tin); + + // Check that all required types are proxy-able - this will create the TypeBuilder, Constructor, + // and property mappings. + // + // We need to create the TypeBuilder and Constructor up front to deal with cycles that can occur + // when generating the proxy properties. + if (!VerifyProxySupport(context, context.Key)) + { + var error = cache[key]; + Debug.Assert(error != null && error.IsError); + throw new InvalidOperationException(error.Error); + } + + Debug.Assert(context.Visited.ContainsKey(context.Key)); + + // Now that we've generated all of the constructors for the proxies, we can generate the rest + // of the type. + foreach (var verificationResult in context.Visited) + { + AddProperties( + context, + verificationResult.Value.TypeBuilder, + verificationResult.Value.Mappings); + + + verificationResult.Value.TypeBuilder.CreateTypeInfo().AsType(); + } + + // We only want to publish the results after all of the proxies are totally generated. + foreach (var verificationResult in context.Visited) + { + cache[verificationResult.Key] = CacheResult.FromTypeBuilder( + verificationResult.Key, + verificationResult.Value.TypeBuilder, + verificationResult.Value.ConstructorBuilder); + } + + return context.Visited[context.Key].TypeBuilder.CreateTypeInfo().AsType(); + } + else if (result.IsError) + { + throw new InvalidOperationException(result.Error); + } + else if (result.TypeBuilder == null) + { + // This is an identity convertion + return null; + } + else + { + return result.TypeBuilder.CreateTypeInfo().AsType(); + } + } + + private static bool VerifyProxySupport(ProxyBuilderContext context, Tuple key) + { + var sourceType = key.Item1; + var targetType = key.Item2; + + if (context.Visited.ContainsKey(key)) + { + // We've already seen this combination and so far so good. + return true; + } + + CacheResult cacheResult; + if (context.Cache.TryGetValue(key, out cacheResult)) + { + // If we get here we've got a published conversion or error, so we can stop searching. + return !cacheResult.IsError; + } + + if (targetType == sourceType || targetType.IsAssignableFrom(sourceType)) + { + // If we find a trivial conversion, then that will work. + return true; + } + + if (!targetType.GetTypeInfo().IsInterface) + { + var message = Resources.FormatConverter_TypeMustBeInterface(targetType.FullName, sourceType.FullName); + context.Cache[key] = CacheResult.FromError(key, message); + + return false; + } + + // This is a combination we haven't seen before, and it *might* support proxy generation, so let's + // start trying. + var verificationResult = new VerificationResult(); + context.Visited.Add(key, verificationResult); + + var propertyMappings = new List>(); + + var sourceProperties = sourceType.GetRuntimeProperties(); + foreach (var targetProperty in targetType.GetRuntimeProperties()) + { + if (!targetProperty.CanRead) + { + var message = Resources.FormatConverter_PropertyMustHaveGetter( + targetProperty.Name, + targetType.FullName); + context.Cache[key] = CacheResult.FromError(key, message); + + return false; + } + + if (targetProperty.CanWrite) + { + var message = Resources.FormatConverter_PropertyMustNotHaveSetter( + targetProperty.Name, + targetType.FullName); + context.Cache[key] = CacheResult.FromError(key, message); + + return false; + } + + if (targetProperty.GetIndexParameters()?.Length > 0) + { + var message = Resources.FormatConverter_PropertyMustNotHaveIndexParameters( + targetProperty.Name, + targetType.FullName); + context.Cache[key] = CacheResult.FromError(key, message); + + return false; + } + + // To allow for flexible versioning, we want to allow missing properties in the source. + // + // For now we'll just store null, and later generate a stub getter that returns default(T). + var sourceProperty = sourceProperties.Where(p => p.Name == targetProperty.Name).FirstOrDefault(); + if (sourceProperty != null) + { + var propertyKey = new Tuple(sourceProperty.PropertyType, targetProperty.PropertyType); + if (!VerifyProxySupport(context, propertyKey)) + { + // There's an error here, so bubble it up and cache it. + var error = context.Cache[propertyKey]; + Debug.Assert(error != null && error.IsError); + + context.Cache[key] = CacheResult.FromError(key, error.Error); + return false; + } + } + + propertyMappings.Add(new KeyValuePair(targetProperty, sourceProperty)); + } + + verificationResult.Mappings = propertyMappings; + + var baseType = typeof(ProxyBase<>).MakeGenericType(sourceType); + var typeBuilder = ModuleBuilder.DefineType( + "ProxyType" + _counter++ + " wrapping:" + sourceType.Name + " to look like:" + targetType.Name, + TypeAttributes.Class, + baseType, + new Type[] { targetType }); + + var constructorBuilder = typeBuilder.DefineConstructor( + MethodAttributes.Public, + CallingConventions.Standard, + new Type[] { sourceType }); + + var il = constructorBuilder.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Castclass, sourceType); + il.Emit(OpCodes.Call, baseType.GetConstructor(new Type[] { sourceType })); + il.Emit(OpCodes.Ret); + + verificationResult.ConstructorBuilder = constructorBuilder; + verificationResult.TypeBuilder = typeBuilder; + + return true; + } + + private static void AddProperties( + ProxyBuilderContext context, + TypeBuilder typeBuilder, + IEnumerable> properties) + { + foreach (var property in properties) + { + var targetProperty = property.Key; + var sourceProperty = property.Value; + + var propertyBuilder = typeBuilder.DefineProperty( + targetProperty.Name, + PropertyAttributes.None, + property.Key.PropertyType, + Type.EmptyTypes); + + var methodBuilder = typeBuilder.DefineMethod( + targetProperty.GetMethod.Name, + targetProperty.GetMethod.Attributes & ~MethodAttributes.Abstract, + targetProperty.GetMethod.CallingConvention, + targetProperty.GetMethod.ReturnType, + Type.EmptyTypes); + propertyBuilder.SetGetMethod(methodBuilder); + typeBuilder.DefineMethodOverride(methodBuilder, targetProperty.GetMethod); + + var il = methodBuilder.GetILGenerator(); + if (sourceProperty == null) + { + // Return a default(T) value. + il.Emit(OpCodes.Initobj, targetProperty.PropertyType); + il.Emit(OpCodes.Ret); + } + else + { + // Push 'this' and get the underlying instance. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, + typeBuilder.BaseType.GetField( + "Instance", + BindingFlags.Instance | BindingFlags.Public)); + + // Call the source property. + il.EmitCall(OpCodes.Callvirt, sourceProperty.GetMethod, null); + + // Create a proxy for the value returned by source property (if necessary). + EmitProxy(context, il, targetProperty.PropertyType, sourceProperty.PropertyType); + il.Emit(OpCodes.Ret); + } + } + } + + private static void EmitProxy(ProxyBuilderContext context, ILGenerator il, Type targetType, Type sourceType) + { + if (sourceType == targetType) + { + // Do nothing. + return; + } + else if (targetType.IsAssignableFrom(sourceType)) + { + il.Emit(OpCodes.Castclass, targetType); + return; + } + + // If we get here, then we actually need a proxy. + var key = new Tuple(sourceType, targetType); + + ConstructorBuilder constructorBuilder = null; + CacheResult cacheResult; + VerificationResult verificationResult; + if (context.Cache.TryGetValue(key, out cacheResult)) + { + Debug.Assert(!cacheResult.IsError); + Debug.Assert(cacheResult.ConstructorBuilder != null); + + // This means we've got a fully-built (published) type. + constructorBuilder = cacheResult.ConstructorBuilder; + } + else if (context.Visited.TryGetValue(key, out verificationResult)) + { + Debug.Assert(verificationResult.ConstructorBuilder != null); + constructorBuilder = verificationResult.ConstructorBuilder; + } + + Debug.Assert(constructorBuilder != null); + + var endLabel = il.DefineLabel(); + var createProxyLabel = il.DefineLabel(); + + // If the 'source' value is null, then just return it. + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brfalse_S, endLabel); + + // If the 'source' value isn't a proxy then we need to create one. + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Isinst, typeof(ProxyBase)); + il.Emit(OpCodes.Brfalse_S, createProxyLabel); + + // If the 'source' value is-a proxy then get the wrapped value. + il.Emit(OpCodes.Isinst, typeof(ProxyBase)); + il.EmitCall(OpCodes.Callvirt, typeof(ProxyBase).GetMethod("get_UnderlyingInstanceAsObject"), null); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Isinst, targetType); + il.Emit(OpCodes.Brtrue_S, endLabel); + + il.MarkLabel(createProxyLabel); + + // Create the proxy. + il.Emit(OpCodes.Newobj, constructorBuilder); + + il.MarkLabel(endLabel); + } + + private class ProxyBuilderContext + { + public ProxyBuilderContext(ConverterCache cache, Type targetType, Type sourceType) + { + Cache = cache; + + Key = new Tuple(sourceType, targetType); + Visited = new Dictionary, VerificationResult>(); + } + + public ConverterCache Cache { get; } + + public Tuple Key { get; } + + public Type SourceType => Key.Item1; + + public Type TargetType => Key.Item2; + + public Dictionary, VerificationResult> Visited { get; } + } + + private class VerificationResult + { + public ConstructorBuilder ConstructorBuilder { get; set; } + + public IEnumerable> Mappings { get; set; } + + public TypeBuilder TypeBuilder { get; set; } + } + } +} +#endif diff --git a/src/Microsoft.Framework.Notification/Internal/ConverterCache.cs b/src/Microsoft.Framework.Notification/Internal/ConverterCache.cs new file mode 100644 index 0000000..2a68858 --- /dev/null +++ b/src/Microsoft.Framework.Notification/Internal/ConverterCache.cs @@ -0,0 +1,16 @@ +// 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 NET45 || DNX451 || DNXCORE50 + +using System; +using System.Collections.Concurrent; + +namespace Microsoft.Framework.Notification.Internal +{ + public class ConverterCache : ConcurrentDictionary, CacheResult> + { + } +} + +#endif diff --git a/src/Microsoft.Framework.Notification/Internal/ProxyBase.cs b/src/Microsoft.Framework.Notification/Internal/ProxyBase.cs new file mode 100644 index 0000000..6ef9265 --- /dev/null +++ b/src/Microsoft.Framework.Notification/Internal/ProxyBase.cs @@ -0,0 +1,27 @@ +// 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 NET45 || DNX451 || DNXCORE50 + +using System; + +namespace Microsoft.Framework.Notification.Internal +{ + public abstract class ProxyBase + { + public readonly Type WrappedType; + + protected ProxyBase(Type wrappedType) + { + WrappedType = wrappedType; + } + + // Used by reflection, don't rename. + public abstract object UnderlyingInstanceAsObject + { + get; + } + } +} + +#endif diff --git a/src/Microsoft.Framework.Notification/Internal/ProxyBaseOfT.cs b/src/Microsoft.Framework.Notification/Internal/ProxyBaseOfT.cs new file mode 100644 index 0000000..44032bb --- /dev/null +++ b/src/Microsoft.Framework.Notification/Internal/ProxyBaseOfT.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#if NET45 || DNX451 || DNXCORE50 + +using Microsoft.Framework.Internal; + +namespace Microsoft.Framework.Notification.Internal +{ + public class ProxyBase : ProxyBase where T : class + { + // Used by reflection, don't rename. + public readonly T Instance; + + public ProxyBase([NotNull] T instance) + : base(typeof(T)) + { + Instance = instance; + } + + public T UnderlyingInstance + { + get + { + return Instance; + } + } + + public override object UnderlyingInstanceAsObject + { + get + { + return Instance; + } + } + } +} + +#endif diff --git a/src/Microsoft.Framework.Notification/Microsoft.Framework.Notification.xproj b/src/Microsoft.Framework.Notification/Microsoft.Framework.Notification.xproj new file mode 100644 index 0000000..2f11b85 --- /dev/null +++ b/src/Microsoft.Framework.Notification/Microsoft.Framework.Notification.xproj @@ -0,0 +1,28 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 4c660d0b-32c5-43d0-899d-73ff60352172 + + + + + + + Microsoft.Framework.Notification + + + Microsoft.Framework.Notification + + + 2.0 + + + True + + + \ No newline at end of file diff --git a/src/Microsoft.Framework.Notification/NotificationNameAttribute.cs b/src/Microsoft.Framework.Notification/NotificationNameAttribute.cs new file mode 100644 index 0000000..aeacef0 --- /dev/null +++ b/src/Microsoft.Framework.Notification/NotificationNameAttribute.cs @@ -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.Framework.Notification +{ + public class NotificationNameAttribute : Attribute + { + public NotificationNameAttribute(string name) + { + Name = name; + } + + public string Name { get; } + } +} diff --git a/src/Microsoft.Framework.Notification/Notifier.cs b/src/Microsoft.Framework.Notification/Notifier.cs new file mode 100644 index 0000000..63f350b --- /dev/null +++ b/src/Microsoft.Framework.Notification/Notifier.cs @@ -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.Concurrent; +using System.Collections.Generic; +using System.Reflection; + +namespace Microsoft.Framework.Notification +{ + public class Notifier : INotifier + { + private readonly ConcurrentDictionary> _notificationNames = new ConcurrentDictionary>(StringComparer.Ordinal); + private readonly INotifyParameterAdapter _parameterAdapter; + + public Notifier(INotifyParameterAdapter parameterAdapter) + { + _parameterAdapter = parameterAdapter; + } + + public void EnlistTarget(object target) + { + var typeInfo = target.GetType().GetTypeInfo(); + + var methodInfos = typeInfo.DeclaredMethods; + + foreach (var methodInfo in methodInfos) + { + var notificationNameAttribute = methodInfo.GetCustomAttribute(); + if (notificationNameAttribute != null) + { + Enlist(notificationNameAttribute.Name, target, methodInfo); + } + } + } + + private void Enlist(string notificationName, object target, MethodInfo methodInfo) + { + var entries = _notificationNames.GetOrAdd( + notificationName, + _ => new List()); + entries.Add(new Entry(target, methodInfo)); + } + + public bool ShouldNotify(string notificationName) + { + return _notificationNames.ContainsKey(notificationName); + } + + public void Notify(string notificationName, object parameters) + { + List entries; + if (_notificationNames.TryGetValue(notificationName, out entries)) + { + foreach (var entry in entries) + { + entry.Send(parameters, _parameterAdapter); + } + } + } + + internal class Entry + { + private MethodInfo _methodInfo; + private object _target; + + public Entry(object target, MethodInfo methodInfo) + { + _target = target; + _methodInfo = methodInfo; + } + + internal void Send(object parameters, INotifyParameterAdapter parameterAdapter) + { + var methodParameterInfos = _methodInfo.GetParameters(); + var methodParameterCount = methodParameterInfos.Length; + var methodParameterValues = new object[methodParameterCount]; + + var objectTypeInfo = parameters.GetType().GetTypeInfo(); + for (var index = 0; index != methodParameterCount; ++index) + { + var objectPropertyInfo = objectTypeInfo.GetDeclaredProperty(methodParameterInfos[index].Name); + if (objectPropertyInfo == null) + { + continue; + } + + var objectPropertyValue = objectPropertyInfo.GetValue(parameters); + if (objectPropertyValue == null) + { + continue; + } + + var methodParameterInfo = methodParameterInfos[index]; + if (methodParameterInfo.ParameterType.GetTypeInfo().IsAssignableFrom(objectPropertyInfo.PropertyType.GetTypeInfo())) + { + methodParameterValues[index] = objectPropertyValue; + } + else + { + methodParameterValues[index] = parameterAdapter.Adapt(objectPropertyValue, methodParameterInfo.ParameterType); + } + } + + _methodInfo.Invoke(_target, methodParameterValues); + } + } + } +} diff --git a/src/Microsoft.Framework.Notification/NotifierParameterAdapter.cs b/src/Microsoft.Framework.Notification/NotifierParameterAdapter.cs new file mode 100644 index 0000000..6fe495f --- /dev/null +++ b/src/Microsoft.Framework.Notification/NotifierParameterAdapter.cs @@ -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.Framework.Notification +{ + public class NotifierParameterAdapter : INotifyParameterAdapter + { +#if NET45 || DNX451 || DNXCORE50 + private readonly Internal.ConverterCache _cache = new Internal.ConverterCache(); +#endif + + public object Adapt(object inputParameter, Type outputType) + { + if (inputParameter == null) + { + return null; + } + +#if NET45 || DNX451 || DNXCORE50 + return Internal.Converter.Convert(_cache, outputType, inputParameter.GetType(), inputParameter); +#else + return inputParameter; +#endif + } + } +} diff --git a/src/Microsoft.Framework.Notification/Properties/Resources.Designer.cs b/src/Microsoft.Framework.Notification/Properties/Resources.Designer.cs new file mode 100644 index 0000000..8d92385 --- /dev/null +++ b/src/Microsoft.Framework.Notification/Properties/Resources.Designer.cs @@ -0,0 +1,94 @@ +// +namespace Microsoft.Framework.Notification +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.Framework.Notification.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The property '{0}' on type '{1}' must define a getter to support proxy generation. + /// + internal static string Converter_PropertyMustHaveGetter + { + get { return GetString("Converter_PropertyMustHaveGetter"); } + } + + /// + /// The property '{0}' on type '{1}' must define a getter to support proxy generation. + /// + internal static string FormatConverter_PropertyMustHaveGetter(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Converter_PropertyMustHaveGetter"), p0, p1); + } + + /// + /// The property '{0}' on type '{1}' must not use index parameters to support proxy generation. + /// + internal static string Converter_PropertyMustNotHaveIndexParameters + { + get { return GetString("Converter_PropertyMustNotHaveIndexParameters"); } + } + + /// + /// The property '{0}' on type '{1}' must not use index parameters to support proxy generation. + /// + internal static string FormatConverter_PropertyMustNotHaveIndexParameters(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Converter_PropertyMustNotHaveIndexParameters"), p0, p1); + } + + /// + /// The property '{0}' on type '{1}' must not define a setter to support proxy generation. + /// + internal static string Converter_PropertyMustNotHaveSetter + { + get { return GetString("Converter_PropertyMustNotHaveSetter"); } + } + + /// + /// The property '{0}' on type '{1}' must not define a setter to support proxy generation. + /// + internal static string FormatConverter_PropertyMustNotHaveSetter(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Converter_PropertyMustNotHaveSetter"), p0, p1); + } + + /// + /// Type '{0}' must be an interface in order to support proxy generation from source type '{1}'. + /// + internal static string Converter_TypeMustBeInterface + { + get { return GetString("Converter_TypeMustBeInterface"); } + } + + /// + /// Type '{0}' must be an interface in order to support proxy generation from source type '{1}'. + /// + internal static string FormatConverter_TypeMustBeInterface(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Converter_TypeMustBeInterface"), p0, p1); + } + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Microsoft.Framework.Notification/Resources.resx b/src/Microsoft.Framework.Notification/Resources.resx new file mode 100644 index 0000000..869a272 --- /dev/null +++ b/src/Microsoft.Framework.Notification/Resources.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The property '{0}' on type '{1}' must define a getter to support proxy generation. + + + The property '{0}' on type '{1}' must not use index parameters to support proxy generation. + + + The property '{0}' on type '{1}' must not define a setter to support proxy generation. + + + Type '{0}' must be an interface in order to support proxy generation from source type '{1}'. + + \ No newline at end of file diff --git a/src/Microsoft.Framework.Notification/project.json b/src/Microsoft.Framework.Notification/project.json new file mode 100644 index 0000000..7916750 --- /dev/null +++ b/src/Microsoft.Framework.Notification/project.json @@ -0,0 +1,57 @@ +{ + "version": "1.0.0-*", + "description": "Logging infrastructure.", + "dependencies": { + "Microsoft.Framework.DependencyInjection.Interfaces": "1.0.0-*", + "Microsoft.Framework.NotNullAttribute.Internal": { "version": "1.0.0-*", "type" : "build" }, + "System.Runtime": "4.0.20-beta-*" + }, + "compilationOptions": { + "define": [ + "TRACE" + ] + }, + "frameworks": { + "net45": { + "frameworkAssemblies": { + "System.Linq": "", + "System.Collections.Concurrent": "" + } + }, + "dnx451": { + "frameworkAssemblies": { + "System.Linq": "", + "System.Collections.Concurrent": "" + } + }, + "dnxcore50": { + "dependencies": { + "System.Collections.Concurrent": "4.0.10-beta-*", + "System.Collections": "4.0.10-beta-*", + "System.Diagnostics.Debug": "4.0.10-beta-*", + "System.Globalization": "4.0.10-beta-*", + "System.Linq": "4.0.0-beta-*", + "System.Threading": "4.0.10-beta-*", + "System.Reflection.Emit": "4.0.0-beta-*", + "System.Reflection.Extensions": "4.0.0-beta-*", + "System.Reflection.TypeExtensions": "4.0.0-beta-*", + "System.Resources.ResourceManager": "4.0.0-beta-*", + "System.Runtime.Extensions": "4.0.10-beta-*" + } + }, + ".NETPortable,Version=v4.6,Profile=Profile151": { + "frameworkAssemblies": { + "System.Collections": "", + "System.Collections.Concurrent": "", + "System.Diagnostics.Debug": "", + "System.Globalization": "", + "System.Reflection": "", + "System.Reflection.Extensions": "", + "System.Resources.ResourceManager": "", + "System.Runtime.Extensions": "", + "System.Linq": "", + "System.Threading": "" + } + } + } +} diff --git a/test/Microsoft.Framework.Notification.Test/Microsoft.Framework.Notification.Test.xproj b/test/Microsoft.Framework.Notification.Test/Microsoft.Framework.Notification.Test.xproj new file mode 100644 index 0000000..cb51804 --- /dev/null +++ b/test/Microsoft.Framework.Notification.Test/Microsoft.Framework.Notification.Test.xproj @@ -0,0 +1,24 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 51e95e1c-fe88-4daf-8e03-3fc3153a03af + Microsoft.Framework.Notification.Test + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + Microsoft.Framework.Notification.Test + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Microsoft.Framework.Notification.Test/NotifierParameterAdapterTest.cs b/test/Microsoft.Framework.Notification.Test/NotifierParameterAdapterTest.cs new file mode 100644 index 0000000..6b097e5 --- /dev/null +++ b/test/Microsoft.Framework.Notification.Test/NotifierParameterAdapterTest.cs @@ -0,0 +1,291 @@ +// 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 Xunit; + +namespace Microsoft.Framework.Notification +{ + public class NotifierParameterAdapterTest + { + [Fact] + public void Adapt_Null() + { + // Arrange + var value = (object)null; + + var adapter = new NotifierParameterAdapter(); + + // Act + var result = adapter.Adapt(value, typeof(string)); + + // Assert + Assert.Null(result); + } + + public static TheoryData Identity_ReferenceTypes_Data + { + get + { + return new TheoryData() + { + { "Hello, world!", typeof(string) }, + { new Person(), typeof(Person) }, + }; + } + } + + [Theory] + [MemberData(nameof(Identity_ReferenceTypes_Data))] + public void Adapt_Identity_ReferenceTypes(object value, Type outputType) + { + // Arrange + var adapter = new NotifierParameterAdapter(); + + // Act + var result = adapter.Adapt(value, outputType); + + // Assert + Assert.IsType(outputType, result); + Assert.Same(value, result); + } + + public static TheoryData Identity_ValueTypes_Data + { + get + { + return new TheoryData() + { + { 19, typeof(int) }, + { new SomeValueType(17), typeof(SomeValueType) }, + }; + } + } + + [Theory] + [MemberData(nameof(Identity_ValueTypes_Data))] + public void Adapt_Identity_ValueTypes(object value, Type outputType) + { + // Arrange + var adapter = new NotifierParameterAdapter(); + + // Act + var result = adapter.Adapt(value, outputType); + + // Assert + Assert.IsType(outputType, result); + Assert.Same(value, result); // This works because of boxing + } + + public static TheoryData Assignable_Data + { + get + { + return new TheoryData() + { + { 5, typeof(IConvertible) }, // Interface assignment + { new DerivedPerson(), typeof(Person) }, // Base-class assignment + { 5.8m, typeof(decimal?) }, // value-type to nullable assignment + }; + } + } + + [Theory] + [MemberData(nameof(Assignable_Data))] + public void Adapt_Assignable(object value, Type outputType) + { + // Arrange + var adapter = new NotifierParameterAdapter(); + + // Act + var result = adapter.Adapt(value, outputType); + + // Assert + Assert.IsType(value.GetType(), result); + Assert.IsAssignableFrom(outputType, result); + Assert.Same(value, result); + } + + [Fact] + public void Adapt_Proxy_DestinationIsNotInterface() + { + // Arrange + var value = new Person(); + var outputType = typeof(string); + + var expectedMessage = string.Format( + "Type '{0}' must be an interface in order to support proxy generation from source type '{1}'.", + outputType.FullName, + value.GetType().FullName); + + var adapter = new NotifierParameterAdapter(); + + // Act + var exception = Assert.Throws(() => adapter.Adapt(value, outputType)); + + // Assert + Assert.Equal(expectedMessage, exception.Message); + } + + [Fact] + public void Adapt_Proxy_InvalidProperty_DestinationIsNotInterface() + { + // Arrange + var value = new Person(); + var outputType = typeof(IBadPerson); + + var expectedMessage = string.Format( + "Type '{0}' must be an interface in order to support proxy generation from source type '{1}'.", + typeof(string), + typeof(Address).FullName); + + var adapter = new NotifierParameterAdapter(); + + // Act + var exception = Assert.Throws(() => adapter.Adapt(value, outputType)); + + // Assert + Assert.Equal(expectedMessage, exception.Message); + } + + [Fact] + public void Adapt_Proxy() + { + // Arrange + var value = new Person() + { + Address = new Address() + { + City = "Redmond", + State = "WA", + Zip = 98002, + }, + FirstName = "Bill", + LastName = "Gates", + }; + + var outputType = typeof(IPerson); + + var adapter = new NotifierParameterAdapter(); + + // Act + var result = adapter.Adapt(value, outputType); + + // Assert + var person = Assert.IsAssignableFrom(result); + Assert.Same(value.Address.City, person.Address.City); + Assert.Same(value.Address.State, person.Address.State); + Assert.Equal(value.Address.Zip, person.Address.Zip); + + // IPerson doesn't define the FirstName property. + Assert.Same(value.LastName, person.LastName); + } + + [Fact] + public void Adapt_Proxy_WithTypeCycle() + { + // Arrange + var value = new C1() + { + C2 = new C2() + { + C1 = new C1() + { + C2 = new C2(), + Tag = "C1.C2.C1", + }, + Tag = "C1.C2", + }, + Tag = "C1", + }; + + var outputType = typeof(IC1); + + var adapter = new NotifierParameterAdapter(); + + // Act + var result = adapter.Adapt(value, outputType); + + // Assert + var c1 = Assert.IsAssignableFrom(result); + Assert.Equal(value.C2.Tag, c1.C2.Tag); + Assert.Equal(value.C2.C1.Tag, c1.C2.C1.Tag); + Assert.Equal(value.C2.C1.C2.Tag, c1.C2.C1.C2.Tag); + Assert.Null(value.C2.C1.C2.C1); + } + + public interface IC1 + { + IC2 C2 { get; } + string Tag { get; } + } + + public interface IC2 + { + IC1 C1 { get; } + string Tag { get; } + } + + public class C1 + { + public C2 C2 { get; set; } + public string Tag { get; set; } + } + + public class C2 + { + public C1 C1 { get; set; } + public string Tag { get; set; } + } + + public interface IPerson + { + string FirstName { get; } + string LastName { get; } + IAddress Address { get; } + } + + public interface IAddress + { + string City { get; } + string State { get; } + int Zip { get; } + } + + public class Person + { + public string FirstName { get; set; } + public string LastName { get; set; } + public Address Address { get; set; } + } + + public interface IBadPerson + { + string FirstName { get; } + string LastName { get; } + string Address { get; } // doesn't match with Person + } + + public class DerivedPerson : Person + { + public double CoolnessFactor { get; set; } + } + + public class Address + { + public string City { get; set; } + public string State { get; set; } + public int Zip { get; set; } + } + + public class SomeValueType + { + public SomeValueType(int value) + { + Value = value; + } + + public int Value { get; private set; } + } + } +} diff --git a/test/Microsoft.Framework.Notification.Test/NotifierTest.cs b/test/Microsoft.Framework.Notification.Test/NotifierTest.cs new file mode 100644 index 0000000..d239782 --- /dev/null +++ b/test/Microsoft.Framework.Notification.Test/NotifierTest.cs @@ -0,0 +1,206 @@ +// 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.Framework.Notification +{ + public class NotifierTest + { + INotifier NewNotifier() + { + return new Notifier(new NotifierParameterAdapter()); + } + + public class OneTarget + { + public int OneCallCount { get; private set; } + + [NotificationName("One")] + public void One() + { + ++OneCallCount; + } + } + + [Fact] + public void ShouldNotifyBecomesTrueAfterEnlisting() + { + var notifier = NewNotifier(); + + Assert.False(notifier.ShouldNotify("One")); + Assert.False(notifier.ShouldNotify("Two")); + + notifier.EnlistTarget(new OneTarget()); + + Assert.True(notifier.ShouldNotify("One")); + Assert.False(notifier.ShouldNotify("Two")); + } + + [Fact] + public void CallingNotifyWillInvokeMethod() + { + var notifier = NewNotifier(); + var target = new OneTarget(); + + notifier.EnlistTarget(target); + + Assert.Equal(0, target.OneCallCount); + notifier.Notify("One", new { }); + Assert.Equal(1, target.OneCallCount); + } + + [Fact] + public void CallingNotifyForNonEnlistedNameIsHarmless() + { + var notifier = new Notifier(new NotifierParameterAdapter()); + var target = new OneTarget(); + + notifier.EnlistTarget(target); + + Assert.Equal(0, target.OneCallCount); + notifier.Notify("Two", new { }); + Assert.Equal(0, target.OneCallCount); + } + + private class TwoTarget + { + public string Alpha { get; private set; } + public string Beta { get; private set; } + public int Delta { get; private set; } + + [NotificationName("Two")] + public void Two(string alpha, string beta, int delta) + { + Alpha = alpha; + Beta = beta; + Delta = delta; + } + } + + [Fact] + public void ParametersWillSplatFromObjectByName() + { + var notifier = NewNotifier(); + var target = new TwoTarget(); + + notifier.EnlistTarget(target); + + notifier.Notify("Two", new { alpha = "ALPHA", beta = "BETA", delta = -1 }); + + Assert.Equal("ALPHA", target.Alpha); + Assert.Equal("BETA", target.Beta); + Assert.Equal(-1, target.Delta); + } + + [Fact] + public void ExtraParametersAreHarmless() + { + var notifier = NewNotifier(); + var target = new TwoTarget(); + + notifier.EnlistTarget(target); + + notifier.Notify("Two", new { alpha = "ALPHA", beta = "BETA", delta = -1, extra = this }); + + Assert.Equal("ALPHA", target.Alpha); + Assert.Equal("BETA", target.Beta); + Assert.Equal(-1, target.Delta); + } + + [Fact] + public void MissingParametersArriveAsNull() + { + var notifier = NewNotifier(); + var target = new TwoTarget(); + + notifier.EnlistTarget(target); + notifier.Notify("Two", new { alpha = "ALPHA", delta = -1 }); + + Assert.Equal("ALPHA", target.Alpha); + Assert.Null(target.Beta); + Assert.Equal(-1, target.Delta); + } + + [Fact] + public void NotificationCanDuckType() + { + var notifier = NewNotifier(); + var target = new ThreeTarget(); + + notifier.EnlistTarget(target); + notifier.Notify("Three", new + { + person = new Person + { + FirstName = "Alpha", + Address = new Address + { + City = "Beta", + State = "Gamma", + Zip = 98028 + } + } + }); + + Assert.Equal("Alpha", target.Person.FirstName); + Assert.Equal("Beta", target.Person.Address.City); + Assert.Equal("Gamma", target.Person.Address.State); + Assert.Equal(98028, target.Person.Address.Zip); + } + + public class ThreeTarget + { + public IPerson Person { get; private set; } + + [NotificationName("Three")] + public void Three(IPerson person) + { + Person = person; + } + } + + public interface IPerson + { + string FirstName { get; } + string LastName { get; } + IAddress Address { get; } + } + + public interface IAddress + { + string City { get; } + string State { get; } + int Zip { get; } + } + + public class Person + { + public string FirstName { get; set; } + public string LastName { get; set; } + public Address Address { get; set; } + } + + public class DerivedPerson : Person + { + public double CoolnessFactor { get; set; } + } + + public class Address + { + public string City { get; set; } + public string State { get; set; } + public int Zip { get; set; } + } + + public class SomeValueType + { + public SomeValueType(int value) + { + Value = value; + } + + public int Value { get; private set; } + } + } +} diff --git a/test/Microsoft.Framework.Notification.Test/project.json b/test/Microsoft.Framework.Notification.Test/project.json new file mode 100644 index 0000000..524b624 --- /dev/null +++ b/test/Microsoft.Framework.Notification.Test/project.json @@ -0,0 +1,14 @@ +{ + "dependencies": { + "Microsoft.Framework.Notification": "1.0.0-*", + "xunit.runner.aspnet": "2.0.0-aspnet-*" + }, + "commands": { + "run": "xunit.runner.aspnet", + "test": "xunit.runner.aspnet" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { } + } +}