Skip to content
This repository has been archived by the owner on Jul 19, 2024. It is now read-only.

Initial work supporting generic EventGridEvent<T> #12

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions src/EventGridExtension/EventGridExtensionConfig.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Azure.WebJobs.Host.Config;
using Microsoft.Azure.WebJobs.Host.Executors;
using Newtonsoft.Json;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Azure.WebJobs.Host.Config;
using Microsoft.Azure.WebJobs.Host.Executors;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Microsoft.Azure.WebJobs.Extensions.EventGrid
{
Expand Down Expand Up @@ -70,9 +71,8 @@ private async Task<HttpResponseMessage> ProcessAsync(HttpRequestMessage req)
if (String.Equals(eventTypeHeader, "SubscriptionValidation", StringComparison.OrdinalIgnoreCase))
{
string jsonArray = await req.Content.ReadAsStringAsync();
SubscriptionValidationEvent validationEvent = null;
List<EventGridEvent> events = JsonConvert.DeserializeObject<List<EventGridEvent>>(jsonArray);
validationEvent = events[0].Data.ToObject<SubscriptionValidationEvent>();
JArray events = JsonConvert.DeserializeObject<JArray>(jsonArray);
SubscriptionValidationEvent validationEvent = events[0].Value<SubscriptionValidationEvent>("data");
SubscriptionValidationResponse validationResponse = new SubscriptionValidationResponse { ValidationResponse = validationEvent.ValidationCode };
var returnMessage = new HttpResponseMessage(HttpStatusCode.OK);
returnMessage.Content = new StringContent(JsonConvert.SerializeObject(validationResponse));
Expand All @@ -83,7 +83,7 @@ private async Task<HttpResponseMessage> ProcessAsync(HttpRequestMessage req)
else if (String.Equals(eventTypeHeader, "Notification", StringComparison.OrdinalIgnoreCase))
{
string jsonArray = await req.Content.ReadAsStringAsync();
List<EventGridEvent> events = JsonConvert.DeserializeObject<List<EventGridEvent>>(jsonArray);
JArray events = JsonConvert.DeserializeObject<JArray>(jsonArray);

foreach (var ev in events)
{
Expand Down
102 changes: 63 additions & 39 deletions src/EventGridExtension/EventGridTriggerAttributeBindingProvider.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
// 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.Globalization;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Extensions.Bindings;
using Microsoft.Azure.WebJobs.Host.Bindings;
using Microsoft.Azure.WebJobs.Host.Listeners;
using Microsoft.Azure.WebJobs.Host.Protocols;
using Microsoft.Azure.WebJobs.Host.Triggers;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Threading.Tasks;

namespace Microsoft.Azure.WebJobs.Extensions.EventGrid
{
Expand Down Expand Up @@ -40,7 +40,7 @@ public Task<ITriggerBinding> TryCreateAsync(TriggerBindingProviderContext contex
return Task.FromResult<ITriggerBinding>(null);
}

if (!isSupportBindingType(parameter.ParameterType))
if (!IsSupportBindingType(parameter.ParameterType))
{
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
"Can't bind EventGridTriggerAttribute to type '{0}'.", parameter.ParameterType));
Expand All @@ -50,9 +50,24 @@ public Task<ITriggerBinding> TryCreateAsync(TriggerBindingProviderContext contex

}

public bool isSupportBindingType(Type t)
public static bool IsSupportBindingType(Type t)
{
return (t == typeof(EventGridEvent) || t == typeof(string));
return t == typeof(string) || t == typeof(EventGridEvent) || GetEventGridEventGenericType(t) != null;
}

private static Type GetEventGridEventGenericType(Type nextType)
{
do
{
if (nextType.IsGenericType && nextType.GetGenericTypeDefinition() == typeof(EventGridEvent<>))
{
return nextType;
}

nextType = nextType.BaseType;
} while (nextType != typeof(object));

return null;
}

internal class EventGridTriggerBinding : ITriggerBinding
Expand All @@ -67,10 +82,19 @@ public EventGridTriggerBinding(ParameterInfo parameter, EventGridExtensionConfig
_listenersStore = listenersStore;
_parameter = parameter;
_functionName = functionName;
_bindingContract = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)

_bindingContract = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);

var parameterType = parameter.ParameterType;

if (parameterType == typeof(string))
{
{"data",typeof(JObject) }
};
_bindingContract.Add("data", typeof(JObject));
}
else
{
_bindingContract.Add("data", GetEventGridEventGenericType(parameterType).GetGenericArguments()[0]);
}
}

public IReadOnlyDictionary<string, Type> BindingDataContract
Expand All @@ -80,52 +104,46 @@ public EventGridTriggerBinding(ParameterInfo parameter, EventGridExtensionConfig

public Type TriggerValueType
{
get { return typeof(EventGridEvent); }
get { return typeof(JObject); }
}

public Task<ITriggerData> BindAsync(object value, ValueBindingContext context)
{
// convert value to EventGridEvent, extract {data} as JObject
EventGridEvent triggerValue = null;
if (value is string stringValue)
if (value == null)
{
try
{
triggerValue = JsonConvert.DeserializeObject<EventGridEvent>(stringValue);
}
catch (Exception)
{
throw new FormatException($"Unable to parse {stringValue} to {typeof(EventGridEvent)}");
}

}
else
{
// default casting
triggerValue = value as EventGridEvent;
throw new ArgumentNullException(nameof(value));
}

if (triggerValue == null)
{
throw new InvalidOperationException($"Unable to bind {value} to type {_parameter.ParameterType}");
}
var jsonObject = value as JObject;

var bindingData = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
if(jsonObject == null)
{
{"data", triggerValue.Data}
};
throw new InvalidOperationException($"The value specified for binding was of type {value.GetType().FullName}, but only {typeof(JObject).Name} is supported.");
}

object argument;
object data;

if (_parameter.ParameterType == typeof(string))
{
argument = JsonConvert.SerializeObject(triggerValue, Formatting.Indented);
argument = jsonObject.ToString(Formatting.Indented);

data = jsonObject.Value<JObject>("data");
}
else
{
argument = triggerValue;
argument = jsonObject.ToObject(_parameter.ParameterType);

data = GetDataFromEventGridEvent(argument);
}

IValueBinder valueBinder = new EventGridValueBinder(_parameter, argument);

var bindingData = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
{
{ "data", data }
};

return Task.FromResult<ITriggerData>(new TriggerData(valueBinder, bindingData));
}

Expand All @@ -149,12 +167,18 @@ public ParameterDescriptor ToParameterDescriptor()
};
}

private object GetDataFromEventGridEvent(object value)
{
// TODO: reflection performance is likely painful here, consider switching away from reflection and instead compile Expression per-EventGridEvent<T>
return value.GetType().GetProperty("Data").GetValue(value);
}

private class EventGridTriggerParameterDescriptor : TriggerParameterDescriptor
{
public override string GetTriggerReason(IDictionary<string, string> arguments)
{
// TODO: Customize your Dashboard display string
return string.Format("EventGrid trigger fired at {0}", DateTime.Now.ToString("o"));
return string.Format("EventGrid trigger fired at {0}", DateTime.UtcNow.ToString("o"));
}
}

Expand Down
12 changes: 8 additions & 4 deletions src/EventGridExtension/EventGridevent.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Newtonsoft.Json;
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;

namespace Microsoft.Azure.WebJobs.Extensions.EventGrid
{
Expand All @@ -16,7 +16,11 @@ public class SubscriptionValidationEvent
public string ValidationCode { get; set; }
}

public class EventGridEvent
public class EventGridEvent : EventGridEvent<JObject>
{
}

public class EventGridEvent<TEventData>
{
/*
{
Expand All @@ -37,7 +41,7 @@ public class EventGridEvent
public string Subject { get; set; }

[JsonProperty(PropertyName = "data")]
public JObject Data { get; set; }
public TEventData Data { get; set; }

[JsonProperty(PropertyName = "eventType")]
public string EventType { get; set; }
Expand Down
16 changes: 8 additions & 8 deletions test/Extension.tests/JobhostEndToEnd.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using Microsoft.Azure.WebJobs.Extensions.EventGrid;
using Newtonsoft.Json;
using System.Collections.Generic;
using Xunit;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Extensions.EventGrid.Tests.Common;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit;

namespace Microsoft.Azure.WebJobs.Extensions.EventGrid.Tests
{
Expand All @@ -15,26 +15,26 @@ public class JobhostEndToEnd
public async Task ConsumeEventGridEventTest()
{

EventGridEvent eve = JsonConvert.DeserializeObject<EventGridEvent>(FakeData.singleEvent);
JObject eve = JsonConvert.DeserializeObject<JObject>(FakeData.singleEvent);
var args = new Dictionary<string, object>{
{ "value", eve }
};

var host = TestHelpers.NewHost<MyProg1>();

await host.CallAsync("MyProg1.TestEventGrid", args);
Assert.Equal(functionOut, eve.Subject);
Assert.Equal(functionOut, eve["subject"].Value<string>());
functionOut = null;

await host.CallAsync("MyProg1.TestEventGridToString", args);
Assert.Equal(functionOut, eve.Subject);
Assert.Equal(functionOut, eve["subject"].Value<string>());
functionOut = null;
}

[Fact]
public async Task UseInputBlobBinding()
{
EventGridEvent eve = JsonConvert.DeserializeObject<EventGridEvent>(FakeData.singleEvent);
JObject eve = JsonConvert.DeserializeObject<JObject>(FakeData.singleEvent);
var args = new Dictionary<string, object>{
{ "value", eve }
};
Expand Down
69 changes: 57 additions & 12 deletions test/Extension.tests/TestListener.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.EventGrid;
using Newtonsoft.Json;
using System.Collections.Generic;
using Xunit;
using System.IO;
using System;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Host.Config;
using System.Net.Http;
using System.Threading;
using System;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.EventGrid;
using Microsoft.Azure.WebJobs.Host.Config;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit;

namespace Microsoft.Azure.WebJobs.Extensions.EventGrid.Tests
{
Expand Down Expand Up @@ -91,14 +89,51 @@ public async Task TestDispatch()
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
}

[Fact]
public async Task TestDispatchGeneric()
{
var ext = new EventGridExtensionConfig();

var host = TestHelpers.NewHost<MyProg2>(ext);

await host.StartAsync(); // add listener

var request = CreateDispatchRequest("TestEventGridGeneric", new EventGridEvent<FakePayload>
{
Subject = "One",
Data = new FakePayload
{
Prop = "alpha"
}
},
new EventGridEvent<FakePayload>
{
Subject = "Two",
Data = new FakePayload
{
Prop = "beta"
}
});
IAsyncConverter<HttpRequestMessage, HttpResponseMessage> handler = ext;
var response = await handler.ConvertAsync(request, CancellationToken.None);

// Verify that the user function was dispatched twice, in order.
// Also verifies each instance gets its own proper binding data (from FakePayload.Prop)
Assert.Equal("[Dispatch:One,alpha,alpha][Dispatch:Two,beta,beta]", _log.ToString());

// TODO - Verify that we return from webhook before the dispatch is finished
// https://github.com/Azure/azure-functions-eventing-extension/issues/10
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
}

static HttpRequestMessage CreateUnsubscribeRequest(string funcName)
{
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/?functionName=" + funcName);
request.Headers.Add("aeg-event-type", "Unsubscribe");
return request;
}

static HttpRequestMessage CreateDispatchRequest(string funcName, params EventGridEvent[] items)
static HttpRequestMessage CreateDispatchRequest<T>(string funcName, params EventGridEvent<T>[] items)
{
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/?functionName=" + funcName);
request.Headers.Add("aeg-event-type", "Notification");
Expand All @@ -123,5 +158,15 @@ public class MyProg1
_log.Append("[Dispatch:" + value.Subject + "," + prop + "]");
}
}

public class MyProg2
{
public void TestEventGridGeneric(
[EventGridTrigger] EventGridEvent<FakePayload> value,
[BindingData("{data.prop}")] string prop)
{
_log.Append("[Dispatch:" + value.Subject + "," + value.Data.Prop + "," + prop + "]");
}
}
}
}
Loading