In the old .Net framework 4.X, building a console application was fairly straightforward. You would simply use the console project template. This would give you pretty much what you needed.
Then came .Net core with its goal of allowing the developer to only use what they needed and to make things modular. If you use the current Console Project template in Visual Studio 2022, you get this:
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
This is short and to the point, which is what, n.Net core is all about. But what if you want to log, use app settings, or leverage dependency injection for your application? If you want these things, you have to add them yourself. Every similar to how the other .Net core applications are built.
I have dug in and developed 9 steps you need to take to build a great console application that you can use in production. The following are the steps defined. While the project template is only 1 line for the console app. My better console app is over 50 lines. The vast majority of the lines are just plumbing. But this plumbing greatly enhances your application.
The Steps
1. Install the NuGet packages needed
2. Add AppSettings.json file
3. Setup to read the AppSettings
4. Instantiate the Configuration
5. Set up the Logger
6. Create your run class
7. Create the "Host"
8. Configure the Service to call
9. Call your Service to run
Install the NuGackages
Add these NuGet packages:
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
Since .Net core allows the developer to change and configure the plumbing in its application, we will be replacing the default .Net logger with the Serilog System.
The Microsoft.Extensions.Hosting will give us the hosting environment where we can use the configuration and the built-in dependency injection system.
Add AppSetings.json
You have to manually add this file because Visual Studio does not add one by default. Please note that the name of the file has to be appsetting.json.
Under properties for appsetting.json file, select the "Copy to Output Directory" to "Copy Always". If you do not do this the file will not be in a location that the application accesses it and your app will not be able to read in any of the configuration settings.
Set Up AppSetting Reader
This is the part that gets a little confusing. To use the appsettings file, your configuration needs to be set up but to set up your configuration, you need appsettings.
To get around this, We build the configuration first:
static void BuildConfig(IConfigurationBuilder builder)
{
builder.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional:true)
.AddEnvironmentVariables();
}
Notice this is a separate method. The code should be self-explanatory. We need to get the app path to find the app settings file, and then we can set up other locations to load app settings from. These additional locations are out of the scope of this post, but these add even more customization to your application.
Instantiate the Configuration
We just need to call this method from the main method:
//setup our configuration
var builder = new ConfigurationBuilder();
BuildConfig(builder);
Now we have our configuration object we can inject into whoever needs it.
Set Up Logging
With using Serilog, we have a lot of options for where, what, how, and when to log. The full use of Serilog is outside the scope of this post as well.
To Setup the logging i, in main:
//set up logging
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Build())
.Enrich.FromLogContext()
.WriteTo.Console()
.CreateLogger();
Log.Logger.Information("Application Starting");
Again the code should be self-explanatory. We instantiate the logger and set some of the configurations. There are additional configurations in the appsettings.Json file:
//Serilog settings
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Information",
"System": "Warning"
}
},
"Using": [ "Serilog.Sinks.Console" ],
"Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ],
"Properties": {
"Application": "Serilog.Sinks.Console.Sample"
}
}
As you can see, there is a lot of configuration available. It is worth the time to learn the full power of Serilog to use it in a production environment.
Create a Run Class
This class does the work you need to do in your console application. For this post, we are calling our class "DoTheWorkService". We will also create an interface for this class.
In our class we are going to have a constructor, to inject configuration and logging, and a Run method does the actual work.
public DoTheWorkService(ILogger<DoTheWorkService> log, IConfiguration config)
{
_config = config;
_log = log;
}
public void Run()
{
for (int i = 0; i < _config.GetValue<int>("LoopTimes"); i++)
{
_log.LogInformation("Hello {i}", i);
}
}
You can see that the system is passing in the configuration and the logger we created in Main.
You will also notice in the Run method, there is a for loop that just logs a message. We are using a configuration value to show the number of times to run the loop. We will get this value from the appsetting.
//Times to log greeting
"LoopTimes": 5,
It is important to note that in the log.Information method it is better not to use the string intorpulation. When you use it like it is shown, Serilog treats I as an object. This allows Serilog to track that object across the logs making it easier to search and debug.
Create Host
The next step is to set up the host for the application. In the host, we will configure our services and instantiant Serilog.
//setup our HOST
var host = Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
})
.UseSerilog()
.Build();
Configure the Service
This setup allows us to add our DoTheWorkService just like any service in any of the other applications you write in .Net Core. In our case, it will inject the configuration and the logger into our service. We add our service to the host:
.ConfigureServices((context, services) =>
{
//Greeting Service
services.AddTransient<IDoTheWorkService, DoTheWorkService>();
})
Call your Service to run
The last step is to just create and call our service to do the work:
//run our greeting service
var svc = ActivatorUtilities.CreateInstance<DoTheWorkService>(host.Services);
svc.Run();
The ActivatorUtilities is a .Net Core helper for the dependency inject system.
Run the application and see the results.
Summary
All but the DoTheWorkService class, all the other code is just plumbing, even though there is a lot of it. At first, I was not a fan of the new console application structure. I thought it caused a lot of extra work. I just need to do the work to understand the advantages of the new way of building a console. Lesson learned, understand before making an opinion
You can take this project and make a project template and all your new console applications or just using for a quick reference when you need it.
Comments
Post a Comment