Extensions instead of partial classes
All checks were successful
Branch Build / build-test-deploy (push) Successful in 44s
All checks were successful
Branch Build / build-test-deploy (push) Successful in 44s
This commit is contained in:
@@ -9,17 +9,14 @@ namespace AMWD.Net.Api.LinkMobility
|
||||
public interface ILinkMobilityClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a text message to a list of recipients.
|
||||
/// Performs a POST request to the LINK mobility API.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">The type of the response.</typeparam>
|
||||
/// <typeparam name="TRequest">The type of the request.</typeparam>
|
||||
/// <param name="requestPath">The path of the API endpoint.</param>
|
||||
/// <param name="request">The request data.</param>
|
||||
/// <param name="queryParams">Optional query parameters.</param>
|
||||
/// <param name="cancellationToken">A cancellation token to propagate notification that operations should be canceled.</param>
|
||||
Task<SendMessageResponse> SendTextMessage(SendTextMessageRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a binary message to a list of recipients.
|
||||
/// </summary>
|
||||
/// <param name="request">The request data.</param>
|
||||
/// <param name="cancellationToken">A cancellation token to propagate notification that operations should be canceled.</param>
|
||||
Task<SendMessageResponse> SendBinaryMessage(SendBinaryMessageRequest request, CancellationToken cancellationToken = default);
|
||||
Task<TResponse> PostAsync<TResponse, TRequest>(string requestPath, TRequest? request, IQueryParameter? queryParams = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AMWD.Net.Api.LinkMobility
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of text messaging (SMS). <see href="https://developer.linkmobility.eu/sms-api/rest-api">API</see>
|
||||
/// </summary>
|
||||
public partial class LinkMobilityClient : ILinkMobilityClient, IDisposable
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public Task<SendMessageResponse> SendTextMessage(SendTextMessageRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.MessageContent))
|
||||
throw new ArgumentException("A message must be provided.", nameof(request.MessageContent));
|
||||
|
||||
if (request.RecipientAddressList == null || request.RecipientAddressList.Count == 0)
|
||||
throw new ArgumentException("At least one recipient must be provided.", nameof(request.RecipientAddressList));
|
||||
|
||||
foreach (string recipient in request.RecipientAddressList)
|
||||
{
|
||||
if (!IsValidMSISDN(recipient))
|
||||
throw new ArgumentException($"Recipient address '{recipient}' is not a valid MSISDN format.", nameof(request.RecipientAddressList));
|
||||
}
|
||||
|
||||
return PostAsync<SendMessageResponse, SendTextMessageRequest>("/smsmessaging/text", request, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<SendMessageResponse> SendBinaryMessage(SendBinaryMessageRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
|
||||
if (request.MessageContent?.Count > 0)
|
||||
{
|
||||
// Validate that the string is a valid Base64 string
|
||||
// Might throw a ArgumentNullException or FormatException
|
||||
foreach (string str in request.MessageContent)
|
||||
Convert.FromBase64String(str);
|
||||
}
|
||||
|
||||
if (request.RecipientAddressList == null || request.RecipientAddressList.Count == 0)
|
||||
throw new ArgumentException("At least one recipient must be provided.", nameof(request.RecipientAddressList));
|
||||
|
||||
foreach (string recipient in request.RecipientAddressList)
|
||||
{
|
||||
if (!IsValidMSISDN(recipient))
|
||||
throw new ArgumentException($"Recipient address '{recipient}' is not a valid MSISDN format.", nameof(request.RecipientAddressList));
|
||||
}
|
||||
|
||||
return PostAsync<SendMessageResponse, SendBinaryMessageRequest>("/smsmessaging/binary", request, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/MSISDN
|
||||
private static bool IsValidMSISDN(string msisdn)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(msisdn))
|
||||
return false;
|
||||
|
||||
return Regex.IsMatch(msisdn, @"^[1-9][0-9]{7,14}$", RegexOptions.Compiled);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ namespace AMWD.Net.Api.LinkMobility
|
||||
/// <summary>
|
||||
/// Provides a client for interacting with the Link Mobility API.
|
||||
/// </summary>
|
||||
public partial class LinkMobilityClient : ILinkMobilityClient, IDisposable
|
||||
public class LinkMobilityClient : ILinkMobilityClient, IDisposable
|
||||
{
|
||||
private readonly ClientOptions _clientOptions;
|
||||
private readonly HttpClient _httpClient;
|
||||
@@ -78,6 +78,52 @@ namespace AMWD.Net.Api.LinkMobility
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<TResponse> PostAsync<TResponse, TRequest>(string requestPath, TRequest? request, IQueryParameter? queryParams = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ValidateRequestPath(requestPath);
|
||||
|
||||
string requestUrl = BuildRequestUrl(requestPath, queryParams);
|
||||
var httpContent = ConvertRequest(request);
|
||||
|
||||
var httpRequest = new HttpRequestMessage
|
||||
{
|
||||
Method = HttpMethod.Post,
|
||||
RequestUri = new Uri(requestUrl, UriKind.Relative),
|
||||
Content = httpContent,
|
||||
};
|
||||
|
||||
var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
|
||||
var response = await GetResponse<TResponse>(httpResponse, cancellationToken).ConfigureAwait(false);
|
||||
return response;
|
||||
}
|
||||
|
||||
private string BuildRequestUrl(string requestPath, IQueryParameter? queryParams = null)
|
||||
{
|
||||
string path = requestPath.Trim().TrimStart('/');
|
||||
var param = new Dictionary<string, string>();
|
||||
|
||||
if (_clientOptions.DefaultQueryParams.Count > 0)
|
||||
{
|
||||
foreach (var kvp in _clientOptions.DefaultQueryParams)
|
||||
param[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
var customQueryParams = queryParams?.GetQueryParameters();
|
||||
if (customQueryParams?.Count > 0)
|
||||
{
|
||||
foreach (var kvp in customQueryParams)
|
||||
param[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
if (param.Count == 0)
|
||||
return path;
|
||||
|
||||
string queryString = string.Join("&", param.Select(kvp => $"{WebUtility.UrlEncode(kvp.Key)}={WebUtility.UrlEncode(kvp.Value)}"));
|
||||
return $"{path}?{queryString}";
|
||||
}
|
||||
|
||||
private void ValidateClientOptions()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_clientOptions.BaseUrl))
|
||||
@@ -148,51 +194,6 @@ namespace AMWD.Net.Api.LinkMobility
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<TResponse> PostAsync<TResponse, TRequest>(string requestPath, TRequest? request, IQueryParameter? queryParams = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ValidateRequestPath(requestPath);
|
||||
|
||||
string requestUrl = BuildRequestUrl(requestPath, queryParams);
|
||||
var httpContent = ConvertRequest(request);
|
||||
|
||||
var httpRequest = new HttpRequestMessage
|
||||
{
|
||||
Method = HttpMethod.Post,
|
||||
RequestUri = new Uri(requestUrl, UriKind.Relative),
|
||||
Content = httpContent,
|
||||
};
|
||||
|
||||
var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
|
||||
var response = await GetResponse<TResponse>(httpResponse, cancellationToken).ConfigureAwait(false);
|
||||
return response;
|
||||
}
|
||||
|
||||
private string BuildRequestUrl(string requestPath, IQueryParameter? queryParams = null)
|
||||
{
|
||||
string path = requestPath.Trim().TrimStart('/');
|
||||
var param = new Dictionary<string, string>();
|
||||
|
||||
if (_clientOptions.DefaultQueryParams.Count > 0)
|
||||
{
|
||||
foreach (var kvp in _clientOptions.DefaultQueryParams)
|
||||
param[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
var customQueryParams = queryParams?.GetQueryParameters();
|
||||
if (customQueryParams?.Count > 0)
|
||||
{
|
||||
foreach (var kvp in customQueryParams)
|
||||
param[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
if (param.Count == 0)
|
||||
return path;
|
||||
|
||||
string queryString = string.Join("&", param.Select(kvp => $"{WebUtility.UrlEncode(kvp.Key)}={WebUtility.UrlEncode(kvp.Value)}"));
|
||||
return $"{path}?{queryString}";
|
||||
}
|
||||
|
||||
private static HttpContent? ConvertRequest<T>(T request)
|
||||
{
|
||||
if (request == null)
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace AMWD.Net.Api.LinkMobility
|
||||
/// <summary>
|
||||
/// <see cref="Type.Text"/>, <see cref="Type.Binary"/>:
|
||||
/// <br/>
|
||||
/// <see cref="AddressType.International"/> – defines the number format of the mobile originated <see cref="SenderAddress"/>.
|
||||
/// <see cref="AddressType.International"/> - defines the number format of the mobile originated <see cref="SenderAddress"/>.
|
||||
/// International numbers always includes the country prefix.
|
||||
/// </summary>
|
||||
[JsonProperty("senderAddressType")]
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SendBinaryMessageRequest"/> class.
|
||||
/// </summary>
|
||||
/// <param name="recipientAddressList">The recipient list.</param>
|
||||
public SendBinaryMessageRequest(IReadOnlyCollection<string> recipientAddressList)
|
||||
/// <param name="messageContent">A binary message as base64 encoded lines.</param>
|
||||
/// <param name="recipientAddressList">A list of recipient numbers.</param>
|
||||
public SendBinaryMessageRequest(IReadOnlyCollection<string> messageContent, IReadOnlyCollection<string> recipientAddressList)
|
||||
{
|
||||
MessageContent = messageContent;
|
||||
RecipientAddressList = recipientAddressList;
|
||||
}
|
||||
|
||||
@@ -41,7 +43,7 @@
|
||||
/// The binary data is transmitted without being changed (using 8 bit alphabet).
|
||||
/// </remarks>
|
||||
[JsonProperty("messageContent")]
|
||||
public IReadOnlyCollection<string>? MessageContent { get; set; }
|
||||
public IReadOnlyCollection<string> MessageContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <em>Optional</em>.
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SendTextMessageRequest"/> class.
|
||||
/// </summary>
|
||||
/// <param name="messageContent">The message.</param>
|
||||
/// <param name="recipientAddressList">The recipient list.</param>
|
||||
/// <param name="messageContent">A text message.</param>
|
||||
/// <param name="recipientAddressList">A list of recipient numbers.</param>
|
||||
public SendTextMessageRequest(string messageContent, IReadOnlyCollection<string> recipientAddressList)
|
||||
{
|
||||
MessageContent = messageContent;
|
||||
|
||||
@@ -11,12 +11,6 @@
|
||||
[JsonProperty("clientMessageId")]
|
||||
public string? ClientMessageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The actual number of generated SMS.
|
||||
/// </summary>
|
||||
[JsonProperty("smsCount")]
|
||||
public int? SmsCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Status code.
|
||||
/// </summary>
|
||||
|
||||
14
src/LinkMobility/Responses/SendTextMessageResponse.cs
Normal file
14
src/LinkMobility/Responses/SendTextMessageResponse.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace AMWD.Net.Api.LinkMobility
|
||||
{
|
||||
/// <summary>
|
||||
/// Response of a text message sent to a list of recipients.
|
||||
/// </summary>
|
||||
public class SendTextMessageResponse : SendMessageResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The actual number of generated SMS.
|
||||
/// </summary>
|
||||
[JsonProperty("smsCount")]
|
||||
public int? SmsCount { get; set; }
|
||||
}
|
||||
}
|
||||
78
src/LinkMobility/TextMessageExtensions.cs
Normal file
78
src/LinkMobility/TextMessageExtensions.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AMWD.Net.Api.LinkMobility.Utils;
|
||||
|
||||
namespace AMWD.Net.Api.LinkMobility
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of text messaging (SMS). <see href="https://developer.linkmobility.eu/sms-api/rest-api">API</see>
|
||||
/// </summary>
|
||||
public static class TextMessageExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a text message to a list of recipients.
|
||||
/// </summary>
|
||||
/// <param name="client">The <see cref="ILinkMobilityClient"/> instance.</param>
|
||||
/// <param name="request">The request data.</param>
|
||||
/// <param name="cancellationToken">A cancellation token to propagate notification that operations should be canceled.</param>
|
||||
public static Task<SendTextMessageResponse> SendTextMessage(this ILinkMobilityClient client, SendTextMessageRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.MessageContent))
|
||||
throw new ArgumentException("A message must be provided.", nameof(request.MessageContent));
|
||||
|
||||
ValidateRecipientList(request.RecipientAddressList);
|
||||
ValidateContentCategory(request.ContentCategory);
|
||||
|
||||
return client.PostAsync<SendTextMessageResponse, SendTextMessageRequest>("/smsmessaging/text", request, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a binary message to a list of recipients.
|
||||
/// </summary>
|
||||
/// <param name="client">The <see cref="ILinkMobilityClient"/> instance.</param>
|
||||
/// <param name="request">The request data.</param>
|
||||
/// <param name="cancellationToken">A cancellation token to propagate notification that operations should be canceled.</param>
|
||||
public static Task<SendTextMessageResponse> SendBinaryMessage(this ILinkMobilityClient client, SendBinaryMessageRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
|
||||
if (request.MessageContent == null || request.MessageContent.Count == 0)
|
||||
throw new ArgumentException("A message must be provided.", nameof(request.MessageContent));
|
||||
|
||||
// Easiest way to validate that the string is a valid Base64 string.
|
||||
// Might throw a ArgumentNullException or FormatException.
|
||||
foreach (string str in request.MessageContent)
|
||||
Convert.FromBase64String(str);
|
||||
|
||||
ValidateRecipientList(request.RecipientAddressList);
|
||||
ValidateContentCategory(request.ContentCategory);
|
||||
|
||||
return client.PostAsync<SendTextMessageResponse, SendBinaryMessageRequest>("/smsmessaging/binary", request, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
private static void ValidateRecipientList(IReadOnlyCollection<string>? recipientAddressList)
|
||||
{
|
||||
if (recipientAddressList == null || recipientAddressList.Count == 0)
|
||||
throw new ArgumentException("At least one recipient must be provided.", nameof(recipientAddressList));
|
||||
|
||||
foreach (string recipient in recipientAddressList)
|
||||
{
|
||||
if (!Validation.IsValidMSISDN(recipient))
|
||||
throw new ArgumentException($"Recipient address '{recipient}' is not a valid MSISDN format.", nameof(recipientAddressList));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateContentCategory(ContentCategory? contentCategory)
|
||||
{
|
||||
if (!contentCategory.HasValue)
|
||||
return;
|
||||
|
||||
if (contentCategory.Value != ContentCategory.Informational && contentCategory.Value != ContentCategory.Advertisement)
|
||||
throw new ArgumentException($"Content category '{contentCategory.Value}' is not valid.", nameof(contentCategory));
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/LinkMobility/Utils/Validation.cs
Normal file
25
src/LinkMobility/Utils/Validation.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AMWD.Net.Api.LinkMobility.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Validation helper for LINK Mobility API requirements.
|
||||
/// </summary>
|
||||
public static class Validation
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates whether the provided string is a valid MSISDN (E.164 formatted).
|
||||
/// <br/>
|
||||
/// See <see href="https://en.wikipedia.org/wiki/MSISDN">Wikipedia: MSISDN</see> for more information.
|
||||
/// </summary>
|
||||
/// <param name="msisdn">The string to validate.</param>
|
||||
/// <returns><see langword="true"/> for a valid MSISDN number, <see langword="false"/> otherwise.</returns>
|
||||
public static bool IsValidMSISDN(string msisdn)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(msisdn))
|
||||
return false;
|
||||
|
||||
return Regex.IsMatch(msisdn, @"^[1-9][0-9]{7,14}$", RegexOptions.Compiled);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user