Blazor not receiving signalr created on server side - .net-core

Following many tutorials, examples, this example below I call on the server side, but the client side does not receive, sometimes it works but sometimes it doesn’t (more doesn’t work than it works)
It was supposed to be very simple, but it's not, any suggest will help me so much!
Server side
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.AddControllers().AddNewtonsoftJson(options =>
{
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
});
var connection = #"data source=comandai.database.windows.net;initial catalog=HojeTaPago;persist security info=True;user id=Comandai;password=Ck#21112009;MultipleActiveResultSets=True;";
services.AddDbContext<ComandaiContext>(options => options.UseSqlServer(connection));
services.AddSignalR(options => options.KeepAliveInterval = TimeSpan.FromSeconds(5));
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "HojeTaPago API", Version = "v1" });
c.AddSecurityDefinition("basic", new OpenApiSecurityScheme
{
Name = "Authorization",
Type = SecuritySchemeType.Http,
Scheme = "basic",
In = ParameterLocation.Header,
Description = "Basic Authorization header using the Bearer scheme."
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "basic"
}
},
new string[] {}
}
});
});
services.AddCors(options => options.AddPolicy("CorsPolicy",
builder =>
{
builder.AllowAnyMethod().AllowAnyHeader()
.AllowCredentials();
}));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseResponseCompression();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "HojeTaPago API V1");
c.RoutePrefix = string.Empty;
});
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors("CorsPolicy");
app.UseAuthentication();
app.UseAuthorization();
app.UseMiddleware<AuthenticationMiddleware>();
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<NovoPedidoHub>("/novopedidohub");
endpoints.MapControllers();
});
}
}
Where im using the signalr
await _novoPedidoContext.Clients.All.SendAsync("NovoPedido", ListaComandaItem);
Client side - Blazor
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.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddBlazoredLocalStorage();
services.AddBootstrapCss();
services.AddTransient<HubConnectionBuilder>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
}
Where i call..
protected override async Task OnInitializedAsync()
{
DataService dataService = new DataService();
PedidosParaAceitar = new List<Comanda>(await dataService.BuscarComandasAbertas());
connection = _hubConnectionBuilder.WithUrl(dataService.servidor + "novopedidohub",
opt =>
{
opt.Transports = HttpTransportType.WebSockets;
opt.SkipNegotiation = true;
}).Build();
connection.On<List<ComandaItem>>("NovoPedido", async lista =>
{
var idEstabelecimento = await localStorage.GetItemAsync<int>("IdEstabelecimento");
if (lista.FirstOrDefault().Comanda.IdEstabelecimento == idEstabelecimento)
{
if (PedidosParaAceitar == null)
PedidosParaAceitar = new List<Comanda>();
if (PedidosParaAceitar.Count(x => x.Id == lista.FirstOrDefault().IdComanda) > 0)
foreach (var comandaitem in lista)
{
PedidosParaAceitar.FirstOrDefault(x => x.Id == lista.FirstOrDefault().IdComanda).ComandaItem.Add(comandaitem);
}
else
PedidosParaAceitar.Add(await dataService.BuscarComandaAberta(lista.FirstOrDefault().IdComanda));
StateHasChanged();
}
});
await connection.StartAsync();
}

You didn't specify in the tags if this was client-side (WASM) or server-side Blazor.
Looking at the question I noticed this line in ConfigureServices:
services.AddServerSideBlazor();
So you're attempting to use SignalR, a client-side communication library from the Server. In server-side Blazor all the C# code runs on the server. In this respect, SignalR is redundant since it's already being used by Blazor for communicating between the clients and the server.
By a very fortunate coincidence, I actually wrote an app to test this out recently. I created a server-side Blazor app, and wrote this service:
public class TalkService
{
public TalkService()
{
history = new List<string>();
}
public Action<string> OnChange { get; set; }
// inform all users of new message
public Task SendAsync(string message)
{
// add to history
history.Add(message);
// ensure only last 10 shown
if (history.Count > 10) history.RemoveAt(0);
OnChange.Invoke(message);
return Task.FromResult(0);
}
private readonly List<string> history;
public IReadOnlyList<string> GetHistory() => history;
}
I then registered it as a Singleton on the server (all clients use the same service)
in Startup.cs in the ConfigureServices() method:
services.AddSingleton<TalkService>();
Then rewrote Index.razor as follows:
#page "/"
#inject TalkService service
<p>Talk App started</p>
<p>Send a message: <input type="text"#bind="#message" />
<button class="btn btn-sm btn-primary" #onclick="Send" >Send</button>
</p>
#foreach (var m in messages)
{
<p>#m</p>
}
#code {
string message;
async Task Send()
{
if(!string.IsNullOrWhiteSpace(message))
await service.SendAsync(message);
message = string.Empty;
}
List<string> messages;
protected override void OnParametersSet()
{
// load history
messages = service.GetHistory().ToList();
// register for updates
service.OnChange += ChangeHandler;
}
protected void ChangeHandler(string message)
{
messages.Add(message);
InvokeAsync(StateHasChanged);
}
}
The talk service is a basic "chat" example of course. It's a Singleton so all pages/clients that reference it use the same instance. The service has a simple event OnChange that clients (like the Index page) can listen to for changes elsewhere.
SignalR isn't needed for this app since it's already "there" for server-side.
Demo App
The demo app also has a background service that generates time messages as well. I've pushed this to GitHub to help as a guide:
https://github.com/conficient/BlazorServerWithSignalR

Related

asp.net Core Add Response Headers in Middleware after UseRouting

I'm trying to add a server-side latency header through middleware. I've looked at several SO posts that have similar questions, but their solutions aren't working for me. Here's what I have in my Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory factory)
{
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapRazorPages();
});
app.MyHeaderMiddleware();
}
I have to register MyHeaderMiddleware after UseRouting and UseEndpoints because it also needs the endpoint data.
Then in a extension method class, I've got the following.
public static void MyHeaderMiddleware(this IApplicationBuilder app)
{
app.MyHeaderMiddleware((context, logger) =>
{
var actionDescriptor = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>();
int status = context.Response.StatusCode;
if (context.Request.Host != null)
{
logger.PutProperty("Host", context.Request.Host.Value);
}
if (context.Request?.HttpContext?.Connection?.RemoteIpAddress != null)
{
logger.PutProperty("SourceIp", context.Request.HttpContext.Connection.RemoteIpAddress.ToString());
}
if (context.Request.Headers.TryGetValue("X-Forwarded-For", out StringValues value) && !String.IsNullOrEmpty(value) && value.Count > 0)
{
logger.PutProperty("X-Forwarded-For", value.ToArray());
}
return Task.CompletedTask;
});
}
public static void MyHeaderMiddleware(this IApplicationBuilder app, Func<HttpContext, IMetricsLogger, Task> metricsSetup)
{
app.Use(async (context, next) =>
{
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
var logger = context.RequestServices.GetRequiredService<IMetricsLogger>();
await metricsSetup(context, logger);
context.Response.OnStarting(() =>
{
stopWatch.Stop();
context.Response.Headers.Add("X-Server-Side-Latency", stopWatch.ElapsedMilliseconds.ToString());
return Task.CompletedTask;
});
await next();
});
}
If I register my middleware before UseRouting, the header is added as expected, but if it comes after, it executes, but the header is not seen in the response. I need to setup my metrics logger as part of this because I'm both recording the server-side latency for every API call as well as putting it in the http response.
I changed some of the ordering of steps that ended up getting this to work. The major change was this.
await next(); // this will make the endpoints available in the logger setup
await metricsSetup(context, logger);
and to a lesser degree, this:
app.UseRouting();
app.UseEmfMiddleware();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapRazorPages();
});
By calling next() before the metrics logger is setup, the endpoints are made available even though they come after this middleware in the startup.

Blazor Open ID Connect authentication error "The request included multiple client credentials"

I have implemented Open ID Connect with Blazor using the following method:
Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
this.Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSignalR(e =>
{
e.MaximumReceiveMessageSize = 102400000;
});
services.AddBlazoredModal();
services.AddHttpClient();
services.AddScoped<AccessTokenStorage>();
services.AddAuthentication(opt =>
{
opt.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
opt.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
}).AddCookie().AddOpenIdConnect("oidc", options =>
{
options.Authority = Credentials.Authority;
options.ClientId = Credentials.ClientId;
options.ClientSecret = Credentials.ClientSecret;
options.ResponseType = "code";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.UseTokenLifetime = false;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.TokenValidationParameters = new TokenValidationParameters { NameClaimType = "name" };
options.Events = new OpenIdConnectEvents
{
OnAccessDenied = context =>
{
context.HandleResponse();
context.Response.Redirect("/");
return Task.CompletedTask;
},
};
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
}
Another vital part:
Login.cshtml.cs
public class LoginModel : PageModel
{
public async Task OnGet(string redirectUri)
{
await HttpContext.ChallengeAsync("oidc", new AuthenticationProperties {
RedirectUri = redirectUri });
}
}
It seems to work OK with demo.identityserver.io.
However, when changing it to my company identity provider, sometimes I retrieve the following error:
FBTOAU228E The request included multiple client credentials. OAuth 2.0
protocol requests can have one client credential only. For example,
the request cannot have client credentials in both the BA header and
the request body.
Is this a Blazor side issue or problem with the identity provider?
It happens seemingly at random, but it always happens when removing aspnetcore cookie in browser. Doing this should just get you back to the login screen, but throws this error instead. (Does not happen with demo.identiserver.io...)
Solved it. Seems this is problematic line:
options.GetClaimsFromUserInfoEndpoint = true;
I removed it/set it to false and it works like it should. I had to get the claims a little bit different.

Cors issue with Blazor webassembly standalone client

I am struggling with some cors issue for DELETE and POST.
GET works fine.
Setup is: .Net core API in Azure API Management and Blazor webassembly standalone client (Azure app service) that calls the API.
Error I get when try to DELETE is.
"has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present"
So I find it difficult to understand what code that is needed here. It is so many example everywhere on CORS for .net core and also the different Blazor setups (server, hosted wasm etc).
I guess I need to handle the preflight request in some way for this to work?
This is what I use right now:
My ServiceTimes.razor that calls the API
#code {
private const string ServiceEndpoint = "https://MyProdAPI.azure-api.net/api/ServiceTimes";
private LWS_ServiceTimes[] servicetimes;
LWS_ServiceTimes servicetimeObj = new LWS_ServiceTimes();
string ids = "0";
bool showAddrow = false;
bool loadFailed;
protected override async Task OnInitializedAsync()
{
ids = "0";
try
{
servicetimes = await Http.GetFromJsonAsync<LWS_ServiceTimes[]>(ServiceEndpoint);
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
}
// Delete Method
protected async Task DeleteServiceTimes(long ServiceTimesID)
{
showAddrow = false;
ids = ServiceTimesID.ToString();
await Http.DeleteAsync("https://MyprodAPI.azure-api.net/api/ServiceTimes/1"); //Deletes the ID=1
}
Blazor webassembly standalone client Program.cs
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri("https://prodsite.azurewebsites.net") });
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("api://xxxxxxxx-xxxx-xxxx-xxxx-xxxx/API.Access"); //API.Acess my scope in API
});
builder.Services.AddHttpClient("ServerAPI",
client => client.BaseAddress = new Uri("https://MyprodAPI.azure-api.net"))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
.CreateClient("ServerAPI"));
await builder.Build().RunAsync();
}
API Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<LwsSpiderContext>(options => options.UseSqlServer(Configuration.GetConnectionString("MyDatabase")));
services.AddSwaggerGen();
services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("https://prodsite.azurewebsites.net");
builder.AllowAnyMethod();
builder.WithHeaders(HeaderNames.ContentType, HeaderNames.Authorization, "x-custom-header");
builder.AllowCredentials();
});
});
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseSwagger();
// app.UseHttpsRedirection();
app.UseRouting();
app.UseCors("MyAllowSpecificOrigins");
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}

How to get value of specific key from resource file in ASPNET Core?

I wanna get value of a key in resource (.resx) file in .net core API. I followed the marked answer and implemented in my code. But When I am fetching value by key then only Key name is coming.
The linked is below which I followed :
How to get the .resx file strings in asp.net core
Please help me for this.
Thanks
Here is a working example , you could refer to :
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
var connection = #"Server=(localdb)\mssqllocaldb;Database=WebAPIDbContext;Trusted_Connection=True;ConnectRetryCount=0";
services.AddDbContext<RestaurantContext>(options => options.UseSqlServer(connection));
services.AddLocalization(o => o.ResourcesPath = "Resources");
services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
//Request Localization
//var options = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
//app.UseRequestLocalization(options.Value);
var supportedCultures = new[]
{
new CultureInfo("en-US"),
new CultureInfo("zh-cn"),
};
app.UseRequestLocalization(new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture("en-US"),
// Formatting numbers, dates, etc.
SupportedCultures = supportedCultures,
// UI strings that we have localized.
SupportedUICultures = supportedCultures
});
app.UseHttpsRedirection();
app.UseMvc();
}
Controller
private readonly IStringLocalizer<SharedResource> _sharedLocalizer;
public ValuesController(IStringLocalizer<SharedResource> sharedLocalizer)
{
_sharedLocalizer = sharedLocalizer;
}
// GET api/values/5
[HttpGet("{id}")]
public ActionResult<string> Get(int id)
{
var value = _sharedLocalizer["Title"];
return "value";
}
Resource file path
Note : the namespace of SharedResource.cs should be the root of application
namespace RestaurantReviewApi
{
public class SharedResource
{
}
}
Request url : https://localhost:44318/api/values/3?culture=zh-cn
The screenshot of result :
Reference : https://learn.microsoft.com/en-us/aspnet/core/fundamentals/localization?view=aspnetcore-2.2

Trying to configure multiple .NET Core pipelines, HttpContext ends up null

I am trying to follow this guide to create a logical separation in my MVC app between the API portion and the MVC portion:
https://www.strathweb.com/2017/04/running-multiple-independent-asp-net-core-pipelines-side-by-side-in-the-same-application/
Here is my implementation of the extension method he is using:
public static IApplicationBuilder UseBranchWithServices(this IApplicationBuilder app,
PathString path, Action<IServiceCollection> servicesConfiguration,
Action<IApplicationBuilder> appBuilderConfiguration)
{
var webhost = new WebHostBuilder()
.UseKestrel()
.ConfigureServices(servicesConfiguration)
.UseStartup<StartupHarness>()
.Build()
/* ojO */ .CreateOrMigrateDatabase(); /* Ojo */
var serviceProvider = webhost.Services;
var featureCollection = webhost.ServerFeatures;
var appFactory = serviceProvider.GetRequiredService<IApplicationBuilderFactory>();
var branchBuilder = appFactory.CreateBuilder(featureCollection);
var factory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
branchBuilder.Use(async (context, next) => {
using (var scope = factory.CreateScope()) {
context.RequestServices = scope.ServiceProvider;
await next();
}
});
appBuilderConfiguration(branchBuilder);
var branchDelegate = branchBuilder.Build();
return app.Map(path, builder => { builder.Use(async (context, next) => {
await branchDelegate(context);
});
});
}
When I try to use this and the SignInManager together, HttpContext is always null.
Here's a ridiculously simplified version of my AccountController:
public AccountController(SignInManager<AppUser> mgr) {
_mgr = mgr;
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (ModelState.IsValid)
{
var result = await _mgr.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, lockoutOnFailure: false);
}
}
I am calling app.UseAuthentication() in the ApplicationBuilder; for purposes of brevity, I have that setup as an extension method. This works:
public void ConfigureServices(IServiceCollection services)
{
services.ConfigureMvcServices(Configuration);
}
public void Configure(IApplicationBuilder builder, IHostingEnvironment env)
{
builder.ConfigureMvcBuilder(env);
}
This does not:
public void Configure(IApplicationBuilder builder, IHostingEnvironment env)
{
builder.UseBranchWithServices(StaticConfiguration.RootPath,
services =>
{
services.ConfigureMvcServices(Configuration);
},
app => { app.ConfigureMvcBuilder(env); }
);
builder
.Run(async c =>
await c.Response.WriteAsync("This should work"));
}
In the latter example, the services object ends up with a Count of 335 descriptors, in the former (working) example it has 356.
I tried looking over this blog to see if I could figure out what is going on, but I don't see a strong correlation between what's in the method and what Gordon is describing. So I'm totally lost. Any help would be appreciated. If there's a way to split up the extension method and pass in the services for each pipeline, that would be fine with me.
Yes I know you only see one pipeline here. The point is to add more.
Here's what I ended up with:
This was the wrong way to solve this problem. I needed one properly configured pipeline. I combined my API and Web projects into a single project (though that probably doesn't matter, as long as everything is registered).
(For other reasons, I added async. Nothing to do with it.)
Program.cs:
public static async Task Main(string[] args)
{
IWebHost webhost = CreateWebHostBuilder(args).Build();
await webhost.CreateOrMigrateDatabase();
await webhost.RunAsync();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
new WebHostBuilder()
.ConfigureAppConfiguration((host, config) =>
{
config.SetBasePath(Directory.GetCurrentDirectory());
config.AddJsonFile("appsettings.json");
if (host.HostingEnvironment.IsDevelopment())
config.AddUserSecrets<Startup>();
else
config.AddEnvironmentVariables(prefix: "MYAPP_");
})
.UseKestrel()
.ConfigureLogging((app, logging) =>
{
//snip
})
.UseStartup<Startup>()
.UseUrls("http://*:4213");
}
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
var connString = Configuration.GetConnectionString("default");
services.ConfigureJwt(_signingKey, Configuration);
// https://learn.microsoft.com/en-us/aspnet/core/fundamentals/localization?view=aspnetcore-2.2#configure-localization
services.AddLocalization(options => options.ResourcesPath = StartupConfiguration.ResourcesFolder);
services.AddDbContext<AppDbContext>(builder =>
{
builder.UseLazyLoadingProxies()
.UseSqlServer(connString, with => with.MigrationsAssembly("App.Data"));
});
services.ConfigureAuthentication(Configuration);
var mapperConfig = new MapperConfiguration(maps =>
{
maps.ConfigureMvc();
maps.ConfigureApi();
});
var mapper = mapperConfig.CreateMapper();
services.AddSingleton(mapper);
// https://learn.microsoft.com/en-us/aspnet/core/fundamentals/localization?view=aspnetcore-2.2#configure-localization
services.AddMvc() // (config => { config.Filters.Add(new AuthorizeFilter()); }) // <~ for JWT auth
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSpaStaticFiles(angularApp =>
{
angularApp.RootPath = "dist"; // TODO
});
//snip
}
I split the maps (IMapper, unrelated; they are just extension methods to configure the ViewModels for the two namespaces) but I did not split the pipelines. One properly configured pipeline, with the static website in {folder}/dist. Runs both the API and the MVC project. And it's a lot less to manage.
The full code, for the interested, is here.

Resources