Ozzah
Ozzah

Reputation: 10701

Dotnet Core 3.1 console app hosting optional Web API for control

I'm writing a console app in Dotnet Core 3.1. It is already configured to use dependency injection using Microsoft.Extensions.DependencyInjection in the following way:

public static class Program
{
  public static IServiceProvider ServiceProvider { get; private set; }

  public static int Main(string[] args)
  {
    // ...
    ServiceProvider = ConfigureServices().BuildServiceProvider();
    // ...
  }

  public static IServiceCollection ConfigureServices()
  {
    return new ServiceCollection()
      .AddLogging(cfg =>
      {
        // ...
      }
      // ...
  }
}

I'm trying to set up a simple HTTP API to provide some basic control of the app. I'd like to avoid ASP.Net MVC or anything too heavy. I just need to be able to issue simple instructions and get basic status. It will all be JSON - no need for Razor or anything like that.

I have another two (unfinished) classes:

public class ApiRunner
{
  public IWebHost WebHost { get; }

  public ApiRunner()
  {
    WebHost = new WebHostBuilder()
      .UseKestrel()
      .UseUrls("http://*:5000")
      .UseStartup<ApiStartup>()
      .Build();
  }

  public void Start()
  {
    Task.Run(() => WebHost.Run());
  }

  public void Stop()
  {
    WebHost.StopAsync();
  }
}

and

public class ApiStartup
{
  public void Configure(IApplicationBuilder app)
  {
    app.UseRouter(r =>
    {
      r.MapGet("/", async (request, response, routeData) =>
      {
        response.Headers["content-type"] = "text/plan";
        response.WriteAsync("Hello World!");
      });
    }
  }
}

The above does not work unless I add to my ApiStartup class:

public void ConfigureServices(IServiceCollection services)
{
  services.AddRouting();
}

but this seems like there are two DI stacks running on top of one another: one for the main program, and one for the API. I did try to add services.AddRouting(); to the main DI configuration in Program.cs, but (1) that didn't work - I got the same exception as when I didn't have it at all, leading me to believe that the API is wanting to use its own DI, and (2) I don't necessarily want to pollute my main DI with an API-specific service that I see as a somewhat separate module.

All I need is a lightweight HTTP server running in my console app that allows me to issue simple commands and get status. Can I please have some pointers how I can achieve this? Thank you.

Upvotes: 1

Views: 3475

Answers (2)

aleksander_si
aleksander_si

Reputation: 1377

First, every ASP.NET Core app is a console app and only becomes a web app with DI and relvant services registered.

Second, you are not following the standard pattern for the service registration; there is no need to instantiate a service collection yourself, the WebHostBuilder already does it first. Only register services in the ApiStartup class. So yes, you are registering in two places. See example with the added benefit of logging config demo:

https://github.com/akovac35/Logging.Samples/tree/master/WebApp

Upvotes: 1

Brando Zhang
Brando Zhang

Reputation: 28257

As far as I know, if you used the WebHostBuilder, it will add some common service to your application.

The WebHostBuilder build method will register the common service service like the logger, route or else by calling the BuildCommonServices method().

In my opinion, there is no need to create a service ServiceProvider again, since the asp.core has already done the same thing(Startup.cs configure service.). If you don't want other service like razor or else, you could not add the razor service inside the Startup configure service method just use services.AddControllers(); method or you could create a custom api service which you could use for your web api which doesn't contain any razor related result.

Below is some part of the source codes for the webhost.

The web builder source codes:

public IWebHost Build()
    {
      if (this._webHostBuilt)
        throw new InvalidOperationException(Resources.WebHostBuilder_SingleInstance);
      this._webHostBuilt = true;
      AggregateException hostingStartupErrors;
      IServiceCollection serviceCollection1 = this.BuildCommonServices(out hostingStartupErrors);
      IServiceCollection serviceCollection2 = serviceCollection1.Clone();
      IServiceProvider providerFromFactory = GetProviderFromFactory(serviceCollection1);
      .....

    WebHost webHost = new WebHost(serviceCollection2, providerFromFactory, this._options, this._config, hostingStartupErrors);
      try
      {
        webHost.Initialize();
        return (IWebHost) webHost;
      }
      catch
      {
        webHost.Dispose();
        throw;
      }

    IServiceProvider GetProviderFromFactory(IServiceCollection collection)
      {
        ServiceProvider serviceProvider = collection.BuildServiceProvider();
        IServiceProviderFactory<IServiceCollection> service = ((IServiceProvider) serviceProvider).GetService<IServiceProviderFactory<IServiceCollection>>();
        if (service == null)
          return (IServiceProvider) serviceProvider;
        using (serviceProvider)
          return service.CreateServiceProvider(service.CreateBuilder(collection));
      }
    }

The BuildCommonServices:

private IServiceCollection BuildCommonServices(
      out AggregateException hostingStartupErrors)
    {
        .....
     ServiceCollection services = new ServiceCollection();
        services.AddTransient<IApplicationBuilderFactory, ApplicationBuilderFactory>();
        services.AddTransient<IHttpContextFactory, HttpContextFactory>();
        services.AddScoped<IMiddlewareFactory, MiddlewareFactory>();
        services.AddOptions();
        services.AddLogging();
        services.AddTransient<IStartupFilter, AutoRequestServicesStartupFilter>();
        services.AddTransient<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>();

    .....

      foreach (Action<WebHostBuilderContext, IServiceCollection> servicesDelegate in this._configureServicesDelegates)
        servicesDelegate(this._context, (IServiceCollection) services);
      return (IServiceCollection) services;
    }

How to register the startup.cs:

/// <summary>Specify the startup type to be used by the web host.</summary>
    /// <param name="hostBuilder">The <see cref="T:Microsoft.AspNetCore.Hosting.IWebHostBuilder" /> to configure.</param>
    /// <param name="startupType">The <see cref="T:System.Type" /> to be used.</param>
    /// <returns>The <see cref="T:Microsoft.AspNetCore.Hosting.IWebHostBuilder" />.</returns>
    public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder,Type startupType)
    {
      string name = startupType.GetTypeInfo().Assembly.GetName().Name;
      return hostBuilder.UseSetting(WebHostDefaults.ApplicationKey, name).ConfigureServices((Action<IServiceCollection>) (services =>
      {
        if (typeof (IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo()))
          ServiceCollectionServiceExtensions.AddSingleton(services, typeof (IStartup), startupType);
        else
          ServiceCollectionServiceExtensions.AddSingleton(services, typeof (IStartup), (Func<IServiceProvider, object>) (sp =>
          {
            IHostingEnvironment requiredService = sp.GetRequiredService<IHostingEnvironment>();
            return (object) new ConventionBasedStartup(StartupLoader.LoadMethods(sp, startupType, requiredService.EnvironmentName));
          }));
      }));
    }

    /// <summary>Specify the startup type to be used by the web host.</summary>
    /// <param name="hostBuilder">The <see cref="T:Microsoft.AspNetCore.Hosting.IWebHostBuilder" /> to configure.</param>
    /// <typeparam name="TStartup">The type containing the startup methods for the application.</typeparam>
    /// <returns>The <see cref="T:Microsoft.AspNetCore.Hosting.IWebHostBuilder" />.</returns>
    public static IWebHostBuilder UseStartup<TStartup>(this IWebHostBuilder hostBuilder)
      where TStartup : class
    {
      return hostBuilder.UseStartup(typeof (TStartup));
    }

Upvotes: 0

Related Questions