runtime icon indicating copy to clipboard operation
runtime copied to clipboard

Can't get ASP.NET Core app routes to work when hosted via hostfxr in native app

Open NightWulfe opened this issue 1 year ago • 3 comments

Hello,

I cannot seem to get an ASP.NET Web REST API application to work when hosted in a native app with hostfxr. I am sorry if this is something I missed, but I've been searching and debugging for the last two days trying to get this to work and my brain is fried.

The issue is this:

  • When launching the ASP.NET Core app through its default executable, the REST API and built-in Swagger UI work as expected.
  • When launched through my native app with hostfxr, the server will start, but no routes match and attempting to access swagger results in a blank white page.

I have tried:

  • Launching with various combinations of hostfxr_initialize_for_runtime_config and hostfxr_initialize_for_dotnet_command_line_fn with hdt_load_assembly_and_get_function_pointer and hdt_get_function_pointer
  • Wrapping my managed code with using (AssemblyLoadContext.EnterContextualReflection(typeof(Program).Assembly))
  • Inspecting trace files created with COREHOST_TRACE (all dependencies seem to be loading)

I also added code to log the available parts/controllers (based on this article. I noticed that when I run with the default app generated by the C# compiler, it correctly dumps the parts and controllers. However, when hosted from hostfxr, the controller list is empty.

My VS Solution and Project files are attached as RESTedConfig.zip

My Native C++ Code is (NOTE I have a few hard-coded paths just trying to get this to work):

#include"coreclr_delegates.h"
#include"hostfxr.h"

#include<array>
#include<cassert>
#include<condition_variable>
#include<mutex>
#include<nethost.h>
#include<string>
#include<thread>

extern "C" {
    #define WINAPI __stdcall

    using HMODULE = void*;
    using BOOL    = int;
    using DWORD   = unsigned long;

    using PHANDLER_ROUTINE = BOOL (WINAPI *)(DWORD);
    
    constexpr static BOOL  TRUE  = 1;
    constexpr static bool  FALSE = 0;
    constexpr static DWORD ATTACH_PARENT_PROCESS = -1;

    extern HMODULE WINAPI LoadLibraryW(wchar_t const* lpLibFileName);
    extern   void* WINAPI GetProcAddress(HMODULE hModule, char const* lpProcName);

    extern    BOOL WINAPI SetConsoleCtrlHandler(PHANDLER_ROUTINE HandlerRoutine, BOOL Add);

    extern    BOOL WINAPI AttachConsole(DWORD dwProcessId);
}

namespace {
    constexpr static auto Arguments = std::array { L"D:\\Prototypes\\RESTedConfig\\bin\\ConfigurationApi.dll" };

    auto signaled = false;
    auto signal_lock = std::mutex { };
    auto signal = std::condition_variable {};
    
    //auto dotnet_get_context = hostfxr_initialize_for_runtime_config_fn {};
    auto dotnet_get_cmdline_context = hostfxr_initialize_for_dotnet_command_line_fn {};
    auto dotnet_get_entry_point = hostfxr_get_runtime_delegate_fn {};
    auto dotnet_run = hostfxr_run_app_fn {};
    auto dotnet_set = hostfxr_set_runtime_property_value_fn {};
    auto dotnet_get = hostfxr_get_runtime_property_value_fn {};
    auto dotnet_free_context = hostfxr_close_fn {};

    auto stop_server = component_entry_point_fn {};

    void dotnet_load();
    //auto dotnet_get_assembly_main(wchar_t const* assemblyConfigurationPath) -> load_assembly_and_get_function_pointer_fn;
    BOOL WINAPI OnConsoleEvent(DWORD eventCode);
}

int main([[maybe_unused]]int argc, 
         [[maybe_unused]]char** argv)
{
    //_putenv("COREHOST_TRACE=1");
    //_putenv("COREHOST_TRACEFILE=trace.txt");
    //_putenv("COREHOST_TRACE_VERBOSITY=4");

    dotnet_load();

    auto context = hostfxr_handle {};
    auto rc = dotnet_get_cmdline_context(Arguments.size(), (wchar_t const**)Arguments.data(), nullptr, &context);
    assert(rc == 0 && context != nullptr);

    auto dotnet_get_assembly_main = load_assembly_and_get_function_pointer_fn {};
    rc = dotnet_get_entry_point(context, hdt_load_assembly_and_get_function_pointer, (void**)&dotnet_get_assembly_main);
    assert(rc == 0 && dotnet_get_assembly_main != nullptr);
    
    auto dotnet_get_function_pointer = get_function_pointer_fn {};
    rc = dotnet_get_entry_point(context, hdt_get_function_pointer, (void**)&dotnet_get_function_pointer);
    assert(rc == 0 && dotnet_get_function_pointer != nullptr);

    auto entryPoint = component_entry_point_fn { };

    rc = dotnet_get_function_pointer(L"ConfigurationApi.Program, ConfigurationApi", L"NativeHostEntryPoint", nullptr, nullptr, nullptr, (void**)&entryPoint);
    assert(rc == 0 && entryPoint != nullptr);

    rc = dotnet_get_function_pointer(L"ConfigurationApi.Program, ConfigurationApi", L"NativeHostRequestStop", nullptr, nullptr, nullptr, (void**)&stop_server);
    assert(rc == 0 && stop_server != nullptr);

    //wchar_t const* output = nullptr;
    //dotnet_get(context, L"APP_PATHS", &output);

    dotnet_free_context(context);

    entryPoint(nullptr, 0);

    // HACK: The ASP.NET Core host hijacks the console CTRL+C event. This prevents our handler
    //       from being called (event if the event is cancelled in the .NET app). Calling 
    //       AttachConsole here resets the Console Event Handlers,
    //       allowing us to restore our handler.
    AttachConsole(ATTACH_PARENT_PROCESS);
    SetConsoleCtrlHandler(&OnConsoleEvent, TRUE);

    auto lock = std::unique_lock { signal_lock };
    while (!signaled)
        signal.wait(lock);

    stop_server(nullptr, 0);

    return 0;
}

namespace {
    void dotnet_load()
    {
        auto path = std::wstring(260, '\0');
        auto size = path.size();

        auto rc = get_hostfxr_path(path.data(), &size, nullptr);
        assert(rc == 0);

        // Load our functions
        // NOTE: We don't clean this up as the library stays active throughout the entire
        //       process lifetime. So we let the OS clean up for us.
        auto lib = LoadLibraryW(path.c_str());
        assert(lib);

        dotnet_get_cmdline_context = (decltype(dotnet_get_cmdline_context))GetProcAddress(lib, "hostfxr_initialize_for_dotnet_command_line");
        dotnet_get_entry_point = (decltype(dotnet_get_entry_point))GetProcAddress(lib, "hostfxr_get_runtime_delegate");
        dotnet_run = (decltype(dotnet_run))GetProcAddress(lib, "hostfxr_run_app");
        dotnet_set = (decltype(dotnet_set))GetProcAddress(lib, "hostfxr_set_runtime_property_value");
        dotnet_get = (decltype(dotnet_get))GetProcAddress(lib, "hostfxr_get_runtime_property_value");
        dotnet_free_context = (decltype(dotnet_free_context))GetProcAddress(lib, "hostfxr_close");

        assert(dotnet_get_cmdline_context && dotnet_run && dotnet_free_context);
    }

    BOOL WINAPI OnConsoleEvent([[maybe_unused]]DWORD eventCode)
    {
        auto _ = std::unique_lock { signal_lock };
        
        signaled = true;
        signal.notify_one();

        return TRUE;
    }
}

C# API Code is

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;

namespace ConfigurationApi
{
    public struct Name
    {
        public string ClientName { get; set; }
    }

    [Route("api/[controller]")]
    public class TestController : ControllerBase
    {
        [HttpGet]
        public IActionResult Get()
        {
            return Ok(new Name[]
            {
                new Name { ClientName = "One" },
                new Name { ClientName = "Two" },
                new Name { ClientName = "Three" },
            });
        }
    }

    class PartsLogger : IHostedService
    {
        private readonly ILogger<PartsLogger> _logger;
        private readonly ApplicationPartManager _partManager;

        public PartsLogger(ILogger<PartsLogger> logger, ApplicationPartManager manager)
        {
            _logger = logger;
            _partManager = manager;
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            // Get the names of all the application parts. This is the short assembly name for AssemblyParts
            var applicationParts = _partManager.ApplicationParts.Select(x => x.Name);

            // Create a controller feature, and populate it from the application parts
            var controllerFeature = new ControllerFeature();
            _partManager.PopulateFeature(controllerFeature);

            // Get the names of all of the controllers
            var controllers = controllerFeature.Controllers.Select(x => x.Name);

            // Log the application parts and controllers
            _logger.LogInformation("Found the following application parts: '{ApplicationParts}' with the following controllers: '{Controllers}'",
                string.Join(", ", applicationParts), string.Join(", ", controllers));

            return Task.CompletedTask;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
    }


    public class Program
    {
        private static WebApplication? _app = null;

        public static void Main(string[] args)
        {
            LaunchServer();
            _app.WaitForShutdown();
        }

        public static int NativeHostEntryPoint(IntPtr args, int argCount)
        {
            LaunchServer();
            return 0;
        }

        public static int NativeHostRequestStop(IntPtr args, int argCount)
        {
            _app?.StopAsync().Wait();
            _app?.DisposeAsync().AsTask().Wait();

            return 0;
        }

        private static void LaunchServer()
        {
            var builder = WebApplication.CreateBuilder();

            builder.Services.AddControllers();
            builder.Services.AddHostedService<PartsLogger>();

            // Enable Swagger UI
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();
            
            _app = builder.Build();

            if (_app.Environment.IsDevelopment())
            {
                _app.UseSwagger()
                    .UseSwaggerUI();
            }

            _app.UseAuthorization();
            _app.MapControllers();

            _app.RunAsync();
        }
    }
}

Any help would be greatly appreciated.

Thank you

  • Josh

NightWulfe avatar Aug 06 '22 12:08 NightWulfe

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

Tagging subscribers to this area: @vitek-karas, @agocke, @vsadov See info in area-owners.md if you want to be subscribed.

Issue Details

Hello,

I cannot seem to get an ASP.NET Web REST API application to work when hosted in a native app with hostfxr. I am sorry if this is something I missed, but I've been searching and debugging for the last two days trying to get this to work and my brain is fried.

The issue is this:

  • When launching the ASP.NET Core app through its default executable, the REST API and built-in Swagger UI work as expected.
  • When launched through my native app with hostfxr, the server will start, but no routes match and attempting to access swagger results in a blank white page.

I have tried:

  • Launching with various combinations of hostfxr_initialize_for_runtime_config and hostfxr_initialize_for_dotnet_command_line_fn with hdt_load_assembly_and_get_function_pointer and hdt_get_function_pointer
  • Wrapping my managed code with using (AssemblyLoadContext.EnterContextualReflection(typeof(Program).Assembly))
  • Inspecting trace files created with COREHOST_TRACE (all dependencies seem to be loading)

I also added code to log the available parts/controllers (based on this article. I noticed that when I run with the default app generated by the C# compiler, it correctly dumps the parts and controllers. However, when hosted from hostfxr, the controller list is empty.

My VS Solution and Project files are attached as RESTedConfig.zip

My Native C++ Code is (NOTE I have a few hard-coded paths just trying to get this to work):

#include"coreclr_delegates.h"
#include"hostfxr.h"

#include<array>
#include<cassert>
#include<condition_variable>
#include<mutex>
#include<nethost.h>
#include<string>
#include<thread>

extern "C" {
    #define WINAPI __stdcall

    using HMODULE = void*;
    using BOOL    = int;
    using DWORD   = unsigned long;

    using PHANDLER_ROUTINE = BOOL (WINAPI *)(DWORD);
    
    constexpr static BOOL  TRUE  = 1;
    constexpr static bool  FALSE = 0;
    constexpr static DWORD ATTACH_PARENT_PROCESS = -1;

    extern HMODULE WINAPI LoadLibraryW(wchar_t const* lpLibFileName);
    extern   void* WINAPI GetProcAddress(HMODULE hModule, char const* lpProcName);

    extern    BOOL WINAPI SetConsoleCtrlHandler(PHANDLER_ROUTINE HandlerRoutine, BOOL Add);

    extern    BOOL WINAPI AttachConsole(DWORD dwProcessId);
}

namespace {
    constexpr static auto Arguments = std::array { L"D:\\Prototypes\\RESTedConfig\\bin\\ConfigurationApi.dll" };

    auto signaled = false;
    auto signal_lock = std::mutex { };
    auto signal = std::condition_variable {};
    
    //auto dotnet_get_context = hostfxr_initialize_for_runtime_config_fn {};
    auto dotnet_get_cmdline_context = hostfxr_initialize_for_dotnet_command_line_fn {};
    auto dotnet_get_entry_point = hostfxr_get_runtime_delegate_fn {};
    auto dotnet_run = hostfxr_run_app_fn {};
    auto dotnet_set = hostfxr_set_runtime_property_value_fn {};
    auto dotnet_get = hostfxr_get_runtime_property_value_fn {};
    auto dotnet_free_context = hostfxr_close_fn {};

    auto stop_server = component_entry_point_fn {};

    void dotnet_load();
    //auto dotnet_get_assembly_main(wchar_t const* assemblyConfigurationPath) -> load_assembly_and_get_function_pointer_fn;
    BOOL WINAPI OnConsoleEvent(DWORD eventCode);
}

int main([[maybe_unused]]int argc, 
         [[maybe_unused]]char** argv)
{
    //_putenv("COREHOST_TRACE=1");
    //_putenv("COREHOST_TRACEFILE=trace.txt");
    //_putenv("COREHOST_TRACE_VERBOSITY=4");

    dotnet_load();

    auto context = hostfxr_handle {};
    auto rc = dotnet_get_cmdline_context(Arguments.size(), (wchar_t const**)Arguments.data(), nullptr, &context);
    assert(rc == 0 && context != nullptr);

    auto dotnet_get_assembly_main = load_assembly_and_get_function_pointer_fn {};
    rc = dotnet_get_entry_point(context, hdt_load_assembly_and_get_function_pointer, (void**)&dotnet_get_assembly_main);
    assert(rc == 0 && dotnet_get_assembly_main != nullptr);
    
    auto dotnet_get_function_pointer = get_function_pointer_fn {};
    rc = dotnet_get_entry_point(context, hdt_get_function_pointer, (void**)&dotnet_get_function_pointer);
    assert(rc == 0 && dotnet_get_function_pointer != nullptr);

    auto entryPoint = component_entry_point_fn { };

    rc = dotnet_get_function_pointer(L"ConfigurationApi.Program, ConfigurationApi", L"NativeHostEntryPoint", nullptr, nullptr, nullptr, (void**)&entryPoint);
    assert(rc == 0 && entryPoint != nullptr);

    rc = dotnet_get_function_pointer(L"ConfigurationApi.Program, ConfigurationApi", L"NativeHostRequestStop", nullptr, nullptr, nullptr, (void**)&stop_server);
    assert(rc == 0 && stop_server != nullptr);

    //wchar_t const* output = nullptr;
    //dotnet_get(context, L"APP_PATHS", &output);

    dotnet_free_context(context);

    entryPoint(nullptr, 0);

    // HACK: The ASP.NET Core host hijacks the console CTRL+C event. This prevents our handler
    //       from being called (event if the event is cancelled in the .NET app). Calling 
    //       AttachConsole here resets the Console Event Handlers,
    //       allowing us to restore our handler.
    AttachConsole(ATTACH_PARENT_PROCESS);
    SetConsoleCtrlHandler(&OnConsoleEvent, TRUE);

    auto lock = std::unique_lock { signal_lock };
    while (!signaled)
        signal.wait(lock);

    stop_server(nullptr, 0);

    return 0;
}

namespace {
    void dotnet_load()
    {
        auto path = std::wstring(260, '\0');
        auto size = path.size();

        auto rc = get_hostfxr_path(path.data(), &size, nullptr);
        assert(rc == 0);

        // Load our functions
        // NOTE: We don't clean this up as the library stays active throughout the entire
        //       process lifetime. So we let the OS clean up for us.
        auto lib = LoadLibraryW(path.c_str());
        assert(lib);

        dotnet_get_cmdline_context = (decltype(dotnet_get_cmdline_context))GetProcAddress(lib, "hostfxr_initialize_for_dotnet_command_line");
        dotnet_get_entry_point = (decltype(dotnet_get_entry_point))GetProcAddress(lib, "hostfxr_get_runtime_delegate");
        dotnet_run = (decltype(dotnet_run))GetProcAddress(lib, "hostfxr_run_app");
        dotnet_set = (decltype(dotnet_set))GetProcAddress(lib, "hostfxr_set_runtime_property_value");
        dotnet_get = (decltype(dotnet_get))GetProcAddress(lib, "hostfxr_get_runtime_property_value");
        dotnet_free_context = (decltype(dotnet_free_context))GetProcAddress(lib, "hostfxr_close");

        assert(dotnet_get_cmdline_context && dotnet_run && dotnet_free_context);
    }

    BOOL WINAPI OnConsoleEvent([[maybe_unused]]DWORD eventCode)
    {
        auto _ = std::unique_lock { signal_lock };
        
        signaled = true;
        signal.notify_one();

        return TRUE;
    }
}

C# API Code is

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;

namespace ConfigurationApi
{
    public struct Name
    {
        public string ClientName { get; set; }
    }

    [Route("api/[controller]")]
    public class TestController : ControllerBase
    {
        [HttpGet]
        public IActionResult Get()
        {
            return Ok(new Name[]
            {
                new Name { ClientName = "One" },
                new Name { ClientName = "Two" },
                new Name { ClientName = "Three" },
            });
        }
    }

    class PartsLogger : IHostedService
    {
        private readonly ILogger<PartsLogger> _logger;
        private readonly ApplicationPartManager _partManager;

        public PartsLogger(ILogger<PartsLogger> logger, ApplicationPartManager manager)
        {
            _logger = logger;
            _partManager = manager;
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            // Get the names of all the application parts. This is the short assembly name for AssemblyParts
            var applicationParts = _partManager.ApplicationParts.Select(x => x.Name);

            // Create a controller feature, and populate it from the application parts
            var controllerFeature = new ControllerFeature();
            _partManager.PopulateFeature(controllerFeature);

            // Get the names of all of the controllers
            var controllers = controllerFeature.Controllers.Select(x => x.Name);

            // Log the application parts and controllers
            _logger.LogInformation("Found the following application parts: '{ApplicationParts}' with the following controllers: '{Controllers}'",
                string.Join(", ", applicationParts), string.Join(", ", controllers));

            return Task.CompletedTask;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
    }


    public class Program
    {
        private static WebApplication? _app = null;

        public static void Main(string[] args)
        {
            LaunchServer();
            _app.WaitForShutdown();
        }

        public static int NativeHostEntryPoint(IntPtr args, int argCount)
        {
            LaunchServer();
            return 0;
        }

        public static int NativeHostRequestStop(IntPtr args, int argCount)
        {
            _app?.StopAsync().Wait();
            _app?.DisposeAsync().AsTask().Wait();

            return 0;
        }

        private static void LaunchServer()
        {
            var builder = WebApplication.CreateBuilder();

            builder.Services.AddControllers();
            builder.Services.AddHostedService<PartsLogger>();

            // Enable Swagger UI
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();
            
            _app = builder.Build();

            if (_app.Environment.IsDevelopment())
            {
                _app.UseSwagger()
                    .UseSwaggerUI();
            }

            _app.UseAuthorization();
            _app.MapControllers();

            _app.RunAsync();
        }
    }
}

Any help would be greatly appreciated.

Thank you

  • Josh
Author: NightWulfe
Assignees: -
Labels:

area-Host, untriaged

Milestone: -

msftbot[bot] avatar Aug 06 '22 12:08 msftbot[bot]

It's likely you need to manually add the application parts (not just log them). The default way that applications parts are found is using IHostingEnvironment.ApplicationName and this defaults to Assembly.GetEntryAssembly().Name which might be the expected name in this case.

Try adding this:

builder.Services.AddControllers().AddApplicationPart(typeof(TestController).Assembly);

davidfowl avatar Aug 08 '22 03:08 davidfowl

Thank you. That resolved the issue for my Test controller. I also tried setting the ApplicationName to "ConfigurationApi" and that worked as well.

I thought Swagger was failing for a similar reason, but it turns out it was never being initialized due to the IsDevelopment check. Adding the following line to my native app, before invoking the .NET entry point, resolved the Swagger issue:

putenv("ASPNETCORE_ENVIRONMENT=Development);

Thank you so much for your help!

NightWulfe avatar Aug 08 '22 04:08 NightWulfe

One more thing unrelated to your question but it's bugging me, you should signal back to native code when the application has started. Right now you're calling RunAsync but instead you should be using StartAsync (and not ignoring it). Your native API should look like:

public static int NativeHostEntryPoint(IntPtr args, int argCount)
{
    LaunchServer();
    return 0;
}

public static int NativeHostRequestStop(IntPtr args, int argCount)
{
    _app?.StopAsync().Wait();
    return 0;
}

private static void LaunchServer()
{
    var builder = WebApplication.CreateBuilder();

    builder.Services.AddControllers();
    builder.Services.AddHostedService<PartsLogger>();

    // Enable Swagger UI
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    
    _app = builder.Build();

    if (_app.Environment.IsDevelopment())
    {
        _app.UseSwagger()
            .UseSwaggerUI();
    }

    _app.UseAuthorization();
    _app.MapControllers();

    _app.Start();
}

Other problems:

  • Handlig startup errors. What do you want to do when there are errors in Start.

davidfowl avatar Aug 08 '22 06:08 davidfowl

The default way that applications parts are found is using IHostingEnvironment.ApplicationName and this defaults to Assembly.GetEntryAssembly().Name which might be the expected name in this case.

@davidfowl Would it make sense to have a check in ASP.NET that the application parts are populated from an existing assembly? Currently the above API probably returns null or empty string, so I assume ASP.NET will silently do nothing. Would it make sense to throw?

vitek-karas avatar Aug 08 '22 13:08 vitek-karas

One more thing unrelated to your question but it's bugging me, you should signal back to native code when the application has started. Right now you're calling RunAsync but instead you should be using StartAsync (and not ignoring it).

...

Other problems:

* Handlig startup errors. What do you want to do when there are errors in Start.

Agreed, thank you. I'll fix the Start/StartAsync issue. The code is a proof of concept, so error handling isn't a priority. If this works out and I end up using it, it'll get refactored with proper error handling (probably wrapped with std::error_code and std::system_error). Signalling back into the native code from .NET is next on my list of things to check.

I realize this is overkill and I could just host the REST API using the ConfigurationApi executable, but I'm using this as an exercise to learn interop between a native .NET host and .NET libraries. My hope is to allow plugins/extensions to our native application to be written in .NET languages.

NightWulfe avatar Aug 08 '22 15:08 NightWulfe

Seems like all the questions have been answered. So closing this.

vitek-karas avatar Aug 22 '22 15:08 vitek-karas

@davidfowl Would it make sense to have a check in ASP.NET that the application parts are populated from an existing assembly? Currently the above API probably returns null or empty string, so I assume ASP.NET will silently do nothing. Would it make sense to throw?

Throw would be too breaking but we need better behavior here.

davidfowl avatar Aug 22 '22 15:08 davidfowl

I created https://github.com/dotnet/aspnetcore/issues/43460 to track this on the aspnetcore side.

vitek-karas avatar Aug 22 '22 15:08 vitek-karas