Where It All Started.

Where It All Started.

Life, Stock Trading, Investments, Business and Startup. Most are programming stuff.

Tag: web

Dirty Logging With Serilog For ASP.NET 5

Here we are again on yet-another tutorial on using Serilog1 as your primary logging framework for both development as well as production.

If you look on the internet you’ll see there are many ways to integrate Serilog in your existing application, on this tutorial I’ll show you what I’ve been using for all the projects that I’ve handled.

You may ask what is the difference of my setup compared to others?


Programming isn’t about what you know; it’s about what you can figure out.

— Chris Pine.

The setup I’ll show you allows easy addition of new sinks and on the fly modification of log format without the need of recompiling your app again.

So how does that work?

Follow me and let’s dive on how to implement it. 👆

Prerequisites

First of all, you must have a .NET 5.0 SDK (Software Development Kit) installed in your computer and also I assumed you are currently running Windows 10 or Linux with proper environment set.

If you are on Windows 10 and already have a Visual Studio2 2019, just update it to the most recent version, that way would ensure your system to have the latest .NET Core SDK3 version.

So where do we start?

First, let’s create a test bed project. Type the command below on an existing shell console (e.g. bash, power shell, cmd, .etc).

dotnet new web -f net5.0 --no-https --name SerilogDemo

The command above will create a new project that will use .NET 5 framework. And from the above flags --no-https will setup the project to use non-secured (non-SSL) empty web API (it means will not generate a cert and an HTTPS URL).

After the project creation, change directory to the project root. And install the following nuget dependencies.

dotnet add package Serilog
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Extensions.Hosting
dotnet add package Serilog.Extensions.Logging
dotnet add package Serilog.Settings.Configuration
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File

Then we will now dive to C# implementation, first import the Serilog library on Program.cs.

using Serilog;

After that, still on the Program.cs file go to the method CreateHostBuilder and add /or chain the UseSerilog method to our application startup.

webBuilder.UseStartup<Startup>().UseSerilog();

The UseSerilog call will initialize the Serilog instance. Then we move to Serilog hooks to our main thread. On the Main method convert it first from void to int, the reason for this is to be able to return different numeric exit code when an error happens.

Then we initialize the appsettings configuration early on, normally this will be initialized on startup. The reason we initialize this first, is we will use the appsettings to provide configuration to Serilog logger instance that will be called on the this Main method.

IConfigurationRoot configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile(path: "appsettings.json", optional: false, reloadOnChange: true)
    .Build();

Next on, initialize the logger instance and handle further exceptions coming from the host builder creation. Will just use generic Exception to catch all types of exception (this is not advisable specially in production environment – if ever use specific type exception).

Log.Logger = new LoggerConfiguration()
    .ReadFrom.Configuration(configuration)
    .CreateLogger();

Log.Information("SerilogDemo Args: {a}", args);

try
{
    var host = CreateHostBuilder(args).Build();
    Log.Information("Starting serilog demo service");
    host.Run();
    return 0;
}
catch (Exception ex)
{
    Log.Fatal(ex, "SerilogDemo service terminated unexpectedly");
    return 1;
}
finally
{
    Log.CloseAndFlush();
}

When all changes done, re-check all your codes above. Check whether its the same setup, if all is fine we move to the appsettings.json as that is our main key in this tutorial.

using System;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Serilog;

namespace SerilogDemo
{
    public sealed class Program
    {
        public static int Main(string[] args)
        {
            IConfigurationRoot configuration = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile(path: "appsettings.json", optional: false, reloadOnChange: true)
                .Build();

            Log.Logger = new LoggerConfiguration()
                .ReadFrom.Configuration(configuration)
                .CreateLogger();

            Log.Information("Token Validator Args: {a}", args);

            try
            {
                var host = CreateHostBuilder(args).Build();
                Log.Information("Starting token validator service");
                host.Run();
                return 0;
            }
            catch (Exception ex)
            {
                Log.Fatal(ex, "Token validator service terminated unexpectedly");
                return 1;
            }
            finally
            {
                Log.CloseAndFlush();
            }
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>()
                        .UseSerilog();
                });
    }
}

On the appsettings.json, add the following JSON structure inside the root.

"Serilog": {
  "Using": [
    "Serilog.Sinks.Console",
    "Serilog.Sinks.File"
  ],
  "Enrich": [
    "FromLogContext",
    "WithMachineName",
    "WithThreadId"
  ],
  "WriteTo": [
    {
      "Name": "Console",
      "Args": {
        "outputTemplate": "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}"
      }
    },
    {
      "Name": "File",
      "Args": {
        "path": "logs\\log_.txt",
        "rollingInterval": "Day"
      }
    }
  ]
}

Analyze the JSON structure, if you look carefully on the Using its specified to use the Console, and File. What does this mean is it will log on console and the specified log file.

Then on Enrich key, its specify what type of data needed to be added on the log and where to get it. The WriteTo key allows you to configure each sink (e.g. change format of log).

After that, we will now check if everything’s working fine by compiling it and running the program. If everything is okay, you should now see logs in your console and a file will be created containing the previous logs.

dotnet run

That’s all basically, if you want to use the Serilog on your controller check below.

(OPTIONAL)

If you want to use Serilog on a controller or any class, then what you will do is import first the abstract logging class.

using Microsoft.Extensions.Logging;

Then on controller or class constructor add the ILogger with the name of the current class. Below is the sample basic setup.

private readonly ILogger<InformHub> _logger;

public InformHub(ILogger<InformHub> logger)
{
    _logger = logger;
}

public async Task Command()
{
    _logger.LogInformation("Hello, World!");
}

To use the logger on class, just call the logger instance and add call to the method of log verbosity you want. On the above we use LogInformation or INFO verbosity.

(ANOTHER OPTIONAL)

This is for IIS, if you ever want to use a file logging you may encounter that on default setup it will not log to files. In order to log to files, first create the log directory (specified on the appsettings.json). Then change the folder’s ACL settings, add the the user IIS_USRS\<name-of-iis-site-name>.

Fig. 1: Properties folder

Add Write access to the log folder for the user IIS_USRS\<name-of-iis-site-name>.

Fig. 2: Advance permission editing modal

Then just restart the site and check if everything is working. If its still not working just redo it base on above.

That’s all guys!

Conclusion

Logging is one of the most important thing to do in development and production. Having no loggers in your web application is not advisable (with the exception of fully tested, high security application), loggers can easily point out easily what exception and details occurred on client side.

You can found the complete repository here.

Let me know in the comments if you have questions or queries, you can also DM me directly. Follow me for similar article, tips, and tricks ❤.


  1. Serilog is a diagnostic logging library for .NET applications. It is easy to set up, has a clean API, and runs on all recent .NET platforms. While it’s useful even in the simplest applications, Serilog’s support for structured logging shines when instrumenting complex, distributed, and asynchronous applications and systems. ↩︎
  2. Microsoft Visual Studio is an integrated development environment (IDE) from Microsoft. It is used to develop computer programs, as well as websites, web apps, web services and mobile apps. Visual Studio uses Microsoft software development platforms such as Windows API, Windows Forms, Windows Presentation Foundation, Windows Store and Microsoft Silverlight. It can produce both native code and managed code. ↩︎
  3. .NET (previously named .NET Core) is a free and open-source, managed computer software framework for Windows, Linux, and macOS operating systems. It is a cross-platform successor to .NET Framework. The project is primarily developed by Microsoft employees by way of the .NET Foundation, and released under the MIT License. ↩︎

Sending Email Using MailKit in ASP.NET Core Web API

You do not need to know precisely what is happening, or exactly where it is all going. What you need is to recognize the possibilities and challenges offered by the present moment, and to embrace them with courage, faith and hope.

— Thomas Merton.

Hey guys, recently I’ve been working on an ASP.NET Core project that needed email services to send reports. And I’ve been dumbfounded by some tutorial on how they implemented the email service functionality. Some are over complicated while others were over simplified.

So here I am creating yet another tutorial for sending email using MailKit and .NET Core.

Let’s jump in!

Prerequisites

First of all, you must have a .NET Core 3.1 SDK (Software Development Kit) installed in your computer and also I assumed you are currently running Windows 10 or some Linux with proper environment set.

And MailKit on which this package becomes a de-facto standard in sending emails as its being preferred and recommended by Microsoft in their tutorials over the standard System.Mail.Net .

So where do we start?

First we create our ASP.NET Web API project on the command-line. Execute the command below to create the project.

dotnet new webapi --name MyProject

The dotnet new command creates a project folder base on the declared template on which in our case is webapi . The --name flag indicates that the next argument would be its output path and project name.

After that go inside the project root folder. Then we add a reference to MailKit nuget package to project. If the project already reference the MailKit then try running dotnet restore to get and update the reference assemblies locally.

dotnet add package MailKit

Then we create and add the SMTP ( Simple Mail Transfer Protocol) settings to the appsettings.json and appsettings.Development.json . In the example below I’ve used Gmail setup, just fill it up with your own account settings and be sure to use an app password in the password field.

If you have a custom SMTP server just replace the port and server as well as other needed fields.

"SmtpSettings": {
  "Server": "smtp.gmail.com",
  "Port": 587,
  "SenderName": "My Name",
  "SenderEmail": "<my-account-user>@gmail.com",
  "Username": "<my-account-user>@gmail.com",
  "Password": "<my-account-password>"
}

Create an entity structure which will store the SMTP settings. This structure will receive the settings we setup above in the appsettings.json .

namespace MyProject.Entities
{
    public class SmtpSettings
    {
        public string Server { get; set; }
        public int Port { get; set; }
        public string SenderName { get; set; }
        public string SenderEmail { get; set; }
        public string Username { get; set; }
        public string Password { get; set; }
    }
}

Next is we setup the mailer interface to provide to our controllers. This IMailer interface exposes one method just to send email asynchronously. You can add more methods but I feel one is enough.

public interface IMailer
{
    Task SendEmailAsync(string email, string subject, string body);
}

Implement the mailer class structure with basic defaults, and after that try and build it, check if there are any error. Check if linting provides any warning or programming mistakes.

public class Mailer : IMailer
{
    public async Task SendEmailAsync(string email, string subject, string body)
    {
        await Task.Completed;
    }
}

If all things implemented properly, we create the template sender functionality. This method will accept recipient email, the subject of the email and the message body.

using MailKit.Net.Smtp;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using MimeKit;
using System;
using System.Threading.Tasks;
using MyProject.Entities;

namespace MyProject.Services
{
    public interface IMailer
    {
        Task SendEmailAsync(string email, string subject, string body);
    }

    public class Mailer : IMailer
    {
        private readonly SmtpSettings _smtpSettings;
        private readonly IWebHostEnvironment _env;

        public Mailer(IOptions<SmtpSettings> smtpSettings, IWebHostEnvironment env)
        {
            _smtpSettings = smtpSettings.Value;
            _env = env;
        }

        public async Task SendEmailAsync(string email, string subject, string body)
        {
            try
            {
                var message = new MimeMessage();
                message.From.Add(new MailboxAddress(_smtpSettings.SenderName, _smtpSettings.SenderEmail));
                message.To.Add(new MailboxAddress(email));
                message.Subject = subject;
                message.Body = new TextPart("html")
                {
                    Text = body
                };

                using (var client = new SmtpClient())
                {
                    client.ServerCertificateValidationCallback = (s, c, h, e) => true;

                    if (_env.IsDevelopment())
                    {
                        await client.ConnectAsync(_smtpSettings.Server, _smtpSettings.Port, true);
                    }
                    else
                    {
                        await client.ConnectAsync(_smtpSettings.Server);
                    }

                    await client.AuthenticateAsync(_smtpSettings.Username, _smtpSettings.Password);
                    await client.SendAsync(message);
                    await client.DisconnectAsync(true);
                }
            }
            catch (Exception e)
            {
                throw new InvalidOperationException(e.Message);
            }
        }
    }
}

In the source above we create first a MimeMessage which contains all the needed data for an email body and header, it contains MAIL FROM, RCPT TO , and DATA .

After that we setup SMTP client with the fields we setup in our appsettings.json . The client.AuthenticateAsync can be omitted if the SMTP server doesn’t have an authentication flow.

When everything is done in Mailer, we now edit the Startup.cs file in project root folder. We then insert SMTP settings parser and initialize a singleton object that will handle mail service in ConfigureServices .

services.Configure<SmtpSettings>(Configuration.GetSection("SmtpSettings"));
services.AddSingleton<IMailer, Mailer>();

After setting up the services in startup, we head onto the WeatherForecastController.cs which is included when we bootstrap the project. This files are part of the webapi template, you can use your own custom controller function to call on the IMailer interface.

private readonly IMailer _mailer;

public WeatherForecastController(ILogger<WeatherForecastController> logger, IMailer mailer)
{
    _logger = logger;
    _mailer = mailer;
}

Look on how we add the IMailer mailer variable as this becomes available for us when we did setup and add a singleton object in our startup. We then store the variable in our private variable for future usage.

We also create another method to handle new route /export for sending temporary weather report. Change it according to your own setup.

[HttpGet]
[Route("export")]
public async Task<IActionResult> ExportWeatherReport()
{
    await _mailer.SendEmailAsync("[email protected]", "Weather Report", "Detailed Weather Report");
    return NoContent();
}

In the code above we simply insert the mailer and called our exposed method SendEmailAsync . Check the full source below for details on what to packages needed to import on the module.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using MyProject.Services;

namespace MyProject.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;
        private readonly IMailer _mailer;

        public WeatherForecastController(ILogger<WeatherForecastController> logger, IMailer mailer)
        {
            _logger = logger;
            _mailer = mailer;
        }

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }

        [HttpGet]
        [Route("export")]
        public async Task<IActionResult> ExportWeatherReport()
        {
            await _mailer.SendEmailAsync("[email protected]", "Weather Report", "Detailed Weather Report");
            return NoContent();
        }
    }
}

When everything’s done we build and test the web API project. Execute the code below to check if there are any errors.

dotnet build

Then deploy or publish it on IIS (Internet Information Services), or rather just run it in isolated form which you can use dotnet run .

Conclusion

If you’re doing an email service always consider to make it as simple as possible to avoid any unintended bugs. Sending emails has never been easier this time around and you don’t need complicated flows as we switch to MailKit.

You can found the complete repository here .

Follow me for similar article, tips, and tricks ❤.