.NET DependencyInjection and Serilog without using IHost (for console app) - c#

When you look at solutions for Dependency Injection and Serilog on .NET projects, most documentation and blogs refers to the IHost model. It does not matter much if for current .NET 6 or other versions of .NET Core.
This is my favorite way for applications providing REST APIs since .NET Core 2.1.
I first defined a BuildLogger() method to configure Serilog with min log level and add all my enrichers/sinks I need.
So my Program.Main method used to look like:
Log.Logger = LoggerBuilder.BuildLogger();
var builder = Host.CreateDefaultBuilder(args).UseSerilog(); // serilog injected in Host.
builder.ConfigureServices(services => services.AddMyServices());
using var source = new CancellationTokenSource();
await builder.Build().RunAsync(source.Token).ConfigureAwait(false);
All fine.
Question is: how to do reuse DI and Serilog without using the IHost model from .NET 6?
My today's user case if for a console app which is only running a few tests and then close, and still reusing some parts I wrote for my previous apps.

So instead of the above, I defined that
Log.Logger = LoggerBuilder.BuildLogger();
await using var serviceProvider = new ServiceCollection()
.AddMyServices()
.AddLogging(logBuilder => logBuilder.AddSerilog()) // attach serilog.
.BuildServiceProvider()
RunTests(serviceProvider);
This seems to work as expected.
The trick with AddLogging/AddSerilog() instead of UseSerilog() might be helpful for others!

Related

C# ConsoleApp injection startup?

I have a simple C# .NET Core 5 Console Application that I need to add dependency injection (Microsoft.Extensions.DependencyInjection) to. I know how to do this if it suppose to start a micro service but what If I want to just run it as a regular Console Application with DI?
I got this code :
static void Main(string[] args)
{
var serviceName =
System.Reflection.Assembly.GetExecutingAssembly().GetName().Name;
var configurationBuilder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json").Build();
var appSettings = configurationBuilder.Get<AppSettings>();
Log.Information("{#serviceName} test starting up.", serviceName);
Host.CreateDefaultBuilder(args)
.UseMyAppMQ(context => context.UseSettings(appSettings.MQSettings))
.UseSerilog((hostingContext, loggerConfiguration) => loggerConfiguration.ReadFrom.Configuration(hostingContext.Configuration))
.ConfigureServices((hostContext, services) =>
{
services
.Configure<MQSettings>(configurationBuilder.GetSection("MQSettings"))
.AddTransient<ITestController>(s => new TestController());
})
.Build().Run();
Log.Information("{#serviceName} test closing down.", serviceName);
}
I need a entry point where I can run my underlaying class run method, but how?
Regards
You need to implement IHostedService interface (or extend BackgroundService base class) and register it with services.AddHostedService<YourServiceClass>() or
builder.Services.AddHostedService<YourServiceClass>(), if using .NET 6 minimal API as described in official docs. In this case IHostedService.StartAsync will be your entry point.
However, it does seem very inefficient, if all you need is just a simple console app with DI. As #Panagiotis Kanavos suggests you can build the host without running it, because then you can use it as a wrapper around DI container and resolve any registered service. In this case the entry point for your code is the next line after you've built your host, where you can resolve any registered dependency with
var host = Host.CreateDefaultBuilder(args)
...
.Build();
host.Services.GetService<YouService>()
But it is still an inefficient solution for just a console app, because you only need DI container, but not the entire host. Just use any third-party DI framework (like Autofac, Ninject or any other) instead of Microsoft.Extensions.DependencyInjection. Their setup is usually quite minimalistic and you will get just DI container with you services without anything else. You can still use configuration package and loggers, just register them in your container similarly to how you've done it before with UseXxxx methods.

simple webjob - process the response from a web link and save it to blob on a regular time interval - imposible to find an example or solution

I am looking for an example for a simple webjob:
the task would be to process the response from a web link and save it to blob on a regular time interval.
first of all the ms documentation is confusing me as far as time triggers are concerned:
https://learn.microsoft.com/en-us/azure/app-service/webjobs-create#ncrontab-expressions
https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-timer?tabs=csharp#example
and also how exactly should I proceed on building the WebJob, should I use an azure webjob template (.net 4.x.x), or .net core console app ??
https://learn.microsoft.com/en-us/azure/app-service/webjobs-sdk-how-to
https://github.com/Azure/azure-webjobs-sdk-samples/tree/master/BasicSamples
https://learn.microsoft.com/en-us/azure/app-service/webjobs-sdk-get-started
https://learn.microsoft.com/en-us/azure/app-service/webjobs-create
all this resource and no simple example for a time scheduled task that would get a web response, also the confusion on building the webjob VS, wth?? I want to build a c# app in VS and deploy to azure as webjob via azure devops.
wasted 3 days on this since im not a .net developer...
Webjobs have changed and grown over the years including contributions from Azure Functions, which is also built on top of the Webjobs SDK. I can see how this can get confusing, but the short answer is that all of the different methods are still valid, but some are newer than others. Of the two timer trigger styles, the second is more current.
I generally recommend Functions instead of Webjobs for something like this since at this point as it will save you some boiler-plate code, but it is entirely up to you. As I mentioned, the foundations are very similar. You can deploy Functions apps to any App Service plan, including the Consumption plan- this is specific to Functions that is pay-by-usage instead of a monthly fee like you would need for WebJobs.
As far as .NET Framework vs. .NET Core, you can use it will depend on what runtime you used to set up your App Service. If you have a choice, I would recommend using Core since that will be the only version moving forward. If you elect to use Functions, you will definitely want to use Core.
As far as the Console App question, all WebJobs are essentially console apps. From a code perspective, they are a console app that implements the Webjobs SDK. You could run them outside of Azure if you wanted to. Functions apps are different. The Function's host is what actually runs behind the scenes and you are creating a class library that the host consumes.
Visual Studio vs. Visual Studio Code is very much a personal preference. I prefer VS for Webjobs and work with both VS and VS Code for Functions apps depending on which language I am working in.
The most basic version of a Webjob in .NET Core that pulls data from a webpage on a schedule and outputs it to blob storage would look something like this. A Function app would use exactly the same GetWebsiteData() method plus a [FunctionName("GetWebsiteData")] at the beginning, but you wouldn't need the Main method as that part is handled by the host process.
public class Program
{
static async Task Main(string[] args)
{
var builder = new HostBuilder();
builder.ConfigureWebJobs(b =>
{
b.AddAzureStorageCoreServices();
b.AddAzureStorage();
b.AddTimers();
});
builder.ConfigureAppConfiguration((context, configurationBuilder) =>
{
configurationBuilder
.AddJsonFile($"appsettings.json", optional: true);
});
var host = builder.Build();
using (host)
{
await host.RunAsync();
}
}
public async static void GetWebsiteData(
[TimerTrigger("0 */1 * * * *")] TimerInfo timerInfo,
[Blob("data/websiteData", FileAccess.Write)] Stream outputBlob,
ILogger logger)
{
using(var client = new HttpClient())
{
var url = "https://microsoft.com";
var result = await client.GetAsync(url);
//you may need to do some additional work here to get the output format you want
outputBlob = await result.Content.ReadAsStreamAsync();
}
}
}

How can I end to end test this .net core console application?

I have a .Net Core 2.0 console application. The main() method is like this:
public static async Task Main()
{
// create host builder
var hostBuilder = new HostBuilder();
var myApp = new MyApplication();
// Build host
var host = hostBuilder
.ConfigureHostConfiguration(myApp.ConfigureHostConfiguration)
.ConfigureServices(myApp.ConfigureServices)
.ConfigureLogging(myApp.ConfigureLogging)
.Build();
// Initialise
await myApp.InitialiseAppAsync(host);
// Run host
await host.RunAsync(CancellationToken.None);
}
The MyApplication class sets up the application configuration in ConfigureHostConfiguration(), it then configures up the dependencies in ConfigureServices() some of which register a Message Handlers to handle specific Messages types from an Azure Service Bus. The application needs to do some initialisation from within InitialiseAppAsync(). When host.RunAsync() is called, the a console application is run indefinitely and the Message Handler receives execution as soon as a message is available on the Azure Service Bus. This is all great and working fine.
What I'd like to do is create a new project under the same solution which contains some end to end tests (using XUnit). I'd like to be able to override some of the dependencies (with test mocks, using NSubstitute), leaving the other dependencies as they are configured in the service.
I'm guessing I'd need to create my own HostBuilder in my test, so I'll need to be able to setup the mocks before the host.RunAsync() call is made within the test.
Does anyone know how I can do this? Or what is the best practice for doing this?
Ultimately, what I'm trying to do is be able to override some (but not all) of my real dependencies in my Console Application with mocks, so I can do some end to end tests.
Thanks in advance
There are multiple ways to achieve this. You can set up the environment variable "environment" when you start up your application. Then you would need to run your application passing it like this:
dotnet "MyApplication.dll" --environment end2endTests
Then you will be able to find the value you passed as the environment at IHostEnvironment instance which is injectable. This is how your DI registrations would look like:
services.AddScoped<IFoo>(provider =>
{
var env = provider.GetRequiredService<IHostEnvironment>();
if (env.EnvironmentName == "end2endTests")
{
return new TestFoo();
}
return new RealFoo();
});

How to use Serilog in .NET Core Console app

I wanted my application to have capability of logging to a file, so I started to look for something more than default .NET Core 2.2 logging framework. I see that Serilog might do the job. However, I cannot find any document on how to setup Serilog in .NET Core Console application with Dependency Injection. All I see is ASP.NET materials, which is probably not what I need.
I started doing it myself. I installed (Nuget):
Serilog
Serilog.Extensions.Logging
Serilog.Sinks.File
Serilog.Sinks.Console (to use Serilog for all my logging)
I created an extension forServiceCollection
public static void AddLogging(this IServiceCollection services, Microsoft.Extensions.Logging.LogLevel logLevel)
{
var serilogLogger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File("log.txt")
.CreateLogger();
services.AddLogging(builder =>
{
builder.SetMinimumLevel(logLevel);
builder.AddSerilog(logger: serilogLogger, dispose: true);
});
}
Logging works, however:
log level is not what I set it to. It seems that serilog is using INFO level, although I wanted to have DEBUG. Why isn't my setting respected? After all, I'm still using NET Core's logging framework, so I'm using it to setup the log level
am I actually doing this setup correctly? I am not really sure if dispose should be true. Generally, I want NET Core's Dependency Injection framework to take care of disposal of services.
I'm not sure about builder.SetMinimumLevel (it doesn't use the Serilog enum).
We set the logger level when creating the LoggerConfiguration object.
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Debug) // restricted... is Optional
(...)
.CreateLogger();
BTW. It's worth pointing out the following section from Configuration Basics
Logger vs. sink minimums - it is important to realize that the logging level can only be raised for sinks, not lowered. So, if the logger's MinimumLevel is set to Information then a sink with Debug as its specified level will still only see Information level events. This is because the logger-level configuration controls which logging statements will result in the creation of events, while the sink-level configuration only filters these. To create a single logger with a more verbose level, use a separate LoggerConfiguration.
I'm not sure about builder.AddSerilog.
Here's what works for me.
using Serilog;
(...)
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
(...)
.CreateLogger();
(...)
return new HostBuilder()
.ConfigureHostConfiguration(...)
.ConfigureServices(...)
.UseSerilog();

Serilog RavenDb sink not working in an asp.net 5 app

I am looking at serilog and running a few tests. So far it is working fine writing to the console or file. However I am not having any luck getting it to work with the RavenDb sink. I am trying to get this working in an asp.net 5 app.
I have reviewed the following articles:
http://nblumhardt.com/2015/05/diagnostic-logging-in-dnx-asp-net-5/
http://nblumhardt.com/2013/06/serilog-and-ravendb/
I started with an empty app, and added the following dependencies in project.json.
"Serilog.Framework.Logging": "1.0.0-rc1-final-10071",
"Serilog.Sinks.RavenDB": "1.5.4",
"RavenDB.Client": "3.0.30000"
I also removed dnxcore.
Then I added the following code in startup.cs:
public Startup()
{
var documentStore = new DocumentStore()
{
Url = "http://localhost:8080",
DefaultDatabase = "Logs"
}.Initialize();
Log.Logger = new LoggerConfiguration()
.WriteTo.File(#"c:\temp\log.txt")
.WriteTo.Console()
.WriteTo.RavenDB(documentStore)
.CreateLogger();
}
public void ConfigureServices(IServiceCollection services)
{
}
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
loggerFactory.AddSerilog();
app.UseIISPlatformHandler();
app.Run(async (context) =>
{
Log.Information("Hello World");
await context.Response.WriteAsync("Hello World!");
});
}
Everything gets logged to the file and console just fine, and the Logs database gets created, but no log entries are stored in RavenDb.
I have tried various log levels. I tried reducing the batch size. I suspected this had something to do with the lifecycle of the document store, so I added the following in the ConfigureServices method.
services.AddSingleton(x =>
{
return new DocumentStore()
{
Url = "http://localhost:8080/",
DefaultDatabase = "Test",
}.Initialize();
}
Then I moved the logger configuration code into the Configure method and used DI instance, but that doesn't work either. I can store other objects in RavenDb using the same DocumentStore just fine.
Have I missed a configuration setting or something?
I was able to get this working with the latest RavenDb client. I created a new (package) style library, added Serilog nuget package v2.0.0-beta-403, added the RavenDB.Client v3 nuget package, and dropped in the .cs files from the existing Serilog.Sinks.RavenDb library. It compiled and worked, I didn't have to change any code.
I haven't had a chance to test it much yet, but it seem to be working fine. Of course I don't know how stable Serilog v2 beta is, or how long until it is released. The nice thing is that serilog v2 supports .netcore. Unfortunately the RavenDb client doesn't, at least not yet.

Categories

Resources