How to Send Email Using ASP.NET Core Application – Windows ASP.NET Core Hosting 2024 | Review and Comparison

Using a Asp.NET Core Web application, it is very easy to send an email and get the SMTP settings (SMTP Server, From Address and Alias(Display Name)) from file. The purpose of this blog is about how to get SMTP settings from file, so email content (Subject and Message) is simple. Follow the below steps for that. Let’s get started!

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

Best ASP.NET Hosting

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: "[email protected]",  
            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: "[email protected]",  
                   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?