C# / .NET SDK
ASP.NET Core Integration
Complete ASP.NET Core integration with dependency injection, configuration management, and production-ready patterns.
This example uses .NET 8 best practices including the Options pattern, Minimal APIs, ProblemDetails, and built-in rate limiting.
Project Structure
├── src/
│ ├── Controllers/MessagesController.cs
│ ├── DTOs/
│ │ ├── SendMessageRequest.cs
│ │ └── SendMessageResponse.cs
│ ├── Validators/SendMessageRequestValidator.cs
│ ├── Services/
│ │ ├── ISentMessageService.cs
│ │ └── SentMessageService.cs
│ ├── Configuration/SentOptions.cs
│ ├── Filters/GlobalExceptionFilter.cs
│ ├── MinimalApis/MessageEndpoints.cs
│ └── Program.cs
├── tests/SentMessageServiceTests.cs
└── appsettings.jsonConfiguration
// Configuration/SentOptions.cs
public class SentOptions
{
public const string SectionName = "Sent";
[Required] public string ApiKey { get; set; } = string.Empty;
public string? WebhookSecret { get; set; }
public string BaseUrl { get; set; } = "https://api.sent.dm";
[Range(1, 300)] public int TimeoutSeconds { get; set; } = 30;
[Range(0, 10)] public int MaxRetries { get; set; } = 3;
}Dependency Injection Setup
// Program.cs
using MyApp.Configuration;
using MyApp.Filters;
using MyApp.MinimalApis;
using MyApp.Services;
using MyApp.Validators;
using SentDM.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Configure options
builder.Services.AddOptions<SentOptions>()
.Bind(builder.Configuration.GetSection(SentOptions.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart();
// Add Sent client
builder.Services.AddSentDM((sp, options) =>
{
var opts = sp.GetRequiredService<IOptions<SentOptions>>().Value;
options.ApiKey = opts.ApiKey;
options.BaseUrl = opts.BaseUrl;
options.Timeout = TimeSpan.FromSeconds(opts.TimeoutSeconds);
options.MaxRetries = opts.MaxRetries;
});
// Services
builder.Services.AddScoped<ISentMessageService, SentMessageService>();
builder.Services.AddValidatorsFromAssemblyContaining<SendMessageRequestValidator>();
// API Versioning
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
})
.AddMvc()
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
// Swagger & Health Checks
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddHealthChecks();
// Rate Limiting
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("sent-api", limiterOptions =>
{
limiterOptions.PermitLimit = 100;
limiterOptions.Window = TimeSpan.FromMinutes(1);
});
});
// Controllers with global exception filter
builder.Services.AddControllers(options =>
options.Filters.Add<GlobalExceptionFilter>());
builder.Services.AddProblemDetails();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseRateLimiter();
app.UseAuthorization();
app.MapHealthChecks("/health");
app.MapMessageEndpoints();
app.MapControllers();
app.Run();Environment Variables
| Variable | Description | Default |
|---|---|---|
SENT_DM_API_KEY | Your Sent DM API key (required) | - |
SENT_DM_WEBHOOK_SECRET | Webhook signature secret | null |
SENT_DM_BASE_URL | API base URL | https://api.sent.dm |
SENT_DM_TIMEOUT_SECONDS | Request timeout | 30 |
SENT_DM_MAX_RETRIES | Max retry attempts | 3 |
# Linux/macOS
export SENT_DM_API_KEY="sent_your_api_key_here"
export ASPNETCORE_ENVIRONMENT="Development"
# Windows PowerShell
$env:SENT_DM_API_KEY="sent_your_api_key_here"DTOs with Validation
// DTOs/SendMessageRequest.cs
public class SendMessageRequest
{
[Required, Phone, StringLength(20, MinimumLength = 10)]
public string PhoneNumber { get; set; } = string.Empty;
[Required, StringLength(100)]
public string TemplateId { get; set; } = string.Empty;
public Dictionary<string, string>? Variables { get; set; }
public List<string>? Channels { get; set; }
}
// DTOs/SendMessageResponse.cs
public class SendMessageResponse
{
public string MessageId { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public decimal? Price { get; set; }
public DateTime SentAt { get; set; }
}FluentValidation
// Validators/SendMessageRequestValidator.cs
public class SendMessageRequestValidator : AbstractValidator<SendMessageRequest>
{
private static readonly string[] ValidChannels = ["whatsapp", "sms", "telegram"];
public SendMessageRequestValidator()
{
RuleFor(x => x.PhoneNumber)
.NotEmpty()
.Matches(@"^\+[1-9]\d{1,14}$")
.WithMessage("Phone number must be in E.164 format");
RuleFor(x => x.TemplateId).NotEmpty();
RuleForEach(x => x.Channels)
.Must(c => ValidChannels.Contains(c?.ToLower()))
.When(x => x.Channels != null);
}
}Service Layer
// Services/ISentMessageService.cs
public interface ISentMessageService
{
Task<SendMessageResponse> SendMessageAsync(
SendMessageRequest request, CancellationToken ct = default);
Task<SendMessageResponse> SendWelcomeMessageAsync(
string phoneNumber, string? name = null, CancellationToken ct = default);
}
// Services/SentMessageService.cs
public class SentMessageService : ISentMessageService
{
private readonly ISentClient _sentClient;
private readonly ILogger<SentMessageService> _logger;
public SentMessageService(ISentClient sentClient, ILogger<SentMessageService> logger)
{
_sentClient = sentClient;
_logger = logger;
}
public async Task<SendMessageResponse> SendMessageAsync(
SendMessageRequest request, CancellationToken ct = default)
{
var messageRequest = new SendMessageRequest
{
PhoneNumber = request.PhoneNumber,
TemplateId = request.TemplateId,
Channel = request.Channels?.FirstOrDefault() ?? "whatsapp",
Variables = request.Variables ?? new Dictionary<string, string>()
};
var result = await _sentClient.Messages.SendAsync(messageRequest, ct);
if (!result.Success)
throw new SentException($"Failed to send message: {result.Error?.Message}", result.Error);
_logger.LogInformation("Message sent: {MessageId}", result.Data.Id);
return new SendMessageResponse
{
MessageId = result.Data.Id,
Status = result.Data.Status,
Price = result.Data.Price,
SentAt = DateTime.UtcNow
};
}
public async Task<SendMessageResponse> SendWelcomeMessageAsync(
string phoneNumber, string? name = null, CancellationToken ct = default)
{
var request = new SendMessageRequest
{
PhoneNumber = phoneNumber,
TemplateId = "welcome-template",
Channel = "whatsapp",
Variables = new Dictionary<string, string> { ["name"] = name ?? "Valued Customer" }
};
var result = await _sentClient.Messages.SendAsync(request, ct);
return new SendMessageResponse
{
MessageId = result.Data.Id,
Status = result.Data.Status,
SentAt = DateTime.UtcNow
};
}
}Exception Filter
// Filters/GlobalExceptionFilter.cs
public class GlobalExceptionFilter : IExceptionFilter
{
private readonly ILogger<GlobalExceptionFilter> _logger;
private readonly IHostEnvironment _environment;
public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger, IHostEnvironment environment)
{
_logger = logger;
_environment = environment;
}
public void OnException(ExceptionContext context)
{
var exception = context.Exception;
var traceId = context.HttpContext.TraceIdentifier;
_logger.LogError(exception, "Unhandled exception. TraceId: {TraceId}", traceId);
var problemDetails = exception switch
{
SentException sentEx => CreateSentProblemDetails(sentEx, traceId),
ValidationException valEx => CreateValidationProblemDetails(valEx, traceId),
_ => CreateGenericProblemDetails(exception, traceId)
};
context.Result = new ObjectResult(problemDetails) { StatusCode = problemDetails.Status };
context.ExceptionHandled = true;
}
private static ProblemDetails CreateSentProblemDetails(SentException exception, string traceId)
{
var statusCode = exception.Error?.Code switch
{
"invalid_api_key" => StatusCodes.Status401Unauthorized,
"insufficient_credits" => StatusCodes.Status402PaymentRequired,
"rate_limited" => StatusCodes.Status429TooManyRequests,
"invalid_phone_number" => StatusCodes.Status400BadRequest,
"template_not_found" => StatusCodes.Status404NotFound,
_ => StatusCodes.Status500InternalServerError
};
return new ProblemDetails
{
Status = statusCode,
Title = "Message Service Error",
Detail = exception.Message,
Extensions = { ["traceId"] = traceId, ["errorCode"] = exception.Error?.Code }
};
}
private static ProblemDetails CreateValidationProblemDetails(ValidationException exception, string traceId)
{
return new ProblemDetails
{
Status = StatusCodes.Status400BadRequest,
Title = "Validation Failed",
Detail = exception.Message,
Extensions = { ["traceId"] = traceId }
};
}
private ProblemDetails CreateGenericProblemDetails(Exception exception, string traceId)
{
var problemDetails = new ProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Title = "Internal Server Error",
Extensions = { ["traceId"] = traceId }
};
if (_environment.IsDevelopment())
{
problemDetails.Detail = exception.Message;
problemDetails.Extensions["stackTrace"] = exception.StackTrace;
}
else
{
problemDetails.Detail = "An unexpected error occurred. Please try again later.";
}
return problemDetails;
}
}Minimal API Endpoints
// MinimalApis/MessageEndpoints.cs
public static class MessageEndpoints
{
public static IEndpointRouteBuilder MapMessageEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v{version:apiVersion}/messages")
.WithApiVersionSet()
.HasApiVersion(1.0)
.WithTags("Messages")
.WithRateLimiter("sent-api")
.WithOpenApi();
group.MapPost("/send", async (
SendMessageRequest request,
ISentMessageService messageService,
IValidator<SendMessageRequest> validator,
CancellationToken ct) =>
{
var validationResult = await validator.ValidateAsync(request, ct);
if (!validationResult.IsValid)
return Results.ValidationProblem(validationResult.ToDictionary());
var response = await messageService.SendMessageAsync(request, ct);
return Results.Ok(response);
})
.WithName("SendMessage")
.Produces<SendMessageResponse>(StatusCodes.Status200OK)
.ProducesValidationProblem();
group.MapPost("/welcome", async (
[FromBody] WelcomeMessageRequest request,
ISentMessageService messageService,
CancellationToken ct) =>
{
var response = await messageService.SendWelcomeMessageAsync(request.PhoneNumber, request.Name, ct);
return Results.Ok(response);
})
.WithName("SendWelcomeMessage")
.Produces<SendMessageResponse>(StatusCodes.Status200OK);
return app;
}
}MVC Controller
// Controllers/MessagesController.cs
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
[Produces("application/json")]
public class MessagesController : ControllerBase
{
private readonly ISentMessageService _messageService;
public MessagesController(ISentMessageService messageService)
{
_messageService = messageService;
}
[HttpPost("send")]
[ProducesResponseType(typeof(SendMessageResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<ActionResult<SendMessageResponse>> SendMessage(
[FromBody] SendMessageRequest request,
CancellationToken cancellationToken)
{
var response = await _messageService.SendMessageAsync(request, cancellationToken);
return Ok(response);
}
[HttpPost("welcome")]
[ProducesResponseType(typeof(SendMessageResponse), StatusCodes.Status200OK)]
public async Task<ActionResult<SendMessageResponse>> SendWelcome(
[FromBody] WelcomeMessageRequest request,
CancellationToken cancellationToken)
{
var response = await _messageService.SendWelcomeMessageAsync(
request.PhoneNumber, request.Name, cancellationToken);
return Ok(response);
}
}Testing
// tests/SentMessageServiceTests.cs
public class SentMessageServiceTests
{
private readonly Mock<ISentClient> _mockSentClient;
private readonly Mock<ILogger<SentMessageService>> _mockLogger;
private readonly SentMessageService _service;
public SentMessageServiceTests()
{
_mockSentClient = new Mock<ISentClient>();
_mockLogger = new Mock<ILogger<SentMessageService>>();
_service = new SentMessageService(_mockSentClient.Object, _mockLogger.Object);
}
[Fact]
public async Task SendMessageAsync_WithValidRequest_ReturnsSuccessResponse()
{
var request = new SendMessageRequest
{
PhoneNumber = "+1234567890",
TemplateId = "welcome-template"
};
var mockResult = new SentResult<MessageResponse>
{
Success = true,
Data = new MessageResponse { Id = "msg_123", Status = "queued", Price = 0.05m }
};
_mockSentClient
.Setup(x => x.Messages.SendAsync(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(mockResult);
var result = await _service.SendMessageAsync(request);
Assert.Equal("msg_123", result.MessageId);
Assert.Equal("queued", result.Status);
Assert.Equal(0.05m, result.Price);
}
[Fact]
public async Task SendMessageAsync_WithFailedResult_ThrowsSentException()
{
var request = new SendMessageRequest
{
PhoneNumber = "+1234567890",
TemplateId = "invalid-template"
};
var mockResult = new SentResult<MessageResponse>
{
Success = false,
Error = new SentError { Code = "template_not_found", Message = "Template not found" }
};
_mockSentClient
.Setup(x => x.Messages.SendAsync(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(mockResult);
var exception = await Assert.ThrowsAsync<SentException>(() => _service.SendMessageAsync(request));
Assert.Contains("Template not found", exception.Message);
}
}Configuration Files
// appsettings.json
{
"Sent": {
"ApiKey": "",
"WebhookSecret": "",
"BaseUrl": "https://api.sent.dm",
"TimeoutSeconds": 30,
"MaxRetries": 3
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}// appsettings.Development.json
{
"Sent": {
"ApiKey": "dev_api_key_here",
"WebhookSecret": "dev_webhook_secret"
},
"Logging": {
"LogLevel": {
"Default": "Debug"
}
}
}Required NuGet Packages
<PackageReference Include="SentDM" Version="1.0.0" />
<PackageReference Include="FluentValidation" Version="11.9.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.0" />
<PackageReference Include="Asp.Versioning.Mvc" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<!-- Test project -->
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="Moq" Version="4.20.70" />Next Steps
- Learn about best practices for production deployments
- Set up webhooks for delivery status updates
- Explore the C# SDK reference for advanced features