Skip to content

Commit c4127bd

Browse files
committed
Initial implementation of C# binding arguments validation.
1 parent 126c09c commit c4127bd

File tree

6 files changed

+113
-37
lines changed

6 files changed

+113
-37
lines changed

sample/TimerTrigger-CSharp/run.csx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ using System.Diagnostics;
33
using Microsoft.Azure.WebJobs;
44
using Microsoft.Azure.WebJobs.Host;
55

6-
public static void Run(TimerInfo timerInfo, out string message, TraceWriter log)
6+
public static void Run(TimerInfo input, out string message, TraceWriter log)
77
{
88
log.Verbose("CSharp Timer trigger function executed.");
99

src/WebJobs.Script/Description/CSharp/CSharpConstants.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,13 @@ public static class CSharpConstants
1010
public const string ProjectFileName = "Project.json";
1111

1212
public const string ProjectLockFileName = "Project.lock.json";
13+
14+
public const string MissingFunctionEntryPointCompilationCode = "AF001";
15+
16+
public const string AmbiguousFunctionEntryPointsCompilationCode = "AF002";
17+
18+
public const string MissingTriggerArgumentCompilationCode = "AF003";
19+
20+
public const string MissingBindingArgumentCompilationCode = "AF004";
1321
}
1422
}

src/WebJobs.Script/Description/CSharp/CSharpFunctionInvoker.cs

Lines changed: 72 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -106,22 +106,24 @@ private void ReloadScript()
106106
ImmutableArray<Diagnostic> compilationResult = compilation.GetDiagnostics();
107107

108108
stopwatch.Stop();
109-
TraceWriter.Verbose(string.Format(CultureInfo.InvariantCulture, "Compilation completed ({0} milliseconds).", stopwatch.ElapsedMilliseconds));
110109

111-
foreach (var diagnostic in compilationResult)
112-
{
113-
var traceEvent = new TraceEvent(GetTraceLevelFromDiagnostic(diagnostic), diagnostic.ToString());
114-
TraceWriter.Trace(traceEvent);
115-
}
110+
CSharpFunctionSignature signature = CSharpFunctionSignature.FromCompilation(compilation, _functionEntryPointResolver);
111+
compilationResult = ValidateFunctionBindingArguments(signature, compilationResult.ToBuilder());
112+
113+
TraceCompilationDiagnostics(compilationResult);
114+
115+
bool compilationSucceeded = !compilationResult.Any(d => d.Severity == DiagnosticSeverity.Error);
116+
117+
TraceWriter.Verbose(string.Format(CultureInfo.InvariantCulture, "Compilation {0} ({1} ms).",
118+
compilationSucceeded ? "succeeded" : "failed", stopwatch.ElapsedMilliseconds));
116119

117120
// If the compilation succeeded, AND:
118-
// - We're referencing local function types (i.e. POCOs defined in the function)
119-
// OR
120-
// - Our our function signature has changed
121+
// - We haven't cached a function (failed to compile on load), OR
122+
// - We're referencing local function types (i.e. POCOs defined in the function) AND Our our function signature has changed
121123
// Restart our host.
122-
if (!compilationResult.Any(d => d.Severity == DiagnosticSeverity.Error) &&
123-
(_functionSignature.HasLocalTypeReference ||
124-
!_functionSignature.Equals(CSharpFunctionSignature.FromCompilation(compilation, _functionEntryPointResolver))))
124+
if (compilationSucceeded &&
125+
(_functionSignature == null ||
126+
(_functionSignature.HasLocalTypeReference || !_functionSignature.Equals(signature))))
125127
{
126128
_host.RestartEvent.Set();
127129
}
@@ -172,7 +174,7 @@ public override async Task Invoke(object[] parameters)
172174
}
173175
catch (Exception ex)
174176
{
175-
TraceWriter.Error(ex.Message, ex);
177+
TraceWriter.Error(ex.Message, ex is CompilationErrorException ? null : ex);
176178
TraceWriter.Verbose("Function completed (Failure)");
177179
throw;
178180
}
@@ -205,23 +207,18 @@ internal MethodInfo GetFunctionTarget()
205207
{
206208
_assemblyLoader.ReleaseContext(Metadata);
207209

208-
TraceWriter.Verbose("Compiling function script.");
209-
var stopwatch = new Stopwatch();
210-
stopwatch.Start();
211-
212210
Script<object> script = CreateScript();
213211
Compilation compilation = GetScriptCompilation(script, debug);
212+
CSharpFunctionSignature functionSignature = CSharpFunctionSignature.FromCompilation(compilation, _functionEntryPointResolver);
213+
214+
ValidateFunctionBindingArguments(functionSignature, throwIfFailed: true);
214215

215216
using (assemblyStream = new MemoryStream())
216217
{
217218
using (pdbStream = new MemoryStream())
218219
{
219220
var result = compilation.Emit(assemblyStream, pdbStream);
220221

221-
stopwatch.Stop();
222-
223-
TraceWriter.Verbose(string.Format(CultureInfo.InvariantCulture, "Compilation completed ({0} milliseconds).", stopwatch.ElapsedMilliseconds));
224-
225222
if (!result.Success)
226223
{
227224
throw new CompilationErrorException("Script compilation failed.", result.Diagnostics);
@@ -233,26 +230,73 @@ internal MethodInfo GetFunctionTarget()
233230
// Get our function entry point
234231
System.Reflection.TypeInfo scriptType = assembly.DefinedTypes.FirstOrDefault(t => string.Compare(t.Name, ScriptClassName, StringComparison.Ordinal) == 0);
235232
_function = _functionEntryPointResolver.GetFunctionEntryPoint(scriptType.DeclaredMethods.ToList());
236-
_functionSignature = CSharpFunctionSignature.FromCompilation(compilation, _functionEntryPointResolver);
233+
_functionSignature = functionSignature;
237234
}
238235
}
239236
}
240237
catch (CompilationErrorException ex)
241238
{
242239
TraceWriter.Error("Function compilation error");
243-
244-
foreach (var diagnostic in ex.Diagnostics.Where(d => !d.IsSuppressed))
245-
{
246-
TraceLevel level = GetTraceLevelFromDiagnostic(diagnostic);
247-
TraceWriter.Trace(new TraceEvent(level, diagnostic.ToString()));
248-
}
240+
TraceCompilationDiagnostics(ex.Diagnostics);
249241
throw;
250242
}
251243
}
252244

253245
return _function;
254246
}
255247

248+
private void TraceCompilationDiagnostics(ImmutableArray<Diagnostic> diagnostics)
249+
{
250+
foreach (var diagnostic in diagnostics.Where(d => !d.IsSuppressed))
251+
{
252+
TraceLevel level = GetTraceLevelFromDiagnostic(diagnostic);
253+
TraceWriter.Trace(new TraceEvent(level, diagnostic.ToString()));
254+
}
255+
}
256+
257+
private ImmutableArray<Diagnostic> ValidateFunctionBindingArguments(CSharpFunctionSignature functionSignature,
258+
ImmutableArray<Diagnostic>.Builder builder = null, bool throwIfFailed = false)
259+
{
260+
var resultBuilder = builder ?? ImmutableArray<Diagnostic>.Empty.ToBuilder();
261+
262+
if (!functionSignature.Parameters.Any(p => string.Compare(p.Name, _triggerInputName, StringComparison.Ordinal) == 0))
263+
{
264+
string message = string.Format(CultureInfo.InvariantCulture, "Missing a trigger argument named '{0}'.", _triggerInputName);
265+
var descriptor = new DiagnosticDescriptor(CSharpConstants.MissingTriggerArgumentCompilationCode,
266+
"Missing trigger argument", message, "AzureFunctions", DiagnosticSeverity.Error, true);
267+
268+
resultBuilder.Add(Diagnostic.Create(descriptor, Location.None));
269+
}
270+
271+
var bindings = _inputBindings.Where(b => !b.IsTrigger).Union(_outputBindings);
272+
273+
foreach (var binding in bindings)
274+
{
275+
if (binding.Type == "http")
276+
{
277+
continue;
278+
}
279+
280+
if (!functionSignature.Parameters.Any(p => string.Compare(p.Name, binding.Name, StringComparison.Ordinal) == 0))
281+
{
282+
string message = string.Format(CultureInfo.InvariantCulture, "Missing binding argument named '{0}'.", binding.Name);
283+
var descriptor = new DiagnosticDescriptor(CSharpConstants.MissingBindingArgumentCompilationCode,
284+
"Missing binding argument", message, "AzureFunctions", DiagnosticSeverity.Warning, true);
285+
286+
resultBuilder.Add(Diagnostic.Create(descriptor, Location.None));
287+
}
288+
}
289+
290+
ImmutableArray<Diagnostic> result = resultBuilder.ToImmutable();
291+
292+
if (throwIfFailed && result.Any(d => d.Severity == DiagnosticSeverity.Error))
293+
{
294+
throw new CompilationErrorException("Function compilation failed.", result);
295+
}
296+
297+
return resultBuilder.ToImmutable();
298+
}
299+
256300
private Compilation GetScriptCompilation(Script<object> script, bool debug)
257301
{
258302
Compilation compilation = script.GetCompilation();

src/WebJobs.Script/Description/CSharp/CSharpFunctionSignature.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ private CSharpFunctionSignature(ImmutableArray<IParameterSymbol> parameters)
3030
/// </summary>
3131
public bool HasLocalTypeReference { get; set; }
3232

33+
public ImmutableArray<IParameterSymbol> Parameters
34+
{
35+
get
36+
{
37+
return _parameters;
38+
}
39+
}
40+
3341
public static CSharpFunctionSignature FromCompilation(Compilation compilation, IFunctionEntryPointResolver entryPointResolver)
3442
{
3543
if (compilation == null)

src/WebJobs.Script/Description/CSharp/FunctionEntryPointResolver.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,12 @@ public T GetFunctionEntryPoint<T>(IEnumerable<T> methods) where T : IMethodRefer
5252

5353
if (runMethods.Count > 1)
5454
{
55-
throw CreateCompilationException("AF002", "Ambiguous function entry points. Multiple 'Run' methods.",
56-
"Multiple methods named 'Run'. Consider renaming methods.");
55+
throw CreateCompilationException(CSharpConstants.AmbiguousFunctionEntryPointsCompilationCode,
56+
"Ambiguous function entry points. Multiple 'Run' methods.", "Multiple methods named 'Run'. Consider renaming methods.");
5757
}
5858

59-
throw CreateCompilationException("AF001", "Missing function entry point", "Your function must contain a single method, or a single public entry point method named 'Run'.");
59+
throw CreateCompilationException(CSharpConstants.MissingFunctionEntryPointCompilationCode,
60+
"Missing function entry point", "Your function must contain a single method, or a single public entry point method named 'Run'.");
6061
}
6162

6263
private static CompilationErrorException CreateCompilationException(string code, string title, string messageFormat)

src/WebJobs.Script/Description/FunctionDescriptorProvider.cs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,29 @@ public virtual bool TryCreate(FunctionMetadata functionMetadata, out FunctionDes
4949

5050
string scriptFilePath = Path.Combine(Config.RootScriptPath, functionMetadata.Source);
5151

52-
IFunctionInvoker invoker = CreateFunctionInvoker(scriptFilePath, triggerMetadata, functionMetadata, inputBindings, outputBindings);
52+
IFunctionInvoker invoker = null;
5353

54-
Collection<CustomAttributeBuilder> methodAttributes = new Collection<CustomAttributeBuilder>();
55-
Collection<ParameterDescriptor> parameters = GetFunctionParameters(invoker, functionMetadata, triggerMetadata, methodAttributes, inputBindings, outputBindings);
54+
try
55+
{
56+
invoker = CreateFunctionInvoker(scriptFilePath, triggerMetadata, functionMetadata, inputBindings, outputBindings);
57+
58+
Collection<CustomAttributeBuilder> methodAttributes = new Collection<CustomAttributeBuilder>();
59+
Collection<ParameterDescriptor> parameters = GetFunctionParameters(invoker, functionMetadata, triggerMetadata, methodAttributes, inputBindings, outputBindings);
60+
61+
functionDescriptor = new FunctionDescriptor(functionMetadata.Name, invoker, functionMetadata, parameters, methodAttributes);
5662

57-
functionDescriptor = new FunctionDescriptor(functionMetadata.Name, invoker, functionMetadata, parameters, methodAttributes);
63+
return true;
64+
}
65+
catch (Exception)
66+
{
67+
IDisposable disposableInvoker = invoker as IDisposable;
68+
if (disposableInvoker != null)
69+
{
70+
disposableInvoker.Dispose();
71+
}
5872

59-
return true;
73+
throw;
74+
}
6075
}
6176

6277
protected virtual Collection<ParameterDescriptor> GetFunctionParameters(IFunctionInvoker functionInvoker, FunctionMetadata functionMetadata,

0 commit comments

Comments
 (0)