diff --git a/.gitignore b/.gitignore
index 537870fd..4bef1e8b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -363,6 +363,10 @@ MigrationBackup/
# Fody - auto-generated XML schema
FodyWeavers.xsd
+### VisualStudio Patch ###
+# Additional files built by Visual Studio
+*.tlog
+
# WinMerge
*.bak
diff --git a/Source/StrongGrid.IntegrationTests/StrongGrid.IntegrationTests.csproj b/Source/StrongGrid.IntegrationTests/StrongGrid.IntegrationTests.csproj
index ae461ff1..e5c903a6 100644
--- a/Source/StrongGrid.IntegrationTests/StrongGrid.IntegrationTests.csproj
+++ b/Source/StrongGrid.IntegrationTests/StrongGrid.IntegrationTests.csproj
@@ -2,7 +2,7 @@
Exe
- netcoreapp3.1
+ net5.0
StrongGrid.IntegrationTests
StrongGrid.IntegrationTests
@@ -15,7 +15,7 @@
-
+
diff --git a/Source/StrongGrid.IntegrationTests/Tests/ContactsAndCustomFields.cs b/Source/StrongGrid.IntegrationTests/Tests/ContactsAndCustomFields.cs
index 335092a2..128ac053 100644
--- a/Source/StrongGrid.IntegrationTests/Tests/ContactsAndCustomFields.cs
+++ b/Source/StrongGrid.IntegrationTests/Tests/ContactsAndCustomFields.cs
@@ -84,6 +84,20 @@ public async Task RunAsync(IClient client, TextWriter log, CancellationToken can
if (contacts.Any())
{
+ var batchById = await client.Contacts.GetMultipleAsync(contacts.Take(10).Select(c => c.Id), cancellationToken).ConfigureAwait(false);
+ await log.WriteLineAsync($"Retrieved {batchById.Length} contacts by ID in a single API call.").ConfigureAwait(false);
+ foreach (var record in batchById)
+ {
+ await log.WriteLineAsync($"\t{record.FirstName} {record.LastName}").ConfigureAwait(false);
+ }
+
+ var batchByEmailAddress = await client.Contacts.GetMultipleByEmailAddressAsync(contacts.Take(10).Select(c => c.Email), cancellationToken).ConfigureAwait(false);
+ await log.WriteLineAsync($"Retrieved {batchByEmailAddress.Length} contacts by email address in a single API call.").ConfigureAwait(false);
+ foreach (var record in batchByEmailAddress)
+ {
+ await log.WriteLineAsync($"\t{record.FirstName} {record.LastName}").ConfigureAwait(false);
+ }
+
var contact = await client.Contacts.GetAsync(contacts.First().Id).ConfigureAwait(false);
await log.WriteLineAsync($"Retrieved contact {contact.Id}").ConfigureAwait(false);
await log.WriteLineAsync($"\tEmail: {contact.Email}").ConfigureAwait(false);
diff --git a/Source/StrongGrid.IntegrationTests/Tests/Mail.cs b/Source/StrongGrid.IntegrationTests/Tests/Mail.cs
index f1f0e0b1..c52a1b07 100644
--- a/Source/StrongGrid.IntegrationTests/Tests/Mail.cs
+++ b/Source/StrongGrid.IntegrationTests/Tests/Mail.cs
@@ -27,6 +27,7 @@ public async Task RunAsync(IBaseClient client, TextWriter log, CancellationToken
{
new MailPersonalization
{
+ From = new MailAddress("alternate_sender@example.com", "Alternate Sender"),
To = new[] { to1, to1 },
Cc = new[] { to1 },
Bcc = new[] { to1 },
diff --git a/Source/StrongGrid.UnitTests/StrongGrid.UnitTests.csproj b/Source/StrongGrid.UnitTests/StrongGrid.UnitTests.csproj
index bb5d1166..f7be1be2 100644
--- a/Source/StrongGrid.UnitTests/StrongGrid.UnitTests.csproj
+++ b/Source/StrongGrid.UnitTests/StrongGrid.UnitTests.csproj
@@ -1,14 +1,14 @@
- net461;net472;netcoreapp3.1
+ net461;net472;net5.0
StrongGrid.UnitTests
StrongGrid.UnitTests
-
-
+
+
diff --git a/Source/StrongGrid/BaseClient.cs b/Source/StrongGrid/BaseClient.cs
index 5e435df9..887b960a 100644
--- a/Source/StrongGrid/BaseClient.cs
+++ b/Source/StrongGrid/BaseClient.cs
@@ -277,7 +277,7 @@ public BaseClient(string apiKey, HttpClient httpClient, bool disposeClient, Stro
{
_mustDisposeHttpClient = disposeClient;
_httpClient = httpClient;
- _options = options ?? GetDefaultOptions();
+ _options = options;
_logger = logger ?? NullLogger.Instance;
_fluentClient = new FluentClient(new Uri(SENDGRID_V3_BASE_URI), httpClient)
@@ -337,12 +337,6 @@ public BaseClient(string apiKey, HttpClient httpClient, bool disposeClient, Stro
#region PUBLIC METHODS
- ///
- /// When overridden in a derived class, returns the default options.
- ///
- /// The default options.
- public abstract StrongGridClientOptions GetDefaultOptions();
-
///
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
///
diff --git a/Source/StrongGrid/Client.cs b/Source/StrongGrid/Client.cs
index b8465377..2205547b 100644
--- a/Source/StrongGrid/Client.cs
+++ b/Source/StrongGrid/Client.cs
@@ -11,6 +11,12 @@ namespace StrongGrid
///
public class Client : BaseClient, IClient
{
+ private static readonly StrongGridClientOptions _defaultOptions = new StrongGridClientOptions()
+ {
+ LogLevelSuccessfulCalls = LogLevel.Debug,
+ LogLevelFailedCalls = LogLevel.Error
+ };
+
#region PROPERTIES
///
@@ -72,7 +78,7 @@ public class Client : BaseClient, IClient
/// Options for the SendGrid client.
/// Logger.
public Client(string apiKey, StrongGridClientOptions options = null, ILogger logger = null)
- : base(apiKey, null, false, options, logger)
+ : base(apiKey, null, false, options ?? _defaultOptions, logger)
{
Init();
}
@@ -85,7 +91,7 @@ public Client(string apiKey, StrongGridClientOptions options = null, ILogger log
/// Options for the SendGrid client.
/// Logger.
public Client(string apiKey, IWebProxy proxy, StrongGridClientOptions options = null, ILogger logger = null)
- : base(apiKey, new HttpClient(new HttpClientHandler { Proxy = proxy, UseProxy = proxy != null }), true, options, logger)
+ : base(apiKey, new HttpClient(new HttpClientHandler { Proxy = proxy, UseProxy = proxy != null }), true, options ?? _defaultOptions, logger)
{
Init();
}
@@ -98,7 +104,7 @@ public Client(string apiKey, IWebProxy proxy, StrongGridClientOptions options =
/// Options for the SendGrid client.
/// Logger.
public Client(string apiKey, HttpMessageHandler handler, StrongGridClientOptions options = null, ILogger logger = null)
- : base(apiKey, new HttpClient(handler), true, options, logger)
+ : base(apiKey, new HttpClient(handler), true, options ?? _defaultOptions, logger)
{
Init();
}
@@ -111,30 +117,13 @@ public Client(string apiKey, HttpMessageHandler handler, StrongGridClientOptions
/// Options for the SendGrid client.
/// Logger.
public Client(string apiKey, HttpClient httpClient, StrongGridClientOptions options = null, ILogger logger = null)
- : base(apiKey, httpClient, false, options, logger)
+ : base(apiKey, httpClient, false, options ?? _defaultOptions, logger)
{
Init();
}
#endregion
- #region PUBLIC METHODS
-
- ///
- /// Return the default options.
- ///
- /// The default options.
- public override StrongGridClientOptions GetDefaultOptions()
- {
- return new StrongGridClientOptions()
- {
- LogLevelSuccessfulCalls = LogLevel.Debug,
- LogLevelFailedCalls = LogLevel.Error
- };
- }
-
- #endregion
-
#region PRIVATE METHODS
private void Init()
diff --git a/Source/StrongGrid/Extensions/Internal.cs b/Source/StrongGrid/Extensions/Internal.cs
index 4d027ec0..28cd79c2 100644
--- a/Source/StrongGrid/Extensions/Internal.cs
+++ b/Source/StrongGrid/Extensions/Internal.cs
@@ -7,6 +7,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
@@ -22,30 +23,45 @@ namespace StrongGrid
///
internal static class Internal
{
+ internal enum UnixTimePrecision
+ {
+ Seconds = 0,
+ Milliseconds = 1
+ }
+
+ private static readonly DateTime EPOCH = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+
///
- /// Converts a 'unix time' (which is expressed as the number of seconds since midnight on
- /// January 1st 1970) to a .Net .
+ /// Converts a 'unix time' (which is expressed as the number of seconds/milliseconds since
+ /// midnight on January 1st 1970) to a .Net .
///
/// The unix time.
+ /// The desired precision.
///
/// The .
///
- internal static DateTime FromUnixTime(this long unixTime)
+ internal static DateTime FromUnixTime(this long unixTime, UnixTimePrecision precision = UnixTimePrecision.Seconds)
{
- return Utils.Epoch.AddSeconds(unixTime);
+ if (precision == UnixTimePrecision.Seconds) return EPOCH.AddSeconds(unixTime);
+ if (precision == UnixTimePrecision.Milliseconds) return EPOCH.AddMilliseconds(unixTime);
+ throw new Exception($"Unknown precision: {precision}");
}
///
/// Converts a .Net into a 'Unix time' (which is expressed as the number
- /// of seconds since midnight on January 1st 1970).
+ /// of seconds/milliseconds since midnight on January 1st 1970).
///
/// The date.
+ /// The desired precision.
///
- /// The numer of seconds since midnight on January 1st 1970.
+ /// The numer of seconds/milliseconds since midnight on January 1st 1970.
///
- internal static long ToUnixTime(this DateTime date)
+ internal static long ToUnixTime(this DateTime date, UnixTimePrecision precision = UnixTimePrecision.Seconds)
{
- return Convert.ToInt64((date.ToUniversalTime() - Utils.Epoch).TotalSeconds);
+ var diff = date.ToUniversalTime() - EPOCH;
+ if (precision == UnixTimePrecision.Seconds) return Convert.ToInt64(diff.TotalSeconds);
+ if (precision == UnixTimePrecision.Milliseconds) return Convert.ToInt64(diff.TotalMilliseconds);
+ throw new Exception($"Unknown precision: {precision}");
}
///
@@ -279,16 +295,16 @@ internal static async Task> AsPaginatedResponse(this IRe
internal static IRequest WithJsonBody(this IRequest request, T body, bool omitCharSet = false)
{
return request.WithBody(bodyBuilder =>
- {
- var httpContent = bodyBuilder.Model(body, new MediaTypeHeaderValue("application/json"));
+ {
+ var httpContent = bodyBuilder.Model(body, new MediaTypeHeaderValue("application/json"));
- if (omitCharSet && !string.IsNullOrEmpty(httpContent.Headers.ContentType.CharSet))
- {
- httpContent.Headers.ContentType.CharSet = string.Empty;
- }
+ if (omitCharSet && !string.IsNullOrEmpty(httpContent.Headers.ContentType.CharSet))
+ {
+ httpContent.Headers.ContentType.CharSet = string.Empty;
+ }
- return httpContent;
- });
+ return httpContent;
+ });
}
///
@@ -652,6 +668,18 @@ internal static void CheckForSendGridErrors(this IResponse response)
}
}
+ internal static async Task CompressAsync(this Stream source)
+ {
+ var compressedStream = new MemoryStream();
+ using (var gzip = new GZipStream(compressedStream, CompressionMode.Compress, true))
+ {
+ await source.CopyToAsync(gzip).ConfigureAwait(false);
+ }
+
+ compressedStream.Position = 0;
+ return compressedStream;
+ }
+
private static async Task<(bool, string)> GetErrorMessage(HttpResponseMessage message)
{
// Assume there is no error
@@ -696,7 +724,7 @@ internal static void CheckForSendGridErrors(this IResponse response)
}
I have also seen cases where the JSON string looks like this:
- {
+ {
"error": "Name already exists"
}
*/
diff --git a/Source/StrongGrid/LegacyClient.cs b/Source/StrongGrid/LegacyClient.cs
index 855e8091..f30b4dfb 100644
--- a/Source/StrongGrid/LegacyClient.cs
+++ b/Source/StrongGrid/LegacyClient.cs
@@ -10,6 +10,12 @@ namespace StrongGrid
///
public class LegacyClient : BaseClient, ILegacyClient
{
+ private static readonly StrongGridClientOptions _defaultOptions = new StrongGridClientOptions()
+ {
+ LogLevelSuccessfulCalls = LogLevel.Debug,
+ LogLevelFailedCalls = LogLevel.Debug
+ };
+
#region PROPERTIES
///
@@ -79,7 +85,7 @@ public class LegacyClient : BaseClient, ILegacyClient
/// Options for the SendGrid client.
/// Logger.
public LegacyClient(string apiKey, StrongGridClientOptions options = null, ILogger logger = null)
- : base(apiKey, null, false, options, logger)
+ : base(apiKey, null, false, options ?? _defaultOptions, logger)
{
Init();
}
@@ -92,7 +98,7 @@ public LegacyClient(string apiKey, StrongGridClientOptions options = null, ILogg
/// Options for the SendGrid client.
/// Logger.
public LegacyClient(string apiKey, IWebProxy proxy, StrongGridClientOptions options = null, ILogger logger = null)
- : base(apiKey, new HttpClient(new HttpClientHandler { Proxy = proxy, UseProxy = proxy != null }), true, options, logger)
+ : base(apiKey, new HttpClient(new HttpClientHandler { Proxy = proxy, UseProxy = proxy != null }), true, options ?? _defaultOptions, logger)
{
Init();
}
@@ -105,7 +111,7 @@ public LegacyClient(string apiKey, IWebProxy proxy, StrongGridClientOptions opti
/// Options for the SendGrid client.
/// Logger.
public LegacyClient(string apiKey, HttpMessageHandler handler, StrongGridClientOptions options = null, ILogger logger = null)
- : base(apiKey, new HttpClient(handler), true, options, logger)
+ : base(apiKey, new HttpClient(handler), true, options ?? _defaultOptions, logger)
{
Init();
}
@@ -118,30 +124,13 @@ public LegacyClient(string apiKey, HttpMessageHandler handler, StrongGridClientO
/// Options for the SendGrid client.
/// Logger.
public LegacyClient(string apiKey, HttpClient httpClient, StrongGridClientOptions options = null, ILogger logger = null)
- : base(apiKey, httpClient, false, options, logger)
+ : base(apiKey, httpClient, false, options ?? _defaultOptions, logger)
{
Init();
}
#endregion
- #region PUBLIC METHODS
-
- ///
- /// Return the default options.
- ///
- /// The default options.
- public override StrongGridClientOptions GetDefaultOptions()
- {
- return new StrongGridClientOptions()
- {
- LogLevelSuccessfulCalls = LogLevel.Debug,
- LogLevelFailedCalls = LogLevel.Debug
- };
- }
-
- #endregion
-
#region PRIVATE METHODS
private void Init()
diff --git a/Source/StrongGrid/Models/MailPersonalization.cs b/Source/StrongGrid/Models/MailPersonalization.cs
index dad668b6..fd7b8d61 100644
--- a/Source/StrongGrid/Models/MailPersonalization.cs
+++ b/Source/StrongGrid/Models/MailPersonalization.cs
@@ -1,4 +1,4 @@
-using Newtonsoft.Json;
+using Newtonsoft.Json;
using StrongGrid.Utilities;
using System;
using System.Collections.Generic;
@@ -13,6 +13,16 @@ namespace StrongGrid.Models
///
public class MailPersonalization
{
+ ///
+ /// Gets or sets the 'From' email address used to deliver the message.
+ /// This address should be a verified sender in your Twilio SendGrid account.
+ ///
+ ///
+ /// From.
+ ///
+ [JsonProperty("from", NullValueHandling = NullValueHandling.Ignore)]
+ public MailAddress From { get; set; }
+
///
/// Gets or sets an array of recipients.
/// Each object within this array may contain the recipient’s
diff --git a/Source/StrongGrid/Properties/AssemblyInfo.cs b/Source/StrongGrid/Properties/AssemblyInfo.cs
index 0480d67b..faac79e3 100644
--- a/Source/StrongGrid/Properties/AssemblyInfo.cs
+++ b/Source/StrongGrid/Properties/AssemblyInfo.cs
@@ -1,3 +1,4 @@
+using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -9,3 +10,5 @@
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
+
+[assembly: CLSCompliant(true)]
diff --git a/Source/StrongGrid/Resources/Contacts.cs b/Source/StrongGrid/Resources/Contacts.cs
index 8b22a86f..d0907dc1 100644
--- a/Source/StrongGrid/Resources/Contacts.cs
+++ b/Source/StrongGrid/Resources/Contacts.cs
@@ -226,6 +226,75 @@ public Task GetAsync(string contactId, CancellationToken cancellationTo
.AsObject();
}
+ ///
+ /// Retrieve multiple contacts by ID.
+ ///
+ /// An enumeration of contact identifiers.
+ /// The cancellation token.
+ ///
+ /// An array of .
+ ///
+ public Task GetMultipleAsync(IEnumerable contactIds, CancellationToken cancellationToken = default)
+ {
+ var data = new JObject()
+ {
+ { "ids", new JArray(contactIds) }
+ };
+
+ return _client
+ .PostAsync($"{_endpoint}/batch")
+ .WithJsonBody(data)
+ .WithCancellationToken(cancellationToken)
+ .AsObject("result");
+ }
+
+ ///
+ /// Retrieve multiple contacts by email address.
+ ///
+ /// An enumeration of email addresses.
+ /// The cancellation token.
+ ///
+ /// An array of .
+ ///
+ public async Task GetMultipleByEmailAddressAsync(IEnumerable emailAdresses, CancellationToken cancellationToken = default)
+ {
+ var data = new JObject()
+ {
+ { "emails", new JArray(emailAdresses) }
+ };
+
+ var response = await _client
+ .PostAsync($"{_endpoint}/search/emails")
+ .WithJsonBody(data)
+ .WithCancellationToken(cancellationToken)
+ .AsResponse()
+ .ConfigureAwait(false);
+
+ // If no contact is found, SendGrid return HTTP 404
+ if (response.Status == HttpStatusCode.NotFound)
+ {
+ return Array.Empty();
+ }
+
+ var result = await response.AsRawJsonObject("result").ConfigureAwait(false);
+ var contacts = new List();
+#if DEBUG
+ var errors = new List<(string EmailAddress, string ErrorMessage)>();
+#endif
+
+ foreach (var record in result)
+ {
+ var emailAddress = record.Key;
+ var resultValue = (JObject)record.Value;
+ if (resultValue["contact"] != null) contacts.Add(resultValue["contact"].ToObject());
+#if DEBUG
+ if (resultValue["error"] != null) errors.Add((emailAddress, resultValue["error"].Value()));
+#endif
+ }
+
+ return contacts.ToArray();
+ }
+
///
/// Searches for contacts matching the specified conditions.
///
@@ -316,15 +385,15 @@ public Task SearchAsync(IEnumerable
- /// Request all contacts to be exported.
+ /// Import contacts.
///
- /// Use the "job id" returned by this method with the CheckExportJobStatusAsync
- /// method to verify if the export job is completed.
+ /// Use the "job id" returned by this method with the GetImportJobAsync
+ /// method to verify if the import job is completed.
///
/// The stream containing the data to import.
- /// File type for export file. Choose from json or csv.
+ /// File type for import file. Choose from json or csv.
/// List of field_definition IDs to map the uploaded CSV columns to. For example, [null, "w1", "_rf1"] means to skip Col[0], map Col[1] => CustomField w1, map Col[2] => ReservedField _rf1.
- /// Ids of the contact lists you want to export.
+ /// All contacts will be added to each of the specified lists.
/// The cancellation token.
///
/// The job id.
@@ -333,7 +402,7 @@ public async Task ImportFromStreamAsync(Stream stream, FileType fileType
{
var data = new JObject();
data.AddPropertyIfValue("list_ids", listIds);
- data.AddPropertyIfValue("file_type", fileType);
+ data.AddPropertyIfEnumValue("file_type", (Parameter)fileType);
data.AddPropertyIfValue("field_mappings", fieldsMapping);
var importRequest = await _client
@@ -343,15 +412,20 @@ public async Task ImportFromStreamAsync(Stream stream, FileType fileType
.AsRawJsonObject()
.ConfigureAwait(false);
- var importJobId = importRequest["id"].Value();
- var uploadUrl = importRequest["upload_url"].Value();
- var uploadHeaders = importRequest["upload_headers"].Value[]>();
+ var importJobId = importRequest.GetPropertyValue("job_id");
+ var uploadUrl = importRequest.GetPropertyValue("upload_uri");
+ var uploadHeaders = ((JArray)importRequest["upload_headers"])
+ .Select(hdr => new KeyValuePair(hdr.GetPropertyValue("header"), hdr.GetPropertyValue("value")))
+ .ToArray();
- var request = new HttpRequestMessage(HttpMethod.Post, uploadUrl)
+ var request = new HttpRequestMessage(HttpMethod.Put, uploadUrl)
{
- Content = new StreamContent(stream)
+ Content = new StreamContent(await stream.CompressAsync().ConfigureAwait(false))
};
- request.Headers.Clear();
+
+ request.Headers.AcceptEncoding.TryParseAdd("gzip");
+ request.Headers.Add("User-Agent", _client.BaseClient.DefaultRequestHeaders.UserAgent.First().ToString());
+
foreach (var header in uploadHeaders)
{
request.Headers.Add(header.Key, header.Value);
@@ -359,7 +433,8 @@ public async Task ImportFromStreamAsync(Stream stream, FileType fileType
using (var client = new HttpClient())
{
- await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
+ var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
+ if (!response.IsSuccessStatusCode) throw new SendGridException($"File upload failed: {response.ReasonPhrase}", response, "Diagnostic log unavailable");
}
return importJobId;
diff --git a/Source/StrongGrid/Resources/IContacts.cs b/Source/StrongGrid/Resources/IContacts.cs
index b439fa7c..4d8ac65f 100644
--- a/Source/StrongGrid/Resources/IContacts.cs
+++ b/Source/StrongGrid/Resources/IContacts.cs
@@ -127,6 +127,26 @@ Task UpsertAsync(
///
Task GetAsync(string contactId, CancellationToken cancellationToken = default);
+ ///
+ /// Retrieve multiple contacts by ID.
+ ///
+ /// An enumeration of contact identifiers.
+ /// The cancellation token.
+ ///
+ /// An array of .
+ ///
+ Task GetMultipleAsync(IEnumerable contactIds, CancellationToken cancellationToken = default);
+
+ ///
+ /// Retrieve multiple contacts by email address.
+ ///
+ /// An enumeration of email addresses.
+ /// The cancellation token.
+ ///
+ /// An array of .
+ ///
+ Task GetMultipleByEmailAddressAsync(IEnumerable emailAdresses, CancellationToken cancellationToken = default);
+
///
/// Searches for contacts matching the specified conditions.
///
diff --git a/Source/StrongGrid/StrongGrid.csproj b/Source/StrongGrid/StrongGrid.csproj
index c0df2d6c..3657ffb8 100644
--- a/Source/StrongGrid/StrongGrid.csproj
+++ b/Source/StrongGrid/StrongGrid.csproj
@@ -1,7 +1,7 @@
- net461;net472;netstandard2.0
+ net461;net472;netstandard2.0;net5.0
anycpu
true
Library
@@ -35,17 +35,17 @@
-
+
-
+
-
+
@@ -56,15 +56,7 @@
-
-
- $(DefineConstants);NETFULL
-
-
-
- $(DefineConstants);NETSTANDARD
-
-
+
true
diff --git a/Source/StrongGrid/Utilities/MailPersonalizationConverter.cs b/Source/StrongGrid/Utilities/MailPersonalizationConverter.cs
index 9aa646d5..f19c61e2 100644
--- a/Source/StrongGrid/Utilities/MailPersonalizationConverter.cs
+++ b/Source/StrongGrid/Utilities/MailPersonalizationConverter.cs
@@ -1,9 +1,8 @@
-using Newtonsoft.Json;
+using Newtonsoft.Json;
using StrongGrid.Models;
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Reflection;
namespace StrongGrid.Utilities
{
@@ -71,13 +70,7 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
writer.WriteStartObject();
-#if NETSTANDARD1
- var props = value.GetType().GetTypeInfo().DeclaredProperties;
-#else
- var props = value.GetType().GetProperties();
-#endif
-
- foreach (var propertyInfo in props)
+ foreach (var propertyInfo in value.GetType().GetProperties())
{
var propertyCustomAttributes = propertyInfo.GetCustomAttributes(false);
var propertyConverterAttribute = propertyCustomAttributes.OfType().FirstOrDefault();
diff --git a/Source/StrongGrid/Utilities/Utils.cs b/Source/StrongGrid/Utilities/Utils.cs
index 374c613f..8d02f510 100644
--- a/Source/StrongGrid/Utilities/Utils.cs
+++ b/Source/StrongGrid/Utilities/Utils.cs
@@ -12,8 +12,6 @@ internal static class Utils
private static readonly byte[] Secp256R1Prefix = Convert.FromBase64String("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE");
private static readonly byte[] CngBlobPrefix = { 0x45, 0x43, 0x53, 0x31, 0x20, 0, 0, 0 };
- public static DateTime Epoch { get; } = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
-
public static RecyclableMemoryStreamManager MemoryStreamManager { get; } = new RecyclableMemoryStreamManager();
///
diff --git a/Source/StrongGrid/WebhookParser.cs b/Source/StrongGrid/WebhookParser.cs
index 516386bc..9699f4da 100644
--- a/Source/StrongGrid/WebhookParser.cs
+++ b/Source/StrongGrid/WebhookParser.cs
@@ -36,7 +36,7 @@ public class WebhookParser
#region CTOR
-#if NETSTANDARD
+#if NETSTANDARD2_0 || NET5_0_OR_GREATER
static WebhookParser()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
@@ -109,15 +109,24 @@ public Event[] ParseSignedEventsWebhook(string requestBody, string publicKey, st
The 'ECDsa.ImportSubjectPublicKeyInfo' method was introduced in .NET core 3.0
and the DSASignatureFormat enum was introduced in .NET 5.0.
+ We can get rid of the code that relies on ECDsaCng and remove reference to
+ System.Security.Cryptography.Cng in csproj when we drop support for net461.
+
We can get rid of the 'ConvertECDSASignature' class and the Utils methods that
- convert public keys when we stop suporting .NET framework and .NET standard
+ convert public keys when we stop suporting .NET framework and .NET standard.
+
+ NET5_0_OR_GREATER was added to the NET SDK very recently and works fine on
+ AppVeyor's Windows image which runs NET SDK 5.0.201. However, it doesn't work
+ on their Ubuntu image because it's still on NET SDK 5.0.101. That's why I added
+ the seemingly redundant "NET5_0" in the conditional block below. It will be safe
+ to remove it when the SDK in AppVeyor's Ubuntu image is upgraded.
Note:
ECDsa is cross-platform and can be used on Windows and Linux/Ubuntu.
ECDsaCng is Windows only.
*/
-#if NET5_0_OR_GREATER
+#if NET5_0 || NET5_0_OR_GREATER
// Verify the signature
var eCDsa = ECDsa.Create();
eCDsa.ImportSubjectPublicKeyInfo(publicKeyBytes, out _);
@@ -139,7 +148,7 @@ ECDsaCng is Windows only.
}
});
var verified = eCDsa.VerifyData(data, sig, HashAlgorithmName.SHA256);
-#else
+#elif NETFRAMEWORK
// Convert the signature and public key provided by SendGrid into formats usable by the ECDsaCng .net crypto class
var sig = ConvertECDSASignature.LightweightConvertSignatureFromX9_62ToISO7816_8(256, signatureBytes);
var cngBlob = Utils.ConvertSecp256R1PublicKeyToEccPublicBlob(publicKeyBytes);
@@ -148,6 +157,8 @@ ECDsaCng is Windows only.
var cngKey = CngKey.Import(cngBlob, CngKeyBlobFormat.EccPublicBlob);
var eCDsaCng = new ECDsaCng(cngKey);
var verified = eCDsaCng.VerifyData(data, sig);
+#else
+#error Unhandled TFM
#endif
if (!verified)
diff --git a/build.cake b/build.cake
index 1bdc5bf6..86ab4c18 100644
--- a/build.cake
+++ b/build.cake
@@ -7,7 +7,7 @@
#tool nuget:?package=xunit.runner.console&version=2.4.1
// Install addins.
-#addin nuget:?package=Cake.Coveralls&version=1.0.0
+#addin nuget:?package=Cake.Coveralls&version=1.0.1
///////////////////////////////////////////////////////////////////////////////
@@ -49,6 +49,9 @@ var outputDir = "./artifacts/";
var codeCoverageDir = $"{outputDir}CodeCoverage/";
var benchmarkDir = $"{outputDir}Benchmark/";
+var solutionFile = $"{sourceFolder}{libraryName}.sln";
+var sourceProject = $"{sourceFolder}{libraryName}/{libraryName}.csproj";
+var integrationTestsProject = $"{sourceFolder}{libraryName}.IntegrationTests/{libraryName}.IntegrationTests.csproj";
var unitTestsProject = $"{sourceFolder}{libraryName}.UnitTests/{libraryName}.UnitTests.csproj";
var benchmarkProject = $"{sourceFolder}{libraryName}.Benchmark/{libraryName}.Benchmark.csproj";
@@ -144,8 +147,28 @@ Task("AppVeyor-Build_Number")
});
});
+Task("Remove-Integration-Tests")
+ .WithCriteria(() => AppVeyor.IsRunningOnAppVeyor)
+ .Does(() =>
+{
+ // Integration tests are intended to be used for debugging purposes and not intended to be executed in CI environment.
+ // Also, the runner for these tests contains windows-specific code (such as resizing window, moving window to center of screen, etc.)
+ // which can cause problems when attempting to run unit tests on an Ubuntu image on AppVeyor.
+
+ Information("Here are the projects in the solution before removing integration tests:");
+ DotNetCoreTool(solutionFile, "sln", "list");
+ Information("");
+
+ DotNetCoreTool(solutionFile, "sln", $"remove {integrationTestsProject.TrimStart(sourceFolder, StringComparison.OrdinalIgnoreCase)}");
+ Information("");
+
+ Information("Here are the projects in the solution after removing integration tests:");
+ DotNetCoreTool(solutionFile, "sln", "list");
+});
+
Task("Clean")
.IsDependentOn("AppVeyor-Build_Number")
+ .IsDependentOn("Remove-Integration-Tests")
.Does(() =>
{
// Clean solution directories.
@@ -178,11 +201,12 @@ Task("Build")
.IsDependentOn("Restore-NuGet-Packages")
.Does(() =>
{
- DotNetCoreBuild($"{sourceFolder}{libraryName}.sln", new DotNetCoreBuildSettings
+ DotNetCoreBuild(solutionFile, new DotNetCoreBuildSettings
{
Configuration = configuration,
NoRestore = true,
- ArgumentCustomization = args => args.Append("/p:SemVer=" + versionInfo.LegacySemVerPadded)
+ ArgumentCustomization = args => args.Append("/p:SemVer=" + versionInfo.LegacySemVerPadded),
+ Framework = IsRunningOnWindows() ? null : "net5.0"
});
});
@@ -195,7 +219,7 @@ Task("Run-Unit-Tests")
NoBuild = true,
NoRestore = true,
Configuration = configuration,
- Framework = IsRunningOnWindows() ? null : "netcoreapp3.1"
+ Framework = IsRunningOnWindows() ? null : "net5.0"
});
});
@@ -271,7 +295,7 @@ Task("Create-NuGet-Package")
}
};
- DotNetCorePack($"{sourceFolder}{libraryName}/{libraryName}.csproj", settings);
+ DotNetCorePack(sourceProject, settings);
});
Task("Upload-AppVeyor-Artifacts")
@@ -432,9 +456,7 @@ Task("AppVeyor")
.IsDependentOn("Publish-GitHub-Release");
Task("AppVeyor-Ubuntu")
- .IsDependentOn("Run-Unit-Tests")
- .IsDependentOn("Create-NuGet-Package")
- .IsDependentOn("Upload-AppVeyor-Artifacts");
+ .IsDependentOn("Run-Unit-Tests");
Task("Default")
.IsDependentOn("Run-Unit-Tests")
@@ -446,3 +468,25 @@ Task("Default")
///////////////////////////////////////////////////////////////////////////////
RunTarget(target);
+
+
+
+///////////////////////////////////////////////////////////////////////////////
+// PRIVATE METHODS
+///////////////////////////////////////////////////////////////////////////////
+private static string TrimStart(this string source, string value, StringComparison comparisonType)
+{
+ if (source == null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ int valueLength = value.Length;
+ int startIndex = 0;
+ while (source.IndexOf(value, startIndex, comparisonType) == startIndex)
+ {
+ startIndex += valueLength;
+ }
+
+ return source.Substring(startIndex);
+}