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.json

Configuration

// 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

VariableDescriptionDefault
SENT_DM_API_KEYYour Sent DM API key (required)-
SENT_DM_WEBHOOK_SECRETWebhook signature secretnull
SENT_DM_BASE_URLAPI base URLhttps://api.sent.dm
SENT_DM_TIMEOUT_SECONDSRequest timeout30
SENT_DM_MAX_RETRIESMax retry attempts3
# 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

On this page