diff --git a/Source/StrongGrid.IntegrationTests/Program.cs b/Source/StrongGrid.IntegrationTests/Program.cs index 7fc6fae5..21183b38 100644 --- a/Source/StrongGrid.IntegrationTests/Program.cs +++ b/Source/StrongGrid.IntegrationTests/Program.cs @@ -1,5 +1,6 @@ using StrongGrid.Logging; using StrongGrid.Models; +using StrongGrid.Models.Search; using StrongGrid.Utilities; using System; using System.Collections.Generic; @@ -144,9 +145,12 @@ static async Task Main() // Return code indicating success/failure var resultCode = (int)ResultCodes.Success; - if (results.Any(result => result.ResultCode == ResultCodes.Exception)) resultCode = (int)ResultCodes.Exception; - else if (results.Any(result => result.ResultCode == ResultCodes.Cancelled)) resultCode = (int)ResultCodes.Cancelled; - else resultCode = (int)results.First(result => result.ResultCode != ResultCodes.Success).ResultCode; + if (results.Any(result => result.ResultCode != ResultCodes.Success)) + { + if (results.Any(result => result.ResultCode == ResultCodes.Exception)) resultCode = (int)ResultCodes.Exception; + else if (results.Any(result => result.ResultCode == ResultCodes.Cancelled)) resultCode = (int)ResultCodes.Cancelled; + else resultCode = (int)results.First(result => result.ResultCode != ResultCodes.Success).ResultCode; + } return await Task.FromResult(resultCode); } @@ -1203,16 +1207,20 @@ private static async Task EmailActivities(IClient client, TextWriter log, Cancel await log.WriteLineAsync("\n***** EMAIL ACTIVITIES *****\n").ConfigureAwait(false); // REQUEST THE ACTIVITIES - var allActivities = await client.EmailActivities.SearchMessagesAsync(20, cancellationToken).ConfigureAwait(false); + var allActivities = await client.EmailActivities.SearchAsync(null, 20, cancellationToken).ConfigureAwait(false); await log.WriteLineAsync($"Activities requested. Found {allActivities.Count()} activities.").ConfigureAwait(false); + if (!allActivities.Any()) return; + // REQUEST THE ACTIVITIES FOR A SPECIFIC MESSAGE - if (allActivities.Any()) - { - var messageId = allActivities.First().MessageId; - var summary = await client.EmailActivities.GetMessageSummaryAsync(messageId, cancellationToken).ConfigureAwait(false); - await log.WriteLineAsync($"There are {summary.Events.Count()} events associated with message {summary.MessageId}.").ConfigureAwait(false); - } + var messageId = allActivities.First().MessageId; + var summary = await client.EmailActivities.GetMessageSummaryAsync(messageId, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync($"There are {summary.Events.Count()} events associated with message {summary.MessageId}.").ConfigureAwait(false); + + // REQUEST THE ACTIVITIES OF A GIVEN TYPE + var activityType = allActivities.First().ActivityType; + var activities = await client.EmailActivities.SearchAsync(new SearchCriteriaEqual(FilterField.ActivityType, activityType), 20, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync($"There are {activities.Count()} '{activityType}' events.").ConfigureAwait(false); } // to get your public IP address we loop through an array diff --git a/Source/StrongGrid.UnitTests/Resources/EmailActivitiesTests.cs b/Source/StrongGrid.UnitTests/Resources/EmailActivitiesTests.cs new file mode 100644 index 00000000..ffb2b766 --- /dev/null +++ b/Source/StrongGrid.UnitTests/Resources/EmailActivitiesTests.cs @@ -0,0 +1,165 @@ +using Newtonsoft.Json; +using RichardSzalay.MockHttp; +using Shouldly; +using StrongGrid.Models; +using StrongGrid.Models.Search; +using StrongGrid.Resources; +using StrongGrid.Utilities; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace StrongGrid.UnitTests.Resources +{ + public class EmailActivitiesTests + { + #region FIELDS + + private const string ENDPOINT = "messages"; + + private const string SINGLE_MESSAGE = @"{ + 'from_email': 'test@example.com', + 'msg_id': 'thtIPCIcR_iFZDws2JCrwA.filter0004p3las1-2776-5ACA5525-31.1', + 'subject': 'Dear customer', + 'to_email': 'bob@example.com', + 'status': 'delivered', + 'opens_count': 2, + 'clicks_count': 1, + 'last_event_time': '2018-04-08T17:47:18Z' + }"; + + private const string NO_MESSAGES_FOUND = "{'messages':[]}"; + private const string ONE_MESSAGE_FOUND = "{'messages':[" + SINGLE_MESSAGE + "]}"; + private const string MULTIPLE_MESSAGES_FOUND = "{'messages':[" + + SINGLE_MESSAGE + "," + + SINGLE_MESSAGE + "," + + SINGLE_MESSAGE + + "]}"; + + #endregion + + [Fact] + public void Parse_json() + { + // Arrange + + // Act + var result = JsonConvert.DeserializeObject(SINGLE_MESSAGE); + + // Assert + result.ShouldNotBeNull(); + result.From.ShouldBe("test@example.com"); + result.MessageId.ShouldBe("thtIPCIcR_iFZDws2JCrwA.filter0004p3las1-2776-5ACA5525-31.1"); + result.Subject.ShouldBe("Dear customer"); + result.To.ShouldBe("bob@example.com"); + result.ActivityType.ShouldBe(EventType.Delivered); + result.OpensCount.ShouldBe(2); + result.ClicksCount.ShouldBe(1); + result.LastEventOn.ShouldBe(new DateTime(2018, 04, 08, 17, 47, 18, DateTimeKind.Utc)); + } + + [Fact] + public async Task SearchMessages_without_criteria() + { + // Arrange + var limit = 25; + + var mockHttp = new MockHttpMessageHandler(); + mockHttp.Expect(HttpMethod.Get, Utils.GetSendGridApiUri(ENDPOINT) + $"?limit={limit}&query=").Respond("application/json", NO_MESSAGES_FOUND); + + var client = Utils.GetFluentClient(mockHttp); + var emailActivities = (IEmailActivities)new EmailActivities(client); + + // Act + var result = await emailActivities.SearchAsync(null, limit, CancellationToken.None).ConfigureAwait(false); + + // Assert + mockHttp.VerifyNoOutstandingExpectation(); + mockHttp.VerifyNoOutstandingRequest(); + result.ShouldNotBeNull(); + result.Length.ShouldBe(0); + } + + [Fact] + public async Task SearchMessages_single_criteria() + { + // Arrange + var limit = 25; + + var mockHttp = new MockHttpMessageHandler(); + mockHttp.Expect(HttpMethod.Get, Utils.GetSendGridApiUri(ENDPOINT) + $"?limit={limit}&query=subject%3D%22thevalue%22").Respond("application/json", ONE_MESSAGE_FOUND); + + var client = Utils.GetFluentClient(mockHttp); + var emailActivities = (IEmailActivities)new EmailActivities(client); + + var criteria = new SearchCriteriaEqual(FilterField.Subject, "thevalue"); + + // Act + var result = await emailActivities.SearchAsync(criteria, limit, CancellationToken.None).ConfigureAwait(false); + + // Assert + mockHttp.VerifyNoOutstandingExpectation(); + mockHttp.VerifyNoOutstandingRequest(); + result.ShouldNotBeNull(); + result.Length.ShouldBe(1); + } + + [Fact] + public async Task SearchMessages_multiple_filter_conditions() + { + // Arrange + var limit = 25; + + var mockHttp = new MockHttpMessageHandler(); + mockHttp.Expect(HttpMethod.Get, Utils.GetSendGridApiUri(ENDPOINT) + $"?limit={limit}&query=campaign_name%3D%22value1%22+AND+unique_args%3D%22value2%22").Respond("application/json", ONE_MESSAGE_FOUND); + + var client = Utils.GetFluentClient(mockHttp); + var emailActivities = (IEmailActivities)new EmailActivities(client); + + var filterConditions = new[] + { + new SearchCriteriaEqual(FilterField.CampaignName, "value1"), + new SearchCriteriaEqual(FilterField.CustomArguments, "value2"), + }; + // Act + var result = await emailActivities.SearchAsync(filterConditions, limit, CancellationToken.None).ConfigureAwait(false); + + // Assert + mockHttp.VerifyNoOutstandingExpectation(); + mockHttp.VerifyNoOutstandingRequest(); + result.ShouldNotBeNull(); + result.Length.ShouldBe(1); + } + + [Fact] + public async Task SearchMessages_complex_filter_conditions() + { + // Arrange + var limit = 25; + + var mockHttp = new MockHttpMessageHandler(); + mockHttp.Expect(HttpMethod.Get, Utils.GetSendGridApiUri(ENDPOINT) + $"?limit={limit}&query=campaign_name%3D%22value1%22+OR+msg_id%3D%22value2%22+AND+subject%3D%22value3%22+AND+teammate%3D%22value4%22").Respond("application/json", ONE_MESSAGE_FOUND); + + var client = Utils.GetFluentClient(mockHttp); + var emailActivities = new EmailActivities(client); + + var filterConditions = new KeyValuePair>[] + { + new KeyValuePair>(SearchLogicalOperator.Or, new[] { new SearchCriteriaEqual(FilterField.CampaignName, "value1"), new SearchCriteriaEqual(FilterField.MessageId, "value2") }), + new KeyValuePair>(SearchLogicalOperator.And, new[] { new SearchCriteriaEqual(FilterField.Subject, "value3"), new SearchCriteriaEqual(FilterField.Teammate, "value4") }), + }; + + // Act + var result = await emailActivities.SearchAsync(filterConditions, limit, CancellationToken.None).ConfigureAwait(false); + + // Assert + mockHttp.VerifyNoOutstandingExpectation(); + mockHttp.VerifyNoOutstandingRequest(); + result.ShouldNotBeNull(); + result.Length.ShouldBe(1); + } + } +} diff --git a/Source/StrongGrid/Models/EmailMessageActivity.cs b/Source/StrongGrid/Models/EmailMessageActivity.cs index c883cf54..595000a8 100644 --- a/Source/StrongGrid/Models/EmailMessageActivity.cs +++ b/Source/StrongGrid/Models/EmailMessageActivity.cs @@ -14,7 +14,7 @@ public class EmailMessageActivity /// /// The 'from' address. /// - [JsonProperty("from_message", NullValueHandling = NullValueHandling.Ignore)] + [JsonProperty("from_email", NullValueHandling = NullValueHandling.Ignore)] public string From { get; set; } /// @@ -45,13 +45,13 @@ public class EmailMessageActivity public string To { get; set; } /// - /// Gets or sets the status. + /// Gets or sets the type of activity. /// /// /// The status. /// [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] - public string Status { get; set; } + public EventType ActivityType { get; set; } /// /// Gets or sets the number of time the message was opened by the recipient. diff --git a/Source/StrongGrid/Models/EventType.cs b/Source/StrongGrid/Models/EventType.cs index ce2a17f0..0baa8b63 100644 --- a/Source/StrongGrid/Models/EventType.cs +++ b/Source/StrongGrid/Models/EventType.cs @@ -29,6 +29,16 @@ public enum EventType [EnumMember(Value = "dropped")] Dropped, + /// + /// Message has not been delivered + /// + /// + /// I believe this is equivalent to 'Dropped'. I have only seen this EventType in the data + /// returned by the 'SearchAsync' method. + /// + [EnumMember(Value = "not_delivered")] + NotDelivered, + /// /// Message has been successfully delivered to the receiving server. /// diff --git a/Source/StrongGrid/Models/Search/FilterField.cs b/Source/StrongGrid/Models/Search/FilterField.cs new file mode 100644 index 00000000..f4387760 --- /dev/null +++ b/Source/StrongGrid/Models/Search/FilterField.cs @@ -0,0 +1,136 @@ +using System.Runtime.Serialization; + +namespace StrongGrid.Models.Search +{ + /// + /// Enumeration to indicate the filter field when searching for email activities + /// + public enum FilterField + { + /// + /// The identifier of the message + /// + [EnumMember(Value = "msg_id")] + MessageId, + + /// + /// The email address of the sender + /// + [EnumMember(Value = "from_email ")] + From, + + /// + /// The subject of the message + /// + [EnumMember(Value = "subject")] + Subject, + + /// + /// The email address of the recipient + /// + [EnumMember(Value = "to_email")] + To, + + /// + /// The type of email activity + /// + [EnumMember(Value = "status")] + ActivityType, + + /// + /// The identifier of the template + /// + [EnumMember(Value = "template_id")] + TemplateId, + + /// + /// The name of the template + /// + [EnumMember(Value = "template_name")] + TemplateName, + + /// + /// The name of the campaign + /// + [EnumMember(Value = "campaign_name")] + CampaignName, + + /// + /// The identifier of the campaign + /// + [EnumMember(Value = "campaign_id")] + CampaignId, + + /// + /// The identifier of the Api Key + /// + [EnumMember(Value = "api_key_id")] + ApiKeyId, + + /// + /// The name of the Api Key + /// + [EnumMember(Value = "api_key_name")] + ApiKeyName, + + /// + /// Seems like a duplicate of 'status'. + /// + [EnumMember(Value = "events")] + Events, + + /// + /// IP address of the person who sent the message + /// + [EnumMember(Value = "originating_ip")] + OriginatingIpAddress, + + /// + /// Custom tags that you create + /// + [EnumMember(Value = "categories")] + Categories, + + /// + /// Custom tracking arguments that you can attach to messages + /// + [EnumMember(Value = "unique_args")] + CustomArguments, + + /// + /// The SendGrid dedicated IP address used to send the email + /// + [EnumMember(Value = "outbound_ip")] + OutboundIpAddress, + + /// + /// Date and time of the last email activity + /// + [EnumMember(Value = "last_event_time")] + LastEventTime, + + /// + /// Number of clicks + /// + [EnumMember(Value = "clicks")] + Clicks, + + /// + /// The name of the unsubscribe group + /// + [EnumMember(Value = "unsubscribe_group_name")] + UnsubscribeGroupName, + + /// + /// The identified of the unsubscribe group + /// + [EnumMember(Value = "unsubscribe_group_id")] + UnsubscribeGroupId, + + /// + /// The teamates username + /// + [EnumMember(Value = "teammate")] + Teammate + } +} diff --git a/Source/StrongGrid/Models/Search/SearchConditionOperator.cs b/Source/StrongGrid/Models/Search/SearchConditionOperator.cs new file mode 100644 index 00000000..4df905ef --- /dev/null +++ b/Source/StrongGrid/Models/Search/SearchConditionOperator.cs @@ -0,0 +1,94 @@ +using System.Runtime.Serialization; + +namespace StrongGrid.Models.Search +{ + /// + /// Enumeration to indicate the filtering operator when searching email activities + /// + public enum SearchConditionOperator + { + /// + /// Equal + /// + [EnumMember(Value = "=")] + Equal, + + /// + /// Not equal + /// + [EnumMember(Value = "!=")] + NotEqual, + + /// + /// Greather than + /// + [EnumMember(Value = ">")] + GreaterThan, + + /// + /// Less than + /// + [EnumMember(Value = "<")] + LessThan, + + /// + /// Greather than or equal + /// + [EnumMember(Value = ">=")] + GreaterEqual, + + /// + /// Less than or equal + /// + [EnumMember(Value = "<=")] + LessEqual, + + /// + /// Between + /// + [EnumMember(Value = "BETWEEN")] + Beetween, + + /// + /// Not between + /// + [EnumMember(Value = "NOT BETWEEN")] + NotBetween, + + /// + /// In + /// + [EnumMember(Value = "IN")] + In, + + /// + /// Not in + /// + [EnumMember(Value = "NOT IN")] + NotIn, + + /// + /// Is + /// + [EnumMember(Value = "IS")] + Is, + + /// + /// Is not + /// + [EnumMember(Value = "IS NOT")] + IsNot, + + /// + /// Like + /// + [EnumMember(Value = "LIKE")] + Like, + + /// + /// Not like + /// + [EnumMember(Value = "NOT LIKE")] + NotLike + } +} diff --git a/Source/StrongGrid/Models/Search/SearchCriteria.cs b/Source/StrongGrid/Models/Search/SearchCriteria.cs new file mode 100644 index 00000000..f9ef6200 --- /dev/null +++ b/Source/StrongGrid/Models/Search/SearchCriteria.cs @@ -0,0 +1,70 @@ +using StrongGrid.Utilities; +using System; +using System.Runtime.Serialization; + +namespace StrongGrid.Models.Search +{ + /// + /// Base class for search criteria classes + /// + public abstract class SearchCriteria + { + /// + /// Gets or sets the filter used to filter the result + /// + public FilterField FilterField { get; protected set; } + + /// + /// Gets or sets the operator used to filter the result + /// + public SearchConditionOperator FilterOperator { get; protected set; } + + /// + /// Gets or sets the value used to filter the result + /// + public object FilterValue { get; protected set; } + + /// + /// Initializes a new instance of the class. + /// + /// The filter field + /// The filter operator + /// The filter value + public SearchCriteria(FilterField filterField, SearchConditionOperator filterOperator, object filterValue) + { + this.FilterField = filterField; + this.FilterOperator = filterOperator; + this.FilterValue = filterValue; + } + + /// + /// Initializes a new instance of the class. + /// + protected SearchCriteria() + { + } + + /// + /// Returns the string representation of a given value as expected by the SendGrid Email Activities API. + /// + /// The value + /// The representation of the value + public static string ValueAsString(object value) + { + var valueAsString = value is DateTime dateValue ? $"TIMESTAMP \"{dateValue.ToUniversalTime():u}\"" : $"\"{value}\""; + return valueAsString; + } + + /// + /// Returns a string representation of the search criteria + /// + /// A representation of the search criteria + public override string ToString() + { + var fieldName = FilterField.GetAttributeOfType().Value; + var filterOperator = FilterOperator.GetAttributeOfType().Value; + var filterValue = ValueAsString(FilterValue); + return $"{fieldName}{filterOperator}{filterValue}"; + } + } +} diff --git a/Source/StrongGrid/Models/Search/SearchCriteriaBetween.cs b/Source/StrongGrid/Models/Search/SearchCriteriaBetween.cs new file mode 100644 index 00000000..ed0c0019 --- /dev/null +++ b/Source/StrongGrid/Models/Search/SearchCriteriaBetween.cs @@ -0,0 +1,36 @@ +namespace StrongGrid.Models.Search +{ + /// + /// Filter the result of a search for the value of a field to be between a lower value and an upper value + /// + public class SearchCriteriaBetween : SearchCriteria + { + /// + /// Gets the upper value + /// + public object UpperValue { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// The filter field + /// The lower value + /// The upper value + public SearchCriteriaBetween(FilterField filterField, object lowerValue, object upperValue) + { + FilterField = filterField; + FilterOperator = SearchConditionOperator.Beetween; + FilterValue = lowerValue; + UpperValue = upperValue; + } + + /// + /// Returns a string representation of the search criteria + /// + /// A representation of the search criteria + public override string ToString() + { + return $"{FilterField} BETWEEN {ValueAsString(FilterValue)} AND {ValueAsString(UpperValue)}"; + } + } +} diff --git a/Source/StrongGrid/Models/Search/SearchCriteriaEqual.cs b/Source/StrongGrid/Models/Search/SearchCriteriaEqual.cs new file mode 100644 index 00000000..214b5461 --- /dev/null +++ b/Source/StrongGrid/Models/Search/SearchCriteriaEqual.cs @@ -0,0 +1,20 @@ +namespace StrongGrid.Models.Search +{ + /// + /// Filter the result of a search for the value of a field to be equal to a value + /// + public class SearchCriteriaEqual : SearchCriteria + { + /// + /// Initializes a new instance of the class. + /// + /// The filter field + /// The filter value + public SearchCriteriaEqual(FilterField filterField, object filterValue) + { + FilterField = filterField; + FilterOperator = SearchConditionOperator.Equal; + FilterValue = filterValue; + } + } +} diff --git a/Source/StrongGrid/Models/Search/SearchCriteriaGreaterEqual.cs b/Source/StrongGrid/Models/Search/SearchCriteriaGreaterEqual.cs new file mode 100644 index 00000000..90e8bc4f --- /dev/null +++ b/Source/StrongGrid/Models/Search/SearchCriteriaGreaterEqual.cs @@ -0,0 +1,20 @@ +namespace StrongGrid.Models.Search +{ + /// + /// Filter the result of a search for the value of a field to be greater than or equal to a value + /// + public class SearchCriteriaGreaterEqual : SearchCriteria + { + /// + /// Initializes a new instance of the class. + /// + /// The filter field + /// The filter value + public SearchCriteriaGreaterEqual(FilterField filterField, object filterValue) + { + FilterField = filterField; + FilterOperator = SearchConditionOperator.GreaterEqual; + FilterValue = filterValue; + } + } +} diff --git a/Source/StrongGrid/Models/Search/SearchCriteriaGreaterThan.cs b/Source/StrongGrid/Models/Search/SearchCriteriaGreaterThan.cs new file mode 100644 index 00000000..7f69a3c9 --- /dev/null +++ b/Source/StrongGrid/Models/Search/SearchCriteriaGreaterThan.cs @@ -0,0 +1,20 @@ +namespace StrongGrid.Models.Search +{ + /// + /// Filter the result of a search for the value of a field to be greater than a value + /// + public class SearchCriteriaGreaterThan : SearchCriteria + { + /// + /// Initializes a new instance of the class. + /// + /// The filter field + /// The filter value + public SearchCriteriaGreaterThan(FilterField filterField, object filterValue) + { + FilterField = filterField; + FilterOperator = SearchConditionOperator.GreaterThan; + FilterValue = filterValue; + } + } +} diff --git a/Source/StrongGrid/Models/Search/SearchCriteriaIn.cs b/Source/StrongGrid/Models/Search/SearchCriteriaIn.cs new file mode 100644 index 00000000..2f1e307f --- /dev/null +++ b/Source/StrongGrid/Models/Search/SearchCriteriaIn.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace StrongGrid.Models.Search +{ + /// + /// Filter the result of a search for the value of a field to be present in an enumeration of values + /// + public class SearchCriteriaIn : SearchCriteria + { + /// + /// Initializes a new instance of the class. + /// + /// The filter field + /// The filter values + public SearchCriteriaIn(FilterField filterField, IEnumerable filterValues) + { + FilterField = filterField; + FilterOperator = SearchConditionOperator.In; + FilterValue = filterValues; + } + } +} diff --git a/Source/StrongGrid/Models/Search/SearchCriteriaIs.cs b/Source/StrongGrid/Models/Search/SearchCriteriaIs.cs new file mode 100644 index 00000000..c6ecb2ac --- /dev/null +++ b/Source/StrongGrid/Models/Search/SearchCriteriaIs.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace StrongGrid.Models.Search +{ + /// + /// Filter the result of a search for the value of a field to be a value + /// + public class SearchCriteriaIs : SearchCriteria + { + /// + /// Initializes a new instance of the class. + /// + /// The filter field + /// The filter values + public SearchCriteriaIs(FilterField filterField, IEnumerable filterValues) + { + FilterField = filterField; + FilterOperator = SearchConditionOperator.Is; + FilterValue = filterValues; + } + } +} diff --git a/Source/StrongGrid/Models/Search/SearchCriteriaIsNot.cs b/Source/StrongGrid/Models/Search/SearchCriteriaIsNot.cs new file mode 100644 index 00000000..ff2a6925 --- /dev/null +++ b/Source/StrongGrid/Models/Search/SearchCriteriaIsNot.cs @@ -0,0 +1,20 @@ +namespace StrongGrid.Models.Search +{ + /// + /// Filter the result of a search for the value of a field to not be a value + /// + public class SearchCriteriaIsNot : SearchCriteria + { + /// + /// Initializes a new instance of the class. + /// + /// The filter field + /// The filter value + public SearchCriteriaIsNot(FilterField filterField, object filterValue) + { + FilterField = filterField; + FilterOperator = SearchConditionOperator.IsNot; + FilterValue = filterValue; + } + } +} diff --git a/Source/StrongGrid/Models/Search/SearchCriteriaLessEqual.cs b/Source/StrongGrid/Models/Search/SearchCriteriaLessEqual.cs new file mode 100644 index 00000000..8193c7ee --- /dev/null +++ b/Source/StrongGrid/Models/Search/SearchCriteriaLessEqual.cs @@ -0,0 +1,20 @@ +namespace StrongGrid.Models.Search +{ + /// + /// Filter the result of a search for the value of a field to be less than or equal to a value + /// + public class SearchCriteriaLessEqual : SearchCriteria + { + /// + /// Initializes a new instance of the class. + /// + /// The filter field + /// The filter value + public SearchCriteriaLessEqual(FilterField filterField, object filterValue) + { + FilterField = filterField; + FilterOperator = SearchConditionOperator.LessEqual; + FilterValue = filterValue; + } + } +} diff --git a/Source/StrongGrid/Models/Search/SearchCriteriaLessThan.cs b/Source/StrongGrid/Models/Search/SearchCriteriaLessThan.cs new file mode 100644 index 00000000..4c86fb94 --- /dev/null +++ b/Source/StrongGrid/Models/Search/SearchCriteriaLessThan.cs @@ -0,0 +1,20 @@ +namespace StrongGrid.Models.Search +{ + /// + /// Filter the result of a search for the value of a field to be less than a value + /// + public class SearchCriteriaLessThan : SearchCriteria + { + /// + /// Initializes a new instance of the class. + /// + /// The filter field + /// The filter value + public SearchCriteriaLessThan(FilterField filterField, object filterValue) + { + FilterField = filterField; + FilterOperator = SearchConditionOperator.LessThan; + FilterValue = filterValue; + } + } +} diff --git a/Source/StrongGrid/Models/Search/SearchCriteriaLike.cs b/Source/StrongGrid/Models/Search/SearchCriteriaLike.cs new file mode 100644 index 00000000..a04650b7 --- /dev/null +++ b/Source/StrongGrid/Models/Search/SearchCriteriaLike.cs @@ -0,0 +1,20 @@ +namespace StrongGrid.Models.Search +{ + /// + /// Filter the result of a search for the value of a field to be like a value + /// + public class SearchCriteriaLike : SearchCriteria + { + /// + /// Initializes a new instance of the class. + /// + /// The filter field + /// The filter value + public SearchCriteriaLike(FilterField filterField, object filterValue) + { + FilterField = filterField; + FilterOperator = SearchConditionOperator.Like; + FilterValue = filterValue; + } + } +} diff --git a/Source/StrongGrid/Models/Search/SearchCriteriaNotBetween.cs b/Source/StrongGrid/Models/Search/SearchCriteriaNotBetween.cs new file mode 100644 index 00000000..0943a720 --- /dev/null +++ b/Source/StrongGrid/Models/Search/SearchCriteriaNotBetween.cs @@ -0,0 +1,36 @@ +namespace StrongGrid.Models.Search +{ + /// + /// Filter the result of a search for the value of a field to be less than a lower value or greater than an upper value + /// + public class SearchCriteriaNotBetween : SearchCriteria + { + /// + /// Gets the upper value + /// + public object UpperValue { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// The filter field + /// The lower value + /// The upper value + public SearchCriteriaNotBetween(FilterField filterField, object lowerValue, object upperValue) + { + FilterField = filterField; + FilterOperator = SearchConditionOperator.Beetween; + FilterValue = lowerValue; + UpperValue = upperValue; + } + + /// + /// Returns a string representation of the search criteria + /// + /// A representation of the search criteria + public override string ToString() + { + return $"{FilterField} NOT BETWEEN {ValueAsString(FilterValue)} AND {ValueAsString(UpperValue)}"; + } + } +} diff --git a/Source/StrongGrid/Models/Search/SearchCriteriaNotEqual.cs b/Source/StrongGrid/Models/Search/SearchCriteriaNotEqual.cs new file mode 100644 index 00000000..38338624 --- /dev/null +++ b/Source/StrongGrid/Models/Search/SearchCriteriaNotEqual.cs @@ -0,0 +1,20 @@ +namespace StrongGrid.Models.Search +{ + /// + /// Filter the result of a search for the value of a field to be different than a value + /// + public class SearchCriteriaNotEqual : SearchCriteria + { + /// + /// Initializes a new instance of the class. + /// + /// The filter field + /// The filter value + public SearchCriteriaNotEqual(FilterField filterField, object filterValue) + { + FilterField = filterField; + FilterOperator = SearchConditionOperator.NotEqual; + FilterValue = filterValue; + } + } +} diff --git a/Source/StrongGrid/Models/Search/SearchCriteriaNotIn.cs b/Source/StrongGrid/Models/Search/SearchCriteriaNotIn.cs new file mode 100644 index 00000000..3fcf9ac6 --- /dev/null +++ b/Source/StrongGrid/Models/Search/SearchCriteriaNotIn.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace StrongGrid.Models.Search +{ + /// + /// Filter the result of a search for the value of a field to be absent from an enumeration of values + /// + public class SearchCriteriaNotIn : SearchCriteria + { + /// + /// Initializes a new instance of the class. + /// + /// The filter field + /// The filter values + public SearchCriteriaNotIn(FilterField filterField, IEnumerable filterValues) + { + FilterField = filterField; + FilterOperator = SearchConditionOperator.NotIn; + FilterValue = filterValues; + } + } +} diff --git a/Source/StrongGrid/Models/Search/SearchCriteriaNotLike.cs b/Source/StrongGrid/Models/Search/SearchCriteriaNotLike.cs new file mode 100644 index 00000000..32d642ff --- /dev/null +++ b/Source/StrongGrid/Models/Search/SearchCriteriaNotLike.cs @@ -0,0 +1,20 @@ +namespace StrongGrid.Models.Search +{ + /// + /// Filter the result of a search for the value of a field to not be like a value + /// + public class SearchCriteriaNotLike : SearchCriteria + { + /// + /// Initializes a new instance of the class. + /// + /// The filter field + /// The filter value + public SearchCriteriaNotLike(FilterField filterField, object filterValue) + { + FilterField = filterField; + FilterOperator = SearchConditionOperator.NotLike; + FilterValue = filterValue; + } + } +} diff --git a/Source/StrongGrid/Models/Search/SearchCriteriaNull.cs b/Source/StrongGrid/Models/Search/SearchCriteriaNull.cs new file mode 100644 index 00000000..9846c4bd --- /dev/null +++ b/Source/StrongGrid/Models/Search/SearchCriteriaNull.cs @@ -0,0 +1,26 @@ +namespace StrongGrid.Models.Search +{ + /// + /// Filter the result of a search for the value of a field to be NULL + /// + public class SearchCriteriaNull : SearchCriteria + { + /// + /// Initializes a new instance of the class. + /// + /// The filter field + public SearchCriteriaNull(FilterField filterField) + { + FilterField = filterField; + } + + /// + /// Returns a string representation of the search criteria + /// + /// A representation of the search criteria + public override string ToString() + { + return $"{FilterField} NULL"; + } + } +} diff --git a/Source/StrongGrid/Models/Search/SearchLogicalOperator.cs b/Source/StrongGrid/Models/Search/SearchLogicalOperator.cs new file mode 100644 index 00000000..352b0929 --- /dev/null +++ b/Source/StrongGrid/Models/Search/SearchLogicalOperator.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Runtime.Serialization; + +namespace StrongGrid.Models.Search +{ + /// + /// Enumeration to indicate a logical operator + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum SearchLogicalOperator + { + /// + /// And + /// + [EnumMember(Value = "AND")] + And, + + /// + /// Or + /// + [EnumMember(Value = "OR")] + Or + } +} diff --git a/Source/StrongGrid/Resources/EmailActivities.cs b/Source/StrongGrid/Resources/EmailActivities.cs index 47025103..635b9253 100644 --- a/Source/StrongGrid/Resources/EmailActivities.cs +++ b/Source/StrongGrid/Resources/EmailActivities.cs @@ -1,8 +1,12 @@ using Pathoschild.Http.Client; using StrongGrid.Models; +using StrongGrid.Models.Search; using StrongGrid.Utilities; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net.Http; +using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; @@ -30,18 +34,32 @@ internal EmailActivities(Pathoschild.Http.Client.IClient client) } /// - /// Get all of the details about the messages matching the criteria. + /// Get all of the details about the messages matching the filtering conditions. /// + /// Filtering conditions. /// Number of IP activity entries to return. /// Cancellation token /// /// An array of . /// - public Task SearchMessagesAsync(int limit = 20, CancellationToken cancellationToken = default(CancellationToken)) + public Task SearchAsync(IEnumerable>> filterConditions, int limit = 20, CancellationToken cancellationToken = default(CancellationToken)) { + var conditions = new List(filterConditions?.Count() ?? 0); + if (filterConditions != null) + { + foreach (var criteria in filterConditions) + { + var enumMemberValue = criteria.Key.GetAttributeOfType().Value; + conditions.Add(string.Join($" {enumMemberValue} ", criteria.Value.Select(criteriaValue => criteriaValue.ToString()))); + } + } + + var query = string.Join(" AND ", conditions); + return _client .GetAsync(_endpoint) .WithArgument("limit", limit) + .WithArgument("query", query.ToString()) .WithCancellationToken(cancellationToken) .AsSendGridObject("messages"); } diff --git a/Source/StrongGrid/Resources/IEmailActivities.cs b/Source/StrongGrid/Resources/IEmailActivities.cs index 57ec910e..e33ab5da 100644 --- a/Source/StrongGrid/Resources/IEmailActivities.cs +++ b/Source/StrongGrid/Resources/IEmailActivities.cs @@ -1,4 +1,6 @@ using StrongGrid.Models; +using StrongGrid.Models.Search; +using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -15,14 +17,15 @@ namespace StrongGrid.Resources public interface IEmailActivities { /// - /// Get all of the details about the messages matching the criteria. + /// Get all of the details about the messages matching the filtering conditions. /// - /// Number of IP activity entries to return. + /// Filtering conditions. + /// Maximum number of activity entries to return. /// Cancellation token /// /// An array of . /// - Task SearchMessagesAsync(int limit = 20, CancellationToken cancellationToken = default(CancellationToken)); + Task SearchAsync(IEnumerable>> filterConditions, int limit = 20, CancellationToken cancellationToken = default(CancellationToken)); /// /// Get all of the details about the specified message. diff --git a/Source/StrongGrid/Utilities/Extensions.cs b/Source/StrongGrid/Utilities/Extensions.cs index 4a6c4591..14d812da 100644 --- a/Source/StrongGrid/Utilities/Extensions.cs +++ b/Source/StrongGrid/Utilities/Extensions.cs @@ -2,12 +2,16 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Pathoschild.Http.Client; +using StrongGrid.Models; +using StrongGrid.Models.Search; +using StrongGrid.Resources; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; +using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -423,6 +427,55 @@ public static async Task ForEachAsync(this IEnumerable items, Func + /// Get all of the details about the messages matching the criteria. + /// + /// The email activities resource + /// Filtering criteria. + /// Number of IP activity entries to return. + /// Cancellation token + /// + /// An array of . + /// + public static Task SearchAsync(this IEmailActivities emailActivities, SearchCriteria criteria, int limit = 20, CancellationToken cancellationToken = default(CancellationToken)) + { + var filterCriteria = criteria == null ? Enumerable.Empty() : new[] { criteria }; + return emailActivities.SearchAsync(filterCriteria, limit, cancellationToken); + } + + /// + /// Get all of the details about the messages matching the criteria. + /// + /// The email activities resource + /// Filtering conditions. + /// Number of IP activity entries to return. + /// Cancellation token + /// + /// An array of . + /// + public static Task SearchAsync(this IEmailActivities emailActivities, IEnumerable filterConditions, int limit = 20, CancellationToken cancellationToken = default(CancellationToken)) + { + var filters = new List>>(); + if (filterConditions != null && filterConditions.Any()) filters.Add(new KeyValuePair>(SearchLogicalOperator.And, filterConditions)); + return emailActivities.SearchAsync(filters, limit, cancellationToken); + } + + /// + /// Gets the attribute of the specified type. + /// + /// The type of the desired attribute + /// The enum value. + /// The attribute + public static T GetAttributeOfType(this Enum enumVal) + where T : Attribute + { + return enumVal.GetType() + .GetTypeInfo() + .DeclaredMembers + .SingleOrDefault(x => x.Name == enumVal.ToString()) + ?.GetCustomAttribute(false); + } + /// Asynchronously converts the JSON encoded content and converts it to a 'SendGrid' object of the desired type. /// The response model to deserialize into. /// The content diff --git a/build.cake b/build.cake index a668f62f..cd1ecf06 100644 --- a/build.cake +++ b/build.cake @@ -150,8 +150,6 @@ Task("Restore-NuGet-Packages") { Sources = new [] { "https://www.myget.org/F/xunit/api/v3/index.json", - "https://dotnet.myget.org/F/dotnet-core/api/v3/index.json", - "https://dotnet.myget.org/F/cli-deps/api/v3/index.json", "https://api.nuget.org/v3/index.json", } }); @@ -161,7 +159,7 @@ Task("Build") .IsDependentOn("Restore-NuGet-Packages") .Does(() => { - DotNetCoreBuild(sourceFolder + libraryName + ".sln", new DotNetCoreBuildSettings + DotNetCoreBuild($"{sourceFolder}{libraryName}.sln", new DotNetCoreBuildSettings { Configuration = configuration, NoRestore = true,