Skip to content

Commit

Permalink
SDK honors [FunctionName] attribute
Browse files Browse the repository at this point in the history
Resolve #1170

Add new JobHost.CallAsync(string) endpoint which can honor the method name.
  • Loading branch information
MikeStall committed Jul 20, 2017
1 parent 9d04711 commit ae97f5b
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 12 deletions.
14 changes: 14 additions & 0 deletions src/Microsoft.Azure.WebJobs.Host/Indexers/FunctionIndex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
19 changes: 16 additions & 3 deletions src/Microsoft.Azure.WebJobs.Host/Indexers/FunctionIndexer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -333,13 +333,26 @@ internal static FunctionDescriptor FromMethod(
TraceLevelAttribute traceAttribute = TypeUtility.GetHierarchicalAttributeOrNull<TraceLevelAttribute>(method);

bool hasCancellationToken = method.GetParameters().Any(p => p.ParameterType == typeof(CancellationToken));


string logName = method.Name;
string shortName = method.GetShortName();
FunctionNameAttribute nameAttribute = method.GetCustomAttribute<FunctionNameAttribute>();
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
42 changes: 34 additions & 8 deletions src/Microsoft.Azure.WebJobs.Host/JobHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -266,11 +266,41 @@ public Task CallAsync(MethodInfo method, IDictionary<string, object> arguments,
return CallAsyncCore(method, arguments, cancellationToken);
}

/// <summary>Calls a job method.</summary>
/// <param name="name">The name of the function to call.</param>
/// <param name="arguments">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. </param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>A <see cref="Task"/> that will call the job method.</returns>
public async Task CallAsync(string name, IDictionary<string, object> 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<string, object> 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);
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public class FunctionDescriptor
public IEnumerable<ParameterDescriptor> Parameters { get; set; }
#if PUBLICPROTOCOL
#else
/// <summary>Gets or sets the name used for logging. This is 'Method'. </summary>
/// <summary>Gets or sets the name used for logging. This is 'Method' or the value overwritten by [FunctionName] </summary>
[JsonIgnore]
internal string LogName { get; set; }

Expand Down
6 changes: 6 additions & 0 deletions src/Microsoft.Azure.WebJobs/FunctionNameAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -26,5 +27,10 @@ public FunctionNameAttribute(string name)
/// Gets the function name.
/// </summary>
public string Name => _name;

/// <summary>
/// Validation for name.
/// </summary>
public static readonly Regex FunctionNameValidationRegex = new Regex(@"^[a-z][a-z0-9_\-]{0,127}$(?<!^host$)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ public void Test(TestJobHost<ConfigTestDefaultToMethodName> host)

host.Call("Func2", new { k = 1 });
Assert.Equal("Func2", _log);

host.Call("FuncRename", new { k = 1 });
Assert.Equal("newname", _log);
}

string _log;
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ public IFunctionDefinition Lookup(MethodInfo method)
return null;
}

public IFunctionDefinition LookupByName(string name)
{
return null;
}

public IEnumerable<IFunctionDefinition> ReadAll()
{
return Enumerable.Empty<IFunctionDefinition>();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<MyProg>(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<InvalidOperationException>(async () => await host.CallAsync("Test"));
await Assert.ThrowsAsync<InvalidOperationException>(async () => await host.CallAsync("MyProg.Test"));
}

[Fact]
public void TestInvalidName()
{
var host = TestHelpers.NewJobHost<ProgInvalidName>();
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<FunctionInstanceLogEntry>
{
public List<string> _items = new List<string>();

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++;
}
}
}
}

0 comments on commit ae97f5b

Please sign in to comment.