Quantcast
Viewing latest article 19
Browse Latest Browse All 663

Getting the ASP.NET Core Server Hosting Urls at Startup and in Requests

Image may be NSFW.
Clik here to view.
Startup Banner

Today I was helping out a customer with their ASP.NET core application that wasn't starting up on a live server, hosted in IIS. Not uncommon in a first install, and when this happens I often resort to starting the app directly using Kestrel first, rather than trying to debug startup problems through the IIS system context and hunting down errors in the unwieldy event log.

To do this I can simply run the .exe file of the project (or dotnet run if the SDK is installed) and 90% of the time it's easy to track down issues this way especially if running in debug mode.

Which Port am I Hosting?

But in this case there was a problem: We couldn't figure out which port the app was running on. 😄 A lot of hilarity (NOT!) ensued trying to guess port numbers - the default port of 5000, the dev port that I typically use 5200/5201 plus a few others. Long story short none of those ports worked.

...because... it turns out the developer had changed the default startup Url in code and so the server started a custom port that neither of us would have guessed - until we looked at the source code.

Image may be NSFW.
Clik here to view.

Wouldn't it be nice if we could display that Url somewhere reliably? As it turns we can do that with some startup code, but it's not what I would call easily discoverable, and there are different ways that work at different times in the ASP.NET lifecycle.

Now, you can override the startup Urls via command line and environment options, but in this case even that didn't work because of the explicit override. But for most scenarios explicitly using --urls "<urlList>" or --https_ports works to override the Url.

Overiding Hosting Urls on Startup

FWIW there are million ways to specify startup Urls, and I'm going to refer you to Andrew Lock's in depth post that shows 8 ways to set the hosting Urls.

Getting the Server Urls is not Easy

Looking in the source code solved the problem, but that shouldn't be necessary and really what I want in all of my apps is a reliable way that shows me the actual startup urls that are being used and display them on startup as part of an application banner.

I'm a big fan of writing out some useful startup information that tells me the startup Urls, platform and environment at minimum. Something like this:

Image may be NSFW.
Clik here to view.
Startup Information

Figure 1 - Showing a startup banner that includes the startup hosting Urls

Specifically I want to see those startup Urls and I want to see them consistently regardless of whether I'm running in dev with Launch Profiles or environment variables or whether the Urls were set in code.

Besides providing valuable information on what port to use, it's also nice to have the Urls right there during development, because you can click on them to open the browser from the terminal, which is handy.

Lots of ways, but use the Right Way!

So the task at hand is to get the Hosting Urls as part of the startup code in program.cs so that it can be written out as part of a start up banner as shown in Figure 1.

I have actually written about this topic before(updated now) and in that previous post I mentioned a different approach that works in most cases but didn't work in this case:

// in program.cs -  Don't use this
var urls = builder.WebHost
                  .GetSetting(WebHostDefaults.ServerUrlsKey)?
                  .Replace(";", " "); // make clickable

This code works, but only with assigned Urls from Launch Profile, explicitly assigned urls from the command line (--urls) or environment variables. It does not work with the default url, or urls that are explicitly assigned in the WebBuilder startup. It does however work before the server is started because it essentially looks at explicitly assigned Urls.

The problem is that there are a few ways that the port gets assigned that bypasses explicit assignment. Specifically if you use the default port or if the port is assigned directly as part of WebBuilder setup.

To reliably retrieve the actually running Urls rather than the assigned values, you have to actually have the app start running.

This code works in all situations, but it does require that you not use app.Run() and instead use app.Start() and app.WaitForShutdown() to execute your server event loop, and the code to retrieve the host Urls must live somewhere between these two statements. So effectively you have to put the Url retrieval code towards the very end of your startup code.

// at the bottom of program.cs

// app.Run()    // can't use this

// use this instead
app.Start();

Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($@"---------------------------------
West Wind Web Store v{wsApp.Version}
---------------------------------");
Console.ResetColor();

var urlList = app.Urls;
string urls = string.Join(" ", urlList);

Console.Write("    Urls: ");
Console.ForegroundColor = ConsoleColor.DarkCyan;
Console.WriteLine(urls, ConsoleColor.DarkCyan);
Console.ResetColor();

Console.WriteLine($" Runtime: {RuntimeInformation.FrameworkDescription} - {builder.Environment.EnvironmentName}");
Console.WriteLine($"Platform: {RuntimeInformation.OSDescription} ({RuntimeInformation.OSArchitecture})");
Console.WriteLine();

app.WaitForShutdown();

This appears to work for all scenarios and always returned the Urls regardless on how they were set. Yay!

Retrieving Host Urls in a Request

You can also retrieve the host urls inside of a request by requesting a IServerAddressesFeature from an IServer instance and then requesting the feature and the addresses:

[Route("adminservice/systeminfo")]
public object SystemInfo([FromServices] IServer server)
{            
    var addresses = server.Features.Get<IServerAddressesFeature>()?.Addresses;          
    return new {
        Version = wsApp.Version,
        HostAddresses = addresses,
        Platform = $"{RuntimeInformation.FrameworkDescription}- {wsApp.EnvironmentName}",
        Os = RuntimeInformation.OSDescription + " (" + RuntimeInformation.OSArchitecture + ")"
    });
}

You can grab IServer from dependency injection either via method (used here) or ctor injection.

For completeness sake, you can also use this same IServer approach in the Startup code:

app.Start();

var server = app.Services.GetService(typeof(IServer)) as IServer;
var addrs = server.Features.Get<IServerAddressesFeature>()?.Addresses;

...

app.WaitForShutDown();

Like app.Urls this code also works only after app.Start() has fired, and given that app.Urls is simpler and gets you the same result, there's no good reason to use that code. Stick to app.Urls there.

Summary

While it's easy to set hosting Urls with many different ways to specify them, getting them back out at runtime is a little more tricky as there are specific rules that need to be followed. Specifically you can only reliably get the the hosting addresses after the server has been started and you have to use the magically undiscoverable references of Iserver.Features to extract the IServerAddressesFeature to retrieve the addresses at runtime. Well, once you know, you know, and now you know 😄

Resources

Image may be NSFW.
Clik here to view.
this post created and published with the Markdown Monster Editor
© Rick Strahl, West Wind Technologies, 2005-2024
Posted in asp.net  

Viewing latest article 19
Browse Latest Browse All 663

Trending Articles