Skip to content

Design Decisions

Anton Zhaparov edited this page Sep 13, 2024 · 9 revisions

000. Design guidelines

This SDK developed with MS framework design guidelines in mind.
Although they are quite old, they are still relevant in most of aspects covered.

001. Targeting .NET Standard 2.0

There is a goal to support the majority of modern .NET runtime implementations, including latest versions of .NET Framework (4.6-4.8).
And .NET Standard 2.0 seems to be the best option for such scenarios.
https://learn.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0#when-to-target-net80-vs-netstandard

As a tradeof, it is impossible to use a bunch of features, which modern C# language versions provide, but it is acceptable.

002. Developing with DI in mind

Dependency injection is a first-class citizen of the vast majority of modern applications.
.NET library includes default DI container implementation for a long time and it is widely used in application infrastructure.
https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection

So decision was made to develop this SDK with DI in mind, providing extension methods to register required services in default DI container.
That adds some complexity to the implementation, meanwhile, propels better code structuring and following SRP, simplifies unit testing as well.

Ideally, DI container functionality should be created as a separate project/package, but considering that client implementation uses default IHttpClientFactory implementation under the hood, it makes no sense, since .NET DI library will be a dependency anyway.

Support for popular 3rd-party DI containers (Autofac, DryIoc, Simple Inject, etc.) can be added later, as separate projects/packages.

003. Usage of IHttpClientFactory to produce HttpClient instances

Since the project is targeting .NET Standard, it is impossible to use some class implementations, which were introduced in modern .NET versions.
Thus using IHttpClientFactory to produce HttpClient instances is the only reliable way to handle possible port exhausting and stale DNS cache issues in .NET Framework.
https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient-guidelines#recommended-use

That's adds some challenge, because default factory implementation requires use of DI container to be instantiated and used.
That is actually the primary reason, why IMailtrapClientFactory and its default implementation were introduced.
Also that is the reason why DI related functionality isn't created as a separate project/package, which seems to be a common approach.

004. Hiding validation implementation details.

Although library is using FluentValidation under the hood for configuration/requests validation, decision was made not to expose its contracts to the public surface, to minimize consumer dependencies.
Thus validatoin exceptions are converted to standard .NET ArgumentException in the common scenario.

005. Single Send method usage for all channels/endpoints/products.

The idea is to mimic and keep consistency between different API client implementations (Ruby, Node.js, PHP, etc.) and to allow customers to switch channels with minimal source code changes.

Thus there is the single abstraction IEmailClient with Send method which is same for any channel.
Meanwhile, the channel (transactional, bulk or sandbox/test) is defined by configuration.

Alternatively, IMailtrapClient defines methods to get specific channel explicitly.