Sending email in ASP.NET Core using SmtpClient and dependency injection

Using dependency injection to send email in ASP.NET Core

Starting with .NET Core 2.0, Microsoft introduced SmtpClient, identical implementation as in .NET 4. This made sending emails from the application a lot easier as there is functionality out of the box and you do not have to rely on the 3rd party nuget packages.

If you inspect this class, you will see that the only interface it inherits is IDisposable, so it does not give you many options for injection unless you wrap it with your own implementation and interface, but since we are not going to go that deep into the dependency injection, we'll just focus on creating a client instance using .NET Core dependency injection framework.

Since configuration is already injected on the Startup.cs constructor, we can use our configuration file for storing our SmtpClient configuration.

{
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  },
  "Email": {
    "Smtp": {
      "Host": "smtp.gmail.com",
      "Port": 25,
      "Username": "mail_username",
      "Password": "mail_password"
    }
  }

}

    

So let's start with our Startup.cs in ASP.NET Core application to setup the dependency injection for SmtpClient class. There are two options when it comes to injection configured instance of SmtpClient:

  • Scoped service - creates configured SmtpClient instance for every controller instance where SmtpClient is referenced in a constructor
  • Transient service - creates instance of configured SmtpClient on demand

Both of these approaches have their pros and cons, so well do it both ways and compare the approaches

Scoped service

In ASP.NET Core dependency injection context, scoped means on every request. This means you will have SmtpClient instance for each controller instance.

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<SmtpClient>((serviceProvider) =>
            {
                var config = serviceProvider.GetRequiredService<IConfiguration>();
                return new SmtpClient()
                {
                    Host = config.GetValue<String>("Email:Smtp:Host"),
                    Port = config.GetValue<int>("Email:Smtp:Port"),
                    Credentials = new NetworkCredential(
                            config.GetValue<String>("Email:Smtp:Username"), 
                            config.GetValue<String>("Email:Smtp:Password")
                        )
                };
            });
            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc();
        }
    }
    

Now in a controller we need to create a constructor which accepts SmtpClient as a parameter. This constructor will have configured instance of SmtpClient available from the dependency injection context and it will be available further on to all methods in the controller. 

This also means we need to dispose SmtpClient instance on the controller disposal and client instance will be shared among methods in a controller. This can be OK since for every request we will have a separate controller instance. Let's see how it looks.

    [Route("api/[controller]")]
    public class MailController : Controller
    {
        private SmtpClient smtpClient;
        public MailController(SmtpClient smtpClient)
        {
            this.smtpClient = smtpClient;
        }


        [HttpPost]
        public async Task<IActionResult> Post()
        {
            await this.smtpClient.SendMailAsync(new MailMessage(
                to: "sample1.app@noname.test",
                subject: "Test message subject",
                body: "Test message body"
                ));

            return Ok("OK");

        }

        protected override void Dispose(bool disposing)
        {
            this.smtpClient.Dispose();
            base.Dispose(disposing);
        }

    }
    

Unless we need more than one instance of SmtpClient, which is a rare case, we can continue using this approach. However, if you do not use SmtpClient for a call in this controller, you will have an instace created and disposed for nothing. This is a bit of overhead, since you are injecting instances even for the methods you do not actually use SmtpClient.

Note

Do not forget to dispose controller scoped SmtpClient class instance when you are disposing controller. An override of the dispose method of the controller for this approach is mandatory

Transient service

This approach is basically doing dependency injection on demand. When ever you need a configured SmtpClient, you ask the resolver to give you an instance. Startup code stays pretty much the same with a small change on a scope in the ConfigureServices method

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<SmtpClient>((serviceProvider) =>
            {
                var config = serviceProvider.GetRequiredService<IConfiguration>();
                return new SmtpClient()
                {
                    Host = config.GetValue<String>("Email:Smtp:Host"),
                    Port = config.GetValue<int>("Email:Smtp:Port"),
                    Credentials = new NetworkCredential(
                            config.GetValue<String>("Email:Smtp:Username"), 
                            config.GetValue<String>("Email:Smtp:Password")
                        )
                };
            });
            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc();
        }
    }
    

On the controller, instance is not created on the controller constructor, but inside the method where it is used. This allows wrapping the instance with using and no need for explicit disposing, since using will do the disposing for you.

    [Route("api/[controller]")]
    public class MailController : Controller
    {
        [HttpPost]
        public async Task<IActionResult> Post()
        {
            using (var smtpClient = HttpContext.RequestServices.GetRequiredService<SmtpClient>())
            {
                await smtpClient.SendMailAsync(new MailMessage(
                       to: "sample1.app@noname.test",
                       subject: "Test message subject",
                       body: "Test message body"
                       ));

                return Ok("OK");
            }
        }

    }
    

This way it is a lot easier to control the lifetime of the instance, but you need to explicitly call the dependency injection resolver to resolve the SmtpClient instance for you instead of letting it do in the background for you on the constructor like the previous approach. 

How do you inject the SmtpCleint instance in your ASP.NET Core application?

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article