I'm working on creating a UrlHelper for a background worker to create callback urls, which means it's not part of a normal request where I could just ask for it through DI.
In ASP.Net 5 I could just create a HttpRequest and give it the same HttpConfiguration I used to build my app, but in ASP.Net Core 2.0 the UrlHelper depends on a full ActionContext which is a bit harder to craft.
I have a working prototype, but it's using a nasty hack to smuggle the route data out of the application startup process. Is there a better way to do this?
public class Capture
{
public IRouter Router { get; set; }
}
public static class Ext
{
// Step 1: Inject smuggler when building web host
public static IWebHostBuilder SniffRouteData(this IWebHostBuilder builder)
{
return builder.ConfigureServices(svc => svc.AddSingleton<Capture>());
}
// Step 2: Swipe the route data in application startup
public static IApplicationBuilder UseMvcAndSniffRoutes(this IApplicationBuilder app)
{
var capture = app.ApplicationServices.GetRequiredService<Capture>();
IRouteBuilder capturedRoutes = null;
app.UseMvc(routeBuilder => capturedRoutes = routeBuilder);
capture.Router = capturedRoutes?.Build();
return app;
}
// Step 3: Build the UrlHelper using the captured routes and webhost
public static IUrlHelper GetStaticUrlHelper(this IWebHost host, string baseUri)
=> GetStaticUrlHelper(host, new Uri(baseUri));
public static IUrlHelper GetStaticUrlHelper(this IWebHost host, Uri baseUri)
{
HttpContext httpContext = new DefaultHttpContext()
{
RequestServices = host.Services,
Request =
{
Scheme = baseUri.Scheme,
Host = HostString.FromUriComponent(baseUri),
PathBase = PathString.FromUriComponent(baseUri),
},
};
var captured = host.Services.GetRequiredService<Capture>();
var actionContext = new ActionContext
{
HttpContext = httpContext,
RouteData = new RouteData { Routers = { captured.Router }},
ActionDescriptor = new ActionDescriptor(),
};
return new UrlHelper(actionContext);
}
}
// Based on dotnet new webapi
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args);//.Run();
}
public static IWebHost BuildWebHost(string[] args)
{
var captured = new Capture();
var webhost = WebHost.CreateDefaultBuilder(args)
.SniffRouteData()
.UseStartup<Startup>()
.Build();
var urlHelper = webhost.GetStaticUrlHelper("https://my.internal.service:48923/somepath");
Console.WriteLine("YO! " + urlHelper.Link(nameof(ValuesController), null));
return webhost;
}
}
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, Capture capture)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvcAndSniffRoutes();
}
}
[Route("api/[controller]", Name = nameof(ValuesController))]
public class ValuesController : Controller
{
// GET api/values
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// etc
}
Browsing the sources it seems there is no less hacky solution.
In the UseMvc() method the IRouter object being built is passed to the RouterMiddleware, which stores it in a private field and exposes it only to the requests. So reflection would be your only other option, which is obviously out of the running.
However, if you need to generate only static paths using IUrlHelper.Content() you won't need the router as the default implementation won't use it. In this case you can create the helper like this:
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var urlHelper = new UrlHelper(actionContext);