diff --git a/Dockerfile b/Dockerfile index a9ee12341..005486060 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,10 @@ -FROM mono:latest +FROM mcr.microsoft.com/dotnet/sdk:3.1 -ENV FrameworkPathOverride /usr/lib/mono/4.5/ - -RUN apt-get update \ - && apt-get install -y curl make apt-transport-https - -RUN curl -sSL https://packages.microsoft.com/config/ubuntu/19.10/packages-microsoft-prod.deb -o packages-microsoft-prod.deb \ - && dpkg --install packages-microsoft-prod.deb +COPY prism/prism/nginx/cert.crt /usr/local/share/ca-certificates/cert.crt RUN apt-get update \ - && apt-get install -y dotnet-sdk-2.1 - -COPY prism/prism/nginx/cert.crt /usr/local/share/ca-certificates/cert.crt -RUN update-ca-certificates + && apt-get install -y make apt-transport-https \ + && update-ca-certificates COPY . . diff --git a/ExampleCoreProject/ExampleCoreProject.csproj b/ExampleCoreProject/ExampleCoreProject.csproj index 0d5601d03..42acb21cb 100644 --- a/ExampleCoreProject/ExampleCoreProject.csproj +++ b/ExampleCoreProject/ExampleCoreProject.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1 + netcoreapp3.1 Exe false @@ -16,9 +16,7 @@ - - - + diff --git a/ExampleNet45Project/ExampleNet45.csproj b/ExampleNet45Project/ExampleNet45.csproj index e2bdaec5e..3db276096 100644 --- a/ExampleNet45Project/ExampleNet45.csproj +++ b/ExampleNet45Project/ExampleNet45.csproj @@ -18,7 +18,7 @@ - + diff --git a/examples/eventwebhook/consumer/Dockerfile b/examples/eventwebhook/consumer/Dockerfile index bebfb684b..2ffcecb17 100644 --- a/examples/eventwebhook/consumer/Dockerfile +++ b/examples/eventwebhook/consumer/Dockerfile @@ -1,21 +1,21 @@ -FROM microsoft/dotnet:2.1-sdk AS build -WORKDIR /App +FROM mcr.microsoft.com/dotnet/sdk:3.1 AS build +WORKDIR /app # copy csproj and restore as distinct layers COPY *.sln . -COPY Src/EventWebhook/*.csproj ./Src/EventWebhook/ -COPY Tests/EventWebhook.Tests/*.csproj ./Tests/EventWebhook.Tests/ +COPY src/EventWebhook/*.csproj ./src/EventWebhook/ +COPY tests/EventWebhook.Tests/*.csproj ./tests/EventWebhook.Tests/ RUN dotnet restore # copy everything else and build app -COPY Src/EventWebhook/. ./Src/EventWebhook/ -WORKDIR /App/Src/EventWebhook -RUN dotnet publish -c Release -o Out +COPY src/EventWebhook/. ./src/EventWebhook/ +WORKDIR /app/src/EventWebhook +RUN dotnet publish -c Release -o out -FROM microsoft/dotnet:2.1-aspnetcore-runtime AS runtime -WORKDIR /App -COPY --from=build /App/Src/EventWebhook/Out ./ +FROM mcr.microsoft.com/dotnet/aspnet:3.1 AS runtime +WORKDIR /app +COPY --from=build /app/src/EventWebhook/out ./ -RUN echo "ASPNETCORE_URLS=http://0.0.0.0:\$PORT\nDOTNET_RUNNING_IN_CONTAINER=true" > /App/SetupHerokuEnv.sh && chmod +x /App/SetupHerokuEnv.sh +RUN echo "ASPNETCORE_URLS=http://0.0.0.0:\$PORT\nDOTNET_RUNNING_IN_CONTAINER=true" > /app/SetupHerokuEnv.sh && chmod +x /app/SetupHerokuEnv.sh -CMD /bin/bash -c "source /App/SetupHerokuEnv.sh && dotnet EventWebhook.dll" \ No newline at end of file +CMD /bin/bash -c "source /app/SetupHerokuEnv.sh && dotnet EventWebhook.dll" \ No newline at end of file diff --git a/examples/eventwebhook/consumer/README.md b/examples/eventwebhook/consumer/README.md index 9ec23f614..e089e8d52 100644 --- a/examples/eventwebhook/consumer/README.md +++ b/examples/eventwebhook/consumer/README.md @@ -42,7 +42,7 @@ cd sendgrid-csharp/examples/eventwebhook/consumer dotnet restore -dotnet run --project .\Src\EventWebhook\EventWebhook.csproj +dotnet run --project .\src\EventWebhook\EventWebhook.csproj ``` Above will start server listening on a random port like below diff --git a/examples/eventwebhook/consumer/SendGridEventWebhookConsumer.sln b/examples/eventwebhook/consumer/SendGridEventWebhookConsumer.sln index edc1dedce..00eb188e6 100644 --- a/examples/eventwebhook/consumer/SendGridEventWebhookConsumer.sln +++ b/examples/eventwebhook/consumer/SendGridEventWebhookConsumer.sln @@ -3,13 +3,13 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26124.0 MinimumVisualStudioVersion = 15.0.26124.0 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Src", "Src", "{B08185CF-3F2E-4638-877B-587F5F60CA74}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B08185CF-3F2E-4638-877B-587F5F60CA74}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventWebhook", "Src\EventWebhook\EventWebhook.csproj", "{3897C29A-AE26-4FE5-8421-71896EC935C5}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventWebhook", "src\EventWebhook\EventWebhook.csproj", "{3897C29A-AE26-4FE5-8421-71896EC935C5}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{81CAC535-9854-47AD-9D3E-385AC2668C35}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{81CAC535-9854-47AD-9D3E-385AC2668C35}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventWebhook.Tests", "Tests\EventWebhook.Tests\EventWebhook.Tests.csproj", "{5C6AA0EB-57B7-4E76-804A-70F7A7DF4FC0}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventWebhook.Tests", "tests\EventWebhook.Tests\EventWebhook.Tests.csproj", "{5C6AA0EB-57B7-4E76-804A-70F7A7DF4FC0}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Controllers/EventWebhookController.cs b/examples/eventwebhook/consumer/Src/EventWebhook/Controllers/EventWebhookController.cs deleted file mode 100644 index ea544a0f2..000000000 --- a/examples/eventwebhook/consumer/Src/EventWebhook/Controllers/EventWebhookController.cs +++ /dev/null @@ -1,34 +0,0 @@ -using EventWebhook.Models; -using EventWebhook.Parser; -using Microsoft.AspNetCore.Mvc; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace EventWebhook.Controllers -{ - [Route("/")] - public class EventWebhookController : Controller - { - /// - /// GET : Index page - /// - [Route("")] - public IActionResult Index() - { - return View(); - } - - /// - /// POST : Event webhook handler - /// - /// - [Route("/events")] - [HttpPost] - public async Task Events() - { - IEnumerable events = await EventParser.ParseAsync(Request.Body); - - return Ok(); - } - } -} diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Converters/CategoryConverter.cs b/examples/eventwebhook/consumer/Src/EventWebhook/Converters/CategoryConverter.cs deleted file mode 100644 index 966dc2af6..000000000 --- a/examples/eventwebhook/consumer/Src/EventWebhook/Converters/CategoryConverter.cs +++ /dev/null @@ -1,50 +0,0 @@ -using EventWebhook.Models; -using Newtonsoft.Json; -using System; -using System.Linq; - -namespace EventWebhook.Converters -{ - public class CategoryConverter : JsonConverter - { - private readonly Type[] _types; - - public CategoryConverter() - { - _types = new Type[] { typeof(string), typeof(string[]) }; - } - - public override bool CanConvert(Type objectType) - { - return _types.Any(t => t == objectType); - } - - public override bool CanWrite => true; - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.StartArray) - { - return new Category(serializer.Deserialize(reader), JsonToken.StartArray); - } - else - { - return new Category(new[] { serializer.Deserialize(reader) }, reader.TokenType); - } - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (value is Category category) - { - if (category.IsArray) - { - serializer.Serialize(writer, category); - } else - { - serializer.Serialize(writer, category.Value[0]); - } - } - } - } -} diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Converters/UriConverter.cs b/examples/eventwebhook/consumer/Src/EventWebhook/Converters/UriConverter.cs deleted file mode 100644 index 06c55f1e8..000000000 --- a/examples/eventwebhook/consumer/Src/EventWebhook/Converters/UriConverter.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Newtonsoft.Json; -using System; - -namespace EventWebhook.Converters -{ - public class UriConverter : JsonConverter - { - public override bool CanConvert(Type objectType) => objectType == typeof(string); - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) - { - return null; - } - - if (reader.TokenType == JsonToken.String) - { - return new Uri((string)reader.Value); - } - - throw new InvalidOperationException("Invalid Url"); - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (null == value) - { - writer.WriteNull(); - return; - } - - if (value is Uri) - { - writer.WriteValue(((Uri)value).OriginalString); - return; - } - } - } -} diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/EventWebhook.csproj b/examples/eventwebhook/consumer/Src/EventWebhook/EventWebhook.csproj deleted file mode 100644 index 1318472b7..000000000 --- a/examples/eventwebhook/consumer/Src/EventWebhook/EventWebhook.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - netcoreapp2.1 - - - - - - - - - - - - - - diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Models/BounceEvent.cs b/examples/eventwebhook/consumer/Src/EventWebhook/Models/BounceEvent.cs deleted file mode 100644 index 484c8911b..000000000 --- a/examples/eventwebhook/consumer/Src/EventWebhook/Models/BounceEvent.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; - -namespace EventWebhook.Models -{ - public class BounceEvent : DroppedEvent - { - [JsonConverter(typeof(StringEnumConverter))] - public BounceEventType BounceType { get; set; } - } -} diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Models/Category.cs b/examples/eventwebhook/consumer/Src/EventWebhook/Models/Category.cs deleted file mode 100644 index b6762e74d..000000000 --- a/examples/eventwebhook/consumer/Src/EventWebhook/Models/Category.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; - -namespace EventWebhook.Models -{ - public class Category - { - public string[] Value { get; } - private JsonToken _jsonToken; - - public Category(string[] value, JsonToken jsonToken) - { - Value = value; - _jsonToken = jsonToken; - } - - [JsonIgnore] - public bool IsArray => _jsonToken == JsonToken.StartArray; - } -} diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Models/ClickEvent.cs b/examples/eventwebhook/consumer/Src/EventWebhook/Models/ClickEvent.cs deleted file mode 100644 index 94a8be67c..000000000 --- a/examples/eventwebhook/consumer/Src/EventWebhook/Models/ClickEvent.cs +++ /dev/null @@ -1,12 +0,0 @@ -using EventWebhook.Converters; -using Newtonsoft.Json; -using System; - -namespace EventWebhook.Models -{ - public class ClickEvent : OpenEvent - { - [JsonConverter(typeof(UriConverter))] - public Uri Url { get; set; } - } -} diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Models/DeferredEvent.cs b/examples/eventwebhook/consumer/Src/EventWebhook/Models/DeferredEvent.cs deleted file mode 100644 index 2704527a7..000000000 --- a/examples/eventwebhook/consumer/Src/EventWebhook/Models/DeferredEvent.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace EventWebhook.Models -{ - public class DeferredEvent : DeliveredEvent - { - public int Attempt { get; set; } - - } -} diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Models/GroupResubscribeEvent.cs b/examples/eventwebhook/consumer/Src/EventWebhook/Models/GroupResubscribeEvent.cs deleted file mode 100644 index 4034c5384..000000000 --- a/examples/eventwebhook/consumer/Src/EventWebhook/Models/GroupResubscribeEvent.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace EventWebhook.Models -{ - public class GroupResubscribeEvent : GroupUnsubscribeEvent { } -} diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Models/GroupUnsubscribeEvent.cs b/examples/eventwebhook/consumer/Src/EventWebhook/Models/GroupUnsubscribeEvent.cs deleted file mode 100644 index 4763182f2..000000000 --- a/examples/eventwebhook/consumer/Src/EventWebhook/Models/GroupUnsubscribeEvent.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace EventWebhook.Models -{ - public class GroupUnsubscribeEvent : ClickEvent - { - [JsonProperty("asm_group_id")] - public int AsmGroupId { get; set; } - } -} diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Models/SpamReportEvent.cs b/examples/eventwebhook/consumer/Src/EventWebhook/Models/SpamReportEvent.cs deleted file mode 100644 index 0f1b6c738..000000000 --- a/examples/eventwebhook/consumer/Src/EventWebhook/Models/SpamReportEvent.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace EventWebhook.Models -{ - public class SpamReportEvent : Event { } -} diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Models/UnsubscribeEvent.cs b/examples/eventwebhook/consumer/Src/EventWebhook/Models/UnsubscribeEvent.cs deleted file mode 100644 index 350dbf09e..000000000 --- a/examples/eventwebhook/consumer/Src/EventWebhook/Models/UnsubscribeEvent.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace EventWebhook.Models -{ - public class UnsubscribeEvent : Event { } -} diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Parser/EventConverter.cs b/examples/eventwebhook/consumer/Src/EventWebhook/Parser/EventConverter.cs deleted file mode 100644 index 7a5602525..000000000 --- a/examples/eventwebhook/consumer/Src/EventWebhook/Parser/EventConverter.cs +++ /dev/null @@ -1,57 +0,0 @@ -using EventWebhook.Models; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; - -namespace EventWebhook.Parser -{ - public class EventConverter : JsonConverter - { - private static readonly Dictionary> eventConverters = - new Dictionary>() - { - { EventType.Bounce, (json) => JsonConvert.DeserializeObject(json) }, - { EventType.Click, (json) => JsonConvert.DeserializeObject(json) }, - { EventType.Deferred, (json) => JsonConvert.DeserializeObject(json) }, - { EventType.Delivered, (json) => JsonConvert.DeserializeObject(json) }, - { EventType.Dropped, (json) => JsonConvert.DeserializeObject(json) }, - { EventType.GroupResubscribe, (json) => JsonConvert.DeserializeObject(json) }, - { EventType.GroupUnsubscribe, (json) => JsonConvert.DeserializeObject(json) }, - { EventType.Open, (json) => JsonConvert.DeserializeObject(json) }, - { EventType.Processed, (json) => JsonConvert.DeserializeObject(json) }, - { EventType.SpamReport, (json) => JsonConvert.DeserializeObject(json) }, - { EventType.Unsubscribe, (json) => JsonConvert.DeserializeObject(json) }, - }; - - private static Event DeserializeEvent(EventType type, string json) - { - if (!eventConverters.ContainsKey(type)) - { - throw new ArgumentOutOfRangeException($"Unknown event type: {type.ToString()}"); - } - - return eventConverters.GetValueOrDefault(type)(json); - } - - public override bool CanConvert(Type objectType) => typeof(Event) == objectType; - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - var jsonObject = JObject.Load(reader); - - jsonObject.TryGetValue("event", StringComparison.OrdinalIgnoreCase, out JToken eventTypeJsonProperty); - - var eventType = (EventType)eventTypeJsonProperty.ToObject(typeof(EventType)); - - var webhookEvent = DeserializeEvent(eventType, jsonObject.ToString()); - - return webhookEvent; - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new NotImplementedException(); - } - } -} diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Parser/EventParser.cs b/examples/eventwebhook/consumer/Src/EventWebhook/Parser/EventParser.cs deleted file mode 100644 index 31b09b5e7..000000000 --- a/examples/eventwebhook/consumer/Src/EventWebhook/Parser/EventParser.cs +++ /dev/null @@ -1,46 +0,0 @@ -using EventWebhook.Models; -using Newtonsoft.Json; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading.Tasks; - -namespace EventWebhook.Parser -{ - public class EventParser - { - public static async Task> ParseAsync(string json) - { - using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(json))) - { - return await ParseAsync(stream); - } - } - - public static async Task> ParseAsync(Stream stream) - { - var reader = new StreamReader(stream); - - var json = await reader.ReadToEndAsync(); - - return JsonConvert.DeserializeObject>(json, new EventConverter()); - } - - public static IEnumerable Parse(string json) - { - using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(json))) - { - return Parse(stream); - } - } - - public static IEnumerable Parse(Stream stream) - { - var reader = new StreamReader(stream); - - var json = reader.ReadToEnd(); - - return JsonConvert.DeserializeObject>(json, new EventConverter()); - } - } -} diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Program.cs b/examples/eventwebhook/consumer/Src/EventWebhook/Program.cs deleted file mode 100644 index 3e5ac6ad8..000000000 --- a/examples/eventwebhook/consumer/Src/EventWebhook/Program.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace EventWebhook -{ - public class Program - { - public static void Main(string[] args) - { - CreateWebHostBuilder(args).Build().Run(); - } - - public static IWebHostBuilder CreateWebHostBuilder(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseStartup(); - } -} diff --git a/examples/eventwebhook/consumer/src/EventWebhook/Controllers/EventWebhookController.cs b/examples/eventwebhook/consumer/src/EventWebhook/Controllers/EventWebhookController.cs new file mode 100644 index 000000000..956057105 --- /dev/null +++ b/examples/eventwebhook/consumer/src/EventWebhook/Controllers/EventWebhookController.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; +using EventWebhook.Parser; +using Microsoft.AspNetCore.Mvc; + +namespace EventWebhook.Controllers +{ + [Route("/events")] + [ApiController] + public class EventWebhookController : ControllerBase + { + /// + /// POST : Event webhook handler + /// + /// + [HttpPost] + public async Task Events() + { + var events = await EventParser.ParseAsync(Request.Body); + + return Ok(events); + } + } +} diff --git a/examples/eventwebhook/consumer/src/EventWebhook/Controllers/HomeController.cs b/examples/eventwebhook/consumer/src/EventWebhook/Controllers/HomeController.cs new file mode 100644 index 000000000..a6b3c9873 --- /dev/null +++ b/examples/eventwebhook/consumer/src/EventWebhook/Controllers/HomeController.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Inbound.Controllers +{ + [Route("/")] + public class HomeController : Controller + { + // GET + public IActionResult Index() + { + return View(); + } + } +} \ No newline at end of file diff --git a/examples/eventwebhook/consumer/src/EventWebhook/Converters/CategoryConverter.cs b/examples/eventwebhook/consumer/src/EventWebhook/Converters/CategoryConverter.cs new file mode 100644 index 000000000..b66320faa --- /dev/null +++ b/examples/eventwebhook/consumer/src/EventWebhook/Converters/CategoryConverter.cs @@ -0,0 +1,35 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using EventWebhook.Models; + +namespace EventWebhook.Converters +{ + public class CategoryConverter : JsonConverter + { + public override Category Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType == JsonTokenType.StartArray + ? new Category(JsonSerializer.Deserialize(ref reader), true) + : new Category(new []{reader.GetString()},false); + } + + public override void Write(Utf8JsonWriter writer, Category value, JsonSerializerOptions options) + { + if (value.IsArray) + { + writer.WriteStartArray(); + foreach (var item in value.Value) + { + writer.WriteStringValue(item); + } + writer.WriteEndArray(); + } + else + { + writer.WriteStringValue(value.Value.FirstOrDefault()); + } + } + } +} diff --git a/examples/eventwebhook/consumer/src/EventWebhook/Converters/EventConverter.cs b/examples/eventwebhook/consumer/src/EventWebhook/Converters/EventConverter.cs new file mode 100644 index 000000000..24823475e --- /dev/null +++ b/examples/eventwebhook/consumer/src/EventWebhook/Converters/EventConverter.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Reflection; +using System.Runtime.Serialization; +using EventWebhook.Models; + +namespace EventWebhook.Converters +{ + public class EventConverter : JsonConverter> + { + private static readonly IDictionary EnumNameFor = new Dictionary(); + + static EventConverter() + { + var enumType = typeof(EventType); + foreach (var name in enumType.GetEnumNames()) + { + var field = enumType.GetField(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); + var enumMemberAttribute = field?.GetCustomAttribute(true); + if (enumMemberAttribute == null) continue; + EnumNameFor[enumMemberAttribute.Value] = name; + } + } + public override IEnumerable Read(ref Utf8JsonReader reader, Type typeToConvert, + JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartArray) + { + throw new JsonException($"Unrecognized token: {reader.TokenType}"); + } + + var elementType = typeToConvert.IsArray + ? typeToConvert.GetElementType() + : typeToConvert.GenericTypeArguments.FirstOrDefault(); + if (elementType == null) + { + throw new JsonException($"Impossible to read JSON array to fill type: {typeToConvert.Name}"); + } + + var list = typeToConvert.IsArray || typeToConvert.IsAbstract + ? (IList) Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType)) + : (IList) Activator.CreateInstance(typeToConvert); + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + list.Add(ReadObject(ref reader, elementType, options)); + } + + if (!typeToConvert.IsArray) + { + return (IEnumerable) list; + } + + var array = Array.CreateInstance(elementType, list.Count); + list.CopyTo(array, 0); + return (Event[]) array; + } + + private static Event ReadObject(ref Utf8JsonReader reader, Type elementType, JsonSerializerOptions options) + { + using var doc = JsonDocument.ParseValue(ref reader); + if (!doc.RootElement.TryGetProperty("event", out var eventProperty)) + { + throw new JsonException("event property not found"); + } + + var eventTypeString = eventProperty.GetString(); + if (EnumNameFor.TryGetValue(eventTypeString, out var enumName)) + { + eventTypeString = enumName; + } + + if (!Enum.TryParse(eventTypeString, true, out var eventType)) + { + throw new JsonException($"event type not found: [{eventTypeString}]"); + } + + var typeName = string.Join(".", typeof(Event).Namespace, eventType + "Event"); + var type = Type.GetType(typeName); + if (type == null) + { + throw new JsonException($"event type not found: [{typeName}]"); + } + + using var utf8Json = new MemoryStream(); + using (var utf8JsonWriter = new Utf8JsonWriter(utf8Json)) + { + doc.RootElement.WriteTo(utf8JsonWriter); + } + + utf8Json.Seek(0, SeekOrigin.Begin); + return (Event) JsonSerializer.Deserialize(utf8Json.ToArray(), type, options); + } + + public override void Write(Utf8JsonWriter writer, IEnumerable value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, value.GetType(), options); + } + } +} \ No newline at end of file diff --git a/examples/eventwebhook/consumer/src/EventWebhook/Converters/UnixDateTimeConverter.cs b/examples/eventwebhook/consumer/src/EventWebhook/Converters/UnixDateTimeConverter.cs new file mode 100644 index 000000000..670721adc --- /dev/null +++ b/examples/eventwebhook/consumer/src/EventWebhook/Converters/UnixDateTimeConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace EventWebhook.Converters +{ + public class UnixDateTimeConverter : JsonConverter + { + private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String && long.TryParse(reader.GetString(),out var seconds)) + { + return DateTimeOffset.FromUnixTimeSeconds(seconds).UtcDateTime; + } + return DateTimeOffset.FromUnixTimeSeconds(reader.GetInt64()).UtcDateTime; + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + { + var seconds = (long)(value.ToUniversalTime() - UnixEpoch).TotalSeconds; + writer.WriteNumberValue(seconds); + } + } +} \ No newline at end of file diff --git a/examples/eventwebhook/consumer/src/EventWebhook/Converters/UriConverter.cs b/examples/eventwebhook/consumer/src/EventWebhook/Converters/UriConverter.cs new file mode 100644 index 000000000..fc67960f1 --- /dev/null +++ b/examples/eventwebhook/consumer/src/EventWebhook/Converters/UriConverter.cs @@ -0,0 +1,19 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace EventWebhook.Converters +{ + public class UriConverter : JsonConverter + { + public override Uri Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new Uri(reader.GetString()); + } + + public override void Write(Utf8JsonWriter writer, Uri value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.OriginalString); + } + } +} diff --git a/examples/eventwebhook/consumer/src/EventWebhook/Converters/ValueTypeStringConverter.cs b/examples/eventwebhook/consumer/src/EventWebhook/Converters/ValueTypeStringConverter.cs new file mode 100644 index 000000000..7f9fa2b30 --- /dev/null +++ b/examples/eventwebhook/consumer/src/EventWebhook/Converters/ValueTypeStringConverter.cs @@ -0,0 +1,102 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace EventWebhook.Converters +{ + public class ValueTypeStringConverter : JsonConverter + { + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsValueType; + } + + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (typeToConvert == typeof(decimal)) + { + if (reader.TokenType == JsonTokenType.Number && reader.TryGetDecimal(out var value)) + { + return value; + } + if (decimal.TryParse(reader.GetString(), NumberStyles.Currency, CultureInfo.InvariantCulture, + out var @decimal)) + { + return @decimal; + } + } + if (typeToConvert == typeof(float)) + { + if (reader.TokenType == JsonTokenType.Number && reader.TryGetSingle(out var value)) + { + return value; + } + if (float.TryParse(reader.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, + out var @float)) + { + return @float; + } + } + if (typeToConvert == typeof(double)) + { + if (reader.TokenType == JsonTokenType.Number && reader.TryGetDouble(out var value)) + { + return value; + } + if (double.TryParse(reader.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, + out var @double)) + { + return @double; + } + } + if (typeToConvert == typeof(int)) + { + if (reader.TokenType == JsonTokenType.Number && reader.TryGetInt32(out var value)) + { + return value; + } + if (int.TryParse(reader.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var integer)) + { + return integer; + } + } + + if (typeToConvert == typeof(bool)) + { + if (reader.TokenType == JsonTokenType.False || reader.TokenType == JsonTokenType.False) + { + return reader.GetBoolean(); + } + if (bool.TryParse(reader.GetString(), out var @bool)) + { + return @bool; + } + } + return TypeDescriptor.GetConverter(typeToConvert).ConvertFromInvariantString(reader.GetString()); + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + switch (value) + { + case decimal decimalValue: + writer.WriteStringValue(decimalValue.ToString(CultureInfo.InvariantCulture)); + break; + case float floatValue: + writer.WriteStringValue(floatValue.ToString(CultureInfo.InvariantCulture)); + break; + case double doubleValue: + writer.WriteStringValue(doubleValue.ToString(CultureInfo.InvariantCulture)); + break; + case bool boolValue: + writer.WriteStringValue(boolValue.ToString().ToLowerInvariant()); + break; + default: + writer.WriteStringValue(value.ToString()); + break; + } + } + } +} \ No newline at end of file diff --git a/examples/eventwebhook/consumer/src/EventWebhook/EventWebhook.csproj b/examples/eventwebhook/consumer/src/EventWebhook/EventWebhook.csproj new file mode 100644 index 000000000..6b81ca833 --- /dev/null +++ b/examples/eventwebhook/consumer/src/EventWebhook/EventWebhook.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp3.1 + + + + + PreserveNewest + + + appsettings.json + PreserveNewest + + + + + + + + + diff --git a/examples/eventwebhook/consumer/src/EventWebhook/Models/BounceEvent.cs b/examples/eventwebhook/consumer/src/EventWebhook/Models/BounceEvent.cs new file mode 100644 index 000000000..075468109 --- /dev/null +++ b/examples/eventwebhook/consumer/src/EventWebhook/Models/BounceEvent.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace EventWebhook.Models +{ + public class BounceEvent : DroppedEvent + { + [JsonConverter(typeof(JsonStringEnumConverter))] + public BounceEventType BounceType { get; set; } + + public BounceEvent() + { + EventType = EventType.Bounce; + } + } +} diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Models/BounceEventType.cs b/examples/eventwebhook/consumer/src/EventWebhook/Models/BounceEventType.cs similarity index 100% rename from examples/eventwebhook/consumer/Src/EventWebhook/Models/BounceEventType.cs rename to examples/eventwebhook/consumer/src/EventWebhook/Models/BounceEventType.cs diff --git a/examples/eventwebhook/consumer/src/EventWebhook/Models/Category.cs b/examples/eventwebhook/consumer/src/EventWebhook/Models/Category.cs new file mode 100644 index 000000000..67209f9d6 --- /dev/null +++ b/examples/eventwebhook/consumer/src/EventWebhook/Models/Category.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; +using Microsoft.Extensions.Primitives; + +namespace EventWebhook.Models +{ + public class Category + { + public StringValues Value { get; } + + public Category(StringValues value, bool isArray) + { + Value = value; + IsArray = isArray; + } + + [JsonIgnore] + public bool IsArray { get; } + } +} diff --git a/examples/eventwebhook/consumer/src/EventWebhook/Models/ClickEvent.cs b/examples/eventwebhook/consumer/src/EventWebhook/Models/ClickEvent.cs new file mode 100644 index 000000000..40442cc86 --- /dev/null +++ b/examples/eventwebhook/consumer/src/EventWebhook/Models/ClickEvent.cs @@ -0,0 +1,17 @@ +using System; +using System.Text.Json.Serialization; +using EventWebhook.Converters; + +namespace EventWebhook.Models +{ + public class ClickEvent : OpenEvent + { + [JsonConverter(typeof(UriConverter))] + public Uri Url { get; set; } + + public ClickEvent() + { + EventType = EventType.Click; + } + } +} diff --git a/examples/eventwebhook/consumer/src/EventWebhook/Models/DeferredEvent.cs b/examples/eventwebhook/consumer/src/EventWebhook/Models/DeferredEvent.cs new file mode 100644 index 000000000..1c0e21a8a --- /dev/null +++ b/examples/eventwebhook/consumer/src/EventWebhook/Models/DeferredEvent.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; +using EventWebhook.Converters; + +namespace EventWebhook.Models +{ + public class DeferredEvent : DeliveredEvent + { + [JsonConverter(typeof(ValueTypeStringConverter))] + public int Attempt { get; set; } + + public DeferredEvent() + { + EventType = EventType.Deferred; + } + } +} diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Models/DeliveredEvent.cs b/examples/eventwebhook/consumer/src/EventWebhook/Models/DeliveredEvent.cs similarity index 57% rename from examples/eventwebhook/consumer/Src/EventWebhook/Models/DeliveredEvent.cs rename to examples/eventwebhook/consumer/src/EventWebhook/Models/DeliveredEvent.cs index 0a0b7df05..18edce9af 100644 --- a/examples/eventwebhook/consumer/Src/EventWebhook/Models/DeliveredEvent.cs +++ b/examples/eventwebhook/consumer/src/EventWebhook/Models/DeliveredEvent.cs @@ -3,5 +3,10 @@ public class DeliveredEvent : Event { public string Response { get; set; } + + public DeliveredEvent() + { + EventType = EventType.Delivered; + } } } diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Models/DroppedEvent.cs b/examples/eventwebhook/consumer/src/EventWebhook/Models/DroppedEvent.cs similarity index 64% rename from examples/eventwebhook/consumer/Src/EventWebhook/Models/DroppedEvent.cs rename to examples/eventwebhook/consumer/src/EventWebhook/Models/DroppedEvent.cs index d3f38649e..39317d390 100644 --- a/examples/eventwebhook/consumer/Src/EventWebhook/Models/DroppedEvent.cs +++ b/examples/eventwebhook/consumer/src/EventWebhook/Models/DroppedEvent.cs @@ -4,5 +4,10 @@ public class DroppedEvent : Event { public string Reason { get; set; } public string Status { get; set; } + + public DroppedEvent() + { + EventType = EventType.Dropped; + } } } diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Models/Event.cs b/examples/eventwebhook/consumer/src/EventWebhook/Models/Event.cs similarity index 65% rename from examples/eventwebhook/consumer/Src/EventWebhook/Models/Event.cs rename to examples/eventwebhook/consumer/src/EventWebhook/Models/Event.cs index ef23f9a00..7ddb99d86 100644 --- a/examples/eventwebhook/consumer/Src/EventWebhook/Models/Event.cs +++ b/examples/eventwebhook/consumer/src/EventWebhook/Models/Event.cs @@ -1,9 +1,7 @@ -using EventWebhook.Converters; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Linq; -using System; +using System; using System.Collections.Generic; +using System.Text.Json.Serialization; +using EventWebhook.Converters; namespace EventWebhook.Models { @@ -14,20 +12,20 @@ public class Event [JsonConverter(typeof(UnixDateTimeConverter))] public DateTime Timestamp { get; set; } - [JsonProperty("smtp-id")] + [JsonPropertyName("smtp-id")] public string SmtpId { get; set; } - [JsonProperty("event")] - [JsonConverter(typeof(StringEnumConverter))] + [JsonPropertyName("event")] + [JsonConverter(typeof(JsonStringEnumMemberConverter))] public EventType EventType { get; set; } [JsonConverter(typeof(CategoryConverter))] public Category Category { get; set; } - [JsonProperty("sg_event_id")] + [JsonPropertyName("sg_event_id")] public string SendGridEventId { get; set; } - [JsonProperty("sg_message_id")] + [JsonPropertyName("sg_message_id")] public string SendGridMessageId { get; set; } public string TLS { get; set; } @@ -35,10 +33,10 @@ public class Event [JsonExtensionData] public IDictionary UniqueArgs { get; set; } - [JsonProperty("marketing_campaign_id")] + [JsonPropertyName("marketing_campaign_id")] public string MarketingCampainId { get; set; } - [JsonProperty("marketing_campaign_name")] + [JsonPropertyName("marketing_campaign_name")] public string MarketingCampainName { get; set; } } diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Models/EventType.cs b/examples/eventwebhook/consumer/src/EventWebhook/Models/EventType.cs similarity index 100% rename from examples/eventwebhook/consumer/Src/EventWebhook/Models/EventType.cs rename to examples/eventwebhook/consumer/src/EventWebhook/Models/EventType.cs diff --git a/examples/eventwebhook/consumer/src/EventWebhook/Models/GroupResubscribeEvent.cs b/examples/eventwebhook/consumer/src/EventWebhook/Models/GroupResubscribeEvent.cs new file mode 100644 index 000000000..485bab7ac --- /dev/null +++ b/examples/eventwebhook/consumer/src/EventWebhook/Models/GroupResubscribeEvent.cs @@ -0,0 +1,10 @@ +namespace EventWebhook.Models +{ + public class GroupResubscribeEvent : GroupUnsubscribeEvent + { + public GroupResubscribeEvent() + { + EventType = EventType.GroupResubscribe; + } + } +} diff --git a/examples/eventwebhook/consumer/src/EventWebhook/Models/GroupUnsubscribeEvent.cs b/examples/eventwebhook/consumer/src/EventWebhook/Models/GroupUnsubscribeEvent.cs new file mode 100644 index 000000000..4830eac66 --- /dev/null +++ b/examples/eventwebhook/consumer/src/EventWebhook/Models/GroupUnsubscribeEvent.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace EventWebhook.Models +{ + public class GroupUnsubscribeEvent : ClickEvent + { + [JsonPropertyName("asm_group_id")] + public int AsmGroupId { get; set; } + + public GroupUnsubscribeEvent() + { + EventType = EventType.GroupUnsubscribe; + } + } +} diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Models/OpenEvent.cs b/examples/eventwebhook/consumer/src/EventWebhook/Models/OpenEvent.cs similarity index 65% rename from examples/eventwebhook/consumer/Src/EventWebhook/Models/OpenEvent.cs rename to examples/eventwebhook/consumer/src/EventWebhook/Models/OpenEvent.cs index 81439ba38..10321aba4 100644 --- a/examples/eventwebhook/consumer/Src/EventWebhook/Models/OpenEvent.cs +++ b/examples/eventwebhook/consumer/src/EventWebhook/Models/OpenEvent.cs @@ -5,5 +5,10 @@ public class OpenEvent : Event public string UserAgent { get; set; } public string IP { get; set; } + + public OpenEvent() + { + EventType = EventType.Open; + } } } diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Models/Pool.cs b/examples/eventwebhook/consumer/src/EventWebhook/Models/Pool.cs similarity index 94% rename from examples/eventwebhook/consumer/Src/EventWebhook/Models/Pool.cs rename to examples/eventwebhook/consumer/src/EventWebhook/Models/Pool.cs index 0d534d6ce..1a44dbd2c 100644 --- a/examples/eventwebhook/consumer/Src/EventWebhook/Models/Pool.cs +++ b/examples/eventwebhook/consumer/src/EventWebhook/Models/Pool.cs @@ -3,6 +3,7 @@ public class Pool { public string Name { get; set; } + public int Id { get; set; } } } diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Models/ProcessedEvent.cs b/examples/eventwebhook/consumer/src/EventWebhook/Models/ProcessedEvent.cs similarity index 56% rename from examples/eventwebhook/consumer/Src/EventWebhook/Models/ProcessedEvent.cs rename to examples/eventwebhook/consumer/src/EventWebhook/Models/ProcessedEvent.cs index 648429470..0c2af3f85 100644 --- a/examples/eventwebhook/consumer/Src/EventWebhook/Models/ProcessedEvent.cs +++ b/examples/eventwebhook/consumer/src/EventWebhook/Models/ProcessedEvent.cs @@ -3,5 +3,10 @@ public class ProcessedEvent : Event { public Pool Pool { get; set; } + + public ProcessedEvent() + { + EventType = EventType.Processed; + } } } diff --git a/examples/eventwebhook/consumer/src/EventWebhook/Models/SpamReportEvent.cs b/examples/eventwebhook/consumer/src/EventWebhook/Models/SpamReportEvent.cs new file mode 100644 index 000000000..1a64940c7 --- /dev/null +++ b/examples/eventwebhook/consumer/src/EventWebhook/Models/SpamReportEvent.cs @@ -0,0 +1,10 @@ +namespace EventWebhook.Models +{ + public class SpamReportEvent : Event + { + public SpamReportEvent() + { + EventType = EventType.SpamReport; + } + } +} diff --git a/examples/eventwebhook/consumer/src/EventWebhook/Models/UnsubscribeEvent.cs b/examples/eventwebhook/consumer/src/EventWebhook/Models/UnsubscribeEvent.cs new file mode 100644 index 000000000..dcb29648f --- /dev/null +++ b/examples/eventwebhook/consumer/src/EventWebhook/Models/UnsubscribeEvent.cs @@ -0,0 +1,10 @@ +namespace EventWebhook.Models +{ + public class UnsubscribeEvent : Event + { + public UnsubscribeEvent() + { + EventType = EventType.Unsubscribe; + } + } +} diff --git a/examples/eventwebhook/consumer/src/EventWebhook/Parser/EventParser.cs b/examples/eventwebhook/consumer/src/EventWebhook/Parser/EventParser.cs new file mode 100644 index 000000000..4906f1fb4 --- /dev/null +++ b/examples/eventwebhook/consumer/src/EventWebhook/Parser/EventParser.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; +using EventWebhook.Converters; +using EventWebhook.Models; + +namespace EventWebhook.Parser +{ + public class EventParser + { + private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = + { + new EventConverter() + } + }; + + public static async Task> ParseAsync(Stream stream, JsonSerializerOptions options = null) + { + return await JsonSerializer.DeserializeAsync>(stream, options ?? SerializerOptions); + } + + public static IEnumerable Parse(string json, JsonSerializerOptions options = null) + { + return JsonSerializer.Deserialize>(json, options ?? SerializerOptions); + } + + public static IEnumerable Parse(Stream stream, JsonSerializerOptions options = null) + { + using var buffer = new MemoryStream(); + stream.CopyTo(buffer); + return JsonSerializer.Deserialize>(buffer.ToArray(), options ?? SerializerOptions); + } + } +} \ No newline at end of file diff --git a/examples/eventwebhook/consumer/src/EventWebhook/Program.cs b/examples/eventwebhook/consumer/src/EventWebhook/Program.cs new file mode 100644 index 000000000..7c427f079 --- /dev/null +++ b/examples/eventwebhook/consumer/src/EventWebhook/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace EventWebhook +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Startup.cs b/examples/eventwebhook/consumer/src/EventWebhook/Startup.cs similarity index 56% rename from examples/eventwebhook/consumer/Src/EventWebhook/Startup.cs rename to examples/eventwebhook/consumer/src/EventWebhook/Startup.cs index b3e6f8b9a..f4267786f 100644 --- a/examples/eventwebhook/consumer/Src/EventWebhook/Startup.cs +++ b/examples/eventwebhook/consumer/src/EventWebhook/Startup.cs @@ -1,15 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.HttpsPolicy; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Hosting; namespace EventWebhook { @@ -25,11 +20,17 @@ public Startup(IConfiguration configuration) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); + services.AddControllersWithViews() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; + options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { @@ -41,7 +42,10 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) } app.UseHttpsRedirection(); - app.UseMvc(); + app.UseRouting(); + app.UseEndpoints(endpoints => { + endpoints.MapControllers(); + }); } } } diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/Views/EventWebhook/Index.cshtml b/examples/eventwebhook/consumer/src/EventWebhook/Views/Home/Index.cshtml similarity index 100% rename from examples/eventwebhook/consumer/Src/EventWebhook/Views/EventWebhook/Index.cshtml rename to examples/eventwebhook/consumer/src/EventWebhook/Views/Home/Index.cshtml diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/appsettings.Development.json b/examples/eventwebhook/consumer/src/EventWebhook/appsettings.Development.json similarity index 100% rename from examples/eventwebhook/consumer/Src/EventWebhook/appsettings.Development.json rename to examples/eventwebhook/consumer/src/EventWebhook/appsettings.Development.json diff --git a/examples/eventwebhook/consumer/Src/EventWebhook/appsettings.json b/examples/eventwebhook/consumer/src/EventWebhook/appsettings.json similarity index 100% rename from examples/eventwebhook/consumer/Src/EventWebhook/appsettings.json rename to examples/eventwebhook/consumer/src/EventWebhook/appsettings.json diff --git a/examples/eventwebhook/consumer/Tests/EventWebhook.Tests/EventTests.cs b/examples/eventwebhook/consumer/tests/EventWebhook.Tests/EventTests.cs similarity index 50% rename from examples/eventwebhook/consumer/Tests/EventWebhook.Tests/EventTests.cs rename to examples/eventwebhook/consumer/tests/EventWebhook.Tests/EventTests.cs index 94741a3e0..6e4dec9af 100644 --- a/examples/eventwebhook/consumer/Tests/EventWebhook.Tests/EventTests.cs +++ b/examples/eventwebhook/consumer/tests/EventWebhook.Tests/EventTests.cs @@ -1,11 +1,10 @@ -using EventWebhook.Models; -using EventWebhook.Parser; -using Shouldly; using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using EventWebhook.Models; +using EventWebhook.Parser; +using Shouldly; using Xunit; namespace EventWebhook.Tests @@ -15,31 +14,29 @@ public class EventTests [Fact] public async Task AllEvents() { - var jsonStream = new MemoryStream(File.ReadAllBytes("TestData/events.json")); - - IEnumerable events = await EventParser.ParseAsync(jsonStream); + var events = await EventParser.ParseAsync(File.OpenRead("TestData/events.json")); events.Count().ShouldBeGreaterThanOrEqualTo(1); } [Fact] - public async Task ProcessedEventTest() + public void ProcessedEventTest() { var json = @" [ { - 'email': 'example@test.com', - 'timestamp': 1513299569, - 'smtp-id': '<14c5d75ce93.dfd.64b469@ismtpd-555>', - 'event': 'processed', - 'category': 'cat facts', - 'sg_event_id': 'sg_event_id', - 'sg_message_id': 'sg_message_id' + ""email"": ""example@test.com"", + ""timestamp"": 1513299569, + ""smtp-id"": ""<14c5d75ce93.dfd.64b469@ismtpd-555>"", + ""event"": ""processed"", + ""category"": ""cat facts"", + ""sg_event_id"": ""sg_event_id"", + ""sg_message_id"": ""sg_message_id"" } ] "; - IEnumerable events = await EventParser.ParseAsync(json); - events.Count().ShouldBe(1); + var events = EventParser.Parse(json).ToList(); + events.Count.ShouldBe(1); var processedEvent = events.Single(); processedEvent.Email.ShouldBe("example@test.com"); processedEvent.Timestamp.ShouldBe(DateTime.UnixEpoch.AddSeconds(1513299569)); @@ -51,87 +48,87 @@ public async Task ProcessedEventTest() } [Fact] - public async Task DefferedEventTest() + public void DeferredEventTest() { var json = @" [ { - 'email': 'example@test.com', - 'timestamp': 1513299569, - 'smtp-id': '<14c5d75ce93.dfd.64b469@ismtpd-555>', - 'event': 'deferred', - 'category': 'cat facts', - 'sg_event_id': 'sg_event_id', - 'sg_message_id': 'sg_message_id', - 'response': '400 try again later', - 'attempt': '5' + ""email"": ""example@test.com"", + ""timestamp"": 1513299569, + ""smtp-id"": ""<14c5d75ce93.dfd.64b469@ismtpd-555>"", + ""event"": ""deferred"", + ""category"": ""cat facts"", + ""sg_event_id"": ""sg_event_id"", + ""sg_message_id"": ""sg_message_id"", + ""response"": ""400 try again later"", + ""attempt"": ""5"" } ] "; - IEnumerable events = await EventParser.ParseAsync(json); - events.Count().ShouldBe(1); - var defferedEvent = (DeferredEvent)events.Single(); - defferedEvent.Email.ShouldBe("example@test.com"); - defferedEvent.Timestamp.ShouldBe(DateTime.UnixEpoch.AddSeconds(1513299569)); - defferedEvent.SmtpId.ShouldBe("<14c5d75ce93.dfd.64b469@ismtpd-555>"); - defferedEvent.EventType.ShouldBe(EventType.Deferred); - defferedEvent.Category.Value[0].ShouldBe("cat facts"); - defferedEvent.SendGridEventId.ShouldBe("sg_event_id"); - defferedEvent.SendGridMessageId.ShouldBe("sg_message_id"); - defferedEvent.Response.ShouldBe("400 try again later"); - defferedEvent.Attempt.ShouldBe(5); + var events = EventParser.Parse(json).ToList(); + events.Count.ShouldBe(1); + var deferredEvent = (DeferredEvent)events.Single(); + deferredEvent.Email.ShouldBe("example@test.com"); + deferredEvent.Timestamp.ShouldBe(DateTime.UnixEpoch.AddSeconds(1513299569)); + deferredEvent.SmtpId.ShouldBe("<14c5d75ce93.dfd.64b469@ismtpd-555>"); + deferredEvent.EventType.ShouldBe(EventType.Deferred); + deferredEvent.Category.Value[0].ShouldBe("cat facts"); + deferredEvent.SendGridEventId.ShouldBe("sg_event_id"); + deferredEvent.SendGridMessageId.ShouldBe("sg_message_id"); + deferredEvent.Response.ShouldBe("400 try again later"); + deferredEvent.Attempt.ShouldBe(5); } [Fact] - public async Task DeleveredEventTest() + public void DeleveredEventTest() { var json = @" [ { - 'email': 'example@test.com', - 'timestamp': 1513299569, - 'smtp-id': '<14c5d75ce93.dfd.64b469@ismtpd-555>', - 'event': 'delivered', - 'category': 'cat facts', - 'sg_event_id': 'sg_event_id', - 'sg_message_id': 'sg_message_id', - 'response': '200 OK' + ""email"": ""example@test.com"", + ""timestamp"": 1513299569, + ""smtp-id"": ""<14c5d75ce93.dfd.64b469@ismtpd-555>"", + ""event"": ""delivered"", + ""category"": ""cat facts"", + ""sg_event_id"": ""sg_event_id"", + ""sg_message_id"": ""sg_message_id"", + ""response"": ""200 OK"" } ] "; - IEnumerable events = await EventParser.ParseAsync(json); - events.Count().ShouldBe(1); - var defferedEvent = (DeliveredEvent)events.Single(); - defferedEvent.Email.ShouldBe("example@test.com"); - defferedEvent.Timestamp.ShouldBe(DateTime.UnixEpoch.AddSeconds(1513299569)); - defferedEvent.SmtpId.ShouldBe("<14c5d75ce93.dfd.64b469@ismtpd-555>"); - defferedEvent.EventType.ShouldBe(EventType.Delivered); - defferedEvent.Category.Value[0].ShouldBe("cat facts"); - defferedEvent.SendGridEventId.ShouldBe("sg_event_id"); - defferedEvent.SendGridMessageId.ShouldBe("sg_message_id"); - defferedEvent.Response.ShouldBe("200 OK"); + var events = EventParser.Parse(json).ToList(); + events.Count.ShouldBe(1); + var deferredEvent = (DeliveredEvent)events.Single(); + deferredEvent.Email.ShouldBe("example@test.com"); + deferredEvent.Timestamp.ShouldBe(DateTime.UnixEpoch.AddSeconds(1513299569)); + deferredEvent.SmtpId.ShouldBe("<14c5d75ce93.dfd.64b469@ismtpd-555>"); + deferredEvent.EventType.ShouldBe(EventType.Delivered); + deferredEvent.Category.Value[0].ShouldBe("cat facts"); + deferredEvent.SendGridEventId.ShouldBe("sg_event_id"); + deferredEvent.SendGridMessageId.ShouldBe("sg_message_id"); + deferredEvent.Response.ShouldBe("200 OK"); } [Fact] - public async Task OpenEventTest() + public void OpenEventTest() { var json = @" [ { - 'email': 'example@test.com', - 'timestamp': 1513299569, - 'smtp-id': '<14c5d75ce93.dfd.64b469@ismtpd-555>', - 'event': 'open', - 'category': 'cat facts', - 'sg_event_id': 'sg_event_id', - 'sg_message_id': 'sg_message_id', - 'useragent': 'Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727)', - 'ip': '255.255.255.255' + ""email"": ""example@test.com"", + ""timestamp"": 1513299569, + ""smtp-id"": ""<14c5d75ce93.dfd.64b469@ismtpd-555>"", + ""event"": ""open"", + ""category"": ""cat facts"", + ""sg_event_id"": ""sg_event_id"", + ""sg_message_id"": ""sg_message_id"", + ""useragent"": ""Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727)"", + ""ip"": ""255.255.255.255"" } ] "; - IEnumerable events = await EventParser.ParseAsync(json); - events.Count().ShouldBe(1); + var events = EventParser.Parse(json).ToList(); + events.Count.ShouldBe(1); var openEvent = (OpenEvent)events.Single(); openEvent.Email.ShouldBe("example@test.com"); openEvent.Timestamp.ShouldBe(DateTime.UnixEpoch.AddSeconds(1513299569)); @@ -146,26 +143,26 @@ public async Task OpenEventTest() } [Fact] - public async Task ClickEventTest() + public void ClickEventTest() { var json = @" [ { - 'email': 'example@test.com', - 'timestamp': 1513299569, - 'smtp-id': '<14c5d75ce93.dfd.64b469@ismtpd-555>', - 'event': 'click', - 'category': 'cat facts', - 'sg_event_id': 'sg_event_id', - 'sg_message_id': 'sg_message_id', - 'useragent': 'Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727)', - 'ip': '255.255.255.255', - 'url': 'http://www.sendgrid.com/' + ""email"": ""example@test.com"", + ""timestamp"": 1513299569, + ""smtp-id"": ""<14c5d75ce93.dfd.64b469@ismtpd-555>"", + ""event"": ""click"", + ""category"": ""cat facts"", + ""sg_event_id"": ""sg_event_id"", + ""sg_message_id"": ""sg_message_id"", + ""useragent"": ""Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727)"", + ""ip"": ""255.255.255.255"", + ""url"": ""http://www.sendgrid.com/"" } ] "; - IEnumerable events = await EventParser.ParseAsync(json); - events.Count().ShouldBe(1); + var events = EventParser.Parse(json).ToList(); + events.Count.ShouldBe(1); var clickEvent = (ClickEvent)events.Single(); clickEvent.Email.ShouldBe("example@test.com"); clickEvent.Timestamp.ShouldBe(DateTime.UnixEpoch.AddSeconds(1513299569)); @@ -177,30 +174,28 @@ public async Task ClickEventTest() clickEvent.UserAgent.ShouldBe("Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727)"); clickEvent.IP.ShouldBe("255.255.255.255"); clickEvent.Url.ToString().ShouldBe("http://www.sendgrid.com/"); - - } [Fact] - public async Task BounceEventTest() + public void BounceEventTest() { var json = @" [ { - 'email': 'example@test.com', - 'timestamp': 1513299569, - 'smtp-id': '<14c5d75ce93.dfd.64b469@ismtpd-555>', - 'event': 'bounce', - 'category': 'cat facts', - 'sg_event_id': 'sg_event_id', - 'sg_message_id': 'sg_message_id', - 'reason': '500 unknown recipient', - 'status': '5.0.0' + ""email"": ""example@test.com"", + ""timestamp"": 1513299569, + ""smtp-id"": ""<14c5d75ce93.dfd.64b469@ismtpd-555>"", + ""event"": ""bounce"", + ""category"": ""cat facts"", + ""sg_event_id"": ""sg_event_id"", + ""sg_message_id"": ""sg_message_id"", + ""reason"": ""500 unknown recipient"", + ""status"": ""5.0.0"" } ] "; - IEnumerable events = await EventParser.ParseAsync(json); - events.Count().ShouldBe(1); + var events = EventParser.Parse(json).ToList(); + events.Count.ShouldBe(1); var bounceEvent = (BounceEvent)events.Single(); bounceEvent.Email.ShouldBe("example@test.com"); bounceEvent.Timestamp.ShouldBe(DateTime.UnixEpoch.AddSeconds(1513299569)); @@ -214,25 +209,25 @@ public async Task BounceEventTest() } [Fact] - public async Task DroppedEventTest() + public void DroppedEventTest() { var json = @" [ { - 'email': 'example@test.com', - 'timestamp': 1513299569, - 'smtp-id': '<14c5d75ce93.dfd.64b469@ismtpd-555>', - 'event': 'dropped', - 'category': 'cat facts', - 'sg_event_id': 'sg_event_id', - 'sg_message_id': 'sg_message_id', - 'reason': 'Bounced Address', - 'status': '5.0.0' + ""email"": ""example@test.com"", + ""timestamp"": 1513299569, + ""smtp-id"": ""<14c5d75ce93.dfd.64b469@ismtpd-555>"", + ""event"": ""dropped"", + ""category"": ""cat facts"", + ""sg_event_id"": ""sg_event_id"", + ""sg_message_id"": ""sg_message_id"", + ""reason"": ""Bounced Address"", + ""status"": ""5.0.0"" } ] "; - IEnumerable events = await EventParser.ParseAsync(json); - events.Count().ShouldBe(1); + var events = EventParser.Parse(json).ToList(); + events.Count.ShouldBe(1); var droppedEvent = (DroppedEvent)events.Single(); droppedEvent.Email.ShouldBe("example@test.com"); droppedEvent.Timestamp.ShouldBe(DateTime.UnixEpoch.AddSeconds(1513299569)); @@ -246,23 +241,23 @@ public async Task DroppedEventTest() } [Fact] - public async Task SpamReportEventTest() + public void SpamReportEventTest() { var json = @" [ { - 'email': 'example@test.com', - 'timestamp': 1513299569, - 'smtp-id': '<14c5d75ce93.dfd.64b469@ismtpd-555>', - 'event': 'spamreport', - 'category': 'cat facts', - 'sg_event_id': 'sg_event_id', - 'sg_message_id': 'sg_message_id' + ""email"": ""example@test.com"", + ""timestamp"": 1513299569, + ""smtp-id"": ""<14c5d75ce93.dfd.64b469@ismtpd-555>"", + ""event"": ""spamreport"", + ""category"": ""cat facts"", + ""sg_event_id"": ""sg_event_id"", + ""sg_message_id"": ""sg_message_id"" } ] "; - IEnumerable events = await EventParser.ParseAsync(json); - events.Count().ShouldBe(1); + var events = EventParser.Parse(json).ToList(); + events.Count.ShouldBe(1); var spamReportEvent = (SpamReportEvent)events.Single(); spamReportEvent.Email.ShouldBe("example@test.com"); spamReportEvent.Timestamp.ShouldBe(DateTime.UnixEpoch.AddSeconds(1513299569)); @@ -274,23 +269,23 @@ public async Task SpamReportEventTest() } [Fact] - public async Task UnsubscribeEventTest() + public void UnsubscribeEventTest() { var json = @" [ { - 'email': 'example@test.com', - 'timestamp': 1513299569, - 'smtp-id': '<14c5d75ce93.dfd.64b469@ismtpd-555>', - 'event': 'unsubscribe', - 'category': 'cat facts', - 'sg_event_id': 'sg_event_id', - 'sg_message_id': 'sg_message_id' + ""email"": ""example@test.com"", + ""timestamp"": 1513299569, + ""smtp-id"": ""<14c5d75ce93.dfd.64b469@ismtpd-555>"", + ""event"": ""unsubscribe"", + ""category"": ""cat facts"", + ""sg_event_id"": ""sg_event_id"", + ""sg_message_id"": ""sg_message_id"" } ] "; - IEnumerable events = await EventParser.ParseAsync(json); - events.Count().ShouldBe(1); + var events = EventParser.Parse(json).ToList(); + events.Count.ShouldBe(1); var spamReportEvent = (UnsubscribeEvent)events.Single(); spamReportEvent.Email.ShouldBe("example@test.com"); spamReportEvent.Timestamp.ShouldBe(DateTime.UnixEpoch.AddSeconds(1513299569)); @@ -302,27 +297,27 @@ public async Task UnsubscribeEventTest() } [Fact] - public async Task GroupUnsubscribeEventTest() + public void GroupUnsubscribeEventTest() { var json = @" [ { - 'email': 'example@test.com', - 'timestamp': 1513299569, - 'smtp-id': '<14c5d75ce93.dfd.64b469@ismtpd-555>', - 'event': 'group_unsubscribe', - 'category': 'cat facts', - 'sg_event_id': 'sg_event_id', - 'sg_message_id': 'sg_message_id', - 'useragent': 'Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727)', - 'ip': '255.255.255.255', - 'url': 'http://www.sendgrid.com/', - 'asm_group_id': 10 + ""email"": ""example@test.com"", + ""timestamp"": 1513299569, + ""smtp-id"": ""<14c5d75ce93.dfd.64b469@ismtpd-555>"", + ""event"": ""group_unsubscribe"", + ""category"": ""cat facts"", + ""sg_event_id"": ""sg_event_id"", + ""sg_message_id"": ""sg_message_id"", + ""useragent"": ""Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727)"", + ""ip"": ""255.255.255.255"", + ""url"": ""http://www.sendgrid.com/"", + ""asm_group_id"": 10 } ] "; - IEnumerable events = await EventParser.ParseAsync(json); - events.Count().ShouldBe(1); + var events = EventParser.Parse(json).ToList(); + events.Count.ShouldBe(1); var groupUnSubscribeEvent = (GroupUnsubscribeEvent)events.Single(); groupUnSubscribeEvent.Email.ShouldBe("example@test.com"); groupUnSubscribeEvent.Timestamp.ShouldBe(DateTime.UnixEpoch.AddSeconds(1513299569)); @@ -338,27 +333,27 @@ public async Task GroupUnsubscribeEventTest() } [Fact] - public async Task GroupResubscribeEventTest() + public void GroupResubscribeEventTest() { var json = @" [ { - 'email': 'example@test.com', - 'timestamp': 1513299569, - 'smtp-id': '<14c5d75ce93.dfd.64b469@ismtpd-555>', - 'event': 'group_resubscribe', - 'category': 'cat facts', - 'sg_event_id': 'sg_event_id', - 'sg_message_id': 'sg_message_id', - 'useragent': 'Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727)', - 'ip': '255.255.255.255', - 'url': 'http://www.sendgrid.com/', - 'asm_group_id': 10 + ""email"": ""example@test.com"", + ""timestamp"": 1513299569, + ""smtp-id"": ""<14c5d75ce93.dfd.64b469@ismtpd-555>"", + ""event"": ""group_resubscribe"", + ""category"": ""cat facts"", + ""sg_event_id"": ""sg_event_id"", + ""sg_message_id"": ""sg_message_id"", + ""useragent"": ""Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727)"", + ""ip"": ""255.255.255.255"", + ""url"": ""http://www.sendgrid.com/"", + ""asm_group_id"": 10 } ] "; - IEnumerable events = await EventParser.ParseAsync(json); - events.Count().ShouldBe(1); + var events = EventParser.Parse(json).ToList(); + events.Count.ShouldBe(1); var groupUnSubscribeEvent = (GroupResubscribeEvent)events.Single(); groupUnSubscribeEvent.Email.ShouldBe("example@test.com"); groupUnSubscribeEvent.Timestamp.ShouldBe(DateTime.UnixEpoch.AddSeconds(1513299569)); diff --git a/examples/eventwebhook/consumer/Tests/EventWebhook.Tests/EventWebhook.Tests.csproj b/examples/eventwebhook/consumer/tests/EventWebhook.Tests/EventWebhook.Tests.csproj similarity index 50% rename from examples/eventwebhook/consumer/Tests/EventWebhook.Tests/EventWebhook.Tests.csproj rename to examples/eventwebhook/consumer/tests/EventWebhook.Tests/EventWebhook.Tests.csproj index 2f6ed144e..73bdb1b93 100644 --- a/examples/eventwebhook/consumer/Tests/EventWebhook.Tests/EventWebhook.Tests.csproj +++ b/examples/eventwebhook/consumer/tests/EventWebhook.Tests/EventWebhook.Tests.csproj @@ -1,25 +1,24 @@ - netcoreapp2.1 - + netcoreapp3.1 false - - - - + + + + - + - - Always + + PreserveNewest diff --git a/examples/eventwebhook/consumer/Tests/EventWebhook.Tests/TestData/events.json b/examples/eventwebhook/consumer/tests/EventWebhook.Tests/TestData/events.json similarity index 100% rename from examples/eventwebhook/consumer/Tests/EventWebhook.Tests/TestData/events.json rename to examples/eventwebhook/consumer/tests/EventWebhook.Tests/TestData/events.json diff --git a/examples/inbound-webhook-handler/.vscode/launch.json b/examples/inbound-webhook-handler/.vscode/launch.json index 47b437d9b..ad8887e68 100644 --- a/examples/inbound-webhook-handler/.vscode/launch.json +++ b/examples/inbound-webhook-handler/.vscode/launch.json @@ -9,9 +9,9 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceFolder}/src/inbound/bin/Debug/netcoreapp2.1/inbound.dll", + "program": "${workspaceFolder}/src/Inbound/bin/Debug/netcoreapp3.1/Inbound.dll", "args": [], - "cwd": "${workspaceFolder}/src/inbound", + "cwd": "${workspaceFolder}/src/Inbound", "stopAtEntry": false, "internalConsoleOptions": "openOnSessionStart", "launchBrowser": { diff --git a/examples/inbound-webhook-handler/.vscode/tasks.json b/examples/inbound-webhook-handler/.vscode/tasks.json index 25a4d86b5..6c359bace 100644 --- a/examples/inbound-webhook-handler/.vscode/tasks.json +++ b/examples/inbound-webhook-handler/.vscode/tasks.json @@ -7,7 +7,7 @@ "type": "process", "args": [ "build", - "${workspaceFolder}/src/inbound/inbound.csproj" + "${workspaceFolder}/src/Inbound/Inbound.csproj" ], "problemMatcher": "$msCompile" } diff --git a/examples/inbound-webhook-handler/Dockerfile b/examples/inbound-webhook-handler/Dockerfile index 5d5f9b080..803993d9a 100644 --- a/examples/inbound-webhook-handler/Dockerfile +++ b/examples/inbound-webhook-handler/Dockerfile @@ -1,21 +1,21 @@ -FROM microsoft/dotnet:2.1-sdk AS build -WORKDIR /App +FROM mcr.microsoft.com/dotnet/sdk:3.1 AS build +WORKDIR /app # copy csproj and restore as distinct layers COPY *.sln . -COPY Src/Inbound/*.csproj ./Src/Inbound/ -COPY Tests/Inbound.Tests/*.csproj ./Tests/Inbound.Tests/ +COPY src/Inbound/*.csproj ./src/Inbound/ +COPY tests/Inbound.Tests/*.csproj ./tests/Inbound.Tests/ RUN dotnet restore # copy everything else and build app -COPY Src/Inbound/. ./Src/Inbound/ -WORKDIR /App/Src/Inbound -RUN dotnet publish -c Release -o Out +COPY src/Inbound/. ./src/Inbound/ +WORKDIR /app/src/Inbound +RUN dotnet publish -c Release -o out -FROM microsoft/dotnet:2.1-aspnetcore-runtime AS runtime -WORKDIR /App -COPY --from=build /App/Src/Inbound/Out ./ +FROM mcr.microsoft.com/dotnet/aspnet:3.1 AS runtime +WORKDIR /app +COPY --from=build /app/src/Inbound/out ./ -RUN echo "ASPNETCORE_URLS=http://0.0.0.0:\$PORT\nDOTNET_RUNNING_IN_CONTAINER=true" > /App/SetupHerokuEnv.sh && chmod +x /App/SetupHerokuEnv.sh +RUN echo "ASPNETCORE_URLS=http://0.0.0.0:\$PORT\nDOTNET_RUNNING_IN_CONTAINER=true" > /app/SetupHerokuEnv.sh && chmod +x /app/SetupHerokuEnv.sh -CMD /bin/bash -c "source /App/SetupHerokuEnv.sh && dotnet Inbound.dll" +CMD /bin/bash -c "source /app/SetupHerokuEnv.sh && dotnet Inbound.dll" diff --git a/examples/inbound-webhook-handler/README.md b/examples/inbound-webhook-handler/README.md index 2560a2483..907e2cfdd 100644 --- a/examples/inbound-webhook-handler/README.md +++ b/examples/inbound-webhook-handler/README.md @@ -39,7 +39,7 @@ cd sendgrid-csharp/examples/inbound-webhook-handler dotnet restore -dotnet run --project .\Src\Inbound\Inbound.csproj +dotnet run --project .\src\Inbound\Inbound.csproj ``` Above will start server listening on a random port like below diff --git a/examples/inbound-webhook-handler/SendGridInbound.sln b/examples/inbound-webhook-handler/SendGridInbound.sln index 55234eeeb..0566bfad1 100644 --- a/examples/inbound-webhook-handler/SendGridInbound.sln +++ b/examples/inbound-webhook-handler/SendGridInbound.sln @@ -3,13 +3,13 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26124.0 MinimumVisualStudioVersion = 15.0.26124.0 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Src", "Src", "{5E71A0CA-F2E2-4762-B020-29F1D8682F75}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5E71A0CA-F2E2-4762-B020-29F1D8682F75}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Inbound", "Src\Inbound\Inbound.csproj", "{9449C214-54EF-40A9-AAB3-4FE212BECA23}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Inbound", "src\Inbound\Inbound.csproj", "{9449C214-54EF-40A9-AAB3-4FE212BECA23}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{1446F41E-1766-4B3E-B7AC-C8766A3E1751}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{1446F41E-1766-4B3E-B7AC-C8766A3E1751}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Inbound.Tests", "Tests\Inbound.Tests\Inbound.Tests.csproj", "{0AF26ED1-3F2D-40F5-9A01-FE5955A8F927}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Inbound.Tests", "tests\Inbound.Tests\Inbound.Tests.csproj", "{0AF26ED1-3F2D-40F5-9A01-FE5955A8F927}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/examples/inbound-webhook-handler/Src/Inbound/Controllers/InboundController.cs b/examples/inbound-webhook-handler/Src/Inbound/Controllers/InboundController.cs deleted file mode 100644 index a8fa52b83..000000000 --- a/examples/inbound-webhook-handler/Src/Inbound/Controllers/InboundController.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Inbound.Parsers; -using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; - -namespace Inbound.Controllers -{ - [Route("/")] - [ApiController] - public class InboundController : Controller - { - [HttpGet] - public IActionResult Index() - { - return View(); - } - - // Process POST from Inbound Parse and print received data. - [HttpPost] - [Route("inbound")] - public IActionResult InboundParse() - { - InboundWebhookParser _inboundParser = new InboundWebhookParser(Request.Body); - - var inboundEmail = _inboundParser.Parse(); - - return Ok(); - } - - private void Log(IDictionary keyValues) - { - if(keyValues == null) - { - return; - } - Console.WriteLine(JsonConvert.SerializeObject(keyValues)); - } - } -} \ No newline at end of file diff --git a/examples/inbound-webhook-handler/Src/Inbound/Inbound.csproj b/examples/inbound-webhook-handler/Src/Inbound/Inbound.csproj deleted file mode 100644 index 95d6c7bfb..000000000 --- a/examples/inbound-webhook-handler/Src/Inbound/Inbound.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - netcoreapp2.1 - - - - - - - - - - - - diff --git a/examples/inbound-webhook-handler/Src/Inbound/Program.cs b/examples/inbound-webhook-handler/Src/Inbound/Program.cs deleted file mode 100644 index c2828bab0..000000000 --- a/examples/inbound-webhook-handler/Src/Inbound/Program.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace Inbound -{ - public class Program - { - public static void Main(string[] args) - { - CreateWebHostBuilder(args).Build().Run(); - } - - public static IWebHostBuilder CreateWebHostBuilder(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseStartup(); - } -} diff --git a/examples/inbound-webhook-handler/Tests/Inbound.Tests/Inbound.Tests.csproj b/examples/inbound-webhook-handler/Tests/Inbound.Tests/Inbound.Tests.csproj deleted file mode 100644 index 6d21a9f9d..000000000 --- a/examples/inbound-webhook-handler/Tests/Inbound.Tests/Inbound.Tests.csproj +++ /dev/null @@ -1,44 +0,0 @@ - - - - netcoreapp2.1 - - false - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - - - - - C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore\2.1.1\lib\netstandard2.0\Microsoft.AspNetCore.dll - - - - - - Always - - - Always - - - Always - - - - diff --git a/examples/inbound-webhook-handler/Tests/Inbound.Tests/IntegrationTests/InboundEndpointsTests.cs b/examples/inbound-webhook-handler/Tests/Inbound.Tests/IntegrationTests/InboundEndpointsTests.cs deleted file mode 100644 index bd3edcf2f..000000000 --- a/examples/inbound-webhook-handler/Tests/Inbound.Tests/IntegrationTests/InboundEndpointsTests.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Testing; -using Shouldly; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Xunit; - -namespace Inbound.Tests.IntegrationTests -{ - public class InboundEndpointsTests : IClassFixture> - { - private readonly WebApplicationFactory applicationFactory; - - public InboundEndpointsTests(WebApplicationFactory factory) - => applicationFactory = factory; - - [Fact] - public async Task Get_IndexPageReturnsSuccessAndCorrectContentType() - { - const string URL = "/"; - - var client = applicationFactory.CreateClient(); - var response = await client.GetAsync(URL); - response.EnsureSuccessStatusCode(); - response.Content.Headers.ContentType.MediaType.ShouldBe("text/html"); - } - - [Fact] - public async Task Get_InboundEndpointReturnsNotFound() - { - const string URL = "/inbound"; - var client = applicationFactory.CreateClient(); - var response = await client.GetAsync(URL); - response.StatusCode.ShouldBe(HttpStatusCode.NotFound); - } - - [Fact] - public async Task Post_InboundEndpointWithDefaultPayload() - { - const string URL = "/inbound"; - var data = File.ReadAllTextAsync("sample_data/default_data.txt").Result; - - var content = new StringContent(data); - content.Headers.Clear(); - content.Headers.Add("Content-Type", "multipart/form-data; boundary=xYzZY"); - - var client = applicationFactory.CreateClient(); - var response = await client.PostAsync(URL, content); - response.EnsureSuccessStatusCode(); - } - - [Fact] - public async Task Post_InboundEndpointWithRawPayloadWithAttachments() - { - const string URL = "/inbound"; - var data = File.ReadAllTextAsync("sample_data/raw_data_with_attachments.txt").Result; - - var content = new StringContent(data); - content.Headers.Clear(); - content.Headers.Add("Content-Type", "multipart/form-data; boundary=xYzZY"); - - var client = applicationFactory.CreateClient(); - var response = await client.PostAsync(URL, content); - response.EnsureSuccessStatusCode(); - } - } -} diff --git a/examples/inbound-webhook-handler/src/Inbound/Controllers/HomeController.cs b/examples/inbound-webhook-handler/src/Inbound/Controllers/HomeController.cs new file mode 100644 index 000000000..a6b3c9873 --- /dev/null +++ b/examples/inbound-webhook-handler/src/Inbound/Controllers/HomeController.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Inbound.Controllers +{ + [Route("/")] + public class HomeController : Controller + { + // GET + public IActionResult Index() + { + return View(); + } + } +} \ No newline at end of file diff --git a/examples/inbound-webhook-handler/src/Inbound/Controllers/InboundController.cs b/examples/inbound-webhook-handler/src/Inbound/Controllers/InboundController.cs new file mode 100644 index 000000000..b04894826 --- /dev/null +++ b/examples/inbound-webhook-handler/src/Inbound/Controllers/InboundController.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using Inbound.Parsers; +using Microsoft.AspNetCore.Mvc; + +namespace Inbound.Controllers +{ + [Route("/inbound")] + [ApiController] + public class InboundController : ControllerBase + { + // Process POST from Inbound Parse and print received data. + [HttpPost] + public async Task InboundParse() + { + var inboundEmail = await InboundWebhookParser.ParseAsync(Request.Body); + + return Ok(inboundEmail); + } + } +} \ No newline at end of file diff --git a/examples/inbound-webhook-handler/src/Inbound/Inbound.csproj b/examples/inbound-webhook-handler/src/Inbound/Inbound.csproj new file mode 100644 index 000000000..e0d047fc8 --- /dev/null +++ b/examples/inbound-webhook-handler/src/Inbound/Inbound.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp3.1 + + + + + PreserveNewest + + + appsettings.json + PreserveNewest + + + + + + + + diff --git a/examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmail.cs b/examples/inbound-webhook-handler/src/Inbound/Models/InboundEmail.cs similarity index 98% rename from examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmail.cs rename to examples/inbound-webhook-handler/src/Inbound/Models/InboundEmail.cs index b3aa5dd03..bd6e251e7 100644 --- a/examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmail.cs +++ b/examples/inbound-webhook-handler/src/Inbound/Models/InboundEmail.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Text; +using System.Text.Json.Serialization; namespace Inbound.Models { @@ -14,6 +15,7 @@ public class InboundEmail /// /// The headers. /// + [JsonIgnore] public KeyValuePair[] Headers { get; set; } /// @@ -121,6 +123,7 @@ public class InboundEmail /// /// The charsets. /// + [JsonIgnore] public KeyValuePair[] Charsets { get; set; } /// @@ -140,6 +143,4 @@ public class InboundEmail /// public string RawEmail { get; set; } } - - } diff --git a/examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmailAddress.cs b/examples/inbound-webhook-handler/src/Inbound/Models/InboundEmailAddress.cs similarity index 83% rename from examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmailAddress.cs rename to examples/inbound-webhook-handler/src/Inbound/Models/InboundEmailAddress.cs index 43e0ea724..689ee4b4f 100644 --- a/examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmailAddress.cs +++ b/examples/inbound-webhook-handler/src/Inbound/Models/InboundEmailAddress.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Inbound.Models { @@ -13,7 +13,7 @@ public class InboundEmailAddress /// /// The email. /// - [JsonProperty("email", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("email")] public string Email { get; set; } /// @@ -22,7 +22,7 @@ public class InboundEmailAddress /// /// The name. /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("name")] public string Name { get; set; } /// @@ -36,6 +36,4 @@ public InboundEmailAddress(string email, string name) Name = name; } } - - -} +} \ No newline at end of file diff --git a/examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmailAttachment.cs b/examples/inbound-webhook-handler/src/Inbound/Models/InboundEmailAttachment.cs similarity index 79% rename from examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmailAttachment.cs rename to examples/inbound-webhook-handler/src/Inbound/Models/InboundEmailAttachment.cs index b548cc44c..1ec9476a1 100644 --- a/examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmailAttachment.cs +++ b/examples/inbound-webhook-handler/src/Inbound/Models/InboundEmailAttachment.cs @@ -1,5 +1,5 @@ -using Newtonsoft.Json; -using System.IO; +using System.IO; +using System.Text.Json.Serialization; namespace Inbound.Models { @@ -22,7 +22,7 @@ public class InboundEmailAttachment /// /// The content-type. /// - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("type")] public string ContentType { get; set; } /// @@ -31,6 +31,7 @@ public class InboundEmailAttachment /// /// The data. /// + [JsonIgnore] public Stream Data { get; set; } /// @@ -39,7 +40,7 @@ public class InboundEmailAttachment /// /// The name of the file. /// - [JsonProperty("filename", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("filename")] public string FileName { get; set; } /// @@ -48,7 +49,7 @@ public class InboundEmailAttachment /// /// The name. /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("name")] public string Name { get; set; } /// @@ -57,9 +58,7 @@ public class InboundEmailAttachment /// /// The content identifier. /// - [JsonProperty("content-id", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("content-id")] public string ContentId { get; set; } } - - } diff --git a/examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmailEnvelope.cs b/examples/inbound-webhook-handler/src/Inbound/Models/InboundEmailEnvelope.cs similarity index 77% rename from examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmailEnvelope.cs rename to examples/inbound-webhook-handler/src/Inbound/Models/InboundEmailEnvelope.cs index f201cb0c7..8922fe95b 100644 --- a/examples/inbound-webhook-handler/Src/Inbound/Models/InboundEmailEnvelope.cs +++ b/examples/inbound-webhook-handler/src/Inbound/Models/InboundEmailEnvelope.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Inbound.Models { @@ -13,7 +13,7 @@ public class InboundEmailEnvelope /// /// To. /// - [JsonProperty("to", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("to")] public string[] To { get; set; } /// @@ -22,9 +22,7 @@ public class InboundEmailEnvelope /// /// From. /// - [JsonProperty("from", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("from")] public string From { get; set; } } - - } diff --git a/examples/inbound-webhook-handler/Src/Inbound/Parsers/InboundWebhookParser.cs b/examples/inbound-webhook-handler/src/Inbound/Parsers/InboundWebhookParser.cs similarity index 63% rename from examples/inbound-webhook-handler/Src/Inbound/Parsers/InboundWebhookParser.cs rename to examples/inbound-webhook-handler/src/Inbound/Parsers/InboundWebhookParser.cs index 29f79037f..e5a10f784 100644 --- a/examples/inbound-webhook-handler/Src/Inbound/Parsers/InboundWebhookParser.cs +++ b/examples/inbound-webhook-handler/src/Inbound/Parsers/InboundWebhookParser.cs @@ -1,33 +1,30 @@ -using HttpMultipartParser; -using Inbound.Models; -using Inbound.Util; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using HttpMultipartParser; +using Inbound.Models; +using Inbound.Util; namespace Inbound.Parsers { public class InboundWebhookParser { - private readonly Stream _payload; - - public InboundWebhookParser(Stream stream) + public static async Task ParseAsync(Stream stream) { - _payload = new MemoryStream(); - stream.CopyTo(_payload); - } + var payload = new MemoryStream(); + + // https://docs.microsoft.com/dotnet/core/compatibility/aspnetcore#http-synchronous-io-disabled-in-all-servers + await stream.CopyToAsync(payload); - public InboundEmail Parse() - { // It's important to rewind the stream - _payload.Position = 0; + payload.Position = 0; // Parse the multipart content received from SendGrid - var parser = new MultipartFormDataParser(_payload, Encoding.UTF8); + var parser = await MultipartFormDataParser.ParseAsync(payload); // Convert the 'headers' from a string into array of KeyValuePair var rawHeaders = parser @@ -37,24 +34,25 @@ public InboundEmail Parse() var headers = rawHeaders .Select(header => { - var splitHeader = header.Split(new[] { ": " }, StringSplitOptions.RemoveEmptyEntries); + var splitHeader = header.Split(new[] {": "}, StringSplitOptions.RemoveEmptyEntries); var key = splitHeader[0]; var value = splitHeader.Length > 1 ? splitHeader[1] : null; return new KeyValuePair(key, value); - }).ToArray(); + }) + .ToArray(); // Raw email var rawEmail = parser.GetParameterValue("email", string.Empty); - + // Combine the 'attachment-info' and Files into an array of Attachments - var attachmentInfoAsJObject = JObject.Parse(parser.GetParameterValue("attachment-info", "{}")); - var attachments = attachmentInfoAsJObject - .Properties() - .Select(prop => + var attachmentInfoAsJsonElement = JsonDocument.Parse(parser.GetParameterValue("attachment-info", "{}")).RootElement; + var attachments = new List(); + if (attachmentInfoAsJsonElement.ValueKind == JsonValueKind.Object) + { + foreach (var prop in attachmentInfoAsJsonElement.EnumerateObject()) { - var attachment = prop.Value.ToObject(); + var attachment = ToObject(prop.Value); attachment.Id = prop.Name; - var file = parser.Files.FirstOrDefault(f => f.Name == prop.Name); if (file != null) { @@ -62,39 +60,41 @@ public InboundEmail Parse() if (string.IsNullOrEmpty(attachment.ContentType)) attachment.ContentType = file.ContentType; if (string.IsNullOrEmpty(attachment.FileName)) attachment.FileName = file.FileName; } - - return attachment; - }).ToArray(); + attachments.Add(attachment); + } + } // Convert the 'envelope' from a JSON string into a strongly typed object - var envelope = JsonConvert.DeserializeObject(parser.GetParameterValue("envelope", "{}")); + var envelope = JsonSerializer.Deserialize(parser.GetParameterValue("envelope", "{}")); // Convert the 'charset' from a string into array of KeyValuePair - var charsetsAsJObject = JObject.Parse(parser.GetParameterValue("charsets", "{}")); - var charsets = charsetsAsJObject - .Properties() - .Select(prop => + var charsetsAsJsonElement = JsonDocument.Parse(parser.GetParameterValue("charsets", "{}")).RootElement; + var charsets = new List>(); + if (charsetsAsJsonElement.ValueKind == JsonValueKind.Object) + { + foreach (var prop in charsetsAsJsonElement.EnumerateObject()) { - var key = prop.Name; - var value = Encoding.GetEncoding(prop.Value.ToString()); - return new KeyValuePair(key, value); - }).ToArray(); + var value = prop.Value.GetString(); + if (string.IsNullOrWhiteSpace(value)) continue; + charsets.Add(new KeyValuePair(prop.Name,Encoding.GetEncoding(value))); + } + } // Create a dictionary of parsers, one parser for each desired encoding. // This is necessary because MultipartFormDataParser can only handle one // encoding and SendGrid can use different encodings for parameters such // as "from", "to", "text" and "html". var encodedParsers = charsets - .Where(c => c.Value != Encoding.UTF8) + .Where(c => !Equals(c.Value, Encoding.UTF8)) .Select(c => c.Value) .Distinct() .Select(encoding => { - _payload.Position = 0; // It's important to rewind the stream + payload.Position = 0; // It's important to rewind the stream return new { Encoding = encoding, - Parser = new MultipartFormDataParser(_payload, encoding) + Parser = MultipartFormDataParser.Parse(payload) }; }) .Union(new[] @@ -116,10 +116,10 @@ public InboundEmail Parse() var cc = InboundWebhookParserHelper.ParseEmailAddresses(rawCc); // Arrange the InboundEmail - var inboundEmail = new InboundEmail + return new InboundEmail { - Attachments = attachments, - Charsets = charsets, + Attachments = attachments.ToArray(), + Charsets = charsets.ToArray(), Dkim = InboundWebhookParserHelper.GetEncodedValue("dkim", charsets, encodedParsers, null), Envelope = envelope, From = from, @@ -135,8 +135,17 @@ public InboundEmail Parse() Cc = cc, RawEmail = rawEmail }; + } - return inboundEmail; + private static T ToObject(JsonElement element, JsonSerializerOptions options = null) + { + using var buffer = new MemoryStream(); + using (var writer = new Utf8JsonWriter(buffer)) + { + element.WriteTo(writer); + } + buffer.Seek(0, SeekOrigin.Begin); + return JsonSerializer.Deserialize(buffer.ToArray(), options); } } } \ No newline at end of file diff --git a/examples/inbound-webhook-handler/src/Inbound/Program.cs b/examples/inbound-webhook-handler/src/Inbound/Program.cs new file mode 100644 index 000000000..db1ce2018 --- /dev/null +++ b/examples/inbound-webhook-handler/src/Inbound/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Inbound +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/examples/inbound-webhook-handler/Src/Inbound/Startup.cs b/examples/inbound-webhook-handler/src/Inbound/Startup.cs similarity index 59% rename from examples/inbound-webhook-handler/Src/Inbound/Startup.cs rename to examples/inbound-webhook-handler/src/Inbound/Startup.cs index 4b9879581..af71ca2ea 100644 --- a/examples/inbound-webhook-handler/Src/Inbound/Startup.cs +++ b/examples/inbound-webhook-handler/src/Inbound/Startup.cs @@ -1,15 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Text.Json; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.HttpsPolicy; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Hosting; namespace Inbound { @@ -25,11 +19,17 @@ public Startup(IConfiguration configuration) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); + services.AddControllersWithViews() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; + options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.JsonSerializerOptions.IgnoreNullValues = true; + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { @@ -41,7 +41,10 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) } app.UseHttpsRedirection(); - app.UseMvc(); + app.UseRouting(); + app.UseEndpoints(endpoints =>{ + endpoints.MapControllers(); + }); } } } diff --git a/examples/inbound-webhook-handler/Src/Inbound/Util/Extensions.cs b/examples/inbound-webhook-handler/src/Inbound/Util/Extensions.cs similarity index 100% rename from examples/inbound-webhook-handler/Src/Inbound/Util/Extensions.cs rename to examples/inbound-webhook-handler/src/Inbound/Util/Extensions.cs diff --git a/examples/inbound-webhook-handler/Src/Inbound/Util/InboundWebhookParserHelper.cs b/examples/inbound-webhook-handler/src/Inbound/Util/InboundWebhookParserHelper.cs similarity index 90% rename from examples/inbound-webhook-handler/Src/Inbound/Util/InboundWebhookParserHelper.cs rename to examples/inbound-webhook-handler/src/Inbound/Util/InboundWebhookParserHelper.cs index e8f134542..844822398 100644 --- a/examples/inbound-webhook-handler/Src/Inbound/Util/InboundWebhookParserHelper.cs +++ b/examples/inbound-webhook-handler/src/Inbound/Util/InboundWebhookParserHelper.cs @@ -1,10 +1,10 @@ -using HttpMultipartParser; -using Inbound.Models; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; +using HttpMultipartParser; +using Inbound.Models; namespace Inbound.Util { @@ -13,7 +13,7 @@ public static class InboundWebhookParserHelper public static InboundEmailAddress[] ParseEmailAddresses(string rawEmailAddresses) { // Split on commas that have an even number of double-quotes following them - const string SPLIT_EMAIL_ADDRESSES = ",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"; + const string splitEmailAddresses = ",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"; /* When we stop supporting .NET 4.5.2 we will be able to use the following: @@ -21,9 +21,9 @@ public static InboundEmailAddress[] ParseEmailAddresses(string rawEmailAddresses */ if (string.IsNullOrEmpty(rawEmailAddresses)) return Enumerable.Empty().ToArray(); - var rawEmails = Regex.Split(rawEmailAddresses, SPLIT_EMAIL_ADDRESSES); + var rawEmails = Regex.Split(rawEmailAddresses, splitEmailAddresses); var addresses = rawEmails - .Select(rawEmail => ParseEmailAddress(rawEmail)) + .Select(ParseEmailAddress) .Where(address => address != null) .ToArray(); return addresses; @@ -66,7 +66,7 @@ private static MultipartFormDataParser GetEncodedParser(string parameterName, IE private static Encoding GetEncoding(string parameterName, IEnumerable> charsets) { - var encoding = charsets.Where(c => c.Key == parameterName); + var encoding = charsets.Where(c => c.Key == parameterName).ToList(); return encoding.Any() ? encoding.First().Value : Encoding.UTF8; } } diff --git a/examples/inbound-webhook-handler/Src/Inbound/Views/Inbound/Index.cshtml b/examples/inbound-webhook-handler/src/Inbound/Views/Home/Index.cshtml similarity index 100% rename from examples/inbound-webhook-handler/Src/Inbound/Views/Inbound/Index.cshtml rename to examples/inbound-webhook-handler/src/Inbound/Views/Home/Index.cshtml diff --git a/examples/inbound-webhook-handler/Src/Inbound/appsettings.Development.json b/examples/inbound-webhook-handler/src/Inbound/appsettings.Development.json similarity index 100% rename from examples/inbound-webhook-handler/Src/Inbound/appsettings.Development.json rename to examples/inbound-webhook-handler/src/Inbound/appsettings.Development.json diff --git a/examples/inbound-webhook-handler/Src/Inbound/appsettings.json b/examples/inbound-webhook-handler/src/Inbound/appsettings.json similarity index 100% rename from examples/inbound-webhook-handler/Src/Inbound/appsettings.json rename to examples/inbound-webhook-handler/src/Inbound/appsettings.json diff --git a/examples/inbound-webhook-handler/tests/Inbound.Tests/Inbound.Tests.csproj b/examples/inbound-webhook-handler/tests/Inbound.Tests/Inbound.Tests.csproj new file mode 100644 index 000000000..0d7dfc26a --- /dev/null +++ b/examples/inbound-webhook-handler/tests/Inbound.Tests/Inbound.Tests.csproj @@ -0,0 +1,26 @@ + + + + netcoreapp3.1 + false + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/examples/inbound-webhook-handler/tests/Inbound.Tests/IntegrationTests/InboundEndpointsTests.cs b/examples/inbound-webhook-handler/tests/Inbound.Tests/IntegrationTests/InboundEndpointsTests.cs new file mode 100644 index 000000000..70847de5a --- /dev/null +++ b/examples/inbound-webhook-handler/tests/Inbound.Tests/IntegrationTests/InboundEndpointsTests.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Mvc.Testing; +using Shouldly; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Xunit; + +namespace Inbound.Tests.IntegrationTests +{ + public class InboundEndpointsTests : IClassFixture> + { + private readonly WebApplicationFactory _factory; + + public InboundEndpointsTests(WebApplicationFactory factory) + => _factory = factory; + + [Fact] + public async Task Get_IndexPageReturnsSuccessAndCorrectContentType() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/"); + response.EnsureSuccessStatusCode(); + response.Content.Headers.ContentType.MediaType.ShouldBe("text/html"); + } + + [Fact] + public async Task Get_InboundEndpointShouldNotReturnsOk() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/inbound"); + response.StatusCode.ShouldNotBe(HttpStatusCode.OK); + } + + [Fact] + public async Task Post_InboundEndpointWithDefaultPayload() + { + var data = await File.ReadAllTextAsync("sample_data/default_data.txt"); + + using var content = new StringContent(data); + content.Headers.Clear(); + content.Headers.ContentType = new MediaTypeHeaderValue("multipart/form-data") + { + Parameters = { new NameValueHeaderValue("boundary","xYzZY") } + }; + + var client = _factory.CreateClient(); + var response = await client.PostAsync("/inbound", content); + response.EnsureSuccessStatusCode(); + } + + [Fact] + public async Task Post_InboundEndpointWithRawPayloadWithAttachments() + { + var data = await File.ReadAllTextAsync("sample_data/raw_data_with_attachments.txt"); + + using var content = new StringContent(data); + content.Headers.Clear(); + content.Headers.ContentType = new MediaTypeHeaderValue("multipart/form-data") + { + Parameters = { new NameValueHeaderValue("boundary","xYzZY") } + }; + + var client = _factory.CreateClient(); + var response = await client.PostAsync("/inbound", content); + response.EnsureSuccessStatusCode(); + } + } +} diff --git a/examples/inbound-webhook-handler/Tests/Inbound.Tests/Parsers/InboundWebhookParserTests.cs b/examples/inbound-webhook-handler/tests/Inbound.Tests/Parsers/InboundWebhookParserTests.cs similarity index 84% rename from examples/inbound-webhook-handler/Tests/Inbound.Tests/Parsers/InboundWebhookParserTests.cs rename to examples/inbound-webhook-handler/tests/Inbound.Tests/Parsers/InboundWebhookParserTests.cs index 3d2a8696e..968f0e4af 100644 --- a/examples/inbound-webhook-handler/Tests/Inbound.Tests/Parsers/InboundWebhookParserTests.cs +++ b/examples/inbound-webhook-handler/tests/Inbound.Tests/Parsers/InboundWebhookParserTests.cs @@ -1,10 +1,10 @@ -using Inbound.Models; -using Inbound.Parsers; +using Inbound.Parsers; using Shouldly; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using System.Threading.Tasks; using Xunit; namespace Inbound.Tests.Parsers @@ -12,15 +12,11 @@ namespace Inbound.Tests.Parsers public class InboundWebhookParserTests { [Fact] - public async void DefaultPayloadWithoutAttachments() + public async Task DefaultPayloadWithoutAttachments() { - Stream stream = new MemoryStream(); - await File.OpenRead("sample_data/default_data.txt").CopyToAsync(stream); - stream.Position = 0; - - var parser = new InboundWebhookParser(stream); - - InboundEmail inboundEmail = parser.Parse(); + var stream = File.OpenRead("sample_data/default_data.txt"); + + var inboundEmail = await InboundWebhookParser.ParseAsync(stream); inboundEmail.ShouldNotBeNull(); @@ -72,15 +68,11 @@ public async void DefaultPayloadWithoutAttachments() } [Fact] - public async void RawPayloadWithAttachments() + public async Task RawPayloadWithAttachments() { - Stream stream = new MemoryStream(); - await File.OpenRead("sample_data/raw_data_with_attachments.txt").CopyToAsync(stream); - stream.Position = 0; - - var parser = new InboundWebhookParser(stream); - - InboundEmail inboundEmail = parser.Parse(); + var stream = File.OpenRead("sample_data/raw_data_with_attachments.txt"); + + var inboundEmail = await InboundWebhookParser.ParseAsync(stream); inboundEmail.ShouldNotBeNull(); diff --git a/examples/inbound-webhook-handler/Tests/Inbound.Tests/sample_data/default_data.txt b/examples/inbound-webhook-handler/tests/Inbound.Tests/sample_data/default_data.txt similarity index 100% rename from examples/inbound-webhook-handler/Tests/Inbound.Tests/sample_data/default_data.txt rename to examples/inbound-webhook-handler/tests/Inbound.Tests/sample_data/default_data.txt diff --git a/examples/inbound-webhook-handler/Tests/Inbound.Tests/sample_data/raw_data_with_attachments.txt b/examples/inbound-webhook-handler/tests/Inbound.Tests/sample_data/raw_data_with_attachments.txt similarity index 100% rename from examples/inbound-webhook-handler/Tests/Inbound.Tests/sample_data/raw_data_with_attachments.txt rename to examples/inbound-webhook-handler/tests/Inbound.Tests/sample_data/raw_data_with_attachments.txt diff --git a/examples/inbound-webhook-handler/Tests/Inbound.Tests/sample_data/raw_email_with_attachments.txt b/examples/inbound-webhook-handler/tests/Inbound.Tests/sample_data/raw_email_with_attachments.txt similarity index 100% rename from examples/inbound-webhook-handler/Tests/Inbound.Tests/sample_data/raw_email_with_attachments.txt rename to examples/inbound-webhook-handler/tests/Inbound.Tests/sample_data/raw_email_with_attachments.txt diff --git a/src/SendGrid/Reliability/RetryDelegatingHandler.cs b/src/SendGrid/Reliability/RetryDelegatingHandler.cs index 617663d84..f480aec5b 100644 --- a/src/SendGrid/Reliability/RetryDelegatingHandler.cs +++ b/src/SendGrid/Reliability/RetryDelegatingHandler.cs @@ -63,7 +63,7 @@ protected override async Task SendAsync(HttpRequestMessage sent = true; } - catch (TaskCanceledException) + catch (OperationCanceledException) { numberOfAttempts++; diff --git a/src/SendGrid/SendGrid.csproj b/src/SendGrid/SendGrid.csproj index c6033a698..7aa2b1494 100644 --- a/src/SendGrid/SendGrid.csproj +++ b/src/SendGrid/SendGrid.csproj @@ -44,7 +44,7 @@ - + diff --git a/tests/SendGrid.Extensions.DependencyInjection.Tests/SendGrid.Extensions.DependencyInjection.Tests.csproj b/tests/SendGrid.Extensions.DependencyInjection.Tests/SendGrid.Extensions.DependencyInjection.Tests.csproj index 96da4a46c..329434ee5 100644 --- a/tests/SendGrid.Extensions.DependencyInjection.Tests/SendGrid.Extensions.DependencyInjection.Tests.csproj +++ b/tests/SendGrid.Extensions.DependencyInjection.Tests/SendGrid.Extensions.DependencyInjection.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1 + netcoreapp3.1 true false @@ -11,10 +11,13 @@ - - - - + + + + + + + diff --git a/tests/SendGrid.Tests/SendGrid.Tests.csproj b/tests/SendGrid.Tests/SendGrid.Tests.csproj index f61ad8027..72dead4f0 100644 --- a/tests/SendGrid.Tests/SendGrid.Tests.csproj +++ b/tests/SendGrid.Tests/SendGrid.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1 + netcoreapp3.1 true false @@ -11,12 +11,12 @@ - - - - + + + + - +