diff --git a/src/Microsoft.Azure.WebJobs.Host/Indexers/FunctionIndex.cs b/src/Microsoft.Azure.WebJobs.Host/Indexers/FunctionIndex.cs index a363b56e6..cc1175be5 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Indexers/FunctionIndex.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Indexers/FunctionIndex.cs @@ -46,6 +46,20 @@ public IFunctionDefinition Lookup(string functionId) return _functionsById[functionId]; } + public IFunctionDefinition LookupByName(string name) + { + foreach (var items in _functionDescriptors) + { + if (string.Equals(items.ShortName, name, StringComparison.OrdinalIgnoreCase)) + { + var id = items.Id; + return Lookup(id); + } + } + // Not found. + return null; + } + public IFunctionDefinition Lookup(MethodInfo method) { if (!_functionsByMethod.ContainsKey(method)) diff --git a/src/Microsoft.Azure.WebJobs.Host/Indexers/FunctionIndexer.cs b/src/Microsoft.Azure.WebJobs.Host/Indexers/FunctionIndexer.cs index fd660e36e..811a6c54b 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Indexers/FunctionIndexer.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Indexers/FunctionIndexer.cs @@ -333,13 +333,26 @@ internal static FunctionDescriptor FromMethod( TraceLevelAttribute traceAttribute = TypeUtility.GetHierarchicalAttributeOrNull(method); bool hasCancellationToken = method.GetParameters().Any(p => p.ParameterType == typeof(CancellationToken)); - + + string logName = method.Name; + string shortName = method.GetShortName(); + FunctionNameAttribute nameAttribute = method.GetCustomAttribute(); + if (nameAttribute != null) + { + logName = nameAttribute.Name; + shortName = logName; + if (!FunctionNameAttribute.FunctionNameValidationRegex.IsMatch(logName)) + { + throw new InvalidOperationException(string.Format("'{0}' is not a valid function name.", logName)); + } + } + return new FunctionDescriptor { Id = method.GetFullName(), - LogName = method.Name, + LogName = logName, FullName = method.GetFullName(), - ShortName = method.GetShortName(), + ShortName = shortName, IsDisabled = disabled, HasCancellationToken = hasCancellationToken, TraceLevel = traceAttribute?.Level ?? TraceLevel.Verbose, diff --git a/src/Microsoft.Azure.WebJobs.Host/Indexers/IFunctionIndexLookup.cs b/src/Microsoft.Azure.WebJobs.Host/Indexers/IFunctionIndexLookup.cs index d294cf915..8a9b53170 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Indexers/IFunctionIndexLookup.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Indexers/IFunctionIndexLookup.cs @@ -10,5 +10,9 @@ internal interface IFunctionIndexLookup IFunctionDefinition Lookup(string functionId); IFunctionDefinition Lookup(MethodInfo method); + + // This uses the function's short name ("Class.Method"), which can also be overriden + // by the FunctionName attribute. + IFunctionDefinition LookupByName(string name); } } diff --git a/src/Microsoft.Azure.WebJobs.Host/JobHost.cs b/src/Microsoft.Azure.WebJobs.Host/JobHost.cs index e16d22eb7..f8879b1fb 100644 --- a/src/Microsoft.Azure.WebJobs.Host/JobHost.cs +++ b/src/Microsoft.Azure.WebJobs.Host/JobHost.cs @@ -266,11 +266,41 @@ public Task CallAsync(MethodInfo method, IDictionary arguments, return CallAsyncCore(method, arguments, cancellationToken); } + /// Calls a job method. + /// The name of the function to call. + /// The argument names and values to bind to parameters in the job method. In addition to parameter values, these may also include binding data values. + /// The token to monitor for cancellation requests. + /// A that will call the job method. + public async Task CallAsync(string name, IDictionary arguments = null, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + ThrowIfDisposed(); + + await EnsureHostStartedAsync(cancellationToken); + + IFunctionDefinition function = _context.FunctionLookup.LookupByName(name); + Validate(function, name); + IFunctionInstance instance = CreateFunctionInstance(function, arguments); + + IDelayedException exception = await _context.Executor.TryExecuteAsync(instance, cancellationToken); + + if (exception != null) + { + exception.Throw(); + } + } + private async Task CallAsyncCore(MethodInfo method, IDictionary arguments, CancellationToken cancellationToken) { await EnsureHostStartedAsync(cancellationToken); - IFunctionDefinition function = ResolveFunctionDefinition(method, _context.FunctionLookup); + IFunctionDefinition function = _context.FunctionLookup.Lookup(method); + Validate(function, method); IFunctionInstance instance = CreateFunctionInstance(function, arguments); IDelayedException exception = await _context.Executor.TryExecuteAsync(instance, cancellationToken); @@ -331,17 +361,13 @@ private static IFunctionInstance CreateFunctionInstance(IFunctionDefinition func return func.InstanceFactory.Create(context); } - private static IFunctionDefinition ResolveFunctionDefinition(MethodInfo method, IFunctionIndexLookup functionLookup) - { - IFunctionDefinition function = functionLookup.Lookup(method); - + private static void Validate(IFunctionDefinition function, object key) + { if (function == null) { - string msg = String.Format(CultureInfo.CurrentCulture, "'{0}' can't be invoked from Azure WebJobs SDK. Is it missing Azure WebJobs SDK attributes?", method); + string msg = String.Format(CultureInfo.CurrentCulture, "'{0}' can't be invoked from Azure WebJobs SDK. Is it missing Azure WebJobs SDK attributes?", key); throw new InvalidOperationException(msg); } - - return function; } private void ThrowIfDisposed() diff --git a/src/Microsoft.Azure.WebJobs.Protocols/FunctionDescriptor.cs b/src/Microsoft.Azure.WebJobs.Protocols/FunctionDescriptor.cs index 119860930..3c70c9275 100644 --- a/src/Microsoft.Azure.WebJobs.Protocols/FunctionDescriptor.cs +++ b/src/Microsoft.Azure.WebJobs.Protocols/FunctionDescriptor.cs @@ -32,7 +32,7 @@ public class FunctionDescriptor public IEnumerable Parameters { get; set; } #if PUBLICPROTOCOL #else - /// Gets or sets the name used for logging. This is 'Method'. + /// Gets or sets the name used for logging. This is 'Method' or the value overwritten by [FunctionName] [JsonIgnore] internal string LogName { get; set; } diff --git a/src/Microsoft.Azure.WebJobs/FunctionNameAttribute.cs b/src/Microsoft.Azure.WebJobs/FunctionNameAttribute.cs index 95e7e8313..35e89887e 100644 --- a/src/Microsoft.Azure.WebJobs/FunctionNameAttribute.cs +++ b/src/Microsoft.Azure.WebJobs/FunctionNameAttribute.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System; +using System.Text.RegularExpressions; namespace Microsoft.Azure.WebJobs { @@ -26,5 +27,10 @@ public FunctionNameAttribute(string name) /// Gets the function name. /// public string Name => _name; + + /// + /// Validation for name. + /// + public static readonly Regex FunctionNameValidationRegex = new Regex(@"^[a-z][a-z0-9_\-]{0,127}$(? host) host.Call("Func2", new { k = 1 }); Assert.Equal("Func2", _log); + + host.Call("FuncRename", new { k = 1 }); + Assert.Equal("newname", _log); } string _log; @@ -113,6 +116,13 @@ public void Func2([Test2] string w) { _log = w; } + + // Missing path, will default to method name + [FunctionName("newname")] + public void FuncRename([Test2] string w) + { + _log = w; + } } // Use OpenType (a general builder), still no converters. diff --git a/test/Microsoft.Azure.WebJobs.Host.UnitTests/EmptyFunctionIndexProvider.cs b/test/Microsoft.Azure.WebJobs.Host.UnitTests/EmptyFunctionIndexProvider.cs index e5e3a16c2..8f50b8441 100644 --- a/test/Microsoft.Azure.WebJobs.Host.UnitTests/EmptyFunctionIndexProvider.cs +++ b/test/Microsoft.Azure.WebJobs.Host.UnitTests/EmptyFunctionIndexProvider.cs @@ -31,6 +31,11 @@ public IFunctionDefinition Lookup(MethodInfo method) return null; } + public IFunctionDefinition LookupByName(string name) + { + return null; + } + public IEnumerable ReadAll() { return Enumerable.Empty(); diff --git a/test/Microsoft.Azure.WebJobs.Host.UnitTests/Indexers/FunctionNameTests.cs b/test/Microsoft.Azure.WebJobs.Host.UnitTests/Indexers/FunctionNameTests.cs new file mode 100644 index 000000000..9bc9e5ca8 --- /dev/null +++ b/test/Microsoft.Azure.WebJobs.Host.UnitTests/Indexers/FunctionNameTests.cs @@ -0,0 +1,98 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.WebJobs.Host.Loggers; +using Microsoft.Azure.WebJobs.Host.TestCommon; +using Xunit; +using System; + +namespace Microsoft.Azure.WebJobs.Host.UnitTests.Indexers +{ + public class FunctionNameTests + { + [Fact] + public async Task Test() + { + var prog = new MyProg(); + var activator = new FakeActivator(); + activator.Add(prog); + var logger = new MyLogger(); + var host = TestHelpers.NewJobHost(activator, logger); + + // Invoke with method Info + var method = prog.GetType().GetMethod("Test"); + host.Call(method); + prog.AssertValid(); + logger.AssertValid(); + + // Invoke with new name. + await host.CallAsync(MyProg.NewName); + prog.AssertValid(); + logger.AssertValid(); + + // Invoke with original name fails + await Assert.ThrowsAsync(async () => await host.CallAsync("Test")); + await Assert.ThrowsAsync(async () => await host.CallAsync("MyProg.Test")); + } + + [Fact] + public void TestInvalidName() + { + var host = TestHelpers.NewJobHost(); + TestHelpers.AssertIndexingError(() => host.Call("Test"), "ProgInvalidName.Test", "'x y' is not a valid function name."); + } + + public class ProgInvalidName + { + [NoAutomaticTrigger] + [FunctionName("x y")] // illegal charecters + public void Test() + { + } + } + + public class MyLogger : IAsyncCollector + { + public List _items = new List(); + + public void AssertValid() + { + Assert.Equal(MyProg.NewName, _items[0]); + _items.Clear(); + } + + public Task AddAsync(FunctionInstanceLogEntry item, CancellationToken cancellationToken = default(CancellationToken)) + { + _items.Add(item.FunctionName); + return Task.CompletedTask; + } + + public Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.CompletedTask; + } + } + + public class MyProg + { + public const string NewName = "otherName"; + public int _called; + + public void AssertValid() + { + Assert.Equal(1, _called); + _called = 0; + } + + [NoAutomaticTrigger] + [FunctionName(NewName)] + public void Test() + { + _called++; + } + } + } +}