Adding key value to TempData results in a blank page - asp.net

I am building my first .NETCoreApp using 1.0.0-rc2-final. I am trying to insert a copy of Model into TempData so that it is accessible after postback.
I added Microsoft.AspNetCore.Session to my project.
I altered my Startup.cs to look like...
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
namespace GamesCore
{
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
if (env.IsDevelopment())
{
// For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709
builder.AddUserSecrets();
}
builder.AddEnvironmentVariables();
Configuration = builder.Build();
}
public static IConfigurationRoot Configuration { get; private set; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
services.AddSession();
services.AddAuthentication(
SharedOptions => SharedOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseCookieAuthentication();
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = Configuration["Authentication:AzureAd:ClientId"],
Authority = Configuration["Authentication:AzureAd:AADInstance"] + Configuration["Authentication:AzureAd:TenantId"],
CallbackPath = Configuration["Authentication:AzureAd:CallbackPath"]
});
app.UseSession(new SessionOptions { IdleTimeout = TimeSpan.FromMinutes(60) });
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
I have the following in one of my Controllers:
public IActionResult Index(Models.ScoreModel scoreModel)
{
if (string.IsNullOrWhiteSpace(scoreModel.Username))
{
scoreModel.GameID = new System.Guid("b90ae557-7e03-4efa-9da1-1a4e89c1f629");
scoreModel.Username = User.Identity.Name;
scoreModel.Score = 0;
scoreModel.ScoringStep = 1;
TempData.Add("scoreModel", scoreModel);
}
return View(scoreModel);
}
When I have the line with TempData in there, the page loads completely blank -- no error, no Shared Layout, etc. If I remove that line, the View loads fine within the Shared Layout. If I look at the debugger, the scoreModel is getting successfully added to TempData so that doesn't seem to be a problem.

I figured this out.
I moved to storing it in Session instead of TempData, using this page as an example:
http://benjii.me/2015/07/using-sessions-and-httpcontext-in-aspnet5-and-mvc6/
I serialized the Model using the extension class outlined on the above page.
One note, is that since this page was written, services.AddCaching(); has changed to services.AddDistributedMemoryCache();
See this link for another sample: https://github.com/aspnet/Session/blob/dev/samples/SessionSample/Startup.cs

Related

Launch ASP.NET Core API from windows service

I have a small API created from a ASP.NET core Api template in visual studio 22 that targets .NET6.
For testing\debuging I'm launching the API using console application and everything works.
But for production I need this API to be started from a windows service and I have no idea how to make it.
I could simply put the .exe in same folder as the service and call it, but far as I now if no user logged in the console app won't start.
So the idea is to pack the API together with all services and start it when windows service starts.
I've made a small test by creating a static class name "Test.cs" with a method named "Start" and copy all the code from "Program.cs" and call the "Start" method from a test application.
The API starts and stays listening to the endpoints but for some reason doesn't map the controllers from project controllers folder.
all OK when starting API from default
controllers not mapped when start API from static method
program.cs
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
//to avoid json serialize camel casing
builder.Services.AddControllers().AddJsonOptions(opts =>
{
opts.JsonSerializerOptions.PropertyNamingPolicy = null;
opts.JsonSerializerOptions.NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals; //para aceitar NAN, infinitos etc no json
});
//////////
//Nedeed
builder.Services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseCors("CorsPolicy");
//app.UseAuthorization();
app.MapControllers();
app.Run();
test.cs
namespace ProjectX.Api
{
public static class Test
{
public static void Start()
{
var builder = WebApplication.CreateBuilder();
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
//to avoid json serialize camel casing
builder.Services.AddControllers().AddJsonOptions(opts =>
{
opts.JsonSerializerOptions.PropertyNamingPolicy = null;
opts.JsonSerializerOptions.NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals; //para aceitar NAN, infinitos etc no json
});
//////////
//Nedeed
builder.Services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
});
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.UseHttpsRedirection();
app.UseCors("CorsPolicy");
//app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
}
tester app
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace ProjectX.Tester
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
try
{
ProjectX.Api.Test.Start();
}
catch (Exception ex )
{
}
}
}
}
You can run the exe file of Web Api directly in Start():
public static void Start()
{
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.CreateNoWindow = false;
startInfo.UseShellExecute = false;
startInfo.FileName = "Your Path\\ProjectName.exe";
startInfo.WindowStyle = ProcessWindowStyle.Normal;
startInfo.Arguments = "";
try
{
using (Process exeProcess = Process.Start(startInfo))
{
exeProcess.WaitForExit();
}
}
catch
{
//Log error.
}
}
Then you can successfully call the API in the project:
If you want to apply Swagger, please add the following code in the production environment in Program.cs:
app.UseSwagger();
app.UseSwaggerUI();
Result:
Hope this can help you.

How to do a health check on a POST url in ASP.NET/blazor

I am trying to implement health checks in my blazor application. To do so, I have used the Microsoft.AspNetCore.Diagnostics.HealthChecks package among others. Below you can see sql and url health checks.
startup.cs
//using AjuaBlazorServerApp.Data;
using HealthChecks.UI.Client;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace AjuaBlazorServerApp
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddHostedService<PeriodicExecutor>();
services.AddHealthChecks().AddUrlGroup(new Uri("https://api.example.com/post"),
name: "Example Endpoint",
failureStatus: HealthStatus.Degraded)
.AddSqlServer(Configuration["sqlString"],
healthQuery: "select 1",
failureStatus: HealthStatus.Degraded,
name: "SQL Server");
services.AddHealthChecksUI(opt =>
{
opt.SetEvaluationTimeInSeconds(5); //time in seconds between check
opt.MaximumHistoryEntriesPerEndpoint(60); //maximum history of checks
opt.SetApiMaxActiveRequests(1); //api requests concurrency
opt.AddHealthCheckEndpoint("Ajua API", "/api/health"); //map health check api
}).AddInMemoryStorage();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
endpoints.MapHealthChecks("/api/health", new HealthCheckOptions()
{
Predicate = _ => true,
ResponseWriter = UIResponseWriter.
WriteHealthCheckUIResponse
});
endpoints.MapHealthChecksUI();
});
}
}
}
The sql one works perfectly. However the url health check returns the following error:
Discover endpoint #0 is not responding with code in 200...299 range, the current status is MethodNotAllowed.
What i would like to know is if there is a way to maybe set the method type and if need be send some test details to the endpoint so that we can actually get a valid response.
AddUrlGroup has an overload that allows you to specify the method through the httpMethod parameter. Try using :
.AddUrlGroup(new Uri("https://api.example.com/post"),
httpMethod: HttpMethod.Post,
name: "Example Endpoint",
failureStatus: HealthStatus.Degraded)
Another overload allows configuring the HttpClient and HttpMessageHandler explicitly, to add specific default headers for example, enable compression or redirection.
.AddUrlGroup(new Uri("https://api.example.com/post"),
httpMethod: HttpMethod.Post,
name: "Example Endpoint",
configureClient: client => {
client.DefaultRequest.Headers.IfModifiedSince=
DateTimeOffset.Now.AddMinutes(-10);
},
failureStatus: HealthStatus.Degraded)
Yet another overload allows explicitly configuring the UriHealthCheckOptions class generated by other AddUrlGroup overloads:
.AddUrlGroup(uriOptions=>{
uriOptions
.UsePost()
.AddUri(someUrl,setup=>{
setup.AddCustomHeader("...","...");
});
});
There's no way to specify content headers because the health check code doesn't send a body.
I accepted the answer above because it technically answered my question. However, this is the implementation I ended up using. Basically you will have to create your own custom healthcheck.
Add a new folder under you projects main directory and name it accordingly
2. Create a new class in that folder and add code similar to what I have below
EndpointHealth.cs
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Net;
using System.IO;
using Microsoft.Extensions.Configuration;
namespace BlazorServerApp.HealthChecks
{
public class EndpointHealth : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken =
default)
{
//create a json string of parameters and send it to the endpoint
var data = new
{
test = "Example",
};
string jsonString = JsonSerializer.Serialize(data);
var httpWebRequest = (HttpWebRequest)WebRequest.Create("https://api.example.com/post");
httpWebRequest.ContentType = Configuration["application/json"];
httpWebRequest.Method = Configuration["POST"];
using (var streamWriter = new StreamWriter(httpWebRequest.GetRequestStream()))
{
streamWriter.Write(jsonString);
}
//Get the endpoint result and use it to return the appropriate health check result
var httpResponse = (HttpWebResponse)httpWebRequest.GetResponse();
if (((int)httpResponse.StatusCode) >= 200 && ((int)httpResponse.StatusCode) < 300)
return Task.FromResult(HealthCheckResult.Healthy());
else
return Task.FromResult(HealthCheckResult.Unhealthy());
}
}
}
Then add the following code to the top of your startup.cs file using BlazorServerApp.HealthChecks;
and finally the below code:
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddHealthChecks()
.AddCheck<EndpointHealth>("Endpoint",null);
}

Angular + Web API Error Only In Production

About the App
I have an Angular 8 App that uses .Net REST APIs that I inherited from a previous employee (I am new to both frameworks). It has been under development for a few months and has been successfully published to the Production server for testing several times throughout development.
The Issue
After the last publish to the Production server, I am receiving two errors in the console stating Unexpected token < in JSON at position 0 for the API call api/MtUsers/GetLoggedInUser which is called on the backend of the home component. I did not update any code in the home component or the MTUsersController since the last time changes were published to production.
Observations
Error only appears in production
Error still exists if I checkout an older (previously working) commit and publish
Visual Studio started complaining about experimental decorators and missing modules on publish (fixed by restarting VS)
Calling the API using postman appears to return index.html in production but returns the MtUser object in localhost
What I've Tried
Clean solution and re-publish
Checkout last known working commit and publish
Recycle application pool and restart website in IIS
Try various code changes related to website configuration
Relevant Code
I'm not too sure what is most "relevant" to this issue, so I am providing the code specified in the error and the startup.cs file. Let me know if something else would be more useful.
home.component.ts
import { Component } from '#angular/core';
import { MtUser } from 'src/app/core/models/mtUser.model'
import { MtUserService } from 'src/app/core/services/mtUser.service';
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
})
export class HomeComponent {
loadingLoggedInUserInfo = true;
loggedInUser: MtUser = <MtUser>{};
/** home ctor */
constructor(
private mtUserService: MtUserService){
document.getElementsByClassName('main-content')[0].scroll(0, 0);
this.mtUserService.GetLoggedInUser()
.subscribe(response => {
this.loadingLoggedInUserInfo = false;
this.loggedInUser = response;
});
}
}
MtUserService
import { Injectable } from '#angular/core';
import { MtUser } from 'src/app/core/models/mtUser.model';
import { environment } from 'src/environments/environment';
import { HttpClient } from '#angular/common/http';
#Injectable({ providedIn: 'root', })
export class MtUserService {
constructor(private http: HttpClient) { }
GetLoggedInUser() {
return this.http.get<MtUser>(environment.apiUrl + '/MtUsers/GetLoggedInUser');
}
}
MtUsersController
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MT8.Data;
using MT8.Models;
using MT8.Utilities;
namespace MT8.Controllers
{
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class MtUsersController : Mt8ControllerBase
{
private readonly Mt8Context _context;
public MtUsersController(Mt8Context context)
{
_context = context;
}
// GET: api/MtUsers/GetLoggedInUser
[HttpGet("GetLoggedInUser")]
public async Task<ActionResult<MtUser>> GetLoggedInUser()
{
var loggedInUserName = ApplicationEnvironment.GetLoggedInUserName(this.HttpContext);
var loggedInUser = await this._context.MtUsers
.SingleOrDefaultAsync(u => u.UserName == loggedInUserName);
if (loggedInUser == null)
return NotFound();
return loggedInUser;
}
}
}
Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.SpaServices.AngularCli;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.EntityFrameworkCore;
using MT8.Data;
using Microsoft.AspNetCore.Server.IISIntegration;
using System.Threading.Tasks;
using MT8.Utilities;
namespace MT8
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
readonly string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
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.AddControllersWithViews();
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
services.AddCors(options =>
{
options.AddPolicy(MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
services.AddDbContext<Mt8Context>(options =>
options.UseSqlServer(Configuration.GetConnectionString("Mt8Context")));
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.AddControllers()
.AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new StringToIntJsonConverter()))
.AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new StringToNullableIntConverter()))
.AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new StringToDecimalJsonConverter()))
.AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new StringToDoubleJsonConverter()))
.AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new StringToDateTimeJsonConverter()))
.AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new StringToNullableDateTimeConverter()));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, Mt8Context dbContext)
{
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();
if (!env.IsDevelopment())
{
app.UseSpaStaticFiles();
}
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
if (!env.IsProduction())
dbContext.InitializeData();
}
}
}
After lots of trial and error, I determined the issue was with a connection string in the appsettings.Production.json file. It was originally set to Integrated Security=True
when the application was first built and something caused this to stop working all of a sudden. I updated the database to use an SQL login and provided the ID and password in the connection string which fixed the issue.

.Net core 2.2 API versioning and proper routing

I am creating an API. I use swagger but due to a huge number of controllers and actions, I want to split API endpoint by domain. To get this I thought about versioning of the API. I thought about using the Status of ApiVersion. The code of my controllers is below.
[ApiVersion("1.0-First")] //This is ApiVersion MajorVersion = 1, Status = "First"
[Route("api/v{version:apiVersion}/[controller]")]
public class FirstController
[ApiVersion("1.0-Second")]
[Route("api/v{version:apiVersion}/other")]
public class SecondController
My swagger looks fine and the definitions of parts of API are good. (I know that path should be without capital letters - this is for test purposes only)
But swagger can't reach any endpoint. Because the valid endpoint is at /api/v1.0-First/First not /api/v1/First.
My startUp class looks like below:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore().AddApiExplorer();
services.AddApiVersioning(c =>
{
c.ApiVersionReader = ApiVersionReader.Combine(
new QueryStringApiVersionReader("V"),
new UrlSegmentApiVersionReader());
c.ReportApiVersions = false;
c.DefaultApiVersion = new ApiVersion(1, 0);
});
services.AddVersionedApiExplorer(options =>
{
options.SubstituteApiVersionInUrl = true;
options.SubstitutionFormat = "V";
options.DefaultApiVersion = new ApiVersion(1, 0);
});
services.RegisterSwaggerConfiguration();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseDeveloperExceptionPage();
app.UseHttpsRedirection();
app.UseMvc();
app.AddSwagger(app.ApplicationServices.GetService<IApiVersionDescriptionProvider>(), Configuration);
}
There is some static class I wrote to add the dependencies based on IApiVersionDescriptionProvider
public static class SwaggerExtension
{
public static void RegisterSwaggerConfiguration(this IServiceCollection services)
{
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
services.AddSwaggerGen();
}
public static void AddSwagger(this IApplicationBuilder app, IApiVersionDescriptionProvider provider, IConfiguration configuration)
{
var prefix = "swagger";
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.RoutePrefix = string.Empty;
foreach (var description in provider.ApiVersionDescriptions)
{
c.SwaggerEndpoint($"{prefix}/{description.GroupName}/swagger.json", description.GroupName);
}
});
}
}
And another class for SwaggerDoc generation.
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IApiVersionDescriptionProvider provider;
private readonly IConfiguration configuration;
public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider, IConfiguration configuration)
{
this.provider = provider;
this.configuration = configuration;
}
public void Configure(SwaggerGenOptions options)
{
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
}
}
private OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
{
var info = new OpenApiInfo()
{
Title = description.GroupName,
Version = description.ApiVersion.ToString(),
};
if (description.IsDeprecated)
{
info.Description += " This API version has been deprecated.";
}
return info;
}
}
I want to get the routing work as api/v1/First or api/v1.0/First (this should not matter).
Maybe writting some custom middleware to handle this case would be good idea?
By now I am out of ideas and in general I couldn't find any articles about using status of ApiVersion.
EDIT:
Changed Title.
We had a similar problem some time ago. We needed to split an Api by a customer privilege/domain. The research took some time as well :), please note that we are using NSwag.
So as you already mentioned (custom middleware) we've created a custom OperationProcessor and used base type checking. Take a look at an example:
services.AddOpenApiDocument(document =>
{
document.Title = "API A";
document.OperationProcessors.Insert(0, new IncludeAApiControllersInSwagger());
});
services.AddOpenApiDocument(document =>
{
document.Title = "API B";
document.OperationProcessors.Insert(0, new IncludeBApiControllersInSwagger());
});
and then
private class IncludeAApiControllersInSwagger : IOperationProcessor
{
public bool Process(OperationProcessorContext context)
{
return IsControllerInType(context, typeof(AApiController));
}
}
private class IncludeBApiControllersInSwagger : IOperationProcessor
{
public bool Process(OperationProcessorContext context)
{
return IsControllerInType(context, typeof(BApiController));
}
}
The last step would be to build a proper inheritance over your controllers.
An API version is always an API version; the values are explicit - by design. There is no universe where 1.0-First can map to an API, but not include the status.
The status is most useful for pre-releases. For example, you might have /first?api-version=1.0-preview.1. When you have a volatile, preview version of an API, this prevents you from having to bump up to 1.1 and so on. 1.0 is greater than 1.0-preview.1.
From your description, it sounds like you want to group or categorize your APIs by an additional level. The Swagger UI only supports a single level of grouping, but ASP.NET API Versioning 7.0+ now has support to make custom group names with API versions easy to configure using the FormatGroupName option.
If your API has a custom group name like this:
[ApiVersion(1.0)]
[ApiExplorerSettings(GroupName = "First")]
[Route("api/v{version:apiVersion}/[controller]")]
public class FirstController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
You can now configure the combination of both like this:
services.AddApiVersioning()
.AddApiExplorer(
options =>
{
options.SubstituteApiVersionInUrl = true;
options.FormatGroupName = (group, version) => $"{version}-{group}";
});
This only works if you set a custom group name and define a callback. The rules are:
Default configuration; formatted ApiVersion
Group name set, but not callback; use group name
Group name and callback set; result for callback with group and formatted ApiVersion
Only callback set; ignored and uses default configuration as there's no group name
The ApiVersion is formatted according to GroupNameFormat. By default, this will simply be ApiVersion.ToString(). You can still use it if you want to. For example, if GroupNameFormat = "'v'VVV";, then the formatted name via the callback will result in v1-First.
Despite all of this configuration and grouping, the route to your API will still be: api/v1/first. I believe that will get you both of your goals.

Can't call client method from server

I'm trying to use SignalR to broadcast a message from the server to the client without the client triggering the message. From tutorials that I've seen, defining a method in the client, like so:
signalRConnection.client.addNewMessage = function(message) {
console.log(message);
};
should allow the following hub code to be used on the server:
public async Task SendMessage(string message)
{
await Clients.All.addNewMessage("Hey from the server!");
}
However, the Clients.All.addNewMessage call causes an error in the C# compiler:
'IClientProxy' does not contain a definition for 'addNewMessage' and no accessible extension method 'addNewMessage' accepting a first argument of type 'IClientProxy' could be found (are you missing a using directive or an assembly reference?)
How do I fix this? The server code is contained within the hub.
This is because you are using ASP.NET Core SignalR but you are calling client method following ASP.NET MVC SignalR. In ASP.NET Core SignalR you have to call the client method as follows:
public async Task SendMessage(string message)
{
await Clients.All.SendAsync("AddNewMessage", message); // here `AddNewMessage` is the method name in the client side.
}
It showing your client side code is also for ASP.NET MVC SignalR. For ASP.NET Core SignalR it should be as follows:
"use strict";
var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();
connection.on("AddNewMessage", function (message) {
// do whatever you want to do with `message`
});
connection.start().catch(function (err) {
return console.error(err.toString());
});
And In the Startup class SignalR setup should be as follows:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSignalR(); // Must add this
}
// 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
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("/chatHub"); // Here is configuring for `ChatHub`
});
app.UseMvc();
}
}
Please follow Get started with ASP.NET Core SignalR this tutorial if you face further problem.

Resources