111

I have a self-hosted .NET Core Console Application.

The web shows examples for ASP.NET Core but I do not have a web server. Just a simple command line application.

Is it possible to do something like this for console applications?

public static void Main(string[] args)
{
    // I don't want a WebHostBuilder. Just a command line

    var host = new WebHostBuilder()
        .UseKestrel()
        .UseContentRoot(Directory.GetCurrentDirectory())
        .UseIISIntegration()
        .UseStartup<Startup>()
        .Build();

    host.Run();
}

I would like to use a Startup.cs like in ASP.NET Core but on console.

How do I to this?

Pang
  • 9,564
  • 146
  • 81
  • 122
Daniel
  • 9,491
  • 12
  • 50
  • 66
  • What are you trying to accomplish? A console application that can serve web pages? Yes, that would be a "self-contained asp.net core application" and there are a few examples available, e.g. http://druss.co/2016/08/deploy-and-run-net-core-application-without-installed-runtime-self-contained-applications/ – Arash Motamedi Dec 31 '16 at 10:33
  • 1
    @ArashMotamedi I do not want to host a ASP.NET Application. I want to have a good old Console Application that starts my class Library project. I thought that i would get Dependency Injection etc. for free. – Daniel Dec 31 '16 at 10:37
  • Got it. But yes, your approach is a bit backwards. Remember that all .net core applications are composed of independent libraries and you're certainly free to reference any of those libraries for any type of project. It just so happens that an Asp.net core application comes preconfigured to reference a lot of those libraries and exposes an http endpoint. But if it's Dependency Injection you need for your console app, simply reference the appropriate library. Here's a guide: http://andrewlock.net/using-dependency-injection-in-a-net-core-console-application/ – Arash Motamedi Dec 31 '16 at 10:47
  • @ArashMotamedi thanks a lot. I stumbled over this article. But i was very uncertain because there is so much information about all that new stuff... Write is as an answer and i will mark it. – Daniel Dec 31 '16 at 10:50

7 Answers7

124

So i came across with this solution, inspired by the accepted answer:

Program.cs

public class Program
{
    public static void Main(string[] args)
    {
        IServiceCollection services = new ServiceCollection();
        // Startup.cs finally :)
        Startup startup = new Startup();
        startup.ConfigureServices(services);
        IServiceProvider serviceProvider = services.BuildServiceProvider();

        //configure console logging
        serviceProvider
            .GetService<ILoggerFactory>()
            .AddConsole(LogLevel.Debug);

        var logger = serviceProvider.GetService<ILoggerFactory>()
            .CreateLogger<Program>();

        logger.LogDebug("Logger is working!");

        // Get Service and call method
        var service = serviceProvider.GetService<IMyService>();
        service.MyServiceMethod();
    }
}

Startup.cs

public class Startup
{
    IConfigurationRoot Configuration { get; }

    public Startup()
    {
        var builder = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json");

        Configuration = builder.Build();
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddLogging();
        services.AddSingleton<IConfigurationRoot>(Configuration);
        services.AddSingleton<IMyService, MyService>();
    }
}

appsettings.json

{
    "SomeConfigItem": {
        "Token": "8201342s223u2uj328",
        "BaseUrl": "http://localhost:5000"
    }
}

MyService.cs

public class MyService : IMyService
{
    private readonly string _baseUrl;
    private readonly string _token;
    private readonly ILogger<MyService> _logger;

    public MyService(ILoggerFactory loggerFactory, IConfigurationRoot config)
    {
        var baseUrl = config["SomeConfigItem:BaseUrl"];
        var token = config["SomeConfigItem:Token"];

        _baseUrl = baseUrl;
        _token = token;
        _logger = loggerFactory.CreateLogger<MyService>();
    }

    public async Task MyServiceMethod()
    {
        _logger.LogDebug(_baseUrl);
        _logger.LogDebug(_token);
    }
}

IMyService.cs

public interface IMyService
{
    Task MyServiceMethod();
}
Daniel
  • 9,491
  • 12
  • 50
  • 66
  • 2
    This was very helpful. It even works if your application uses ASP.NET Core MVC (as long as your controllers are lean and your views don't have application logic in them). The one extra thing I had to do was create my own subclass of Microsoft.AspNetCore.Hosting.Internal.HostingEnvironment called MyHostingEnvironment and set its ContentRootPath = Path.GetFullPath("."); and then before calling new Startup in Program.cs do IHostingEnvironment env = new MyHostingEnvironment(); services.AddSingleton(); and then change "new Startup()" to "new Startup(env)". – Demerit Jul 19 '17 at 14:47
  • Excellent answer! Thank you very much for sharing. I just needed some of the things that the WebHost provides and this definately works like a charm. It should be a template in VS to be honest, it would be most helpful because I just need my app to run on a miniature VPS Linux host without WebHosting. – Piotr Kula Sep 13 '17 at 10:04
  • 2
    Perhaps it's better to expose IConfiguration instead of IConfigurationRoot, it has bettter extension support – BuddhiP May 17 '18 at 04:33
  • 1
    In my case I had to update startup.cs with .... serviceCollection.AddLogging(builder => builder .AddConsole() .AddFilter(level => level >= LogLevel.Information) ); ..... – Aidar Gatin Apr 03 '19 at 20:58
  • @Daniel you added Startup.cs by yourself? – Roxy'Pro May 16 '20 at 21:57
75

This answer is based on the following criteria:

I'd like to use the new Generic Host CreateDefaultBuilder without any of the ASP.NET web stuff, in a simple console app, but also be able to squirrel away the startup logic in startup.cs in order to configure AppConfiguration and Services

So I spent the morning figuring out how you could do such a thing. This is what I came up with...

The only nuget package this method requires is Microsoft.Extensions.Hosting (at the time of this writing it was at version 3.1.7). Here is a link to the nuget package. This package is also required to use CreateDefaultBuilder(), so chances are you already had it added.

After you add the extension (extension code at bottom of answer) to your project, you set your program entry to look similar to this:

using Microsoft.Extensions.Hosting;

class Program
{
    static async Task Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();
        await host.RunAsync();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseStartup<Startup>(); // our new method!
}

You add a Startup.cs that should look like this:

public class Startup
{
    public IConfiguration Configuration { get; }

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        // Configure your services here
    }
}

You then configure your services as you would in a typical ASP.NET Core application (without needing to have ASP.NET Core Web Hosting installed).

Demo Project

I put together a .NET Core 3.1 console demo project doing all kinds of things such as an IHostedService implementation, BackgroundService implementation, transient/singleton services. I also injected in IHttpClientFactory and IMemoryCache for good measure.

Clone that repo and give it a shot.

How It Works

I created a IHostBuilder extension method which simply implements the IHostBuilder UseStartup<TStartup>(this IHostBuilder hostBuilder) pattern that we are all used to.

Since CreateDefaultBuilder() adds in all the basics, there's not much left to add to it. The only thing we are concerned about is getting the IConfiguration and creating our service pipeline via ConfigureServices(IServiceCollection).

Extension Method Source Code

/// <summary>
/// Extensions to emulate a typical "Startup.cs" pattern for <see cref="IHostBuilder"/>
/// </summary>
public static class HostBuilderExtensions
{
    private const string ConfigureServicesMethodName = "ConfigureServices";

    /// <summary>
    /// Specify the startup type to be used by the host.
    /// </summary>
    /// <typeparam name="TStartup">The type containing an optional constructor with
    /// an <see cref="IConfiguration"/> parameter. The implementation should contain a public
    /// method named ConfigureServices with <see cref="IServiceCollection"/> parameter.</typeparam>
    /// <param name="hostBuilder">The <see cref="IHostBuilder"/> to initialize with TStartup.</param>
    /// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
    public static IHostBuilder UseStartup<TStartup>(
        this IHostBuilder hostBuilder) where TStartup : class
    {
        // Invoke the ConfigureServices method on IHostBuilder...
        hostBuilder.ConfigureServices((ctx, serviceCollection) =>
        {
            // Find a method that has this signature: ConfigureServices(IServiceCollection)
            var cfgServicesMethod = typeof(TStartup).GetMethod(
                ConfigureServicesMethodName, new Type[] { typeof(IServiceCollection) });

            // Check if TStartup has a ctor that takes a IConfiguration parameter
            var hasConfigCtor = typeof(TStartup).GetConstructor(
                new Type[] { typeof(IConfiguration) }) != null;

            // create a TStartup instance based on ctor
            var startUpObj = hasConfigCtor ?
                (TStartup)Activator.CreateInstance(typeof(TStartup), ctx.Configuration) :
                (TStartup)Activator.CreateInstance(typeof(TStartup), null);

            // finally, call the ConfigureServices implemented by the TStartup object
            cfgServicesMethod?.Invoke(startUpObj, new object[] { serviceCollection });
        });

        // chain the response
        return hostBuilder;
    }
}
openshac
  • 4,966
  • 5
  • 46
  • 77
Andy
  • 12,859
  • 5
  • 41
  • 56
  • 2
    - oooh - I like this! – KyleMit Aug 26 '20 at 22:31
  • 2
    def gonna toss you the bounty - but gonna leave open for more eyeballs, and if there are more good answers worthy of a bounty, I'll just open another one – KyleMit Aug 26 '20 at 22:33
  • 5
    now we need a PR with `HostBuilderExtensions` into core! – KyleMit Aug 26 '20 at 22:34
  • 4
    @KyleMit -- awesome! This was a lot of fun to play around with. I had *no* idea you could just run an `IHost` without a WebHost. This will come in handy for future projects. Thanks for posting the question. – Andy Aug 26 '20 at 22:35
  • Could you reuse https://github.com/dotnet/aspnetcore/blob/bb9653532ad5ce607d44dc4092cee899591e51f1/src/Hosting/Hosting/src/WebHostBuilderExtensions.cs#L147 – Jeremy Lakeman Aug 27 '20 at 01:03
  • 2
    @JeremyLakeman -- I looked in to that. It's part of the `Microsoft.AspNetCore.Hosting` package which is what he wants to avoid. I thought about re-implementing that part, but it ended up being a ton of "copy and pasted" code from that package so I stopped going down that path. – Andy Aug 27 '20 at 01:14
  • 2
    @KyleMit -- Thank you so much for the bounty. You made my night -- I really appreciate it! – Andy Sep 01 '20 at 02:09
  • 2
    Thanks for one heckuva answer! Another 500 rep anytime you want to update your answer with a PR against core :) – KyleMit Sep 01 '20 at 12:47
  • 1
    @KyleMit -- I don't believe they will accept this code because it's shoe-horning a pattern by using reflection, but, I am going to open a feature request using this code as an example of how we'd want it to work as the end result. BTW, I updated the code. I found a way better way to inject the configuration than the "kludge" I used previously. – Andy Sep 01 '20 at 16:20
  • 3
    @KyleMit -- added a feature request: https://github.com/dotnet/extensions/issues/3489 – Andy Sep 01 '20 at 17:06
  • 1
    Might be able to borrow some of the implementation in [dotnet/aspnetcore/.../WebHostBuilderExtensions.cs](https://github.com/dotnet/aspnetcore/blob/3.0/src/Hosting/Hosting/src/WebHostBuilderExtensions.cs#L71-L99) for a dot net core standards compliant way to add that functionality? – KyleMit Sep 02 '20 at 01:54
  • 2
    Hey @KyleMit -- whoa, I didn't notice you sending more attention at this answer. Thanks for that! I went down that path of looking in to how `IWebHostBuilder` does it and i started re-using a lot of that code, so I stopped. But, if we want to follow that pattern, then we should probably make it similar (create an `IStartup` interface, for example). I will work on that then add to our feature request. Sorry I didn't respond sooner. Totally didn't know you commented. – Andy Sep 08 '20 at 23:30
  • 2
    @KyleMit -- what's interesting is they are basically doing the same thing we are: https://github.com/dotnet/aspnetcore/blob/cfd20ad2d71ff63dc9a06be8bf8628321e5bac99/src/Hosting/Hosting/src/Internal/StartupLoader.cs We just did it in a very simple manner (only covering one use-case, where theirs covers many) I no longer believe we are "shoe-horning" pattern using reflection since they do the same thing. I removed that thought from the feature request. – Andy Sep 08 '20 at 23:38
  • 2
    @Andy, thanks! No pressure - poke at it if you have time / interest. Rep's yours and well earned already – KyleMit Sep 09 '20 at 00:30
  • @KyleMit -- Again, thank you... I will definitely do so. – Andy Sep 09 '20 at 00:54
  • @KyleMit and Andy: This is awesome, however, the primary use case for me is to register "early" dependencies on the host builder's ConfigureService(configureDelegate) call, and then be able to use those dependencies in Startup when registering the rest of the dependencies. That doesn't seem to work here as the ctor of Startup only support an IConfiguration parameter. I need more parameters of any type previously registered on the host builder. Can that be done with a modification here? – bubbleking Sep 23 '20 at 14:15
  • 1
    This should be the accepted answer, thanks a lot :) – Drago Oct 23 '20 at 10:54
  • 2
    @Andy So where in this do we actually run the code for our console app? I must be missing something simple here... – Daniel Ecker Nov 17 '20 at 19:43
  • 1
    @Andy I ended up changing to host.StartAsync() and StopAsync() and running my code in between. Also, a minor issue, but I believe the host should be wrapped in a using statement. – Daniel Ecker Nov 18 '20 at 16:37
  • 4
    @user1034912 -- can you explain? As a developer, you'd think you'd be more explicit than "It doesn't work anymore" -- that's usually what the *customers* say :) – Andy Dec 01 '20 at 15:31
  • it's hard to understate how hot and bothered (a good thing) I am by this snippet. I sure with [the process](https://github.com/dotnet/runtime/issues/42364) for adding things wasn't so slow. – JJS Aug 11 '21 at 19:17
  • 1
    Microsoft [discussion on Github](https://github.com/dotnet/runtime/issues/42364#issuecomment-901267545) propose to use **IServiceConfigurer** interface name instead of Startup name to avoid association to asp.Net pipeline implementation. – Michael Freidgeim May 09 '22 at 21:24
43

All .NET Core applications are composed of well-crafted independent libraries and packages which you're free to reference and use in any type of application. It just so happens that an Asp.net core application comes preconfigured to reference a lot of those libraries and exposes an http endpoint.

But if it's Dependency Injection you need for your console app, simply reference the appropriate library. Here's a guide: https://andrewlock.net/using-dependency-injection-in-a-net-core-console-application/

Pang
  • 9,564
  • 146
  • 81
  • 122
Arash Motamedi
  • 9,284
  • 5
  • 34
  • 43
27

Another way would be using HostBuilder from Microsoft.Extensions.Hosting package.

public static async Task Main(string[] args)
{
    var builder = new HostBuilder()
        .ConfigureAppConfiguration((hostingContext, config) =>
        {
            config.SetBasePath(Directory.GetCurrentDirectory());
            config.AddJsonFile("appsettings.json", true);
            if (args != null) config.AddCommandLine(args);
        })
        .ConfigureServices((hostingContext, services) =>
        {
            services.AddHostedService<MyHostedService>();
        })
        .ConfigureLogging((hostingContext, logging) =>
        {
            logging.AddConfiguration(hostingContext.Configuration);
            logging.AddConsole();
        });

    await builder.RunConsoleAsync();
}
Raj
  • 4,405
  • 13
  • 59
  • 74
10

I know this thread is kinda old, but I decided to share my code anyway, since it also accomplishes the end Daniel wanted (DI in a Console Application), but without a Startup class. Ps.: Note that this solution is valid either for .NET Core or .NET Framework.

Program.cs:

public class Program
    {

        public static void Main(string[] args)
        {
            var services = new ServiceCollection();

            DependencyInjectionConfiguration.ConfigureDI(services);

            var serviceProvider = services.BuildServiceProvider();

            var receiver = serviceProvider.GetService<MyServiceInterface>();

            receiver.YourServiceMethod();
        }
    }

public static class DependencyInjectionConfiguration
    {
        public static void ConfigureDI(IServiceCollection services)
        {
            services.AddScoped<MyServiceInterface, MyService>();
            services.AddHttpClient<MyClient>(); // for example
        }
    }
Pedro Coelho
  • 1,411
  • 3
  • 18
  • 31
5

I came across the same problem and I think this is a good solution:

class Program
{
    static async Task Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();
        await host.RunAsync();
    }

     public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
            .ConfigureServices((hostBuilderContext, serviceCollection)
             => new Startup(hostBuilderContext.Configuration)
            .ConfigureServices(serviceCollection))
}
Michael Freidgeim
  • 26,542
  • 16
  • 152
  • 170
marianog
  • 119
  • 1
  • 4
  • elegant simplicity. – JJS Aug 11 '21 at 20:10
  • what is the new StartUp for? A separate class for simplicity sake? We could configure all services in the CreateHostBuilder – Dipo Jun 18 '22 at 12:45
  • You could simply use a delegate Startup method if your goal is to host it in another file. `public static Action Configure = (hostBuilderContext, services) => { }` – Lee Harrison Aug 03 '22 at 16:56
0

Yes, it is. ASP.NET Core applications can either be self-hosted - as in your example - or hosted inside a web server such as IIS. In .NET Core all apps are console apps.

Ricardo Peres
  • 13,724
  • 5
  • 57
  • 74