Why Startup's IConfigurationRoot is null? - asp.net

I'm trying to pass command line arguments to Startup class. Following this example, I modified my Program class and it looks like this:
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("generalsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables(prefix: "ASPNETCORE_")
.AddCommandLine(args);
var config = builder.Build();
var host = new WebHostBuilder()
.UseUrls("http://*:5000")
.UseConfiguration(config)
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
host.Run();
generalsettings.json contains following data:
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}
Therefore I commented out default Startup class' constructor. Then I noticed that that's where IConfigurationRoot Configuration is assigned, and therefore, when I'm trying to use it from ConfigureServices it's null. I'm trying to use (I think) configuration built in Program's Main. What am I missing here?
UPDATE
To make it clear: I'm trying to use args in Startup class.

It turns out that there has been a fair amount of discussion about this sort of thing in GitHub Aspnet hosting repo issues area. The GitHub repositories are always a good place to look on an interesting problem like this one.
To sum things up for you, do not set up IConfigurationRoot in Program.Main, set it up in Startup. You can pass the command line arguments to the Startup constructor from Program.cs like so:
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.ConfigureServices(services => services
.AddSingleton(new ConsoleArgs(args))
)
.Build();
host.Run();
}
where ConsoleArgs is a holder class you can create yourself that looks like:
public class ConsoleArgs
{
public ConsoleArgs(string[] args)
{
Args = args;
}
public string[] Args { get; }
}
Obviously, configuring a ConsoleArgs service is the key. This will allow it to be injected into the Startup constructor. Then your Startup class constructor looks something like
public Startup(IHostingEnvironment env, ConsoleArgs args)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("generalsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables(prefix: "ASPNETCORE_")
.AddCommandLine(args.Args);
var config = builder.Build();
}
The pattern here is "configure you hosting environment Program, configure you application in Startup"

Related

Serilog missing exception in generic logger

In a controller method, the generic logger doesn't seem to have the defined enrichers.
Here is the controller:
public class TestController : Controller
{
ILogger _logger;
public TestController(ILogger<TestController> logger)
{
_logger = logger;
}
public IActionResult action()
{
try
{
throw new NullReferenceException();
}
catch(Exception e)
{
Serilog.Log.Error(ex, "action KO");
_logger.LogError("action KO", ex);
}
}
}
The appsettings.json:
{
"Serilog": {
"MinimumLevel": {
"Default": "Debug",
},
"WriteTo": [
{
"Name": "Console"
},
{
"Name": "File",
"Args": {
"path": "Log/api.log",
"outputTemplate": "{Timestamp} [{Level:u3}] ({SourceContext}) {Message}{NewLine}{Exception}",
"rollingInterval": "Day",
"retainedFileCountLimit": 7
}
}
],
"Enrich": [
"FromLogContext",
"WithExceptionDetails"
]
}
}
Host building:
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.UseUrls($"http://*:12345");
})
.UseSerilog((hostingContext, loggerConfiguration) => loggerConfiguration
.ReadFrom.Configuration(hostingContext.Configuration)
)
.Build()
;
Output in file / console:
02/18/2021 12:24:57 +01:00 [ERR] () action KO
System.NullReferenceException: Object reference not set to an instance of an object.
at App.TestController`1.action()
02/18/2021 12:24:57 +01:00 [ERR] (App.TestController) action KO
So when I try to use a generic logger, the exception is omitted. Wheras the static logger writes it.
Am I missing something like a provider for controllers logger or is it meant to be done by UseSerilog?
EDIT
Tried UseSerilog with writeToProviders: true => no effect
Tried AddSerilog as a logging builder => no effect
services.AddLogging(loggingBuilder => loggingBuilder.AddSerilog(
new LoggerConfiguration().ReadFrom.Configuration(Configuration).CreateLogger(), true));
Tried AddSerilogServices => no effect
public static IServiceCollection AddSerilogServices(
this IServiceCollection services,
LoggerConfiguration configuration)
{
Log.Logger = configuration.CreateLogger();
AppDomain.CurrentDomain.ProcessExit += (s, e) => Log.CloseAndFlush();
return services.AddSingleton(Log.Logger);
}
First, please change
_logger.LogError("action KO", ex);
to
_logger.LogError(ex, "action KO");
Testing the following try/catch
try
{
throw new NullReferenceException();
}
catch (Exception ex)
{
_logger.LogError(ex, "action KO");
}
... writes this to log file:
02/27/2021 22:55:59 +01:00 [ERR] (MyMicroservice.Controllers.WeatherForecastController) action KO
System.NullReferenceException: Object reference not set to an instance of an object.
at MyMicroservice.Controllers.WeatherForecastController.Get() in C:\Prosjekter\MyMicroservice\WebApp\Controllers\WeatherForecastController.cs:line 51
After checking your configuration with doc, and some testing, the part you've added to your question, seems OK to me.
I've added some words about an interesting finding during testing and reading the docs, and finally there is a Program.cs you may want to have a look at.
TL;DR: Serilog recommends two-stage initialization in order to have a
temporary logger before starting the host. The code below shows
how to skip stage #1 with a tiny change and still get a logger before starting the host.
Serilog.AspNetCore doc:
https://github.com/serilog/serilog-aspnetcore#inline-initialization
At the very beginning of Program#Main, you will have a Serilog.Core.Pipeline.SilentLogger.
If you follow the recommendations, you will have a Serilog.Extensions.Hosting.ReloadableLogger after stage #1.
Stage #1 looks like this and requires Nuget Serilog.Extensions.Hosting
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.Enrich.FromLogContext()
.WriteTo.Console()
.CreateBootstrapLogger();
In order to try to save us for some code lines and an additional dependency, comment out stage #1, and let's try the following approach to see if we can get an initialized logger before starting the web host.
var webHost = CreateHostBuilder(args).Build();
After this line, we do have an instance of Serilog.Core.Logger, which is the same as we'll end up with when using CreateHostBuilder(args).Build().Run(). Hence, I ended up with the below Program.cs where I omitted stage #1 entirely, but kept stage #2.
This should not have any side-effects, doc says:
To address this, Serilog supports two-stage initialization. An initial
"bootstrap" logger is configured immediately when the program starts,
and this is replaced by the fully-configured logger once the host has
loaded.
Please note that after commenting out lines in code from doc, UseSerilog part is now equal to config from question.
I'm using appsettings.json from your question.
I have no Serilog config in Startup.cs.
using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
using Microsoft.Extensions.DependencyInjection;
namespace MyMicroservice
{
public class Program
{
public static void Main(string[] args)
{
// Serilog.Core.Pipeline.SilentLogger at this stage
var webHost = CreateHostBuilder(args).Build();
// Serilog.Core.Logger at this stage
// proof-of-concept: This will log to file before starting the host
var logger = webHost.Services.GetService<ILogger<Program>>();
logger.LogWarning("Hello from Program.cs");
try
{
Log.Information("Starting up");
webHost.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application start-up failed");
}
finally
{
Log.CloseAndFlush();
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog((context, services, configuration) => configuration
.ReadFrom.Configuration(context.Configuration)
/*.ReadFrom.Services(services) not required for this case */
/*.Enrich.FromLogContext() already configured in appsettings.json */
/*.WriteTo.Console() already configured in appsettings.json */
)
.ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>());
}
}

Serilog creates the log file but doesn't write into it

So I've started using Serilog withing my ASP .NET Core 3.1 Web API as follows:
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>()
.UseSerilog((hostingContext, loggerConfiguration) =>
loggerConfiguration.ReadFrom.Configuration(hostingContext.Configuration));
});
However, the log file gets created, but nothing gets written into it, no matter where I call something like Log.Debug("Write this"). I tried using different paths to avoid any writing credential issue of some sort, to no avail. Also, I can't see anything in the Output window of Visual Studio, despite setting a Console log too. Here's what I wrote in my appSettings.json:
"Serilog": {
"MinimumLevel": {
"Default": "Debug"
},
"WriteTo": [
{
"Name": "Console"
},
{
"Name": "File",
"Args": {
"path": "log.txt",
"rollingInterval": "Day",
"retainedFileCountLimit": 7,
"buffered": true
}
}
]
},
Am I missing something?
Update: OP edited the question after my answer below and removed the part o the code that I'm pointing out.
In your example above, you are writing to Serilog at the very start of your App, way before Serilog has even been configured, so your messages are just being discarded...
In order to do what you want, you need to set up Serilog first.
Follow the EarlyInitializationSample in the Serilog.AspNetCore repo to see how it's done, which looks something like this:
using System;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Serilog;
namespace EarlyInitializationSample
{
public class Program
{
public static IConfiguration Configuration { get; } = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true)
.AddEnvironmentVariables()
.Build();
public static int Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(Configuration)
.Enrich.FromLogContext()
.WriteTo.Debug()
.WriteTo.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
.CreateLogger();
try
{
Log.Information("Getting the motors running...");
CreateHostBuilder(args).Build().Run();
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly");
return 1;
}
finally
{
Log.CloseAndFlush();
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.UseSerilog();
}
}
Debugging and Diagnostics
When Serilog is not behaving as you expect, this may be caused by an internal exception or configuration issue. Here are a couple of ways to sort things out.

How to convert configuration into IOptions?

I'd read a lot of articles about that but they all seem to miss the key moments, to be exact the moment of transformation of IConfiguration object into TheirStronglyTypedConfiguration object, so it looks like magic.
In my .NET Core project(NUnit test project) I have appsettings.json:
{
"Configuration": {
"HomePageUrl": "https://homepage.com"
}
}
I load it before all tests:
[BeforeTestRun]
public static void LoadConfiguration()
{
IConfiguration config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.Build();
}
Question: but how to transform it into strongly typed object that would have the string property HomePageUrl?
EDIT:
I try that:
IConfiguration config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build();
config.GetSection("").Bind
but I have no Bind method.
The syntax for model binding has changed from RC1 to RC2.
You need to bind a settings class to your configuration you need in ConfigureServices method of Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<MySettings>(options => Configuration.GetSection("MySettings").Bind(options));
}

ASP.NET Core 1.0 ConfigurationBuilder().AddJsonFile("appsettings.json"); not finding file

So I've finally got round to looking at Core and I've fallen at the first hurdle. I'm following the Pluralsight ASP.NET Core Fundamentals course and I'm getting an exception when trying too add the appsettings.json file to the configuration builder.
public Startup()
{
var builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json");
Configuration = builder.Build();
}
The error I'm getting is The configuration file 'appsettings.json' was not found and is not optional. But I have created the directly under my solution just like in the course video.
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json");
Configuration = builder.Build();
}
This seems to do the trick. However unsure this is the proper way to do it. Kinda feels like a hack.
You need to add the package below:
"Microsoft.Extensions.Configuration.Json": "1.0.0"
Another way:
appsettings.json:
{
"greeting": "A configurable hello, to you!"
}
Startup.cs:
using Microsoft.Extensions.Configuration; // for using IConfiguration
using System.IO; // for using Directory
public class Startup
{
public IConfiguration Configuration { get; set; }
public Startup()
{
var builder = new ConfigurationBuilder();
builder.SetBasePath(Directory.GetCurrentDirectory());
builder.AddJsonFile("appsettings.json");
Configuration = builder.Build();
}
}
In the Configure method:
app.Run(async (context) =>
{
// Don't use:
// string greeting = Configuration["greeting"]; // null
string greeting = Configuration.GetSection("greeting").Value;
await context.Response.WriteAsync(greeting)
});
An alternative solution I found from this blog post works as well. It has the added benefit of not needing to modify the Startup.cs file's Startup method signature.
In the buildOptions section add copyToOutput with the name of the file.
{
"version": "1.0.0-*",
"buildOptions": {
"emitEntryPoint": true,
"copyToOutput": "appsettings.json"
},
.... The rest of the file goes here ....
Right click appsettings.json -> Properties, then makes sure that Copy to Output Directory is set to "Copy Always"
Actually for this you need to provide root path from your environment variable so you need to pass IHostingEnvironment reference to provide root path:
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json");
Configuration = builder.Build();
}
and if you can't find AddJsonFile method then you have to add using Microsoft.Extensions.Configuration.Json;
In .NET Core 2.0, you would update the .csproj file to copy the JSON file to the output directory so that it can be accessed, like so:
<ItemGroup>
<Folder Include="wwwroot\" />
<None Include="appsettings.json" CopyToOutputDirectory="Always" />
</ItemGroup>
The answers that suggest adding .SetBasePath(env.ContentRootPath) in Startup.cs depend upon a prior step.
First, add the line .UseContentRoot(Directory.GetCurrentDirectory()) to your WebHostBuilder construction in Program.cs like so:
public class Program
{
public static void Main(string[] args)
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddEnvironmentVariables()
.Build();
var host = new WebHostBuilder()
.UseConfiguration(config)
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>()
.Build();
host.Run();
}
}
Then the following will work:
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json");
Configuration = builder.Build();
}
If you have done anything with Project Debug Properties, then you may have inadvertently overwritten the starting directory:
Project -> Right-click -> Properties -> Debug -> Profile and then look at the entry in Working Directory.
The simplest is that it be blank.

Adding key value to TempData results in a blank page

I am building my first .NETCoreApp using 1.0.0-rc2-final. I am trying to insert a copy of Model into TempData so that it is accessible after postback.
I added Microsoft.AspNetCore.Session to my project.
I altered my Startup.cs to look like...
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
namespace GamesCore
{
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
if (env.IsDevelopment())
{
// For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709
builder.AddUserSecrets();
}
builder.AddEnvironmentVariables();
Configuration = builder.Build();
}
public static IConfigurationRoot Configuration { get; private set; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
services.AddSession();
services.AddAuthentication(
SharedOptions => SharedOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseCookieAuthentication();
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = Configuration["Authentication:AzureAd:ClientId"],
Authority = Configuration["Authentication:AzureAd:AADInstance"] + Configuration["Authentication:AzureAd:TenantId"],
CallbackPath = Configuration["Authentication:AzureAd:CallbackPath"]
});
app.UseSession(new SessionOptions { IdleTimeout = TimeSpan.FromMinutes(60) });
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
I have the following in one of my Controllers:
public IActionResult Index(Models.ScoreModel scoreModel)
{
if (string.IsNullOrWhiteSpace(scoreModel.Username))
{
scoreModel.GameID = new System.Guid("b90ae557-7e03-4efa-9da1-1a4e89c1f629");
scoreModel.Username = User.Identity.Name;
scoreModel.Score = 0;
scoreModel.ScoringStep = 1;
TempData.Add("scoreModel", scoreModel);
}
return View(scoreModel);
}
When I have the line with TempData in there, the page loads completely blank -- no error, no Shared Layout, etc. If I remove that line, the View loads fine within the Shared Layout. If I look at the debugger, the scoreModel is getting successfully added to TempData so that doesn't seem to be a problem.
I figured this out.
I moved to storing it in Session instead of TempData, using this page as an example:
http://benjii.me/2015/07/using-sessions-and-httpcontext-in-aspnet5-and-mvc6/
I serialized the Model using the extension class outlined on the above page.
One note, is that since this page was written, services.AddCaching(); has changed to services.AddDistributedMemoryCache();
See this link for another sample: https://github.com/aspnet/Session/blob/dev/samples/SessionSample/Startup.cs

Resources