I have created a .net core API, which pushes a message in RabbitMQ queue. I have used IOptions to read configuration data from .json file and added it as dependency.
Below is the code of my controller:
[Route("api/[controller]")]
public class RestController : Controller
{
private RabbitMQConnectionDetail _connectionDetail;
public RestController(IOptions<RabbitMQConnectionDetail> connectionDetail)
{
_connectionDetail = connectionDetail.Value;
}
[HttpPost]
public IActionResult Push([FromBody] OrderItem orderItem)
{
try
{
using (var rabbitMQConnection = new RabbitMQConnection(_connectionDetail.HostName,
_connectionDetail.UserName, _connectionDetail.Password))
{
using (var connection = rabbitMQConnection.CreateConnection())
{
var model = connection.CreateModel();
var helper = new RabbitMQHelper(model, "Topic_Exchange");
helper.PushMessageIntoQueue(orderItem.Serialize(), "Order_Queue");
}
}
}
catch (Exception)
{
return StatusCode((int)HttpStatusCode.BadRequest);
}
return Ok();
}
}
Connection details class has the below properties
public class RabbitMQConnectionDetail
{
public string HostName { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
}
Now I want to unit test it, but since I am going to test it against a blackbox, I'm not able to think of how to unit test it and looking for kind help.
ConnectionClass
public class RabbitMQConnection : IDisposable
{
private static IConnection _connection;
private readonly string _hostName;
private readonly string _userName;
private readonly string _password;
public RabbitMQConnection(string hostName, string userName, string password)
{
_hostName = hostName;
_userName = userName;
_password = password;
}
public IConnection CreateConnection()
{
var _factory = new ConnectionFactory
{
HostName = _hostName,
UserName = _userName,
Password = _password
};
_connection = _factory.CreateConnection();
var model = _connection.CreateModel();
return _connection;
}
public void Close()
{
_connection.Close();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_connection.Close();
}
}
~ RabbitMQConnection()
{
Dispose(false);
}
}
Helper class
public class RabbitMQHelper
{
private static IModel _model;
private static string _exchangeName;
const string RoutingKey = "dummy-key.";
public RabbitMQHelper(IModel model, string exchangeName)
{
_model = model;
_exchangeName = exchangeName;
}
public void SetupQueue(string queueName)
{
_model.ExchangeDeclare(_exchangeName, ExchangeType.Topic);
_model.QueueDeclare(queueName, true, false, false, null);
_model.QueueBind(queueName, _exchangeName, RoutingKey);
}
public void PushMessageIntoQueue(byte[] message, string queue)
{
SetupQueue(queue);
_model.BasicPublish(_exchangeName, RoutingKey, null, message);
}
public byte[] ReadMessageFromQueue(string queueName)
{
SetupQueue(queueName);
byte[] message;
var data = _model.BasicGet(queueName, false);
message = data.Body;
_model.BasicAck(data.DeliveryTag, false);
return message;
}
}
Tightly coupling your Controller to implementation concerns are making it difficult to test your controller without side-effects. From the sample you provided you have shown that you are encapsulating the 3rd part API implementations and only exposing abstractions. Good. You however have not created an abstraction that would allow you to mock them when testing. I suggest a refactor of the RabbitMQConnection to allow for this.
First have your own backing abstraction.
public interface IRabbitMQConnectionFactory {
IConnection CreateConnection();
}
And refactor RabbitMQConnection as follows
public class RabbitMQConnection : IRabbitMQConnectionFactory {
private readonly RabbitMQConnectionDetail connectionDetails;
public RabbitMQConnection(IOptions<RabbitMQConnectionDetail> connectionDetails) {
this.connectionDetails = connectionDetails.Value;
}
public IConnection CreateConnection() {
var factory = new ConnectionFactory {
HostName = connectionDetails.HostName,
UserName = connectionDetails.UserName,
Password = connectionDetails.Password
};
var connection = factory.CreateConnection();
return connection;
}
}
Take some time and review exactly what was done with this refactor. The IOptions was moved from the Controller to the factory and the RabbitMQConnection has also been simplified to do it's intended purpose. Creating a connection.
The Controller now would need to be refactored as well
[Route("api/[controller]")]
public class RestController : Controller {
private readonly IRabbitMQConnectionFactory factory;
public RestController(IRabbitMQConnectionFactory factory) {
this.factory = factory;
}
[HttpPost]
public IActionResult Push([FromBody] OrderItem orderItem) {
try {
using (var connection = factory.CreateConnection()) {
var model = connection.CreateModel();
var helper = new RabbitMQHelper(model, "Topic_Exchange");
helper.PushMessageIntoQueue(orderItem.Serialize(), "Order_Queue");
return Ok();
}
} catch (Exception) {
//TODO: Log error message
return StatusCode((int)HttpStatusCode.BadRequest);
}
}
}
Again note the simplification of the controller. This now allows the factory to be mocked and injected when testing and by extension allows the mocks to be used by the RabbitMQHelper. You can use your mocking framework of choice for dependencies or pure DI.
I dont think it is a unit test scenario. If you want to to test with external component ie database or message queue then i suggest you do it as integration test.
What we do is to have a sand box environment with component SQL database and azure message bus. We have code to correctly set the state for this component ie seed the database and clear the message bus. Then we run test on the environment and check the state of the database or message bus count etc.
Related
This is a follow up question to another post I created around implementing a UI test solution that could toggle which classes to execute code from based on interfaces. The whole goal was to re use test code on versions of apps that are identical (Web vs WPF).
The code compiles fine, but after the test is ran it bombs out on the GetPageModelType method call. Below is my implementation pretty much identical to the linked post, with a few minor adjustments to abstract some of the page object creation on a TestClassBase
UI Test that can determine which classes to execute code from at runtime using interfaces
Interface and corresponding Page Object classes
public interface ILogin
{
void Login(string username, string password);
}
public class WebLogin : ILogin
{
private readonly IWebDriver driver;
public WebLogin(IWebDriver driver)
{
this.driver = driver;
}
public void Login(string username, string password)
{
Console.WriteLine("Web Success!");
}
}
public class WPFLogin : ILogin
{
private readonly WindowsDriver<WindowsElement> session;
public WPFLogin(WindowsDriver<WindowsElement> session)
{
this.session = session;
}
public void Login(string username, string password)
{
Console.WriteLine("WPF Success!");
}
}
Page Object factory classes
public interface IPageModelFactory
{
ILogin CreateLogin();
}
public class WebPageModelFactory : IPageModelFactory
{
private readonly IWebDriver driver;
public WebPageModelFactory(IWebDriver driver)
{
this.driver = driver;
}
public ILogin CreateLogin()
{
return new WebLogin(driver);
}
}
public class WPFPageModelFactory : IPageModelFactory
{
private readonly WindowsDriver<WindowsElement> session;
public WPFPageModelFactory(WindowsDriver<WindowsElement> session)
{
this.session = session;
}
public ILogin CreateLogin()
{
return new WPFLogin(session);
}
}
public class PageModelFactory
{
private readonly object client;
public PageModelFactory(object client)
{
this.client = client;
}
// Create Page Objects
public ILogin CreateLoginPage()
{
var pageModelType = GetPageModelType<ILogin>();
var constructor = pageModelType.GetConstructor(new Type[] { client.GetType() });
return (ILogin)constructor.Invoke(new object[] { client });
}
private Type GetPageModelType<TPageModelInterface>()
{
return client.GetType().Assembly.GetTypes().Single(type => type.IsClass && typeof(TPageModelInterface).IsAssignableFrom(type));
}
}
TestClassBase - base class for tests, simplifies test scripts
[TestFixture]
public class TestClassBase
{
// WinAppDriver variables
private static string WinAppDriverExe = "C:\\Program Files (x86)\\Windows Application Driver\\WinAppDriver.exe";
private string WindowsApplicationDriverUrl = "http://127.0.0.1:4723";
// Sessions
public WindowsDriver<WindowsElement> session;
public IWebDriver driver;
// Declare Page Objects
public ILogin login = null;
[SetUp]
public void SetUp()
{
if (GlobalData.targetHost.Equals("WPF"))
{
// Capabilities
AppiumOptions appCapabilities = new AppiumOptions();
appCapabilities.AddAdditionalCapability("app", GetExeFile());
appCapabilities.AddAdditionalCapability("appWorkingDir", GetWorkingDirectory());
// Create Session
session = new WindowsDriver<WindowsElement>(new Uri(WindowsApplicationDriverUrl), appCapabilities, TimeSpan.FromMinutes(3));
session.Manage().Window.Maximize();
// Pass session to page objects
PageModelFactory wpfPages = new PageModelFactory(session);
login = wpfPages.CreateLoginPage();
} else if (GlobalData.targetHost.Equals("Web"))
{
}
}
[TearDown]
public void TearDown()
{
// Clean up code...
}
}
LoginTests
public class LoginTests : TestClassBase
{
[Test]
public void Login()
{
// Login
login.Login("", "");
}
}
Whats not pictured above is my GlobalData.cs class which just contains a bunch of hardcoded variables that are used in the tests. I have the targetHost variable set to "WPF" while testing this against the WPF host. The StartUp code does launch the app as expected, it fails when we call GetPageModelType on PageModelFactory.CreateLoginPage();
I wasn't able to see this in my answer on your original question. The assembly in which the "client" resides and the assembly in which the page models reside are different. That means the PageModelFactory will need a second constructor parameter to know which assembly to search when initializing new page models:
public class PageModelFactory
{
private readonly object client;
private Assembly Assembly => GetType().Assembly;
public PageModelFactory(object client)
{
this.client = client;
}
// Create Page Objects
public ILogin CreateLoginPage()
{
var pageModelTypes = GetPageModelTypes<ILogin>();
var constructorSignature = new Type[] { client.GetType() };
foreach (var type in pageModelTypes)
{
var constructor = type.GetConstructor(constructorSignature);
if (constructor != null)
return (ILogin)constructor.Invoke(new object[] { client });
}
throw new InvalidOperationException($"No class found implementing ILogin with a constructor that accepts {client.GetType().FullName} as an argument in assembly {Assembly.Name}");
}
private IEnumerable<Type> GetPageModelTypes<TPageModelInterface>()
{
return Assembly.GetTypes()
.Where(type => type.IsClass
&& typeof(TPageModelInterface).IsAssignableFrom(type));
}
}
I am implementing some code where I use a visitors IP address to determine their location. For .net core 2, this is:
var ipAddress = Request.HttpContext.Connection.RemoteIpAddress;
But of course when I test locally I always get the loop back address ::1. Is there a way to simulate external IP addresses while testing locally?
You can create a service for retrieving remote address. Define an interface for it and create 2 implementations and inject them depending on the current environment
public interface IRemoteIpService
{
IPAddress GetRemoteIpAddress();
}
public class RemoteIpService : IRemoteIpService
{
private readonly IHttpContextAccessor _httpContextAccessor;
public RemoteIpService(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public IPAddress GetRemoteIpAddress()
{
return _httpContextAccessor.HttpContext.Connection.RemoteIpAddress;
}
}
public class DummyRemoteIpService : IRemoteIpService
{
public IPAddress GetRemoteIpAddress()
{
//add your implementation
return IPAddress.Parse("120.1.1.99");
}
}
Startup
if (HostingEnvironment.IsProduction())
{
services.AddScoped<IRemoteIpService, RemoteIpService>();
}
else
{
services.AddScoped<IRemoteIpService, DummyRemoteIpService>();
}
Usage
public class TestController : Controller
{
//...
private readonly IRemoteIpService _remoteIpService;
public TestController(IRemoteIpService remoteIpService)
{
//...
_remoteIpService = remoteIpService;
}
//..
[HttpGet]
public IActionResult Test()
{
var ip = _remoteIpService.GetRemoteIpAddress();
return Json(ip.ToString());
}
}
For getting external ip for localhost, you need to send request to retrive the ip, and you could implement an extension for ConnectionInfo like
public static class ConnectionExtension
{
public static IPAddress RemotePublicIpAddress(this ConnectionInfo connection)
{
if (!IPAddress.IsLoopback(connection.RemoteIpAddress))
{
return connection.RemoteIpAddress;
}
else
{
string externalip = new WebClient().DownloadString("http://icanhazip.com").Replace("\n","");
return IPAddress.Parse(externalip);
}
}
}
And use like
var ip = Request.HttpContext.Connection.RemotePublicIpAddress();
I'm creating an Bot Application using Luis and Luis Action Binding like the following sample
Luis action binding sample on GitHub
I want to use dependency injection to pass the data service instance to the action binding
I add a data layer following repository design pattern like that:
public class ProviderService : IProviderService
{
private readonly IProviderRepository ProviderRepository;
private readonly IUnitOfWork UnitOfWork;
public ProviderService(IProviderRepository providerService, IUnitOfWork unitOfWork)
{
this.ProviderRepository = providerService;
this.UnitOfWork = unitOfWork;
}
public void CreateProvider(Provider provider)
{
ProviderRepository.Add(provider);
}
and will use it from the fulfill method in the action binding class like that
public class FindNumberAction : BaseLuisAction
{
IProviderService _provider;
[Required(ErrorMessage = "Please provide a ProviderName")]
public string Provider { get; set; }
public string ProviderType { get; set; }
public FindNumberAction() : this(new ProviderService(new ProviderRepository(new DbFactory()), new UnitOfWork(new DbFactory())))
{
}
public FindNumberAction(IProviderService provider)
{
_provider = provider;
}
public override Task<object> FulfillAsync()
{
var providerData = _provider.GetByProviderName(Provider);
if (!string.IsNullOrEmpty(providerData.ProviderAddress))
{
var result = new ProviderInfo
{
ProviderAddress = providerData.ProviderAddress,
ProviderName = providerData.Provider.ProviderName,
ProviderPhone = providerData.ProviderPhone,
ProviderType = providerData.Provider.ProviderType.ProviderTypeName
};
return Task.FromResult((object)result.GetNumber());
}
return Task.FromResult((object)new ProviderInfo() { ProviderName = Provider, ProviderType = ProviderType }.NoProviderNumberFound());
}
}
https://github.com/Microsoft/BotBuilder/tree/master/CSharp/Samples/AlarmBot
Here is a great example of how to implement AutoFac in the bot framework. You'll see that you not only need to implement the container, but also have to change the way the MessagesController handles the incomming messages.
I have asp.net core application. Given S3 urls, the application needs to download file. These S3 urls may belongs to different AWS regions in the US.
public class Downloader
{
public void DownloadFile(string s3Url, string destFolder)
{
//download files
}
}
The DownloadFile() method gets called concurrently. Each url pass to this method may belong to different region.
AWS documentation shows how to Configure the AWS SDK for .NET with .NET Core. In my case AWS credentials are stored in profile file on server and the same credentials can be used across all US regions. So appsettings.json looks like
"AWS": {
"Profile": "default",
"ProfilesLocation": "C:\\aws\\awsprofile"
},
Issue
Since URLS can belong to different region, i cannot follow documentation code. I cannot register IAmazonS3 with DI framework and inject that instance into Downloader to download files from different regions. Because IAmazonS3 instance tried to a particular region.
Solution
So i created a factory which provides instance of IAmazonS3 given region name.
public interface IS3ClientFactory : IDisposable
{
IAmazonS3 GetS3Client(RegionEndpoint region);
}
public class S3ClientFactory : IS3ClientFactory
{
private bool _disposed = false;
private IDictionary<string, IAmazonS3> _container = null;
private S3ClientFactory()
{
_container = new Dictionary<string, IAmazonS3>();
}
public static IS3ClientFactory Configure(AWSOptions option, RegionEndpoint[] regions)
{
var factory = new S3ClientFactory();
foreach (RegionEndpoint region in regions)
{
option.Region = region;
factory._container.Add(region.SystemName, option.CreateServiceClient<IAmazonS3>());
}
return factory;
}
public IAmazonS3 GetS3Client(RegionEndpoint region)
{
return _container[region.SystemName];
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
if (_container != null && _container.Any())
{
foreach (var s3Client in _container)
{
if (s3Client.Value != null)
{
s3Client.Value.Dispose();
}
}
}
_disposed = true;
}
}
}
}
and in startup.cs register factory with DI
services.AddSingleton<IS3ClientFactory>(S3ClientFactory.Configure(Configuration.GetAWSOptions(),
new RegionEndpoint[]
{
RegionEndpoint.USWest1,
RegionEndpoint.USWest2,
RegionEndpoint.USEast1,
RegionEndpoint.USEast2
}));
and Downloader class would look like
public class Downloader : IDownloader
{
private readonly IS3ClientFactory _factory;
public Downloader(IS3ClientFactory factory)
{
_factory = factory;
}
public void DownloadFile(string s3Url, string destFolder)
{
var s3Uri = new AmazonS3Uri(s3Url);
var s3Client = _factory.GetS3Client(s3Uri.Region);
// use s3Client to download file
}
}
Questions
In S3ClientFactory's Configure method i am dynamically assigning RegionEndpoint to AWSOptions and then call option.CreateServiceClient<IAmazonS3>() is this a correct way of creating region specific instance of IAmazonS3? The code need to be unit testable so i could not use new AmazonS3Client(RegionEndpoint).
foreach (RegionEndpoint region in regions)
{
option.Region = region;
factory._container.Add(region.SystemName, option.CreateServiceClient<IAmazonS3>());
}
Is it okay to have singleton instance of IAmazonS3?
You can just inject IEnumerable of your service interface. And then find the instance that you want using LINQ.
Startup
foreach (string snsRegion in Configuration["SNSRegions"].Split(',', StringSplitOptions.RemoveEmptyEntries))
{
services.AddAWSService<IAmazonSimpleNotificationService>(
string.IsNullOrEmpty(snsRegion) ? null :
new AWSOptions()
{
Region = RegionEndpoint.GetBySystemName(snsRegion)
}
);
}
services.AddSingleton<ISNSFactory, SNSFactory>();
services.Configure<SNSConfig>(Configuration);
SNSConfig
public class SNSConfig
{
public string SNSDefaultRegion { get; set; }
public string SNSSMSRegion { get; set; }
}
appsettings.json
"SNSRegions": "ap-south-1,us-west-2",
"SNSDefaultRegion": "ap-south-1",
"SNSSMSRegion": "us-west-2",
SNS Factory
public class SNSFactory : ISNSFactory
{
private readonly SNSConfig _snsConfig;
private readonly IEnumerable<IAmazonSimpleNotificationService> _snsServices;
public SNSFactory(
IOptions<SNSConfig> snsConfig,
IEnumerable<IAmazonSimpleNotificationService> snsServices
)
{
_snsConfig = snsConfig.Value;
_snsServices = snsServices;
}
public IAmazonSimpleNotificationService ForDefault()
{
return GetSNS(_snsConfig.SNSDefaultRegion);
}
public IAmazonSimpleNotificationService ForSMS()
{
return GetSNS(_snsConfig.SNSSMSRegion);
}
private IAmazonSimpleNotificationService GetSNS(string region)
{
return GetSNS(RegionEndpoint.GetBySystemName(region));
}
private IAmazonSimpleNotificationService GetSNS(RegionEndpoint region)
{
IAmazonSimpleNotificationService service = _snsServices.FirstOrDefault(sns => sns.Config.RegionEndpoint == region);
if (service == null)
{
throw new Exception($"No SNS service registered for region: {region}");
}
return service;
}
}
public interface ISNSFactory
{
IAmazonSimpleNotificationService ForDefault();
IAmazonSimpleNotificationService ForSMS();
}
Now you can get the SNS service for the region that you want in your custom service or controller
public class SmsSender : ISmsSender
{
private readonly IAmazonSimpleNotificationService _sns;
public SmsSender(ISNSFactory snsFactory)
{
_sns = snsFactory.ForSMS();
}
.......
}
public class DeviceController : Controller
{
private readonly IAmazonSimpleNotificationService _sns;
public DeviceController(ISNSFactory snsFactory)
{
_sns = snsFactory.ForDefault();
}
.........
}
(I am sure that I formatted the question badly, I would be happy to revise and fix depending on comments)
I have a static class and I am trying to improve the design with dependency injection. I don't necessarily want this class to be static anymore because I will be using .NET Core, which promotes dependency injection over static class situations.
The simplified code in .NET (not Core):
public static class Utils
{
public static readonly string tokenUrl = ConfigurationManager.AppSettings["tokenUrl"];
public static readonly string tokenKey = ConfigurationManager.AppSettings["tokenKey"];
public async static Task<bool> SendEmail(Email email)
{
var http = new HttpClient();
http.DefaultRequestHeaders.Add("subscription-key", tokenKey);
try
{
await http.PostAsync(tokenUrl + "email", new StringContent(JsonConvert.SerializeObject(email), Encoding.UTF8, "application/json"));
}
catch (Exception e)
{
return false;
}
return true;
}
}
For ConfigurationManager.AppSettings (it does not exist in .NET Core), I am planning to use the method in this link: http://www.danylkoweb.com/Blog/no-configurationmanager-in-aspnet-core-GC
However, for converting this (SendMail) method into a dependency injection, I am quite lost. I have read many examples and articles and I understand the logic of dependency injection but I don't know how to convert this static class into a proper dependency injection. There are other methods in the same Utils class but this is the simplest one and I hope to figure out the others using this one.
An approach that I was thinking off was:
public interface ISendMail
{
FormSettings ConfigSettings { get; set; }
Task<bool> SendEmail(IOptions<FormSettings> settings, Email email);
}
and:
public class SendEmail : ISendMail
{
public async static Task<bool> SendEmail(IOptions<FormSettings> settings, Email email)
{
//do same things
}
}
but I am CLEARLY lost with this because it does not even make sense. Another approach that I was thinking of was:
public class SendEmail
{
FormSettings ConfigSettings { get; set; }
protected Email email = null;
public SendEmail(IOptions<FormSettings> settings, Email email)
{
ConfigSettings = settings.Value;
this.email = email;
}
public async static Task<bool> SendEmailAction()
{
//do same things with "email" and "ConfigSettings"
}
}
I know I am giving a lot of code here and I wasn't sure if I should ask about this in "Code Review" or something. My biggest concern is not the FormSettings part but implementing the functionality of SendEmail in a dependency injection format.
Shortly, how can I convert this "SendEmail" class into a format where I can use it with .NET Core, without having a static class? This particular method does not require change with .NET Core but my other methods do, that is why I am trying to get rid of the static class approach.
I can exclude the tokenUrl and tokenKey parts and simplify the problem if requested, I am just quite lost as to how to approach this situation.
What should do this class? Sending email, right? So interface:
public interface IEmailSender
{
Task<bool> Send(Email email);
}
How we can implement it? Like this:
public class MyEmailSenderOne : IEmailSender
{
public static readonly string tokenUrl = ConfigurationManager.AppSettings["tokenUrl"];
public static readonly string tokenKey = ConfigurationManager.AppSettings["tokenKey"];
public async Task<bool> Send(Email email)
{
var http = new HttpClient();
http.DefaultRequestHeaders.Add("subscription-key", tokenKey);
try
{
await http.PostAsync(tokenUrl + "email", new StringContent(JsonConvert.SerializeObject(email), Encoding.UTF8, "application/json"));
}
catch (Exception e)
{
return false;
}
return true;
}
}
or
public class MyAnotherAwesomeEmailSender : IEmailSender
{
public async Task<bool> Send(Email email)
{
// send with different way
return true;
}
}
How we can inject this?
public class SomeClass
{
private IEmailSender _sender;
public SomeClass(IEmailSender sender)
{
_sender = sender;
}
public void Foo()
{
// do smth useful
_sender.Send(new Email());
}
}
UPD.
Because your email settings persistant (will not change during lifetime), and because this settings related ONLY to your implementation of IEMailSender, you should to inject them in your implementation. Just think about = why caller code (Controller) should know about how your implementation works?
So
public class MyEmailSenderOne : IEmailSender
{
private FormSettings _settings;
public MyEmailSenderOne(IOptions<FormSettings> settings)
{
_settings = settings.Value;
}
public async Task<bool> Send(Email email)
{
var http = new HttpClient();
http.DefaultRequestHeaders.Add("subscription-key", _settings.tokenApiKey);
try
{
await http.PostAsync(_settings.tokenApiUrl + "email", new StringContent(JsonConvert.SerializeObject(email), Encoding.UTF8, "application/json"));
}
catch (Exception e)
{
return false;
}
return true;
}
}
And, controller now dint know about any settings for your implementation, and it looks like
public class CommunicationsController : Controller
{
private IEmailSender _sender;
public CommunicationsController(IEmailSender sender)
{
_sender = sender;
}
public async Task<ActionResult> ContactUsFormSubmit(ContactUs request)
{
...
request.EmailSent = await _sender.SendEmail(new Email() { TemplateId = 3, Body = JsonConvert.SerializeObject(request) });
...
}
}
As you can see, controller is very clean now and you can easily change your implementation of IEmailSender to any other without changing Controller code. This is one of advantages of using DI.
Based on tym32167's answer, I was able to implement the IEmailSender functionality (finally). I will still choose his answer as the correct answer but this is how I implemented dependency injection.
Please read the link I provided in the question, if you'd like to know more about the IOptions and FormSettings class that I am using.
Here is the interface and the class:
public interface IEmailSender
{
Task<bool> SendEmail(Email email, FormSettings settings);
}
public class EmailSender : IEmailSender
{
FormSettings ConfigSettings { get; set; }
public async Task<bool> SendEmail(Email email, FormSettings settings)
{
var http = new HttpClient();
http.DefaultRequestHeaders.Add("subscription-key", settings.tokenApiKey);
try
{
await http.PostAsync(settings.tokenApiUrl + "email", new StringContent(JsonConvert.SerializeObject(email), Encoding.UTF8, "application/json"));
}
catch (Exception e)
{
return false;
}
return true;
}
}
In controller injection:
public class CommunicationsController : Controller
{
private IEmailSender _sender;
private FormSettings ConfigSettings { get; set; }
public CommunicationsController(IEmailSender sender, IOptions<FormSettings> settings)
{
_sender = sender;
ConfigSettings = settings.Value;
}
public async Task<ActionResult> ContactUsFormSubmit(ContactUs request)
{
...
request.EmailSent = await _sender.SendEmail(new Email() { TemplateId = 3, Body = JsonConvert.SerializeObject(request) }, ConfigSettings);
...
}
Here is FormSettings just for easy reference in case the link dies:
public class FormSettings
{
public string tokenApiUrl { get; set; }
public string tokenApiKey { get; set; }
}
I hope I didn't miss any details, so far it didn't give me any errors but since we do not have unit testing in the project, I won't be able to test immediately. Please let me know if there is something missing with the answer.