diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/AttributeCloner.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/AttributeCloner.cs index 071fe0d21..7299ecf72 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Bindings/AttributeCloner.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/AttributeCloner.cs @@ -6,10 +6,11 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; +using Microsoft.Azure.WebJobs.Description; using Microsoft.Azure.WebJobs.Host.Bindings.Path; namespace Microsoft.Azure.WebJobs.Host.Bindings -{ +{ using BindingData = IReadOnlyDictionary; using BindingDataContract = IReadOnlyDictionary; // Func to transform Attribute,BindingData into value for cloned attribute property/constructor arg @@ -34,20 +35,15 @@ internal class AttributeCloner // Compute the values to apply to Settable properties on newly created attribute. private readonly Action[] _propertySetters; - // Optional hook for post-processing the attribute. This is intended for legacy hack rules. - private readonly Func> _hook; - private readonly Dictionary _autoResolves = new Dictionary(); private static readonly BindingFlags Flags = BindingFlags.Instance | BindingFlags.Public; - public AttributeCloner( + internal AttributeCloner( TAttribute source, BindingDataContract bindingDataContract, - INameResolver nameResolver = null, - Func> hook = null) + INameResolver nameResolver = null) { - _hook = hook; nameResolver = nameResolver ?? new EmptyNameResolver(); _source = source; @@ -95,38 +91,58 @@ private BindingDataResolver GetResolver(PropertyInfo propInfo, INameResolver nam object originalValue = propInfo.GetValue(_source); AppSettingAttribute appSettingAttr = propInfo.GetCustomAttribute(); AutoResolveAttribute autoResolveAttr = propInfo.GetCustomAttribute(); - + + if (appSettingAttr == null && autoResolveAttr == null) + { + // No special attributes, treat as literal. + return (newAttr, bindingData) => originalValue; + } + if (appSettingAttr != null && autoResolveAttr != null) { throw new InvalidOperationException($"Property '{propInfo.Name}' cannot be annotated with both AppSetting and AutoResolve."); } + // attributes only work on string properties. + if (propInfo.PropertyType != typeof(string)) + { + throw new InvalidOperationException($"AutoResolve or AppSetting property '{propInfo.Name}' must be of type string."); + } + var str = (string)originalValue; + // first try to resolve with app setting if (appSettingAttr != null) { - return GetAppSettingResolver((string)originalValue, appSettingAttr, nameResolver, propInfo); + return GetAppSettingResolver(str, appSettingAttr, nameResolver, propInfo); } + + // Must have an [AutoResolve] // try to resolve with auto resolve ({...}, %...%) - if (autoResolveAttr != null && originalValue != null) + return GetAutoResolveResolver(str, autoResolveAttr, nameResolver, propInfo, contract); + } + + // Apply AutoResolve attribute + internal BindingDataResolver GetAutoResolveResolver(string originalValue, AutoResolveAttribute autoResolveAttr, INameResolver nameResolver, PropertyInfo propInfo, BindingDataContract contract) + { + if (string.IsNullOrWhiteSpace(originalValue)) + { + if (autoResolveAttr.Default != null) + { + return GetBuiltinTemplateResolver(autoResolveAttr.Default, nameResolver); + } + else + { + return (newAttr, bindingData) => originalValue; + } + } + else { _autoResolves[propInfo] = autoResolveAttr; - return GetTemplateResolver((string)originalValue, autoResolveAttr, nameResolver, propInfo, contract); + return GetTemplateResolver(originalValue, autoResolveAttr, nameResolver, propInfo, contract); } - // resolve the original value - return (newAttr, bindingData) => originalValue; - } - - // AutoResolve - internal static BindingDataResolver GetTemplateResolver(string originalValue, AutoResolveAttribute attr, INameResolver nameResolver, PropertyInfo propInfo, BindingDataContract contract) - { - string resolvedValue = nameResolver.ResolveWholeString(originalValue); - var template = BindingTemplate.FromString(resolvedValue); - IResolutionPolicy policy = GetPolicy(attr.ResolutionPolicyType, propInfo); - template.ValidateContractCompatibility(contract); - return (newAttr, bindingData) => TemplateBind(policy, propInfo, newAttr, template, bindingData); } - // AppSetting + // Apply AppSetting attribute. internal static BindingDataResolver GetAppSettingResolver(string originalValue, AppSettingAttribute attr, INameResolver nameResolver, PropertyInfo propInfo) { string appSettingName = originalValue ?? attr.Default; @@ -145,6 +161,30 @@ internal static BindingDataResolver GetAppSettingResolver(string originalValue, return (newAttr, bindingData) => resolvedValue; } + // Resolve for AutoResolve.Default templates. + // These only have access to the {sys} builtin variable and don't get access to trigger binding data. + internal static BindingDataResolver GetBuiltinTemplateResolver(string originalValue, INameResolver nameResolver) + { + string resolvedValue = nameResolver.ResolveWholeString(originalValue); + + var template = BindingTemplate.FromString(resolvedValue); + + SystemBindingData.ValidateStaticContract(template); + + // For static default contracts, we only have access to the built in binding data. + return (newAttr, bindingData) => template.Bind(SystemBindingData.GetSystemBindingData(bindingData)); + } + + // AutoResolve + internal static BindingDataResolver GetTemplateResolver(string originalValue, AutoResolveAttribute attr, INameResolver nameResolver, PropertyInfo propInfo, BindingDataContract contract) + { + string resolvedValue = nameResolver.ResolveWholeString(originalValue); + var template = BindingTemplate.FromString(resolvedValue); + IResolutionPolicy policy = GetPolicy(attr.ResolutionPolicyType, propInfo); + template.ValidateContractCompatibility(contract); + return (newAttr, bindingData) => TemplateBind(policy, propInfo, newAttr, template, bindingData); + } + // Get a attribute with %% resolved, but not runtime {} resolved. public TAttribute GetNameResolvedAttribute() { @@ -169,7 +209,7 @@ public string GetInvokeString(TAttribute attributeResolved) return invokeString; } - public async Task ResolveFromInvokeStringAsync(string invokeString) + public TAttribute ResolveFromInvokeString(string invokeString) { TAttribute attr; var resolver = _source as IAttributeInvokeDescriptor; @@ -181,20 +221,12 @@ public async Task ResolveFromInvokeStringAsync(string invokeString) { attr = resolver.FromInvokeString(invokeString); } - if (_hook != null) - { - attr = await _hook(attr); - } return attr; } - public async Task ResolveFromBindingDataAsync(BindingContext ctx) + public TAttribute ResolveFromBindingData(BindingContext ctx) { var attr = ResolveFromBindings(ctx.BindingData); - if (_hook != null) - { - attr = await _hook(attr); - } return attr; } @@ -242,7 +274,7 @@ internal TAttribute New(IDictionary overrideProperties) return newAttr; } - internal TAttribute ResolveFromBindings(IReadOnlyDictionary bindingData) + internal TAttribute ResolveFromBindings(BindingData bindingData) { // Invoke ctor var ctorArgs = Array.ConvertAll(_ctorParamResolvers, func => func(_source, bindingData)); @@ -256,7 +288,7 @@ internal TAttribute ResolveFromBindings(IReadOnlyDictionary bind return newAttr; } - private static string TemplateBind(IResolutionPolicy policy, PropertyInfo prop, Attribute attr, BindingTemplate template, IReadOnlyDictionary bindingData) + private static string TemplateBind(IResolutionPolicy policy, PropertyInfo prop, Attribute attr, BindingTemplate template, BindingData bindingData) { if (bindingData == null) { diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingBase.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingBase.cs index 9ed0c1017..de1888e73 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingBase.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingBase.cs @@ -46,7 +46,7 @@ public bool FromAttribute public async Task BindAsync(BindingContext context) { - var attrResolved = await Cloner.ResolveFromBindingDataAsync(context); + var attrResolved = Cloner.ResolveFromBindingData(context); return await BuildAsync(attrResolved, context.ValueContext); } @@ -57,7 +57,7 @@ public async Task BindAsync(object value, ValueBindingContext co { // Called when we invoke from dashboard. // str --> attribute --> obj - var resolvedAttr = await Cloner.ResolveFromInvokeStringAsync(str); + var resolvedAttr = Cloner.ResolveFromInvokeString(str); return await BuildAsync(resolvedAttr, context); } else diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingProviders/AsyncCollectorBindingProvider.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingProviders/AsyncCollectorBindingProvider.cs index 4cbab5d74..39bae87d5 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingProviders/AsyncCollectorBindingProvider.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingProviders/AsyncCollectorBindingProvider.cs @@ -256,13 +256,7 @@ public static ExactBinding TryBuild( var parameter = context.Parameter; var attributeSource = TypeUtility.GetResolvedAttribute(parameter); - - Func> hookWrapper = null; - if (parent.PostResolveHook != null) - { - hookWrapper = (attrResolved) => parent.PostResolveHook(attrResolved, parameter, parent._nameResolver); - } - + Func buildFromAttribute; FuncConverter converter = null; @@ -316,7 +310,7 @@ public static ExactBinding TryBuild( }; } - var cloner = new AttributeCloner(attributeSource, context.BindingDataContract, parent._nameResolver, hookWrapper); + var cloner = new AttributeCloner(attributeSource, context.BindingDataContract, parent._nameResolver); return new ExactBinding(cloner, param, mode, buildFromAttribute, converter); } diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingProviders/BindToInputBindingProvider.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingProviders/BindToInputBindingProvider.cs index 606b01e93..0a983c5c1 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingProviders/BindToInputBindingProvider.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingProviders/BindToInputBindingProvider.cs @@ -141,13 +141,7 @@ public static ExactBinding TryBuild( var parameter = context.Parameter; var attributeSource = TypeUtility.GetResolvedAttribute(parameter); - Func> hookWrapper = null; - if (parent.PostResolveHook != null) - { - hookWrapper = (attrResolved) => parent.PostResolveHook(attrResolved, parameter, parent._nameResolver); - } - - var cloner = new AttributeCloner(attributeSource, context.BindingDataContract, parent._nameResolver, hookWrapper); + var cloner = new AttributeCloner(attributeSource, context.BindingDataContract, parent._nameResolver); Func buildFromAttribute; FuncConverter converter = null; diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingProviders/FluentBindingProvider.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingProviders/FluentBindingProvider.cs index 7a213aec5..4e7249a18 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingProviders/FluentBindingProvider.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingProviders/FluentBindingProvider.cs @@ -14,6 +14,5 @@ namespace Microsoft.Azure.WebJobs.Host internal class FluentBindingProvider { protected internal Func BuildParameterDescriptor { get; set; } - protected internal Func> PostResolveHook { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingProviders/FluidBindingProviderExtensions.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingProviders/FluentBindingProviderExtensions.cs similarity index 77% rename from src/Microsoft.Azure.WebJobs.Host/Bindings/BindingProviders/FluidBindingProviderExtensions.cs rename to src/Microsoft.Azure.WebJobs.Host/Bindings/BindingProviders/FluentBindingProviderExtensions.cs index 2f607a2a0..1d9dea5f9 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingProviders/FluidBindingProviderExtensions.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingProviders/FluentBindingProviderExtensions.cs @@ -11,16 +11,14 @@ namespace Microsoft.Azure.WebJobs.Host { // Internal Extension methods for setting backwards compatibility hooks on certain bindings. // This keeps the hooks out of the public surface. - internal static class FluidBindingProviderExtensions + internal static class FluentBindingProviderExtensions { public static IBindingProvider SetPostResolveHook( this IBindingProvider binder, - Func buildParameterDescriptor = null, - Func> postResolveHook = null) + Func buildParameterDescriptor = null) { var fluidBinder = (FluentBindingProvider)binder; - - fluidBinder.PostResolveHook = postResolveHook; + fluidBinder.BuildParameterDescriptor = buildParameterDescriptor; return binder; } diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingTemplateExtensions.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingTemplateExtensions.cs index 554644b36..94df26c1f 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingTemplateExtensions.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingTemplateExtensions.cs @@ -63,6 +63,10 @@ private static void ValidateContractCompatibility(IEnumerable parameterN { foreach (string parameterName in parameterNames) { + if (string.Equals(parameterName, SystemBindingData.Name, StringComparison.OrdinalIgnoreCase)) + { + continue; + } if (BindingParameterResolver.IsSystemParameter(parameterName)) { continue; diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/ConverterManager.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/ConverterManager.cs index 724886e46..69100743d 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Bindings/ConverterManager.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/ConverterManager.cs @@ -530,6 +530,9 @@ public ExactMatch(Type type) { _type = type; } + + internal Type ExactType => _type; + public override bool IsMatch(Type type) { return type == _type; diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/FunctionBinding.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/FunctionBinding.cs index 3add9f76f..b1cfa6828 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Bindings/FunctionBinding.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/FunctionBinding.cs @@ -31,6 +31,10 @@ internal static BindingContext NewBindingContext( // if bindingData was a mutable dictionary, we could just add it. // But since it's read-only, must create a new one. Dictionary bindingData = new Dictionary(StringComparer.OrdinalIgnoreCase); + + var funcContext = context.FunctionContext; + var methodName = funcContext.MethodName; + if (existingBindingData != null) { foreach (var kv in existingBindingData) @@ -45,7 +49,14 @@ internal static BindingContext NewBindingContext( bindingData[kv.Key] = kv.Value; } } - + + // Add 'sys' binding data. + var sysBindingData = new SystemBindingData + { + MethodName = methodName + }; + sysBindingData.AddToBindingData(bindingData); + BindingContext bindingContext = new BindingContext(context, bindingData); return bindingContext; } diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/FunctionBindingContext.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/FunctionBindingContext.cs index 5b1ff5cfc..16bb4bab0 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Bindings/FunctionBindingContext.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/FunctionBindingContext.cs @@ -3,6 +3,7 @@ using System; using System.Threading; +using Microsoft.Azure.WebJobs.Host.Protocols; namespace Microsoft.Azure.WebJobs.Host.Bindings { @@ -22,11 +23,18 @@ public class FunctionBindingContext /// The instance ID of the function being bound to. /// The to use. /// The trace writer. - public FunctionBindingContext(Guid functionInstanceId, CancellationToken functionCancellationToken, TraceWriter trace) + /// Current function being executed. + public FunctionBindingContext( + Guid functionInstanceId, + CancellationToken functionCancellationToken, + TraceWriter trace, + FunctionDescriptor functionDescriptor = null) { _functionInstanceId = functionInstanceId; _functionCancellationToken = functionCancellationToken; _trace = trace; + + this.MethodName = functionDescriptor?.Method?.Name; } /// @@ -52,5 +60,10 @@ public TraceWriter Trace { get { return _trace; } } + + /// + /// The short name of the current function. + /// + public string MethodName { get; private set; } } } diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/SystemBindingData.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/SystemBindingData.cs new file mode 100644 index 000000000..21be188e4 --- /dev/null +++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/SystemBindingData.cs @@ -0,0 +1,85 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.Azure.WebJobs.Host.Bindings.Path; + +namespace Microsoft.Azure.WebJobs.Host.Bindings +{ + /// + /// Class providing support for built in system binding expressions + /// + /// + /// It's expected this class is created and added to the binding data. + /// + internal class SystemBindingData + { + // The public name for this binding in the binding expressions. + public const string Name = "sys"; + + // An internal name for this binding that uses characters that gaurantee it can't be overwritten by a user. + // This ensures that we can always unambiguously retrieve this later. + private const string InternalKeyName = "$sys"; + + private static readonly IReadOnlyDictionary DefaultSystemContract = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { Name, typeof(SystemBindingData) } + }; + + /// + /// The method name that the binding lives in. + /// The method name can be override by the + /// + public string MethodName { get; set; } + + // Given a full bindingData, create a binding data with just the system object . + // This can be used when resolving default contracts that shouldn't be using an instance binding data. + internal static IReadOnlyDictionary GetSystemBindingData(IReadOnlyDictionary bindingData) + { + var data = GetFromData(bindingData); + var systemBindingData = new Dictionary + { + { Name, data } + }; + return systemBindingData; + } + + // Validate that a template only uses static (non-instance) binding variables. + // Enforces we're not referring to other data from the trigger. + internal static void ValidateStaticContract(BindingTemplate template) + { + try + { + template.ValidateContractCompatibility(SystemBindingData.DefaultSystemContract); + } + catch (InvalidOperationException e) + { + throw new InvalidOperationException($"Default contract can only refer to the '{SystemBindingData.Name}' binding data: " + e.Message); + } + } + + internal void AddToBindingData(Dictionary bindingData) + { + // User data takes precedence, so if 'sys' already exists, add via the internal name. + string sysName = bindingData.ContainsKey(SystemBindingData.Name) ? SystemBindingData.InternalKeyName : SystemBindingData.Name; + bindingData[sysName] = this; + } + + // Given per-instance binding data, extract just the system binding data object from it. + private static SystemBindingData GetFromData(IReadOnlyDictionary bindingData) + { + object val; + if (bindingData.TryGetValue(InternalKeyName, out val)) + { + return val as SystemBindingData; + } + if (bindingData.TryGetValue(Name, out val)) + { + return val as SystemBindingData; + } + return null; + } + } +} diff --git a/src/Microsoft.Azure.WebJobs.Host/Executors/FunctionExecutor.cs b/src/Microsoft.Azure.WebJobs.Host/Executors/FunctionExecutor.cs index 6b5fc55d5..9d8cb1f46 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Executors/FunctionExecutor.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Executors/FunctionExecutor.cs @@ -233,7 +233,11 @@ private async Task ExecuteWithLoggingAsync(IFunctionInstance instance, F TraceWriter traceWriter = new CompositeTraceWriter(functionTraceWriter, functionOutputTextWriter, functionTraceLevel); // Must bind before logging (bound invoke string is included in log message). - FunctionBindingContext functionContext = new FunctionBindingContext(instance.Id, functionCancellationTokenSource.Token, traceWriter); + FunctionBindingContext functionContext = new FunctionBindingContext( + instance.Id, + functionCancellationTokenSource.Token, + traceWriter, + instance.FunctionDescriptor); var valueBindingContext = new ValueBindingContext(functionContext, cancellationToken); var parameters = await instance.BindingSource.BindAsync(valueBindingContext); diff --git a/src/Microsoft.Azure.WebJobs.Host/Extensions/JobHostMetadataProvider.cs b/src/Microsoft.Azure.WebJobs.Host/Extensions/JobHostMetadataProvider.cs index 73911fa7f..2fcee7bee 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Extensions/JobHostMetadataProvider.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Extensions/JobHostMetadataProvider.cs @@ -40,6 +40,8 @@ internal void Init(IBindingProvider root) { converter.AddAssemblies((type) => this.AddAssembly(type)); } + + AddTypesFromGraph(root as IRuleProvider); } // Resolve an assembly from the given name. @@ -257,6 +259,18 @@ internal static void DumpRule(IRuleProvider root, TextWriter output) output.Write(rule.UserType.GetDisplayName()); output.WriteLine(); } - } + } + + private void AddTypesFromGraph(IRuleProvider root) + { + foreach (var rule in root.GetRules()) + { + var type = rule.UserType as ConverterManager.ExactMatch; + if (type != null) + { + AddAssembly(type.ExactType); + } + } + } } } \ No newline at end of file diff --git a/src/Microsoft.Azure.WebJobs.Host/GlobalSuppressions.cs b/src/Microsoft.Azure.WebJobs.Host/GlobalSuppressions.cs index fe6b1b396..fd53e8e62 100644 --- a/src/Microsoft.Azure.WebJobs.Host/GlobalSuppressions.cs +++ b/src/Microsoft.Azure.WebJobs.Host/GlobalSuppressions.cs @@ -70,7 +70,12 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.JobHostQueuesConfiguration.#MaxPollingIntervalInt")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "AddBindingRule", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Config.ExtensionConfigContext.#AddBindingRule`1()")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Loggers.FunctionResultAggregatorFactory.#Create(System.Int32,System.TimeSpan,Microsoft.Extensions.Logging.ILoggerFactory)")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Logging.FunctionResultAggregatorFactory.#Create(System.Int32,System.TimeSpan,Microsoft.Extensions.Logging.ILoggerFactory)")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Logging.FunctionResultAggregatorFactory.#Create(System.Int32,System.TimeSpan,Microsoft.Extensions.Logging.ILoggerFactory)")][assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "AddBindingRule", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Config.ExtensionConfigContext.#AddBindingRule`1()")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "2", Scope = "member", Target = "Microsoft.Azure.WebJobs.DefaultResolutionPolicy.#TemplateBind(System.Reflection.PropertyInfo,System.Attribute,Microsoft.Azure.WebJobs.Host.Bindings.Path.BindingTemplate,System.Collections.Generic.IReadOnlyDictionary`2)")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.SysBindingData.#NewGuid")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.SysBindingData.#UtcNow")][assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Logging.FunctionResultAggregatorFactory.#Create(System.Int32,System.TimeSpan,Microsoft.Extensions.Logging.ILoggerFactory)")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "AddBindingRule", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Config.ExtensionConfigContext.#AddBindingRule`1()")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "2", Scope = "member", Target = "Microsoft.Azure.WebJobs.DefaultResolutionPolicy.#TemplateBind(System.Reflection.PropertyInfo,System.Attribute,Microsoft.Azure.WebJobs.Host.Bindings.Path.BindingTemplate,System.Collections.Generic.IReadOnlyDictionary`2)")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.PropertyHelper.#.ctor(System.Reflection.PropertyInfo)")] \ No newline at end of file +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.PropertyHelper.#.ctor(System.Reflection.PropertyInfo)")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.SysBindingData.#GetFromData(System.Collections.Generic.IReadOnlyDictionary`2)")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.SystemBindingData.#GetFromData(System.Collections.Generic.IReadOnlyDictionary`2)")] \ No newline at end of file diff --git a/src/Microsoft.Azure.WebJobs.Host/WebJobs.Host.csproj b/src/Microsoft.Azure.WebJobs.Host/WebJobs.Host.csproj index c114daf0d..9f211e626 100644 --- a/src/Microsoft.Azure.WebJobs.Host/WebJobs.Host.csproj +++ b/src/Microsoft.Azure.WebJobs.Host/WebJobs.Host.csproj @@ -463,10 +463,11 @@ - + + diff --git a/src/Microsoft.Azure.WebJobs/AutoResolveAttribute.cs b/src/Microsoft.Azure.WebJobs/AutoResolveAttribute.cs index dc4465d3e..eed203086 100644 --- a/src/Microsoft.Azure.WebJobs/AutoResolveAttribute.cs +++ b/src/Microsoft.Azure.WebJobs/AutoResolveAttribute.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System; +using Microsoft.Azure.WebJobs.Description; namespace Microsoft.Azure.WebJobs { @@ -25,5 +26,11 @@ public AutoResolveAttribute() /// in the Microsoft.Azure.WebJobs.Host assembly. /// public Type ResolutionPolicyType { get; set; } + + /// + /// A default value if the property is empty. + /// The default value only has access to the {sys} binding data. + /// + public string Default { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.Azure.WebJobs/GlobalSuppressions.cs b/src/Microsoft.Azure.WebJobs/GlobalSuppressions.cs index 47b59df75..fc8eb9114 100644 --- a/src/Microsoft.Azure.WebJobs/GlobalSuppressions.cs +++ b/src/Microsoft.Azure.WebJobs/GlobalSuppressions.cs @@ -4,4 +4,5 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1019:DefineAccessorsForAttributeArguments", Scope = "type", Target = "Microsoft.Azure.WebJobs.TimeoutAttribute")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Scope = "type", Target = "Microsoft.Azure.WebJobs.TableAttribute")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "AppSetting", Scope = "type", Target = "Microsoft.Azure.WebJobs.AppSettingAttribute")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "AutoResolve", Scope = "type", Target = "Microsoft.Azure.WebJobs.AutoResolveAttribute")] \ No newline at end of file +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "AutoResolve", Scope = "type", Target = "Microsoft.Azure.WebJobs.AutoResolveAttribute")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "AutoResolve", Scope = "type", Target = "Microsoft.Azure.WebJobs.Description.AutoResolveValue")] \ No newline at end of file diff --git a/test/Microsoft.Azure.WebJobs.Host.UnitTests/AttributeClonerTests.cs b/test/Microsoft.Azure.WebJobs.Host.UnitTests/AttributeClonerTests.cs index d2e83dfd5..9c157e008 100644 --- a/test/Microsoft.Azure.WebJobs.Host.UnitTests/AttributeClonerTests.cs +++ b/test/Microsoft.Azure.WebJobs.Host.UnitTests/AttributeClonerTests.cs @@ -11,6 +11,7 @@ using Microsoft.Azure.WebJobs.Host.Bindings.Path; using Microsoft.Azure.WebJobs.Host.TestCommon; using Xunit; +using Microsoft.Azure.WebJobs.Description; namespace Microsoft.Azure.WebJobs.Host.UnitTests { @@ -65,6 +66,22 @@ public class Attr4: Attribute public string AutoResolve { get; set; } } + // Test with DefaultValue.MemberName + public class Attr5 : Attribute + { + [AutoResolve(Default = "{sys.MethodName}")] + public string AutoResolve { get; set; } + } + + + // Test with DefaultValue.MemberName + public class BadDefaultAttr : Attribute + { + // Default can't access instance binding data (x). + [AutoResolve(Default = "{sys.MethodName}-{x}")] + public string AutoResolve { get; set; } + } + public class InvalidAnnotation: Attribute { // only one of appsetting/autoresolve allowed @@ -73,6 +90,13 @@ public class InvalidAnnotation: Attribute public string Required { get; set; } } + public class InvalidNonStringAutoResolve : Attribute + { + // AutoResolve must be string + [AutoResolve] + public bool Required { get; set; } + } + public class AttributeWithResolutionPolicy : Attribute { [AutoResolve(ResolutionPolicyType = typeof(TestResolutionPolicy))] @@ -127,6 +151,16 @@ private static IReadOnlyDictionary GetBindingContract(params strin return d; } + private static IReadOnlyDictionary GetBindingContract(Dictionary values) + { + var d = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var kv in values) + { + d[kv.Key] = typeof(string); + } + return d; + } + private static IReadOnlyDictionary EmptyContract = new Dictionary(); // Enforce binding contracts statically. @@ -149,7 +183,7 @@ public void BindingContractMismatch() // Test on an attribute that does NOT implement IAttributeInvokeDescriptor // Key parameter is a property (not ctor) [Fact] - public async Task InvokeString() + public void InvokeString() { Attr1 a1 = new Attr1 { Path = "%test%" }; @@ -159,7 +193,7 @@ public async Task InvokeString() nameResolver._dict["test"] = "ABC"; var cloner = new AttributeCloner(a1, EmptyContract, nameResolver); - Attr1 attr2 = await cloner.ResolveFromInvokeStringAsync("xy"); + Attr1 attr2 = cloner.ResolveFromInvokeString("xy"); Assert.Equal("xy", attr2.Path); } @@ -167,7 +201,7 @@ public async Task InvokeString() // Test on an attribute that does NOT implement IAttributeInvokeDescriptor // Key parameter is on the ctor [Fact] - public async Task InvokeStringBlobAttribute() + public void InvokeStringBlobAttribute() { foreach (var attr in new BlobAttribute[] { new BlobAttribute("container/{name}"), @@ -176,7 +210,7 @@ public async Task InvokeStringBlobAttribute() }) { var cloner = new AttributeCloner(attr, GetBindingContract("name")); - BlobAttribute attr2 = await cloner.ResolveFromInvokeStringAsync("c/n"); + BlobAttribute attr2 = cloner.ResolveFromInvokeString("c/n"); Assert.Equal("c/n", attr2.BlobPath); Assert.Equal(attr.Access, attr2.Access); @@ -186,7 +220,7 @@ public async Task InvokeStringBlobAttribute() // Test on an attribute that does NOT implement IAttributeInvokeDescriptor // Multiple resolved properties. [Fact] - public async Task InvokeStringMultipleResolvedProperties() + public void InvokeStringMultipleResolvedProperties() { Attr2 attr = new Attr2("{p2}", "constant") { ResolvedProp1 = "{p1}" @@ -201,7 +235,7 @@ public async Task InvokeStringMultipleResolvedProperties() Assert.Equal(attr.ConstantProp, attrResolved.ConstantProp); var invokeString = cloner.GetInvokeString(attrResolved); - var attr2 = await cloner.ResolveFromInvokeStringAsync(invokeString); + var attr2 = cloner.ResolveFromInvokeString(invokeString); Assert.Equal(attrResolved.ResolvedProp1, attr2.ResolvedProp1); Assert.Equal(attrResolved.ResolvedProp2, attr2.ResolvedProp2); @@ -210,7 +244,7 @@ public async Task InvokeStringMultipleResolvedProperties() // Easy case - default ctor and all settable properties. [Fact] - public async Task NameResolver() + public void NameResolver() { Attr1 a1 = new Attr1 { Path = "x%appsetting%y-{k}" }; @@ -226,14 +260,14 @@ public async Task NameResolver() { "k", "v" } }; var ctx = GetCtx(values); - var attr2 = await cloner.ResolveFromBindingDataAsync(ctx); + var attr2 = cloner.ResolveFromBindingData(ctx); Assert.Equal("xABCy-v", attr2.Path); } // Easy case - default ctor and all settable properties. [Fact] - public async Task Easy() + public void Easy() { Attr1 a1 = new Attr1 { Path = "{request.headers.authorization}-{key2}" }; @@ -250,7 +284,7 @@ public async Task Easy() var ctx = GetCtx(values); var cloner = new AttributeCloner(a1, GetBindingContract("request", "key2")); - var attr2 = await cloner.ResolveFromBindingDataAsync(ctx); + var attr2 = cloner.ResolveFromBindingData(ctx); Assert.Equal("ey123-val2", attr2.Path); } @@ -271,7 +305,7 @@ public void Setting() public void Setting_WithNoValueInResolver_ThrowsIfNoDefault() { Attr2 a2 = new Attr2(string.Empty, string.Empty) { ResolvedSetting = "appsetting" }; - Assert.Throws(() => new AttributeCloner(a2, EmptyContract, null)); + Assert.Throws(() => new AttributeCloner(a2, EmptyContract)); } [Fact] @@ -311,17 +345,17 @@ public void AppSettingAttribute_Resolves_IfDefaultMatched() public void AppSettingAttribute_Throws_IfDefaultUnmatched() { Attr3 a3 = new Attr3() { Required = "req" }; - Assert.Throws(() => new AttributeCloner(a3, EmptyContract, null)); + Assert.Throws(() => new AttributeCloner(a3, EmptyContract)); } [Fact] - public async Task Setting_Null() + public void Setting_Null() { Attr2 a2 = new Attr2(string.Empty, string.Empty); - var cloner = new AttributeCloner(a2, EmptyContract, null); + var cloner = new AttributeCloner(a2, EmptyContract); - Attr2 a2Clone = await cloner.ResolveFromBindingDataAsync(GetCtx(null)); + Attr2 a2Clone = cloner.ResolveFromBindingData(GetCtx(null)); Assert.Null(a2Clone.ResolvedSetting); } @@ -362,12 +396,84 @@ public void AppSettingAttribute_DoesNotThrowIfNullValueAndNoDefault() public void AttributeCloner_Throws_IfAppSettingAndAutoResolve() { InvalidAnnotation a = new InvalidAnnotation(); - var exc = Assert.Throws(() => new AttributeCloner(a, EmptyContract, null)); + var exc = Assert.Throws(() => new AttributeCloner(a, EmptyContract)); Assert.Equal("Property 'Required' cannot be annotated with both AppSetting and AutoResolve.", exc.Message); } [Fact] - public async Task CloneNoDefaultCtor() + public void AttributeCloner_Throws_IfAutoResolveIsNotString() + { + var a = new InvalidNonStringAutoResolve(); + var exc = Assert.Throws(() => new AttributeCloner(a, EmptyContract)); + Assert.Equal("AutoResolve or AppSetting property 'Required' must be of type string.", exc.Message); + } + + // Default to MethodName kicks in if the (pre-resolved) value is null. + [Theory] + [InlineData(null, "MyMethod")] + [InlineData("", "MyMethod")] + [InlineData(" ", "MyMethod")] // whitespace + [InlineData("{empty}", "")] + [InlineData("{value}", "123")] + [InlineData("%empty2%", "")] + [InlineData("%value2%", "456")] + [InlineData("foo-{sys.MethodName}", "foo-MyMethod")] + public void DefaultMethodName(string propValue, string expectedValue) + { + Attr5 attr = new Attr5 { AutoResolve = propValue }; // Pick method name + + var nameResolver = new FakeNameResolver() + .Add("empty2", "") + .Add("value2", "456"); + + Dictionary values = new Dictionary() + { + { "empty", "" }, + { "value", "123" } + }; + new SystemBindingData + { + MethodName = "MyMethod" + }.AddToBindingData(values); + + var ctx = GetCtx(values); + + var cloner = new AttributeCloner(attr, GetBindingContract(values), nameResolver); + + var attr2 = cloner.ResolveFromBindingData(ctx); + Assert.Equal(expectedValue, attr2.AutoResolve); + } + + // Default can't access instance binding data. + [Fact] + public void DefaultCantAccessInstanceData() + { + var attr = new BadDefaultAttr(); // use default value, which is bad. + + Dictionary values = new Dictionary() + { + { "x", "123" }, + { SystemBindingData.Name, new SystemBindingData + { + MethodName = "MyMethod" + } } + }; + var ctx = GetCtx(values); + + try + { + new AttributeCloner(attr, GetBindingContract(values)); + Assert.False(true); + } + catch (InvalidOperationException e) + { + // Verify message. + Assert.True(e.Message.StartsWith("Default contract can only refer to the 'sys' binding data: ")); + } + } + + [Fact] + public void CloneNoDefaultCtor() { var a1 = new BlobAttribute("container/{name}.txt", FileAccess.Write); @@ -378,14 +484,14 @@ public async Task CloneNoDefaultCtor() var ctx = GetCtx(values); var cloner = new AttributeCloner(a1, GetBindingContract("name")); - var attr2 = await cloner.ResolveFromBindingDataAsync(ctx); + var attr2 = cloner.ResolveFromBindingData(ctx); Assert.Equal("container/green.txt", attr2.BlobPath); Assert.Equal(a1.Access, attr2.Access); } [Fact] - public async Task CloneNoDefaultCtorShortList() + public void CloneNoDefaultCtorShortList() { // Use shorter parameter list. var a1 = new BlobAttribute("container/{name}.txt"); @@ -397,7 +503,7 @@ public async Task CloneNoDefaultCtorShortList() var ctx = GetCtx(values); var cloner = new AttributeCloner(a1, GetBindingContract("name")); - var attr2 = await cloner.ResolveFromBindingDataAsync(ctx); + var attr2 = cloner.ResolveFromBindingData(ctx); Assert.Equal("container/green.txt", attr2.BlobPath); Assert.Equal(a1.Access, attr2.Access); diff --git a/test/Microsoft.Azure.WebJobs.Host.UnitTests/Common/BindToGenericItemTests.cs b/test/Microsoft.Azure.WebJobs.Host.UnitTests/Common/BindToGenericItemTests.cs index 934ffbf72..dd9214348 100644 --- a/test/Microsoft.Azure.WebJobs.Host.UnitTests/Common/BindToGenericItemTests.cs +++ b/test/Microsoft.Azure.WebJobs.Host.UnitTests/Common/BindToGenericItemTests.cs @@ -10,6 +10,7 @@ using Microsoft.Azure.WebJobs.Host.Bindings; using System.Threading; using Newtonsoft.Json; +using Microsoft.Azure.WebJobs.Description; namespace Microsoft.Azure.WebJobs.Host.UnitTests.Common { @@ -58,6 +59,46 @@ public void Func([Test("{k}")] AlphaType w) } } + // Simple end-2-end case with attribute that default binds to metho dname + // Test with concrete types, no converters. + // Attr-->Widget + [Fact] + public void TestDefaultToMethodName() + { + TestWorker(); + } + + public class ConfigTestDefaultToMethodName : IExtensionConfigProvider, ITest + { + public void Initialize(ExtensionConfigContext context) + { + var rule = context.AddBindingRule(); + rule.BindToInput(attr => attr.Path); + } + + public void Test(TestJobHost host) + { + host.Call("Func", new { k = 1 }); + Assert.Equal("1", _log); + + host.Call("Func2", new { k = 1 }); + Assert.Equal("Func2", _log); + } + + string _log; + + public void Func([Test2(Path = "{k}")] string w) + { + _log = w; + } + + // Missing path, will default to method name + public void Func2([Test2] string w) + { + _log = w; + } + } + // Use OpenType (a general builder), still no converters. [Fact] public void TestOpenTypeNoConverters() @@ -556,6 +597,14 @@ public TestAttribute(string path) public string Path { get; set; } } + // A test attribute for binding. + [Binding] + public class Test2Attribute : Attribute + { + [AutoResolve(Default = "{sys.methodname}")] + public string Path { get; set; } + } + // Converter for building instances of RedType from an attribute class AlphaBuilder : IConverter { diff --git a/test/Microsoft.Azure.WebJobs.Host.UnitTests/PublicSurfaceTests.cs b/test/Microsoft.Azure.WebJobs.Host.UnitTests/PublicSurfaceTests.cs index a00c693df..d79593773 100644 --- a/test/Microsoft.Azure.WebJobs.Host.UnitTests/PublicSurfaceTests.cs +++ b/test/Microsoft.Azure.WebJobs.Host.UnitTests/PublicSurfaceTests.cs @@ -114,7 +114,7 @@ public void WebJobsPublicSurface_LimitedToSpecificTypes() "AppSettingAttribute", "BinderExtensions", "BlobAttribute", - "BlobTriggerAttribute", + "BlobTriggerAttribute", "IBinder", "IAsyncCollector`1", "ICollector`1",