DEV Community

Cover image for .NET 5 Console App with Dependency Injection, Serilog Logging, and AppSettings
Mohamad Lawand
Mohamad Lawand

Posted on

.NET 5 Console App with Dependency Injection, Serilog Logging, and AppSettings

In this article we will be building a .Net 5 console app which support dependency injection, logging and app settings configuration.

You can watch the full Video on Youtube:

And you get the full source code on GitHub:
https://github.com/mohamadlawand087/v22-DotnetConsole

So what's in our agenda today:

  • development ingredients
  • the functionalities we are going to build
  • Coding

Development Ingredients

  • Visual Studio Code
  • Dotnet Core SDK

Functionalities:

  • Dependency Injection
  • Serilog Logger
  • AppSettings.

We are going to build a sample application which will mimic connecting to a database through dependency injection as well as outputting logs.

We will start by creating our application, inside our terminal

dotnet new console -n "SampleApp"
Enter fullscreen mode Exit fullscreen mode

Once the application has been create, open the application in Visual Studio Code and let us build and the application to make sure everything is working.

dotnet build
dotnet run
Enter fullscreen mode Exit fullscreen mode

The next step is installing the packages that we need.

dotnet add package Microsoft.Extensions.Hosting
dotnet add package Serilog.Extensions.Hosting
dotnet add package Serilog.Settings.Configuration 
dotnet add package Serilog.Sinks.Console
Enter fullscreen mode Exit fullscreen mode

The next step will be adding our appsettings.json, to do that in root directory of our application right-click select New File. Name the file appsettings.json

Inside the appsettings we are going to add all of the configuration that we need to setup serilog as well as the connectionString to mimic a database connection

{
    "Serilog" : {
        "MinimalLevel": {
            "Default": "Information",
            "Override": {
                "Microsoft": "Information",
                "System": "Warning"
            }
        }
    },
    "ConnectionStrings": {
        "DefaultConnection": "DataSource=app.db;Cache=Shared"
    }
}
Enter fullscreen mode Exit fullscreen mode

We will start by implementing the logging mechanism. Inside our Program.cs Add the following code, this code responsibility is reading the appsetting.json and making it available to our application.

static void BuildConfig(IConfigurationBuilder builder)
{
    // Check the current directory that the application is running on 
    // Then once the file 'appsetting.json' is found, we are adding it.
    // We add env variables, which can override the configs in appsettings.json
    builder.SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
        .AddEnvironmentVariables();
}
Enter fullscreen mode Exit fullscreen mode

Now we need to create another method which will be out startup method for our application, it will responsible to put everything together. We will define Serilog as well our dependency injection mechanism in .Net Core.

static IHost AppStartup()
{
    var builder = new ConfigurationBuilder();
    BuildConfig(builder);

    // Specifying the configuration for serilog
    Log.Logger = new LoggerConfiguration() // initiate the logger configuration
                    .ReadFrom.Configuration(builder.Build()) // connect serilog to our configuration folder
                    .Enrich.FromLogContext() //Adds more information to our logs from built in Serilog 
                    .WriteTo.Console() // decide where the logs are going to be shown
                    .CreateLogger(); //initialise the logger

    Log.Logger.Information("Application Starting");

    var host = Host.CreateDefaultBuilder() // Initialising the Host 
                .ConfigureServices((context, services) => { // Adding the DI container for configuration

                })
                .UseSerilog() // Add Serilog
                .Build(); // Build the Host

    return host;
}
Enter fullscreen mode Exit fullscreen mode

Now let us implement data service which will mimic a database

Let us create a new class called DataService and an interface called IDataService

// Interface
public interface IDataService
{
     void Connect();
}

// Class
public class DataService : IDataService
{
    private readonly ILogger<DataService> _log;
    private readonly IConfiguration _config;
    public DataService(ILogger<DataService> log, IConfiguration config)
    {
        _log = log;
        _config = config;
    }

    public void Connect()
    {
        // Connect to the database
        var connectionString = _config.GetValue<string>("ConnectionStrings:DefaultConnection");

        _log.LogInformation("Connection String {cs}", connectionString); 
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we need to update our AppStartup method in the Program.cs class to inject the DataService

var host = Host.CreateDefaultBuilder() // Initialising the Host 
                .ConfigureServices((context, services) => { // Adding the DI container for configuration
                    **services.AddTransient<IDataService, DataService>(); // Add transiant mean give me an instance each it is being requested.**
                })
                .UseSerilog() // Add Serilog
                .Build(); // Build the Host
Enter fullscreen mode Exit fullscreen mode

And finally let us put everything together in our main method

static void Main(string[] args)
{
    var host = AppStartup();

    var service = ActivatorUtilities.CreateInstance<DataService>(host.Services);

    service.Connect();
}
Enter fullscreen mode Exit fullscreen mode

Please let me know if you want me to jump into more details about any part of this application or if there is a specific feature which you would like me to cover.

Thanks for reading

Discussion (3)

Collapse
stfs profile image
Stefán Freyr Stefánsson • Edited on

Thanks for a great walkthrough. It really helped me get started with DI in .NET.

I'm just a bit confused why you're using var service = ActivatorUtilities.CreateInstance<DataService>(host.Services); when you've already registered the DataService class. There you're tying that instance creation to a concrete class, effectively nullifying the benefit of DI. In this example, of course, it doesn't really matter because you're doing the registration and instantiation in code and even in the same class, but if you were to move to something like runtime configuration of what implementation provides the IDataService implementation then this would be the wrong way to go.

After a bit of digging around I found that I could do the following which I think might be a better demonstration for this example:

var service = ActivatorUtilities.GetServiceOrCreateInstance<IDataService>(host.Services);

There, you're actually requesting the registered implementation of the IDataService interface, rather than instantiating a concrete class.

Collapse
kvenda profile image
MyProjects

Ugh! I'm such a newbie! My little application works great and picks up all the _config values needed if I run it directly (from cmd line, thru a shortcut, or just double-clicking on the exe). But when I run it from another app (using Process), it interprets the base Directory as the directory of the calling process, and so does not pick up my _config settings from appsettings.json. (I can get the _config info for Logger, because I have the full path hard-coded into builder, but in the rest of the code, the _config data returns null. How to get past this so I can keep my appsettings separate and easily modifiable? Thanks.

Collapse
kvenda profile image
MyProjects • Edited on

Thanks for a great post, and especially the video, which finally made a lot of this more understandable. FYI - I answered my own question, I needed to set the properties for appsettings.json to Copy if Newer. Orig Question: Q: how to reference the appsettings json file if it is in the project folder, not the bin/debug/net5/ folder (as with a typical scaffolded net 5 app). ?