The UseEndpoint MapHub is called when _configure?.Invoke(builder) is called.
when the test is run we see the following error:
System.InvalidOperationException : Endpoint /hub/notification/negotiate contains authorization metadata, but a middleware was not found that supports authorization. Configure your application startup by adding app.UseAuthorization() in the application startup code. If there are calls to app.UseRouting() and app.UseEndpoints(...), the call to app.UseAuthorization() must go between them.
this occurs when we await the connection StartAsync, however if we debug the test, everything works and the test passes.
this is the unit test:
public async Task CanConnectClientHubToServerUsingLongPolling()
{
ConfigureServicesForTest((ctx, services) =>
{
services.AddNotification(builder =>
{
builder.Configure(options =>
{
options.Enabled = true;
});
builder.AddWebNotification(web =>
{
web.AddHubOptions(hub =>
{
});
});
});
});
Configure(app =>
{
app.UseNotificationServices();
});
INotifier notifier = null!;
IAuthenticationTokensAccessor accessor = null!;
FluentActions.Invoking(() =>
{
notifier = Services.GetRequiredService<INotifier>();
accessor = Services.GetRequiredService<IAuthenticationTokensAccessor>();
}).Should().NotThrow();
var conn = CreateConnectionUsingLongPolling(
Services.GetRequiredService<IOptionsMonitor<WebNotificationOptions>>().Get(Options.DefaultName),
async () => (await accessor.GetAuthenticationTokensAsync("bob")).EncodedUserJwt);
string expected = "this is a test";
string recieved = "";
conn.On<string>(HubMethods.RecieveSystemMessage, (msg) =>
{
Console.WriteLine($"System: {msg}");
});
conn.On<string>(HubMethods.RecieveMessage, (msg) =>
{
recieved = msg;
_delayForMessageSentTokenSource.Cancel();
});
await conn.StartAsync();
conn.ConnectionId.Should().NotBeNull();
conn.State.Should().Be(HubConnectionState.Connected);
await notifier.BroadCastAsync(expected);
DelayAsync(TimeSpan.FromSeconds(60));
recieved.Should().Be(expected);
await conn.StopAsync();
}
for a couple days now I have been attempting to resolve this, however it is a head scratcher and I could use some help.
here is the program entry point used by WebApplicationFactory
using SubSonic.Configuration;
using SubSonic.Notification;
var builder = WebApplication.CreateBuilder(args);
if(!builder.Environment.IsTestIntegration())
{
var section = builder.Configuration.GetRequiredSection("Settings");
builder.Services.AddEnvironment(opt =>
{
opt.Environment = section.GetRequiredValue<string>("Environment");
});
builder.Services
.AddHttpContextAccessor()
.AddControllers();
builder.Services.AddNotification(notify =>
{
notify.Configure(options =>
{
options.Enabled = true;
});
notify.AddWebNotification(web =>
{
});
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
}
// program is primarily hosted in a test framework for integration testing of packages
var app = builder.Build();
if (!app.Environment.IsTestIntegration())
{
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseNotificationServices();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
}
app.Run();
here is the log running without debug:
dbug: Microsoft.Extensions.Hosting.Internal.Host1 Hosting starting
dbug: SubSonic.Testing.WebApiTestFactory[0] calling UseRouting
dbug: SubSonic.Testing.WebApiTestFactory[0] Endpoint Routing Middleware Added False
dbug: SubSonic.Testing.WebApiTestFactory[0] Builder Property: [application.Services, Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope]
dbug: SubSonic.Testing.WebApiTestFactory[0] Builder Property: [server.Features, Microsoft.AspNetCore.Http.Features.FeatureCollection]
dbug: SubSonic.Testing.WebApiTestFactory[0] Builder Property: [__EndpointRouteBuilder, Microsoft.AspNetCore.Routing.DefaultEndpointRouteBuilder]
dbug: SubSonic.Testing.WebApiTestFactory[0] Builder Property: [__UseRouting, System.Func`2[Microsoft.AspNetCore.Builder.IApplicationBuilder,Microsoft.AspNetCore.Builder.IApplicationBuilder]]
dbug: SubSonic.Notification.NotificationProviderFactory[0] NotificationHub mapped to /hub/notification
dbug: Microsoft.Extensions.Hosting.Internal.Host2 Hosting started
here is the log running with debug:
dbug: Microsoft.Extensions.Hosting.Internal.Host1 Hosting starting
dbug: SubSonic.Testing.WebApiTestFactory[0] calling UseRouting
dbug: SubSonic.Testing.WebApiTestFactory[0] Endpoint Routing Middleware Added True
dbug: SubSonic.Testing.WebApiTestFactory[0] Builder Property: [application.Services, Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope]
dbug: SubSonic.Testing.WebApiTestFactory[0] Builder Property: [server.Features, Microsoft.AspNetCore.Http.Features.FeatureCollection]
dbug: SubSonic.Testing.WebApiTestFactory[0] Builder Property: [__MiddlewareDescriptions, System.Collections.Generic.List`1[System.String]]
dbug: SubSonic.Testing.WebApiTestFactory[0] Builder Property: [__EndpointRouteBuilder, Microsoft.AspNetCore.Routing.DefaultEndpointRouteBuilder]
dbug: SubSonic.Testing.WebApiTestFactory[0] Builder Property: [__UseRouting, System.Func`2[Microsoft.AspNetCore.Builder.IApplicationBuilder,Microsoft.AspNetCore.Builder.IApplicationBuilder]]
dbug: SubSonic.Testing.WebApiTestFactory[0] Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware
dbug: SubSonic.Testing.WebApiTestFactory[0] Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware
dbug: SubSonic.Testing.WebApiTestFactory[0] Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware
dbug: SubSonic.Notification.NotificationProviderFactory[0] NotificationHub mapped to /hub/notification
dbug: Microsoft.Extensions.Hosting.Internal.Host2 Hosting started
The UseEndpoint MapHub is called when _configure?.Invoke(builder) is called.
when the test is run we see the following error:
System.InvalidOperationException : Endpoint /hub/notification/negotiate contains authorization metadata, but a middleware was not found that supports authorization. Configure your application startup by adding app.UseAuthorization() in the application startup code. If there are calls to app.UseRouting() and app.UseEndpoints(...), the call to app.UseAuthorization() must go between them.
this occurs when we await the connection StartAsync, however if we debug the test, everything works and the test passes.
this is the unit test:
public async Task CanConnectClientHubToServerUsingLongPolling()
{
ConfigureServicesForTest((ctx, services) =>
{
services.AddNotification(builder =>
{
builder.Configure(options =>
{
options.Enabled = true;
});
builder.AddWebNotification(web =>
{
web.AddHubOptions(hub =>
{
});
});
});
});
Configure(app =>
{
app.UseNotificationServices();
});
INotifier notifier = null!;
IAuthenticationTokensAccessor accessor = null!;
FluentActions.Invoking(() =>
{
notifier = Services.GetRequiredService<INotifier>();
accessor = Services.GetRequiredService<IAuthenticationTokensAccessor>();
}).Should().NotThrow();
var conn = CreateConnectionUsingLongPolling(
Services.GetRequiredService<IOptionsMonitor<WebNotificationOptions>>().Get(Options.DefaultName),
async () => (await accessor.GetAuthenticationTokensAsync("bob")).EncodedUserJwt);
string expected = "this is a test";
string recieved = "";
conn.On<string>(HubMethods.RecieveSystemMessage, (msg) =>
{
Console.WriteLine($"System: {msg}");
});
conn.On<string>(HubMethods.RecieveMessage, (msg) =>
{
recieved = msg;
_delayForMessageSentTokenSource.Cancel();
});
await conn.StartAsync();
conn.ConnectionId.Should().NotBeNull();
conn.State.Should().Be(HubConnectionState.Connected);
await notifier.BroadCastAsync(expected);
DelayAsync(TimeSpan.FromSeconds(60));
recieved.Should().Be(expected);
await conn.StopAsync();
}
for a couple days now I have been attempting to resolve this, however it is a head scratcher and I could use some help.
here is the program entry point used by WebApplicationFactory
using SubSonic.Configuration;
using SubSonic.Notification;
var builder = WebApplication.CreateBuilder(args);
if(!builder.Environment.IsTestIntegration())
{
var section = builder.Configuration.GetRequiredSection("Settings");
builder.Services.AddEnvironment(opt =>
{
opt.Environment = section.GetRequiredValue<string>("Environment");
});
builder.Services
.AddHttpContextAccessor()
.AddControllers();
builder.Services.AddNotification(notify =>
{
notify.Configure(options =>
{
options.Enabled = true;
});
notify.AddWebNotification(web =>
{
});
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
}
// program is primarily hosted in a test framework for integration testing of packages
var app = builder.Build();
if (!app.Environment.IsTestIntegration())
{
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseNotificationServices();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
}
app.Run();
here is the log running without debug:
dbug: Microsoft.Extensions.Hosting.Internal.Host1 Hosting starting
dbug: SubSonic.Testing.WebApiTestFactory[0] calling UseRouting
dbug: SubSonic.Testing.WebApiTestFactory[0] Endpoint Routing Middleware Added False
dbug: SubSonic.Testing.WebApiTestFactory[0] Builder Property: [application.Services, Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope]
dbug: SubSonic.Testing.WebApiTestFactory[0] Builder Property: [server.Features, Microsoft.AspNetCore.Http.Features.FeatureCollection]
dbug: SubSonic.Testing.WebApiTestFactory[0] Builder Property: [__EndpointRouteBuilder, Microsoft.AspNetCore.Routing.DefaultEndpointRouteBuilder]
dbug: SubSonic.Testing.WebApiTestFactory[0] Builder Property: [__UseRouting, System.Func`2[Microsoft.AspNetCore.Builder.IApplicationBuilder,Microsoft.AspNetCore.Builder.IApplicationBuilder]]
dbug: SubSonic.Notification.NotificationProviderFactory[0] NotificationHub mapped to /hub/notification
dbug: Microsoft.Extensions.Hosting.Internal.Host2 Hosting started
here is the log running with debug:
dbug: Microsoft.Extensions.Hosting.Internal.Host1 Hosting starting
dbug: SubSonic.Testing.WebApiTestFactory[0] calling UseRouting
dbug: SubSonic.Testing.WebApiTestFactory[0] Endpoint Routing Middleware Added True
dbug: SubSonic.Testing.WebApiTestFactory[0] Builder Property: [application.Services, Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope]
dbug: SubSonic.Testing.WebApiTestFactory[0] Builder Property: [server.Features, Microsoft.AspNetCore.Http.Features.FeatureCollection]
dbug: SubSonic.Testing.WebApiTestFactory[0] Builder Property: [__MiddlewareDescriptions, System.Collections.Generic.List`1[System.String]]
dbug: SubSonic.Testing.WebApiTestFactory[0] Builder Property: [__EndpointRouteBuilder, Microsoft.AspNetCore.Routing.DefaultEndpointRouteBuilder]
dbug: SubSonic.Testing.WebApiTestFactory[0] Builder Property: [__UseRouting, System.Func`2[Microsoft.AspNetCore.Builder.IApplicationBuilder,Microsoft.AspNetCore.Builder.IApplicationBuilder]]
dbug: SubSonic.Testing.WebApiTestFactory[0] Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware
dbug: SubSonic.Testing.WebApiTestFactory[0] Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware
dbug: SubSonic.Testing.WebApiTestFactory[0] Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware
dbug: SubSonic.Notification.NotificationProviderFactory[0] NotificationHub mapped to /hub/notification
dbug: Microsoft.Extensions.Hosting.Internal.Host2 Hosting started
Share Improve this question edited 22 hours ago Jason Pan 21.9k2 gold badges19 silver badges42 bronze badges asked yesterday Kenneth CarterKenneth Carter 911 silver badge9 bronze badges2 Answers
Reset to default 1I ended up rewriting a couple files and separating the implementation of a WebApplication and a WebHost.
here are a few facts to keep track of:
- WebApplicationBuilder when the Build() method is called returns a WebApplication object which supports IApplicationBuilder and IEndpointRouteBuilder.
- WebHostBuilder when the Build() method is called returns an ApplicationBuilder which only supports IApplicationBuilder. This is also dependent on a Startup Approach to web host configuration of the services and configuration of middleware or any initialization that must occur after the Build.
I refactored the Program Entry Point
using Microsoft.AspNetCore;
using SubSonic.Configuration;
using SubSonic.Notification;
namespace Host
{
public partial class Program
{
public static void Main(string[] args)
{
var webApp = CreateWebApplication(args);
webApp.Run();
}
/// <summary>
/// used to initialize a web application for deployment
/// </summary>
/// <param name="args"></param>
/// <returns><see cref="WebApplication"/></returns>
public static WebApplication CreateWebApplication(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var section = builder.Configuration.GetRequiredSection("Settings");
builder.Services.AddEnvironment(opt =>
{
opt.Environment = section.GetRequiredValue<string>("Environment");
});
builder.Services.AddSubSonicAuthentication(security =>
{
var section = builder.Configuration.GetRequiredSection("Security");
security.SymmetricSecurityKey = Encoding.UTF8.GetBytes(section.GetRequiredValue<string>("SymmetricSecurityKey"));
security.TokensExpireInSeconds = TimeSpan.FromSeconds(section.GetValue("TokensExpireInSeconds", 3600));
security.RequireHttpsMetadata = false;
security.Issuer = "localhost";
security.Audience = "localhost";
});
builder.Services
.AddHttpContextAccessor()
.AddControllers();
builder.Services.AddNotification(notify =>
{
notify.Configure(options =>
{
options.Enabled = true;
});
notify.AddWebNotification();
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapNotificationHub(app.Services.GetRequiredService<IOptionsMonitor<WebNotificationOptions>>().Get(Options.DefaultName));
app.MapControllers();
return app;
}
/// <summary>
/// this method is used to create IWebHostBuilder for WebApplicationFactory.CreateWebHostBuilder
/// </summary>
/// <param name="args"></param>
/// <returns><see cref="IWebHostBuilder"></returns>
public static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
return WebHost.CreateDefaultBuilder(args);
}
}
}
we have one method that creates a WebApplication and another method that creates a IWebHostBuilder for the WebApplicationFactory
The following is implemented in class that inherits from WebApplicationFactory and set's up the startup approach.
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseConfiguration(_configuration);
builder.ConfigureServices(_configureServices);
builder.Configure((builder) =>
{
var logger = builder.ApplicationServices.GetRequiredService<ILogger<WebApplicationFactory<TEntryPoint>>>();
_configure.Invoke(builder, logger);
});
}
The following configure is implemented in a TestFixture which purpose is to rig packages for integration with a web host pipeline with full DI support.
private void Configure(IApplicationBuilder builder, ILogger logger)
{
builder
.UseHttpsRedirection();
builder.UseRouting(); // this is the first call on this extension
builder.UseAuthentication();
builder.UseAuthorization();
builder.UseEndpoints(erb =>
{
erb.MapControllers();
});
_configure?.Invoke(builder); //<- into the unit test in order to configure for test.
}
created an extension method called MapNotificationHub(this IEndpointRouteBuilder builder); to support the WebApplication.
we left the following code in the provider to be used by the IWebHostBuilder
public override void Configure(IApplicationBuilder builder, ILogger? logger = null)
{
if (!(builder is IEndpointRouteBuilder))
{
builder.UseEndpoints(endpoints =>
{
endpoints.MapHub<NotificationHub>($"/hub/{Options.Endpoint}", Options.SetActionOptions<HttpConnectionDispatcherOptions>());
});
logger?.LogInformation("NotificationHub mapped to {Endpoint}", $"/hub/{Options.Endpoint}");
}
}
In your first image, we can see app.UseAuthentication();
in it.
But the program entry point used by WebApplicationFactory is missing this line, this is the keypoint.
According to the error message,we can change the code like below.
app.UseHttpsRedirection();
//Add this line
app.UseRouting();
//Add this line
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();