diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingDataPathHelper.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingDataPathHelper.cs
index 38de02e78..92d149a0b 100644
--- a/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingDataPathHelper.cs
+++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingDataPathHelper.cs
@@ -18,9 +18,14 @@ internal static class BindingDataPathHelper
/// strings, JToken, and Guid (which is translated in canonical form without curly braces).
///
/// The parameter value to convert
+ /// Optional format string
/// Path compatible string representation of the given parameter or null if its type is not supported.
- public static string ConvertParameterValueToString(object parameterValue)
+ public static string ConvertParameterValueToString(object parameterValue, string format = null)
{
+ if (string.IsNullOrWhiteSpace(format))
+ {
+ format = null; // normalize.
+ }
if (parameterValue != null)
{
switch (Type.GetTypeCode(parameterValue.GetType()))
@@ -45,10 +50,21 @@ public static string ConvertParameterValueToString(object parameterValue)
return ((Byte)parameterValue).ToString(CultureInfo.InvariantCulture);
case TypeCode.SByte:
return ((SByte)parameterValue).ToString(CultureInfo.InvariantCulture);
+ case TypeCode.DateTime:
+ format = format ?? "yyyy-MM-ddTHH-mm-ssK"; // default to ISO 8601
+ var dateTime = (DateTime)parameterValue;
+ return dateTime.ToString(format, CultureInfo.InvariantCulture);
case TypeCode.Object:
if (parameterValue is Guid)
{
- return parameterValue.ToString();
+ if (format == null)
+ {
+ return parameterValue.ToString();
+ }
+ else
+ {
+ return ((Guid)parameterValue).ToString(format, CultureInfo.InvariantCulture);
+ }
}
if (parameterValue is Newtonsoft.Json.Linq.JToken)
{
diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/Path/BindingParameterResolver.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/Path/BindingParameterResolver.cs
index d31063041..85085216b 100644
--- a/src/Microsoft.Azure.WebJobs.Host/Bindings/Path/BindingParameterResolver.cs
+++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/Path/BindingParameterResolver.cs
@@ -8,6 +8,11 @@
namespace Microsoft.Azure.WebJobs.Host.Bindings.Path
{
+ ///
+ /// Support for adding built-in values to binding data.
+ /// Don't add new resolvers here. Instead, add it to
+ ///
+ [Obsolete("Use SystemBindingData instead")]
internal abstract class BindingParameterResolver
{
private static Collection _resolvers;
@@ -69,6 +74,7 @@ protected string GetFormatOrNull(string value)
return null;
}
+ // This is an alias for 'sys.randguid'
private class RandGuidResolver : BindingParameterResolver
{
public override string Name
@@ -82,18 +88,14 @@ public override string Name
public override string Resolve(string value)
{
string format = GetFormatOrNull(value);
-
- if (!string.IsNullOrEmpty(format))
- {
- return Guid.NewGuid().ToString(format, CultureInfo.InvariantCulture);
- }
- else
- {
- return Guid.NewGuid().ToString();
- }
+ var val = new SystemBindingData().RandGuid;
+ return BindingDataPathHelper.ConvertParameterValueToString(val, format);
}
}
+ // This can't be aliases to 'sys.UtcNow' because
+ // 'sys.UtcNow' always resolves to DateTime.UtcNow.
+ // But 'datetime' may either resolve to user bidning data or to DateTime.UtcNow.
private class DateTimeResolver : BindingParameterResolver
{
public override string Name
@@ -107,16 +109,8 @@ public override string Name
public override string Resolve(string value)
{
string format = GetFormatOrNull(value);
-
- if (!string.IsNullOrEmpty(format))
- {
- return DateTime.UtcNow.ToString(format, CultureInfo.InvariantCulture);
- }
- else
- {
- // default to ISO 8601
- return DateTime.UtcNow.ToString("yyyy-MM-ddTHH-mm-ssK", CultureInfo.InvariantCulture);
- }
+ var val = new SystemBindingData().UtcNow;
+ return BindingDataPathHelper.ConvertParameterValueToString(val, format);
}
}
}
diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/Path/BindingTemplateToken.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/Path/BindingTemplateToken.cs
index 31d28ec20..2dbaece1e 100644
--- a/src/Microsoft.Azure.WebJobs.Host/Bindings/Path/BindingTemplateToken.cs
+++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/Path/BindingTemplateToken.cs
@@ -37,15 +37,27 @@ public static BindingTemplateToken NewLiteral(string literalValue)
public static BindingTemplateToken NewExpression(string expression)
{
+ // BindingData takes precedence over builtins.
+ BindingParameterResolver builtin;
+ BindingParameterResolver.TryGetResolver(expression, out builtin);
+
+ // check for formatter, which is applied to finale results.
+ string format = null;
+ if (builtin == null)
+ {
+ int indexColon = expression.IndexOf(':');
+ if (indexColon > 0)
+ {
+ format = expression.Substring(indexColon + 1);
+ expression = expression.Substring(0, indexColon);
+ }
+ }
+
if (!BindingTemplateParser.IsValidIdentifier(expression))
{
throw new FormatException($"Invalid template expression '{expression}");
}
- // BindingData takes precedence over builtins.
- BindingParameterResolver builtin;
- BindingParameterResolver.TryGetResolver(expression, out builtin);
-
// Expression is just a series of dot operators like: a.b.c
var parts = expression.Split('.');
@@ -66,7 +78,7 @@ public static BindingTemplateToken NewExpression(string expression)
}
}
- return new ExpressionToken(parts, builtin);
+ return new ExpressionToken(parts, format, builtin);
}
public abstract string Evaluate(IReadOnlyDictionary bindingData);
@@ -134,13 +146,16 @@ private class ExpressionToken : BindingTemplateToken
// If non-null, then this could be a builtin object.
private readonly BindingParameterResolver _builtin;
+ private readonly string _format;
+
// The parts of an expression, like a.b.c.
private readonly string[] _expressionParts;
- public ExpressionToken(string[] expressionParts, BindingParameterResolver builtin)
+ public ExpressionToken(string[] expressionParts, string format, BindingParameterResolver builtin)
{
_expressionParts = expressionParts;
_builtin = builtin;
+ _format = format;
}
public override string AsLiteral => null;
@@ -198,7 +213,7 @@ public override string Evaluate(IReadOnlyDictionary bindingData)
}
}
- var strValue = BindingDataPathHelper.ConvertParameterValueToString(current);
+ var strValue = BindingDataPathHelper.ConvertParameterValueToString(current, _format);
return strValue;
}
diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/SystemBindingData.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/SystemBindingData.cs
index 21be188e4..4d5c60a9f 100644
--- a/src/Microsoft.Azure.WebJobs.Host/Bindings/SystemBindingData.cs
+++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/SystemBindingData.cs
@@ -20,6 +20,7 @@ internal class SystemBindingData
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 is never seen by the user.
// This ensures that we can always unambiguously retrieve this later.
private const string InternalKeyName = "$sys";
@@ -34,6 +35,16 @@ internal class SystemBindingData
///
public string MethodName { get; set; }
+ ///
+ /// Get the current UTC date.
+ ///
+ public DateTime UtcNow => DateTime.UtcNow;
+
+ ///
+ /// Return a new random guid. This create a new guid each time it's called.
+ ///
+ public Guid RandGuid => Guid.NewGuid();
+
// 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)
diff --git a/src/Microsoft.Azure.WebJobs.Host/GlobalSuppressions.cs b/src/Microsoft.Azure.WebJobs.Host/GlobalSuppressions.cs
index fd53e8e62..d0e3c7439 100644
--- a/src/Microsoft.Azure.WebJobs.Host/GlobalSuppressions.cs
+++ b/src/Microsoft.Azure.WebJobs.Host/GlobalSuppressions.cs
@@ -78,4 +78,6 @@
[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)")]
[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
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.SystemBindingData.#GetFromData(System.Collections.Generic.IReadOnlyDictionary`2)")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.SystemBindingData.#UtcNow")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.SystemBindingData.#RandGuid")]
\ No newline at end of file
diff --git a/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/BindingDataPathHelperTests.cs b/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/BindingDataPathHelperTests.cs
index 64b543434..41623db09 100644
--- a/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/BindingDataPathHelperTests.cs
+++ b/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/BindingDataPathHelperTests.cs
@@ -70,10 +70,10 @@ public void ConvertParamValueToString_IfGuidParam_ReturnsStringValue()
public void ConvertParamValueToString_IfUnupportedType_ReturnsNull()
{
// Arrange
- DateTime dateTimeParam = DateTime.Now;
+ var obj = new { value = 12 };
// Act
- string stringParamValue = BindingDataPathHelper.ConvertParameterValueToString(dateTimeParam);
+ string stringParamValue = BindingDataPathHelper.ConvertParameterValueToString(obj);
// Assert
Assert.Null(stringParamValue);
diff --git a/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/Path/BindingTemplateTests.cs b/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/Path/BindingTemplateTests.cs
index 193fe239c..e8f20005a 100644
--- a/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/Path/BindingTemplateTests.cs
+++ b/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/Path/BindingTemplateTests.cs
@@ -1,6 +1,7 @@
// 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.Generic;
using System.Linq;
using System.Text.RegularExpressions;
@@ -9,6 +10,7 @@
using Xunit;
using Xunit.Extensions;
using Newtonsoft.Json.Linq;
+using System.Globalization;
namespace Microsoft.Azure.WebJobs.Host.UnitTests.Bindings.Path
{
@@ -319,5 +321,46 @@ public void FromString_IgnoreCase_CreatesCaseInsensitiveTemplate()
result = template.Bind(parameters);
Assert.Equal("A/TestB/TestC", result);
}
+
+
+ [Fact]
+ public void GuidFormats()
+ {
+ var g = Guid.NewGuid();
+ var parameters = new Dictionary
+ {
+ { "g", g }
+ };
+
+ foreach (var format in new string[] { "N", "D", "B", "P", "X", "" })
+ {
+
+ BindingTemplate template = BindingTemplate.FromString(@"{g:" + format + "}");
+
+ string expected = g.ToString(format, CultureInfo.InvariantCulture);
+ string result = template.Bind(parameters);
+
+ Assert.Equal(expected, result);
+ }
+ }
+
+ [Fact]
+ public void DateTimeFormats()
+ {
+ var dt = DateTime.UtcNow;
+ var parameters = new Dictionary
+ {
+ { "dt",dt }
+ };
+
+
+ var format = "YYYYMMdd";
+ BindingTemplate template = BindingTemplate.FromString(@"{dt:" + format + "}");
+
+ string expected = dt.ToString(format, CultureInfo.InvariantCulture);
+ string result = template.Bind(parameters);
+
+ Assert.Equal(expected, result);
+ }
}
}
diff --git a/test/Microsoft.Azure.WebJobs.Host.UnitTests/Common/BindToGenericItemTests.cs b/test/Microsoft.Azure.WebJobs.Host.UnitTests/Common/BindToGenericItemTests.cs
index dd9214348..74b4309ec 100644
--- a/test/Microsoft.Azure.WebJobs.Host.UnitTests/Common/BindToGenericItemTests.cs
+++ b/test/Microsoft.Azure.WebJobs.Host.UnitTests/Common/BindToGenericItemTests.cs
@@ -79,7 +79,7 @@ public void Initialize(ExtensionConfigContext context)
public void Test(TestJobHost host)
{
host.Call("Func", new { k = 1 });
- Assert.Equal("1", _log);
+ Assert.NotNull(_log);
host.Call("Func2", new { k = 1 });
Assert.Equal("Func2", _log);
@@ -87,8 +87,24 @@ public void Test(TestJobHost host)
string _log;
- public void Func([Test2(Path = "{k}")] string w)
+ public void Func([Test2(Path = "{k}*{sys.randGuid:N}*{sys.randGuid:B}*{sys.UtcNow:yyyy}")] string w)
{
+ var parts = w.Split('*');
+ string k = parts[0];
+ Assert.Equal("1", k);
+
+ string guidstr1 = parts[1];
+ var guid1 = Guid.Parse(guidstr1);
+ string guidstr2 = parts[2];
+ var guid2 = Guid.Parse(guidstr2);
+
+ Assert.Equal(guid1.ToString("N"), guidstr1);
+ Assert.Equal(guid2.ToString("B"), guidstr2);
+ Assert.NotEqual(guid1, guid2); // each sys.RandGuid is a different value
+
+ string date = parts[3];
+ Assert.Equal(DateTime.UtcNow.Year.ToString(), date);
+
_log = w;
}