AutoMapper in ASP.NET Core

How to keep a separation between domain models and view models and let them exchange data in an easier and simple way? We write code that allows us to map domain model into view model. As we add more views and domain models, we end up writing more mappers. We write mappers to map domain transfer objects from database layer into domain objects.

This practice is repetitive. AutoMapper solve this problem. It’s a convention-based object-to-object mappers.

We are going to use these NuGet packages for ASP.NET Core 2.1;

AutoMapper.Extensions.Microsoft.DependencyInjection		v3.1.0
Microsoft.Extensions.DependencyInjection.Abstractions		v3.1.0

For ASP.NET Core V2.1, we will need at least V3..0.1 of AutoMapper.Extensions.Microsoft.DependencyInjection. This package will install AutoMapper package automatically.

Configure AutoMapper in Startup.cs class under ConfigureServices method;

//Auto mapper configuration
services.AddAutoMapper();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

The above single line works fine but If we want to explicit in configuration, the alternative is;

var mapperConfig = new MapperConfiguration(mc =>
{
    mc.AddProfile(new MappingProfile());
});
IMapper mapper = mapperConfig.CreateMapper();
services.AddSingleton(mapper);
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

AutoMapper in Action

Create a user model in Model folder.

public class User
{
  public User(int id, string firstName, string lastName, string emailAddress)
  {
            Id = id;
            FirstName = firstName;
            LastName = lastName;
            EmailAddress = emailAddress;
  }

        public int Id { get; }
        public string FirstName { get; }
        public string LastName { get; }
        public string EmailAddress { get; }
}

Create a view model in Model folder that will be used to display User data.

public class UserViewModel

{
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string EmailAddress { get; set; }
 }

We need to tell AutoMapper to map from User Model to User View Model. For that we will use Profile. Profiles in AutoMapper are a way of organizing mapping collections. To create Profile, We create a new class and inherit from Profile. This class will hold mapping configuration of new classes.

public class MappingProfile : Profile
{
        public MappingProfile()
        {
            CreateMap<User, UserViewModel>();
            CreateMap<UserViewModel, User>();
        }
}

The same profile can be created like this;

public class MappingProfile : Profile
{
        public MappingProfile()
        {
            CreateMap<User, UserViewModel>().ReverseMap();
        }
}

We now have a MappingProfile class that creates the mapping between our User Model and User ViewModel. But how does AutoMapper know about our UserProfile class? Well, towards the start of this example we added this line of code to our ConfigureServices method in Startup.cs:

services.AddAutoMapper();

When our application starts up and adds AutoMapper, AutoMapper will scan our assembly and look for classes that inherit from Profile, then load their mapping configurations. I also have an alternative explicit implementation in startup class if you prefer.

Let’s create a new UserController in the Controllers folder and inject the IMapper interface into the constructor:

public class UserController : Controller
{
        private readonly IMapper _mapper;
        public UserController(IMapper mapper)
        {
            _mapper = mapper;
        }

        public IActionResult Index()
        {
            return View();
        }
}

As with Profiles, by calling AddAutoMapper in our Startup.cs ConfigureServices method, it’s taken care of registering IMapper for us. In Index Action method, let’s create a User object and use IMapper interface to call the Map method:

We give the Map method the type we want to map to and the object we would like to map from:

public IActionResult Index()
{
            var user = new User(1, "Shahzad", "Khan", "shahzad@msn.com");
            UserViewModel viewModel = _mapper.Map<UserViewModel>(user);
            return View(viewModel);
}

Finally create the view;

@model UserViewModel

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<div>
    <h4>UserViewModel</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.Id)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Id)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.FirstName)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.FirstName)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.LastName)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.LastName)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.EmailAddress)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.EmailAddress)
        </dd>
    </dl>
</div>
<div>
    @Html.ActionLink("Edit", "Edit", new { /* id = Model.PrimaryKey */ }) |
    <a asp-action="Index">Back to List</a>
</div>

Here is the page;

We just scratched the surface of what AutoMapper has to offer in terms of mapping objects from one to another.

Summary

First, you need both a source and destination type to work with. The destination type’s design can be influenced by the layer in which it lives, but AutoMapper works best as long as the names of the members match up to the source type’s members. If you have a source member called “FirstName”, this will automatically be mapped to a destination member with the name “FirstName”. AutoMapper also supports Flattening, which can get rid of all those pesky null reference exceptions you might encounter along the way.

Once you have your types, and a reference to AutoMapper, you can create a map for the two types.

CreateMap<User, UserViewModel>().ReverseMap();

The type on the left is the source type, and the type on the right is the destination type. To perform a mapping, use the Map method.

var userEntity = await _unitOfWork.GetAllUsersAsync();
List<UserViewModel> vm = Mapper.Map<List<UserViewModel>>(userEntity.Result);

References

https://docs.automapper.org/en/stable/Getting-started.html

https://automapper.org/

https://jimmybogard.com/

https://stackoverflow.com/questions/40275195/how-to-set-up-automapper-in-asp-net-core

https://stackoverflow.com/questions/50411188/trying-to-add-automapper-to-asp-net-core-2

https://stackoverflow.com/questions/13479208/automapper-auto-map-collection-property-for-a-dto-object/13499361

https://stackoverflow.com/questions/52218340/automapper-projectto-adds-tolist-into-child-properties

https://code-maze.com/automapper-net-core/

Send Email after Release Deployment in Azure DevOps

Azure DevOPS sends an email notification to team member after the compilation succeeds or fails. This is default behavior. We are more concerned when the deployment is complete so that manual or automated testing can be started.

Click on Project settings –> Notifications –> New Subscriptions –> Release –> A deployment is completed

The default settings are to send mail to all members of the current project when the deployment is complete. Specific settings can be changed according to project needs.

Now, trigger a deployment. If all goes well, you should be able to receive a similar email notification. I currently have two environments, and according to the configuration just now, any deployment on both environment will be notified.

Why it’s so hard to buy PS 5 console?

I wanted to buy PS5 for my son but couldn’t get one. I saw the listing on Wallmart.com and Gamestop.com. Tried to do “Buy” dance but couldn’t get one. They were sold within minutes.

My son looked at me and ask me Why? Here is the answer;

First things first: The people using bots to buy and resell PS5s probably aren’t standalone coders. They’re professional scalpers, and someone sold them a bot.

Read more here….

Serilog Logging in ASP.NET Core

ASP.NET Core comes with reasonable diagnostic logging; framework and application have access to API’s for structured logging. Log events are written out to handful providers including the terminal and Microsoft Application insights. One of the key sinks is the database logging that Microsoft API’s are lacking.

Serilog is an alternative logging implementation. It supports same structured logging API but adds a stock of features that makes it more appealing including logging to database.

We will need following packages from NuGet to configure Serilog for different sinks;

$ dotnet add package Serilog.ASPNetCore		<version 3.2.0>
$ dotnet add package Serilog.Settings.Configuration	<version 3.1.0>
$ dotnet add package Serilog.Sinks.Console		<version 3.1.1>
$ dotnet add package Serilog.Sinks.File			<version 3.2.0>
$ dotnet add package Serilog.Sinks.MSSqlServer		<version 5.1.3>

All types are in Serilog namespace.

using Serilog;

Initialization and top-level try/catch block

Exceptions thrown during application startup are some of the most challenging one. The very first line of code in a Serilog-enabled application will be in our program class main method;

Using Serilog

public static void Main(string[] args)
        {
            Log.Logger = new LoggerConfiguration()
                .Enrich.FromLogContext()
                .WriteTo.Console()      //this needs to be file system
                .CreateLogger();

            try
            {
                Log.Information("Starting up");
                CreateWebHostBuilder(args).Build().Run();
            }
            catch (Exception ex)
            {
                Log.Fatal(ex, "Application start-up failed");
            }
            finally
            {
                Log.CloseAndFlush();
            }
        }

Note that we have added .Enrich.FromLogContext() to the logger configuration. There are some features that requires this for example some of the properties like RequestId.

Plugging into ASP.NET Core

We have decided to have all log events processed through Serilog logging pipeline and we don’t want ASP.NET Core’s logging.

ASP.NET Core 2.1

public class Program
    {
        public static void Main(string[] args)
        {
            Log.Logger = new LoggerConfiguration()
                .Enrich.FromLogContext()
                .WriteTo.Console()      
                .CreateLogger();
            try
            {
                Log.Information("starting up");
                BuildWebHost(args).Run();
            }
            catch (Exception ex)
            {
                Log.Fatal(ex, "Application start-up failed");
            }
            finally
            {
                Log.CloseAndFlush();
            }
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .UseSerilog()
                .Build();
    }

ASP.NET Core > 3.1

      public class Program
    {
        public static void Main(string[] args)
        {
            Log.Logger = new LoggerConfiguration()
               .Enrich.FromLogContext()
               //.MinimumLevel.Debug()
               .WriteTo.Console()
               .CreateLogger();

            try
            {
                Log.Information("Application starting up");
                CreateHostBuilder(args).Build().Run();
            }
            catch (Exception ex)
            {
                Log.Fatal(ex, "Application start-up failed");
            }
            finally
            {
                Log.CloseAndFlush();
            }
        }

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

Cleaning up default logger

There are few spots in the application that tracks default logger. Serilog is a complete implementation of the .NET Core logging APIs. The benefit of removing default logger is that you are not running two different logging framework where they overlap in functionality.

The “Logging” section in appSettings.json is not used by Serilog so it can be removed;

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

After cleaning appSettings.Development.json, the configuration would be;

{
  "AllowedHosts": "*"
}

Writing your own log events

We have used Serilog Log class directly in Program.cs file to write event. This works well in ASP.NET Core apps and can be used with standard Serilog interfaces like Log, ILogger and LogContext.

Here is the simple log message that I can write in an action method;

Logger.LogInformation("Presentation Layer - Logging information message by calling index method");
Logger.LogWarning("Presentation Layer - Logging warning message by calling index method");
Logger.LogError("Presentation Layer - Logging error message by calling index method"); 

Navigate to your UI project and do this;

dotnet run

Serilog output will be;

Logging with File System

You will need following additional package from NuGet;

$ dotnet add package Serilog.Sinks.File			<version 3.2.0>

Add this line to Program.cs file Main method;

.WriteTo.File("Logs/log-.txt", rollingInterval: RollingInterval.Day,
                   outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")

        public static void Main(string[] args)
        {
            Log.Logger = new LoggerConfiguration()
                .Enrich.FromLogContext()
                .WriteTo.File("Logs/log-.txt", rollingInterval: RollingInterval.Day,
                   outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
                .WriteTo.Console()      
                .CreateLogger();

A Logs folder and file with timestamp will be crated automatically. You will be able to see log in your project root directory;

Logging with SQL Server

You will need following additional package from NuGet;

$ dotnet add package Serilog.Settings.Configuration	<version 3.1.0>
$ dotnet add package Serilog.Sinks.MSSqlServer		<version 5.1.3>

We need to add following configuration in appSettings.json file for the Serilog;

//Logging configuration here
    "Serilog": {
        "ColumnOptions": {
            "addStandardColumns": [ "LogEvent" ],
            "removeStandardColumns": [ "MessageTemplate", "Properties" ],
            "timeStamp": {
                "columnName": "Timestamp",
                "convertToUtc":  false
            }
        },
        "ConnectionStrings": {
            "LogDatabase": "Data Source=MyDBServer;Initial Catalog=MyDb;Persist Security Info=True;Integrated Security=True"
        },
        "SchemaName": "dbo",
        "TableName": "MyLog"
    },

Refactor Program.cs file Main method;

public static void Main(string[] args)
        {
            var configuration = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true)
                .Build();

            Log.Logger = new LoggerConfiguration()
                .WriteTo.MSSqlServer(
                connectionString: configuration.GetSection("Serilog:ConnectionStrings:LogDatabase").Value,
                tableName: configuration.GetSection("Serilog:TableName").Value,
                appConfiguration: configuration,
                autoCreateSqlTable: true,
                columnOptionsSection: configuration.GetSection("Serilog:ColumnOptions"),
                schemaName: configuration.GetSection("Serilog:SchemaName").Value)
                .CreateLogger();

We will be deploying this in multiple environments and would like to standardized connection string. We are going to remove connection string from Serilog and going to add in ConnectionString section.

"ConnectionStrings": {
        "LogDatabase": "Data Source=myDBServer;Initial Catalog=myDB;Persist Security Info=True;Integrated Security=True",
    },

We are going to make this change in Program.cs main method;

            //var logConnectionString = configuration.GetSection("ConnectionStrings:LogDatabase").Value;
            //OR
            var logConnectionString = configuration.GetConnectionString("LogDatabase");

            Log.Logger = new LoggerConfiguration()
                .MinimumLevel.Debug()
                .WriteTo.MSSqlServer(
                    //connectionString: configuration.GetSection("Serilog:ConnectionStrings:LogDatabase").Value,
                    connectionString: logConnectionString,
                    tableName: configuration.GetSection("Serilog:TableName").Value,
                    //appConfiguration: configuration,
                    autoCreateSqlTable: true,
                    columnOptionsSection: configuration.GetSection("Serilog:ColumnOptions"),
                    schemaName: configuration.GetSection("Serilog:SchemaName").Value)
                .CreateLogger();

Let’s write simple message and view in Log table;

Logger.LogInformation("Presentation Layer - Logging information message by calling index method");
Logger.LogWarning("Presentation Layer - Logging warning message by calling index method");
Logger.LogError("Presentation Layer - Logging error message by calling index method");           

Here is the result;

Let’s throw a divide by zero exception;

var a = 1; var b = 0;
var x = a / b;

Here is the result;

Adding Serilog to DataAccess Layer

Add Serilog core library from NuGet (Refer above)

Add Database sink from NuGet (Refer above)

Add following code to your DataAccess layer methods for logging;

using System.Reflection;
using Serilog;

protected T ExecuteCode<T>(Func<T> code)
{
   try
   {
        return code.Invoke();
   }
   catch (SqlException ex)
   {
       string error = $"Unhandled Exception in {this.GetType().Name} class while executing {MethodBase.GetCurrentMethod().Name} method";
       Log.Error(ex, "DataAccess Layer: " + error);
       throw new ApplicationException(string.Format("{0}: {1}", ex.Number, ex.Message));
    }
}

Check your database for the logging info. It should be there.

Working with DI Injection

We can consume ILogger<T> interface from the framework with the help of dependency injection without configuring it in Program.cs class. Here is an example;

public class HomeController : Controller
    {
        private readonly ILogger<HomeController> logger;

        public HomeController(ILogger<HomeController> Logger)
        {
            Logger = logger;
        }

        public IActionResult Index([FromQuery] string name)
        {
            logger.LogInformation($"Hello, {name}!", name);
        }

All we need to do is to append “?name=world” and we will have the usual output.

Minimum Logging

These are Serilog logging levels;

Verbose

 Information is the noisiest level, rarely (if ever) enabled for a production app.

Debug

Debug is used for internal system events that are not necessarily observable from the outside, but useful when determining how something happened.

Information

Information events describe things happening in the system that correspond to its responsibilities and functions. Generally these are the observable actions the system can perform.

Warning

When service is degraded, endangered, or may be behaving outside of its expected parameters, Warning level events are used.

Error

When functionality is unavailable or expectations broken, an Error event is used.

Fatal

The most critical level, Fatal events demand immediate attention.

If we don’t want to log everything that framework generates, we exclude it by overriding;

using Serilog;
using Serilog.Events;

Log.Logger = new LoggerConfiguration()
                .MinimumLevel.Debug()
                .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)

The effect would be to generate events only at or above the warning level when the logger is owned by a type in a “Microsoft.*” namespace. We can specify as many overrides as we needed by adding additional “MinimumLevel.Override” statements;

Log.Logger = new LoggerConfiguration()
      .MinimumLevel.Debug()
      .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
      .MinimumLevel.Override("System", LogEventLevel.Error)

This can also be configured in Json file as;

"Serilog": {
        "MinimumLevel": {
            "Default": "Debug",
            "Override": {
                "Microsoft": "Warning",
                "System":  "Error"
            }
        },

Calling Serilog.Settings.Configuration “JSON” support is a little bit misleading; it works with the Microsoft.Extensions.Configuration subsystem, so we could also control the minimum level through it by setting an environment variable called Serilog:MinimumLevel, or with overrides using Serilog:MinimumLevel:Default and Serilog:MinimumLevel:Override:Microsoft.

Following block could be the easiest way to read and override configuration;

var logConnectionString = configuration.GetConnectionString("LogDatabase");
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)      //read values from config file
       .WriteTo.MSSqlServer(
       	  connectionString: logConnectionString,
          tableName: configuration.GetSection("Serilog:TableName").Value,
          autoCreateSqlTable: true,   //will create table if table doesn't exists
          columnOptionsSection: configuration.GetSection("Serilog:ColumnOptions"),
       	  schemaName: configuration.GetSection("Serilog:SchemaName").Value)      
 .CreateLogger();

There is a logging switch support available. Some time later in the code, if we want minimum level to be information or something else, we can get that by;

using Serilog.Core;

var loggingLevelSwitch = new LoggingLevelSwitch();
loggingLevelSwitch.MinimumLevel = LogEventLevel.Information;

Resources

https://github.com/serilog-mssql/serilog-sinks-mssqlserver

Server permission for Serilog

https://github.com/serilog/serilog/wiki/Getting-Started

https://stackoverflow.com/questions/55245787/what-are-the-specifics-and-basics-of-connecting-serilog-to-a-mssql-database

https://stackoverflow.com/questions/64308665/populate-custom-columns-in-serilog-mssql-sink

https://benfoster.io/blog/serilog-best-practices/

https://nblumhardt.com/2016/07/serilog-2-minimumlevel-override/

https://nblumhardt.com/2014/10/dynamically-changing-the-serilog-level/

https://www.connectionstrings.com/store-and-read-connection-string-in-appsettings-json/

https://nblumhardt.com/2016/03/reading-logger-configuration-from-appsettings-json/