Skip to content

Commit 11fbf1e

Browse files
feat: Add dockerized event webhook consumer example (#812)
1 parent e2e18f2 commit 11fbf1e

33 files changed

+1225
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
FROM microsoft/dotnet:2.1-sdk AS build
2+
WORKDIR /App
3+
4+
# copy csproj and restore as distinct layers
5+
COPY *.sln .
6+
COPY Src/EventWebhook/*.csproj ./Src/EventWebhook/
7+
COPY Tests/EventWebhook.Tests/*.csproj ./Tests/EventWebhook.Tests/
8+
RUN dotnet restore
9+
10+
# copy everything else and build app
11+
COPY Src/EventWebhook/. ./Src/EventWebhook/
12+
WORKDIR /App/Src/EventWebhook
13+
RUN dotnet publish -c Release -o Out
14+
15+
FROM microsoft/dotnet:2.1-aspnetcore-runtime AS runtime
16+
WORKDIR /App
17+
COPY --from=build /App/Src/EventWebhook/Out ./
18+
19+
RUN echo "ASPNETCORE_URLS=http://0.0.0.0:\$PORT\nDOTNET_RUNNING_IN_CONTAINER=true" > /App/SetupHerokuEnv.sh && chmod +x /App/SetupHerokuEnv.sh
20+
21+
CMD /bin/bash -c "source /App/SetupHerokuEnv.sh && dotnet EventWebhook.dll"
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# sendgrid-events-webhook-consumer
2+
This is dockerized SendGrid Event webhook consumer.
3+
# Overview
4+
5+
SendGrid has an [Event Webhook](https://sendgrid.com/docs/API_Reference/Event_Webhook/event.html) which posts events related to your email activity to a URL of your choice. This is an easily deployable solution that allows for customers to easiy get up and running processing (parse and save) their event webhooks.
6+
7+
This is docker-based solution which can be deployed on cloud services like Heroku out of the box.
8+
9+
# Table of Content
10+
- [Prerequisite](#prerequisite)
11+
- [Deploy locally](#deploy_locally)
12+
- [Deploy Heroku](#deploy_heroku)
13+
- [Testing the Source Code](#testing_the_source_code)
14+
15+
<a name="prerequisite"></a>
16+
## Prerequisite
17+
18+
Clone the repository
19+
```
20+
git clone https://github.com/sendgrid/sendgrid-csharp.git
21+
```
22+
23+
Move into the clonned repository
24+
```
25+
cd sendgrid-csharp/examples/eventwebhook/consumer
26+
```
27+
28+
Restore the Packages
29+
```
30+
dotnet restore
31+
```
32+
33+
<a name="deploy_locally"></a>
34+
## Deploy locally
35+
Setup your MX records. Depending on your domain name host, you may need to wait up to 48 hours for the settings to propagate.
36+
37+
Run the Event Webhook Parse listener in your terminal:
38+
```
39+
git clone https://github.com/sendgrid/sendgrid-csharp.git
40+
41+
cd sendgrid-csharp/examples/eventwebhook/consumer
42+
43+
dotnet restore
44+
45+
dotnet run --project .\Src\EventWebhook\EventWebhook.csproj
46+
```
47+
Above will start server listening on a random port like below
48+
49+
In another terminal, use ngrok to allow external access to your machine:
50+
```
51+
ngrok http PORT_NUMBER
52+
```
53+
54+
You can use setup the Event Webhook please refer [this](https://sendgrid.com/docs/API_Reference/Event_Webhook/getting_started_event_webhook.html#-Getting-started)
55+
56+
<a name="deploy_heroku"></a>
57+
## Deploy to Heroku
58+
59+
[Create](https://signup.heroku.com/) Heruko account if not already present
60+
61+
Install the Heroku CLI
62+
63+
Download and install the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-command-line).
64+
65+
If you haven't already, log in to your Heroku account and follow the prompts to create a new SSH public key.
66+
```
67+
$ heroku login
68+
```
69+
70+
Now you can sign into Container Registry.
71+
```
72+
$ heroku container:login
73+
```
74+
75+
Create app in heroku
76+
```
77+
$ heroku apps:create UNIQUE_APP_NAME
78+
```
79+
80+
Create docker image
81+
```
82+
$ docker image -t DOCKER_IMAGE_NAME .
83+
```
84+
85+
Push your Docker-based app
86+
Build the Dockerfile in the current directory and push the Docker image.
87+
```
88+
$ heroku container:push web --app UNIQUE_APP_NAME
89+
```
90+
91+
Deploy the changes
92+
Release the newly pushed images to deploy your app.
93+
```
94+
$ heroku container:release web --app UNIQUE_APP_NAME
95+
```
96+
97+
<a name="testing_the_source_code"></a>
98+
## Testing the Source Code
99+
You can get all the test cases inside the `Tests` folder.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio 15
4+
VisualStudioVersion = 15.0.26124.0
5+
MinimumVisualStudioVersion = 15.0.26124.0
6+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Src", "Src", "{B08185CF-3F2E-4638-877B-587F5F60CA74}"
7+
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventWebhook", "Src\EventWebhook\EventWebhook.csproj", "{3897C29A-AE26-4FE5-8421-71896EC935C5}"
9+
EndProject
10+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{81CAC535-9854-47AD-9D3E-385AC2668C35}"
11+
EndProject
12+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventWebhook.Tests", "Tests\EventWebhook.Tests\EventWebhook.Tests.csproj", "{5C6AA0EB-57B7-4E76-804A-70F7A7DF4FC0}"
13+
EndProject
14+
Global
15+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
16+
Debug|Any CPU = Debug|Any CPU
17+
Debug|x64 = Debug|x64
18+
Debug|x86 = Debug|x86
19+
Release|Any CPU = Release|Any CPU
20+
Release|x64 = Release|x64
21+
Release|x86 = Release|x86
22+
EndGlobalSection
23+
GlobalSection(SolutionProperties) = preSolution
24+
HideSolutionNode = FALSE
25+
EndGlobalSection
26+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
27+
{3897C29A-AE26-4FE5-8421-71896EC935C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
28+
{3897C29A-AE26-4FE5-8421-71896EC935C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
29+
{3897C29A-AE26-4FE5-8421-71896EC935C5}.Debug|x64.ActiveCfg = Debug|Any CPU
30+
{3897C29A-AE26-4FE5-8421-71896EC935C5}.Debug|x64.Build.0 = Debug|Any CPU
31+
{3897C29A-AE26-4FE5-8421-71896EC935C5}.Debug|x86.ActiveCfg = Debug|Any CPU
32+
{3897C29A-AE26-4FE5-8421-71896EC935C5}.Debug|x86.Build.0 = Debug|Any CPU
33+
{3897C29A-AE26-4FE5-8421-71896EC935C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
34+
{3897C29A-AE26-4FE5-8421-71896EC935C5}.Release|Any CPU.Build.0 = Release|Any CPU
35+
{3897C29A-AE26-4FE5-8421-71896EC935C5}.Release|x64.ActiveCfg = Release|Any CPU
36+
{3897C29A-AE26-4FE5-8421-71896EC935C5}.Release|x64.Build.0 = Release|Any CPU
37+
{3897C29A-AE26-4FE5-8421-71896EC935C5}.Release|x86.ActiveCfg = Release|Any CPU
38+
{3897C29A-AE26-4FE5-8421-71896EC935C5}.Release|x86.Build.0 = Release|Any CPU
39+
{5C6AA0EB-57B7-4E76-804A-70F7A7DF4FC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
40+
{5C6AA0EB-57B7-4E76-804A-70F7A7DF4FC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
41+
{5C6AA0EB-57B7-4E76-804A-70F7A7DF4FC0}.Debug|x64.ActiveCfg = Debug|Any CPU
42+
{5C6AA0EB-57B7-4E76-804A-70F7A7DF4FC0}.Debug|x64.Build.0 = Debug|Any CPU
43+
{5C6AA0EB-57B7-4E76-804A-70F7A7DF4FC0}.Debug|x86.ActiveCfg = Debug|Any CPU
44+
{5C6AA0EB-57B7-4E76-804A-70F7A7DF4FC0}.Debug|x86.Build.0 = Debug|Any CPU
45+
{5C6AA0EB-57B7-4E76-804A-70F7A7DF4FC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
46+
{5C6AA0EB-57B7-4E76-804A-70F7A7DF4FC0}.Release|Any CPU.Build.0 = Release|Any CPU
47+
{5C6AA0EB-57B7-4E76-804A-70F7A7DF4FC0}.Release|x64.ActiveCfg = Release|Any CPU
48+
{5C6AA0EB-57B7-4E76-804A-70F7A7DF4FC0}.Release|x64.Build.0 = Release|Any CPU
49+
{5C6AA0EB-57B7-4E76-804A-70F7A7DF4FC0}.Release|x86.ActiveCfg = Release|Any CPU
50+
{5C6AA0EB-57B7-4E76-804A-70F7A7DF4FC0}.Release|x86.Build.0 = Release|Any CPU
51+
EndGlobalSection
52+
GlobalSection(NestedProjects) = preSolution
53+
{3897C29A-AE26-4FE5-8421-71896EC935C5} = {B08185CF-3F2E-4638-877B-587F5F60CA74}
54+
{5C6AA0EB-57B7-4E76-804A-70F7A7DF4FC0} = {81CAC535-9854-47AD-9D3E-385AC2668C35}
55+
EndGlobalSection
56+
EndGlobal
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using EventWebhook.Models;
2+
using EventWebhook.Parser;
3+
using Microsoft.AspNetCore.Mvc;
4+
using System.Collections.Generic;
5+
using System.Threading.Tasks;
6+
7+
namespace EventWebhook.Controllers
8+
{
9+
[Route("/")]
10+
public class EventWebhookController : Controller
11+
{
12+
/// <summary>
13+
/// GET : Index page
14+
/// </summary>
15+
[Route("")]
16+
public IActionResult Index()
17+
{
18+
return View();
19+
}
20+
21+
/// <summary>
22+
/// POST : Event webhook handler
23+
/// </summary>
24+
/// <returns></returns>
25+
[Route("/events")]
26+
[HttpPost]
27+
public async Task<IActionResult> Events()
28+
{
29+
IEnumerable<Event> events = await EventParser.ParseAsync(Request.Body);
30+
31+
return Ok();
32+
}
33+
}
34+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using EventWebhook.Models;
2+
using Newtonsoft.Json;
3+
using System;
4+
using System.Linq;
5+
6+
namespace EventWebhook.Converters
7+
{
8+
public class CategoryConverter : JsonConverter
9+
{
10+
private readonly Type[] _types;
11+
12+
public CategoryConverter()
13+
{
14+
_types = new Type[] { typeof(string), typeof(string[]) };
15+
}
16+
17+
public override bool CanConvert(Type objectType)
18+
{
19+
return _types.Any(t => t == objectType);
20+
}
21+
22+
public override bool CanWrite => true;
23+
24+
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
25+
{
26+
if (reader.TokenType == JsonToken.StartArray)
27+
{
28+
return new Category(serializer.Deserialize<string[]>(reader), JsonToken.StartArray);
29+
}
30+
else
31+
{
32+
return new Category(new[] { serializer.Deserialize<string>(reader) }, reader.TokenType);
33+
}
34+
}
35+
36+
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
37+
{
38+
if (value is Category category)
39+
{
40+
if (category.IsArray)
41+
{
42+
serializer.Serialize(writer, category);
43+
} else
44+
{
45+
serializer.Serialize(writer, category.Value[0]);
46+
}
47+
}
48+
}
49+
}
50+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using Newtonsoft.Json;
2+
using System;
3+
4+
namespace EventWebhook.Converters
5+
{
6+
public class UriConverter : JsonConverter
7+
{
8+
public override bool CanConvert(Type objectType) => objectType == typeof(string);
9+
10+
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
11+
{
12+
if (reader.TokenType == JsonToken.Null)
13+
{
14+
return null;
15+
}
16+
17+
if (reader.TokenType == JsonToken.String)
18+
{
19+
return new Uri((string)reader.Value);
20+
}
21+
22+
throw new InvalidOperationException("Invalid Url");
23+
}
24+
25+
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
26+
{
27+
if (null == value)
28+
{
29+
writer.WriteNull();
30+
return;
31+
}
32+
33+
if (value is Uri)
34+
{
35+
writer.WriteValue(((Uri)value).OriginalString);
36+
return;
37+
}
38+
}
39+
}
40+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netcoreapp2.1</TargetFramework>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<Folder Include="Util\" />
9+
<Folder Include="wwwroot\" />
10+
</ItemGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="Microsoft.AspNetCore.App" />
14+
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.1.2" PrivateAssets="All" />
15+
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.1.1" />
16+
</ItemGroup>
17+
18+
</Project>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using Newtonsoft.Json;
2+
using Newtonsoft.Json.Converters;
3+
4+
namespace EventWebhook.Models
5+
{
6+
public class BounceEvent : DroppedEvent
7+
{
8+
[JsonConverter(typeof(StringEnumConverter))]
9+
public BounceEventType BounceType { get; set; }
10+
}
11+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace EventWebhook.Models
2+
{
3+
public enum BounceEventType
4+
{
5+
Bounce,
6+
Blocked,
7+
Expired
8+
}
9+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using Newtonsoft.Json;
2+
3+
namespace EventWebhook.Models
4+
{
5+
public class Category
6+
{
7+
public string[] Value { get; }
8+
private JsonToken _jsonToken;
9+
10+
public Category(string[] value, JsonToken jsonToken)
11+
{
12+
Value = value;
13+
_jsonToken = jsonToken;
14+
}
15+
16+
[JsonIgnore]
17+
public bool IsArray => _jsonToken == JsonToken.StartArray;
18+
}
19+
}

0 commit comments

Comments
 (0)