Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Self-hosted In Process Web API with Dot net core

I am trying to investigate the plausibility of moving to dot net core now 3.0 has been released. One of our key components allows our (private) nugets to create their own WebAPI, providing events and methods to the consumer. This supports functionality like remote service control, or remote service configuration, allowing the api to provide remote configuration setting/retrieval etc.

This functionality is key to how our micro-service architecture currently works.

I am trying to replicate this with dotnet core, however, I am struggling to find a direct equivalent tutorial/scenario. We essentially followed the process detailed here:

https://docs.microsoft.com/en-us/aspnet/web-api/overview/hosting-aspnet-web-api/use-owin-to-self-host-web-api

However, after checking the compatibility of the nuget packages (and everything looking OK..), I now just get null reference exceptions when calling WebApp.Start<Startup>(baseaddress);

The null reference exception is apparently called by incompatibility of the nuget packages with .net core see here:

NullReferenceException experienced with Owin on Startup .Net Core 2.0 - Settings?

The solution provided in the link is one way, but it uses a third-party application - NancyFx. Is there any way to implement the same functionality with dotnet core in its current form? There was ample documentation for self-host before, but unfortunately given aspnet core runs in its own process, it is decidedly difficult to find a solution!

Can anyone point me in the right direction here?

The code is shown below

//the external library would contain all this code. I.e. this could present the configuration endpoints as mentioned above.

public class Startup
{
    // This code configures Web API. The Startup class is specified as a type
    // parameter in the WebApp.Start method.
    public void Configuration(IAppBuilder appBuilder)
    {
        // Configure Web API for self-host. 
        HttpConfiguration config = new HttpConfiguration();
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
        appBuilder.UseWebApi(config);
    }
}

public class WebAPI:IDisposable
{
    private IDisposable _webApp;
    public WebAPI()
    {
        string baseAddress = "http://localhost:8800/";
        _webApp = WebApp.Start<Startup>(baseAddress); // << This line throws null reference exception 
    }
    #region IDisposable Support
    private bool disposedValue = false; // To detect redundant calls

    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                _webApp.Dispose();
                _webApp = null;
            }                
            disposedValue = true;
        }
    }

    public void Dispose()
    {
        Dispose(true);
    }
    #endregion
}

public class ValuesController:ApiController
{
    // GET api/values 
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }

    // GET api/values/5 
    public string Get(int id)
    {
        return "value";
    }
}

The main app, the host/consumer of the library above.

class Program
{
    static void Main()
    {
        var webapi = new WebApiTest.WebAPI();
        Console.WriteLine("Running...");
        Console.ReadLine();
        webapi.Dispose();
    }
}
like image 429
Chris Watts Avatar asked Sep 24 '19 20:09

Chris Watts


1 Answers

I did eventually figure this out.. basically, I looked at the project stub that was created with the new dotnet core webapi project, and then rewrote the startup class so that it was less fixed. This meant I could use my own dependency injection container from the main application and everything could kinda hook up as expected. I must add this isnt battle tested, or really tested much at all, the project took a backseat, but the principle works and just needs refining/customising to suit.

The only piece of code of interest is the new "Api" class, which in my case starts/stops the API on demand: (the configurations can probably be removed, but was trying to customise the API as much as possible)

public class Api:IDisposable
    {
        private readonly IHost _host;
        private readonly IConfigurationManager _configManager;

        public Api(DryIoc.IContainer container, IConfigurationManager configManager)
        {
            _host = CreateBuilder(Array.Empty<string>(), container).Build();
            _configManager = configManager;
            _configManager.Build($"{nameof(Api)}_IOSource");
        }

        public async void Start()
        {
            await _host.StartAsync();
        }

        public async void Stop()
        {
            await _host.StopAsync();
        }

        private IHostBuilder CreateBuilder(string[] args, DryIoc.IContainer container = null)
        {
            return new HostBuilder()
                //CreateDefaultBuilder, taken from source, to allow custom useserviceproviderfactory
                .UseContentRoot(Directory.GetCurrentDirectory())
                .ConfigureHostConfiguration(config =>
                {
                    config.AddEnvironmentVariables(prefix: "DOTNET_");
                    if (args != null)
                    {
                        config.AddCommandLine(args);
                    }
                })
                .ConfigureAppConfiguration((hostingContext, config) =>
                {
                    var env = hostingContext.HostingEnvironment;

                    config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

                    if (env.IsDevelopment() && !string.IsNullOrEmpty(env.ApplicationName))
                    {
                        var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
                        if (appAssembly != null)
                        {
                            config.AddUserSecrets(appAssembly, optional: true);
                        }
                    }

                    config.AddEnvironmentVariables();

                    if (args != null)
                    {
                        config.AddCommandLine(args);
                    }
                })
                .UseServiceProviderFactory(new DryIocServiceProviderFactory(container))
                .ConfigureLogging((hostingContext, logging) =>
                {
                    var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);

                    // IMPORTANT: This needs to be added *before* configuration is loaded, this lets
                    // the defaults be overridden by the configuration.
                    if (isWindows)
                    {
                        // Default the EventLogLoggerProvider to warning or above
                        logging.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Warning);
                    }

                    logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
                    logging.AddConsole();
                    logging.AddDebug();
                    logging.AddEventSourceLogger();

                    if (isWindows)
                    {
                        // Add the EventLogLoggerProvider on windows machines
                        logging.AddEventLog();
                    }
                })
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                    webBuilder.UseUrls("http://localhost:12000/");
                });
        }

        //This is the original code thats generated by dotnet for new webapi projects
        //private IHostBuilder CreateHostBuilder(string[] args) =>
        //    //new WebHostBuilder().Build();
        //    Host.CreateDefaultBuilder(args)
        //        .ConfigureWebHostDefaults(webBuilder =>
        //        {
        //            webBuilder.UseStartup<Startup>();
        //            webBuilder.UseUrls("http://localhost:12000/");
        //        });

        public void Dispose()
        {
            _host?.Dispose();
        }
    }
like image 192
Chris Watts Avatar answered Oct 03 '22 19:10

Chris Watts