Skip to content

Commit 6b4c50b

Browse files
committed
Refactor binding pipeline to allow Node type info to flow through
1 parent 669ed52 commit 6b4c50b

File tree

17 files changed

+432
-177
lines changed

17 files changed

+432
-177
lines changed

src/WebJobs.Script/Binding/FunctionBinding.cs

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections;
66
using System.Collections.Generic;
77
using System.Collections.ObjectModel;
8+
using System.Dynamic;
89
using System.IO;
910
using System.Reflection.Emit;
1011
using System.Text;
@@ -13,13 +14,15 @@
1314
using Microsoft.Azure.WebJobs.Script.Description;
1415
using Microsoft.Azure.WebJobs.Script.Extensibility;
1516
using Newtonsoft.Json;
17+
using Newtonsoft.Json.Converters;
1618
using Newtonsoft.Json.Linq;
1719

1820
namespace Microsoft.Azure.WebJobs.Script.Binding
1921
{
2022
public abstract class FunctionBinding
2123
{
2224
private readonly ScriptHostConfiguration _config;
25+
private static readonly ExpandoObjectConverter _expandoObjectJsonConverter = new ExpandoObjectConverter();
2326

2427
protected FunctionBinding(ScriptHostConfiguration config, BindingMetadata metadata, FileAccess access)
2528
{
@@ -111,17 +114,14 @@ internal static void AddStorageAccountAttribute(Collection<CustomAttributeBuilde
111114
attributes.Add(attribute);
112115
}
113116

114-
internal static ICollection ReadAsCollection(object value)
117+
internal static IEnumerable ReadAsEnumerable(object value)
115118
{
116-
ICollection values = null;
119+
IEnumerable values = null;
117120

118121
if (value is Stream)
119122
{
120123
// first deserialize the stream as a string
121-
using (var reader = new StreamReader((Stream)value))
122-
{
123-
value = reader.ReadToEnd();
124-
}
124+
ConvertStreamToValue((Stream)value, DataType.String, ref value);
125125
}
126126

127127
string stringValue = value as string;
@@ -141,10 +141,14 @@ internal static ICollection ReadAsCollection(object value)
141141
values = (JArray)token;
142142
}
143143
}
144+
else if (value is Array && !(value is byte[]))
145+
{
146+
values = (IEnumerable)value;
147+
}
144148
else
145149
{
146-
// not json, so add the singleton value
147-
values = new Collection<object>() { value };
150+
// not a collection, so just add the singleton value
151+
values = new object[] { value };
148152
}
149153

150154
return values;
@@ -154,26 +158,49 @@ internal static async Task BindAsyncCollectorAsync<T>(BindingContext context)
154158
{
155159
IAsyncCollector<T> collector = await context.Binder.BindAsync<IAsyncCollector<T>>(context.Attributes);
156160

157-
IEnumerable values = ReadAsCollection(context.Value);
161+
IEnumerable values = ReadAsEnumerable(context.Value);
158162

159163
// convert values as necessary and add to the collector
160164
foreach (var value in values)
161165
{
162166
object converted = null;
163167
if (typeof(T) == typeof(string))
164168
{
165-
converted = value.ToString();
169+
if (value is ExpandoObject)
170+
{
171+
converted = ToJson((ExpandoObject)value);
172+
}
173+
else
174+
{
175+
converted = value.ToString();
176+
}
166177
}
167178
else if (typeof(T) == typeof(JObject))
168179
{
169-
converted = (JObject)value;
180+
if (value is JObject)
181+
{
182+
converted = (JObject)value;
183+
}
184+
else if (value is ExpandoObject)
185+
{
186+
converted = ToJObject((ExpandoObject)value);
187+
}
170188
}
171189
else if (typeof(T) == typeof(byte[]))
172190
{
173191
byte[] bytes = value as byte[];
174192
if (bytes == null)
175193
{
176-
string stringValue = value.ToString();
194+
string stringValue = null;
195+
if (value is ExpandoObject)
196+
{
197+
stringValue = ToJson((ExpandoObject)value);
198+
}
199+
else
200+
{
201+
stringValue = value.ToString();
202+
}
203+
177204
bytes = Encoding.UTF8.GetBytes(stringValue);
178205
}
179206
converted = bytes;
@@ -254,6 +281,11 @@ public static void ConvertValueToStream(object value, Stream stream)
254281
string json = jToken.ToString(Formatting.None);
255282
bytes = Encoding.UTF8.GetBytes(json);
256283
}
284+
else if (value is ExpandoObject)
285+
{
286+
string json = ToJson((ExpandoObject)value);
287+
bytes = Encoding.UTF8.GetBytes(json);
288+
}
257289

258290
using (valueStream = new MemoryStream(bytes))
259291
{
@@ -295,5 +327,16 @@ public static void ConvertStreamToValue(Stream stream, DataType dataType, ref ob
295327
break;
296328
}
297329
}
330+
331+
internal static string ToJson(ExpandoObject value)
332+
{
333+
return JsonConvert.SerializeObject(value, Formatting.None, _expandoObjectJsonConverter);
334+
}
335+
336+
internal static JObject ToJObject(ExpandoObject value)
337+
{
338+
string json = ToJson(value);
339+
return JObject.Parse(json);
340+
}
298341
}
299342
}

src/WebJobs.Script/Binding/HttpBinding.cs

Lines changed: 94 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ namespace Microsoft.Azure.WebJobs.Script.Binding
2121
{
2222
public class HttpBinding : FunctionBinding, IResultProcessingBinding
2323
{
24+
private static readonly DictionaryJsonConverter _dictionaryJsonConverter = new DictionaryJsonConverter();
25+
2426
public HttpBinding(ScriptHostConfiguration config, BindingMetadata metadata, FileAccess access) :
2527
base(config, metadata, access)
2628
{
@@ -31,88 +33,113 @@ public override Collection<CustomAttributeBuilder> GetCustomAttributes(Type para
3133
return null;
3234
}
3335

34-
public override async Task BindAsync(BindingContext context)
36+
public override Task BindAsync(BindingContext context)
3537
{
3638
HttpRequestMessage request = (HttpRequestMessage)context.TriggerValue;
3739

3840
object content = context.Value;
3941
if (content is Stream)
4042
{
4143
// for script language functions (e.g. PowerShell, BAT, etc.) the value
42-
// will be a Stream which we need to convert
43-
using (StreamReader streamReader = new StreamReader((Stream)content))
44-
{
45-
content = await streamReader.ReadToEndAsync();
46-
}
44+
// will be a Stream which we need to convert to string
45+
ConvertStreamToValue((Stream)content, DataType.String, ref content);
4746
}
4847

49-
HttpStatusCode statusCode = HttpStatusCode.OK;
50-
JObject headers = null;
51-
bool isRawResponse = false;
52-
if (content is string)
48+
HttpResponseMessage response = CreateResponse(request, content);
49+
request.Properties[ScriptConstants.AzureFunctionsHttpResponseKey] = response;
50+
51+
return Task.CompletedTask;
52+
}
53+
54+
internal static HttpResponseMessage CreateResponse(HttpRequestMessage request, object content)
55+
{
56+
string stringContent = content as string;
57+
if (stringContent != null)
5358
{
5459
try
5560
{
56-
// attempt to read the content as a JObject
57-
JObject jo = JObject.Parse((string)content);
58-
59-
// if the content is json we capture that so it will be
60-
// serialized as json by WebApi below
61-
content = jo;
62-
63-
// TODO: Improve this logic
64-
// Sniff the object to see if it looks like a response object
65-
// by convention
66-
JToken value = null;
67-
if (jo.TryGetValue("body", StringComparison.OrdinalIgnoreCase, out value))
68-
{
69-
content = value;
70-
71-
if (value is JValue && ((JValue)value).Type == JTokenType.String)
72-
{
73-
// convert raw strings so they get serialized properly below
74-
content = (string)value;
75-
}
76-
77-
if (jo.TryGetValue("headers", StringComparison.OrdinalIgnoreCase, out value) && value is JObject)
78-
{
79-
headers = (JObject)value;
80-
}
81-
82-
if ((jo.TryGetValue("status", StringComparison.OrdinalIgnoreCase, out value) && value is JValue) ||
83-
(jo.TryGetValue("statusCode", StringComparison.OrdinalIgnoreCase, out value) && value is JValue))
84-
{
85-
statusCode = (HttpStatusCode)(int)value;
86-
}
87-
88-
if ((jo.TryGetValue("isRaw", StringComparison.OrdinalIgnoreCase, out value) && value is JValue) &&
89-
value.Type == JTokenType.Boolean)
90-
{
91-
isRawResponse = (bool)value;
92-
}
93-
}
61+
// attempt to read the content as json
62+
content = JObject.Parse(stringContent);
9463
}
9564
catch (JsonException)
9665
{
9766
// not a json response
9867
}
9968
}
10069

101-
HttpResponseMessage response = CreateResponse(request, statusCode, content, headers, isRawResponse);
70+
// see if the content is a response object, defining http response
71+
// properties
72+
IDictionary<string, object> responseObject = null;
73+
if (content is JObject)
74+
{
75+
responseObject = JsonConvert.DeserializeObject<Dictionary<string, object>>(stringContent, _dictionaryJsonConverter);
76+
}
77+
else
78+
{
79+
// Handle ExpandoObjects
80+
responseObject = content as IDictionary<string, object>;
81+
}
82+
83+
HttpStatusCode statusCode = HttpStatusCode.OK;
84+
IDictionary<string, object> responseHeaders = null;
85+
bool isRawResponse = false;
86+
if (responseObject != null)
87+
{
88+
ParseResponseObject(responseObject, ref content, out responseHeaders, out statusCode, out isRawResponse);
89+
}
90+
91+
HttpResponseMessage response = CreateResponse(request, statusCode, content, responseHeaders, isRawResponse);
10292

103-
if (headers != null)
93+
if (responseHeaders != null)
10494
{
10595
// apply any user specified headers
106-
foreach (var header in headers)
96+
foreach (var header in responseHeaders)
10797
{
10898
AddResponseHeader(response, header);
10999
}
110100
}
111101

112-
request.Properties[ScriptConstants.AzureFunctionsHttpResponseKey] = response;
102+
return response;
103+
}
104+
105+
internal static void ParseResponseObject(IDictionary<string, object> responseObject, ref object content, out IDictionary<string, object> headers, out HttpStatusCode statusCode, out bool isRawResponse)
106+
{
107+
headers = null;
108+
statusCode = HttpStatusCode.OK;
109+
isRawResponse = false;
110+
111+
// TODO: Improve this logic
112+
// Sniff the object to see if it looks like a response object
113+
// by convention
114+
object bodyValue = null;
115+
if (responseObject.TryGetValue("body", out bodyValue, ignoreCase: true))
116+
{
117+
// the response content becomes the specified body value
118+
content = bodyValue;
119+
120+
IDictionary<string, object> headersValue = null;
121+
if (responseObject.TryGetValue<IDictionary<string, object>>("headers", out headersValue, ignoreCase: true))
122+
{
123+
headers = headersValue;
124+
}
125+
126+
object statusValue;
127+
if ((responseObject.TryGetValue("statusCode", out statusValue, ignoreCase: true) ||
128+
responseObject.TryGetValue("status", out statusValue, ignoreCase: true)) &&
129+
(statusValue is int || statusValue is string))
130+
{
131+
statusCode = (HttpStatusCode)Convert.ToInt32(statusValue);
132+
}
133+
134+
bool isRawValue;
135+
if (responseObject.TryGetValue<bool>("isRaw", out isRawValue, ignoreCase: true))
136+
{
137+
isRawResponse = isRawValue;
138+
}
139+
}
113140
}
114141

115-
private static HttpResponseMessage CreateResponse(HttpRequestMessage request, HttpStatusCode statusCode, object content, JObject headers, bool isRawResponse)
142+
private static HttpResponseMessage CreateResponse(HttpRequestMessage request, HttpStatusCode statusCode, object content, IDictionary<string, object> headers, bool isRawResponse)
116143
{
117144
if (isRawResponse)
118145
{
@@ -121,11 +148,11 @@ private static HttpResponseMessage CreateResponse(HttpRequestMessage request, Ht
121148
return new HttpResponseMessage(statusCode) { Content = CreateResultContent(content) };
122149
}
123150

124-
JToken contentType = null;
151+
string contentType = null;
125152
MediaTypeHeaderValue mediaType = null;
126153
if (content != null &&
127-
(headers?.TryGetValue("content-type", StringComparison.OrdinalIgnoreCase, out contentType) ?? false) &&
128-
MediaTypeHeaderValue.TryParse(contentType.Value<string>(), out mediaType))
154+
(headers?.TryGetValue<string>("content-type", out contentType, ignoreCase: true) ?? false) &&
155+
MediaTypeHeaderValue.TryParse((string)contentType, out mediaType))
129156
{
130157
MediaTypeFormatter writer = request.GetConfiguration()
131158
.Formatters.FindWriter(content.GetType(), mediaType);
@@ -209,7 +236,7 @@ public bool CanProcessResult(object result)
209236
return result != null;
210237
}
211238

212-
private static void AddResponseHeader(HttpResponseMessage response, KeyValuePair<string, JToken> header)
239+
internal static void AddResponseHeader(HttpResponseMessage response, KeyValuePair<string, object> header)
213240
{
214241
if (header.Value != null)
215242
{
@@ -248,7 +275,16 @@ private static void AddResponseHeader(HttpResponseMessage response, KeyValuePair
248275
}
249276
break;
250277
case "content-md5":
251-
response.Content.Headers.ContentMD5 = header.Value.Value<byte[]>();
278+
byte[] value;
279+
if (header.Value is string)
280+
{
281+
value = Convert.FromBase64String((string)header.Value);
282+
}
283+
else
284+
{
285+
value = header.Value as byte[];
286+
}
287+
response.Content.Headers.ContentMD5 = value;
252288
break;
253289
case "expires":
254290
if (DateTimeOffset.TryParse(header.Value.ToString(), out dateTimeOffset))

0 commit comments

Comments
 (0)