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); +}