simple ASP.NET Core routing gone wrong - c#

I'm having problems with routing, it looks like it thinks query parameters are view names, for some reason.
Config:
public static IServiceCollection AddAssetsSites(this IServiceCollection services, IConfiguration configuration, Action<MvcOptions> mvcOptions = default, params IAppFeatureBase[] features)
{
Identity ident = features.Exists(feat => feat.Type == AppFeatures.Identity) ? (Identity)features.Single(feat => feat.Type == AppFeatures.Identity) : new Identity(default, default);
if (configuration.GetSection(AssetsStatics.ConfigSectionName).Exists())
{
IAssetsConfig cnf = new AssetsConfig(configuration);
configuration.Bind(AssetsStatics.ConfigSectionName, cnf);
services
.AddOptions()
.Configure<IConfiguration>(configuration)
.AddTransient<IAssetsConfigAccessor, AssetsConfigAccessor>()
.AddSingleton(cnf);
}
else
{
throw new ConfigSectionNotFoundException();
}
services
.AddDbContext<AssetsDBContext>(opt => opt.UseSqlServer(AssetsStatics.ConnectionString))
.AddTransient<IAssetsDBContext, AssetsDBContext>()
.AddDbContext<AssetsIdentityDBContext>(opt => opt.UseSqlServer(AssetsStatics.ConnectionString))
.AddTransient<IAssetsIdentityDBContext, AssetsIdentityDBContext>()
.AddTransient<IAssetsDBContextAccessor, AssetsDBContextAccessor>()
.AddHttpContextAccessor()
.AddTransient<IActionContextAccessor, ActionContextAccessor>();
services
.AddTransient<IRepoFactory, RepoFactory>()
.AddTransient<IServiceAccessFactory, ServiceAccessFactory>()
.AddTransient<IQueryableExpressionFactory, QueryableExpressionFactory>()
.AddTransient<IQueriesFactory, QueriesFactory>();
services
.AddIdentity<User, Role>(ident.IdentOptions)
.AddUserManager<UserManager<User>>()
.AddRoleManager<RoleManager<Role>>()
.AddSignInManager<SignInManager<User>>()
.AddEntityFrameworkStores<AssetsIdentityDBContext>()
.AddDefaultTokenProviders().Services.ConfigureApplicationCookie(ident.CookieOptions)
.AddTransient<IIdentityRepo, IdentityRepo>();
if (features.Exists(feat => feat.Type == AppFeatures.SSL))
{
SSL ssl = (SSL)features.Single(feat => feat.Type == AppFeatures.SSL);
services
.AddHttpsRedirection(conf =>
{
conf.HttpsPort = ssl.Port;
});
}
services
.AddAssetsRepos()
.AddTransient<ITagHelperRepo, TagHelperRepo>()
.AddTransient<ISitesHelper, SitesHelper>()
.Configure<CookiePolicyOptions>(opt =>
{
opt.CheckConsentNeeded = context => true;
opt.MinimumSameSitePolicy = SameSiteMode.Unspecified;
})
.AddSession(opt =>
{
opt.IdleTimeout = TimeSpan.FromMinutes(180);
});
if (features.Exists(cnf => cnf.Type == AppFeatures.Localization))
{
Localization local = (Localization)features.Single(cnf => cnf.Type == AppFeatures.Localization);
services
.AddControllersWithViews(mvcOptions)
.AddDataAnnotationsLocalization()
.AddViewLocalization(opt =>
{
opt.ResourcesPath = local.ResourcePath;
})
.SetCompatibilityVersion(CoreStatics.DefaultCompatibilityVersion);
}
else
{
services
.AddControllersWithViews(mvcOptions)
.SetCompatibilityVersion(CoreStatics.DefaultCompatibilityVersion);
}
return services;
}
public static IApplicationBuilder UseAssetsSites(this IApplicationBuilder app, IConfiguration configuration, params IAppFeatureBase[] features)
{
if (features.Exists(feat => feat.Type == AppFeatures.Debug))
{
Debug dg = (Debug)features.Single(feat => feat.Type == AppFeatures.Debug);
if (dg.Environment.IsDevelopment() || dg.IgnoreEnvironment)
{
app.UseDeveloperExceptionPage();
}
}
if (features.Exists(feat => feat.Type == AppFeatures.SSL))
{
app.UseHttpsRedirection();
}
app
.UseStaticFiles()
.UseRouting()
.UseSession()
.UseCookiePolicy(new CookiePolicyOptions
{
CheckConsentNeeded = context => true,
MinimumSameSitePolicy = SameSiteMode.None
});
if (features.Exists(feat => feat.Type == AppFeatures.Localization))
{
Localization local = (Localization)features.Single(feat => feat.Type == AppFeatures.Localization);
app.UseRequestLocalization(opt =>
{
opt.DefaultRequestCulture = local.DefaultCulture;
opt.SupportedCultures = local.SupportedCultures.ToList();
opt.SupportedUICultures = local.SupportedCultures.ToList();
});
}
if (features.Exists(feat => feat.Type == AppFeatures.DefaultRoute))
{
DefaultRoute route = (DefaultRoute)features.Single(feat => feat.Type == AppFeatures.DefaultRoute);
app.UseEndpoints(opt =>
{
opt.MapControllerRoute("default", route.Route);
});
}
else
{
app.UseEndpoints(opt => opt.MapDefaultControllerRoute());
}
return app;
}
}
Controller:
public async Task<IActionResult> Index()
{
return View();
}
[Route("[controller]/[action]/{err?}")]
public async Task<IActionResult> Error([FromRoute(Name = "err")] string err)
{
return View(err);
}
Link:
url = Url.Action("Error", new { err = "missing" });
which generates:
/Home/Error/creds
when the Error views loads I get:
InvalidOperationException: The view 'creds' was not found. The
following locations were searched: /Views/Home/creds.en-US.cshtml
/Views/Home/creds.en.cshtml /Views/Home/creds.cshtml
/Views/Shared/creds.en-US.cshtml /Views/Shared/creds.en.cshtml
/Views/Shared/creds.cshtml
Folder structure:
Views
-Assets
* Index.cshtml
-Home
* Error.cshtml
* Index.cshtml
-Shared
* _Layout.cshtml
-System
* Index.cshtml
* _ViewImports.cshtml
* _ViewStart.cshtml

As suspected. Returning View(string viewname) searches your Views Folder with the pattern Views/ControllerName/ViewName.cshtml.
By adding the string parameter err You are telling it to find the creds.cshtml file in Views/Home/creds.cshtml (Assuming your Controllername is Home) (Hence the error message that states it doesnt exist).
If you wish to display the Error.cshtml a simple return View(); is enough because by default it will search for the *.cshtml file which matches the name of the action (i.e. Error.cshtml)
Some documentation: https://learn.microsoft.com/de-de/aspnet/core/mvc/views/overview?view=aspnetcore-3.1
EDIT
For passing the route parameter to the Error View you can either pass it via a model.
[Route("[controller]/[action]/{err?}")]
public async Task<IActionResult> Error([FromRoute(Name = "err")] string err)
{
var errorModel = new ErrorModel(errorMessage: err);
return View(errorModel);
}
Or a without a Model using the dynamic ViewBag
[Route("[controller]/[action]/{err?}")]
public async Task<IActionResult> Error([FromRoute(Name = "err")] string err)
{
ViewBag.ErrorMessage = err;
return View();
}
On the Error.cshtml you can then access the ViewBag.ErrorMessage and show it in div or something

Related

id parameter in OnGet hadler is reseting to 0 eveytime i refresh the page

I need to implement some functions in a razor page and i need "id" to have immutable value. I am passing "id" from other page into OnGet-handler on my main page. Then when I am refreshing the page,"id" is 0. I was trying different ways to keep the original value setted to "id". Is there any way to make my "id" immutable even if i refresh page many times?
[BindProperty(SupportsGet = true)] public int InterestId { get; set; }
public bool isSet { get; set; }
public async Task<IActionResult> OnGet(int id)
{
if(id != null) //Just in order to load all data, otherwise it jumps to view too fast
{
//var interests = await apiManager.GetInterests();
//Interest = interests.FirstOrDefault(x => x.Id == id);
//var threads = await apiManager.ReturnAllThreads();
//Threads = threads.Where(t => t.InterestId == id).ToList();
//await searchBar.Search(SearchKey, id, Threads);
if (isSet == false)
{
isSet = true; // It's false again after refreshing
InterestId = id;
}
}
return Page();
}
You can try to use Session to keep your id:
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
............
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromDays(1);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
.........
app.UseAuthorization();
//add UseSession here
app.UseSession();
app.UseEndpoints(endpoints =>
{
.........
});
}
cshtml.cs:
public async Task<IActionResult> OnGet(int id)
{
if(id != null) //Just in order to load all data, otherwise it jumps to view too fast
{
//var interests = await apiManager.GetInterests();
//Interest = interests.FirstOrDefault(x => x.Id == id);
//var threads = await apiManager.ReturnAllThreads();
//Threads = threads.Where(t => t.InterestId == id).ToList();
//await searchBar.Search(SearchKey, id, Threads);
if (isSet == false)
{
isSet = true; // It's false again after refreshing
InterestId = id;
}
}
return Page();
}
Thank you for replying.
I walked around my problem just by adding additional if-statement, specifying what should be returned if id is equal to 0. Its not exactly the solution I was looking for but in terms of my project its good enough.

OData ASPNetCore CamelCase

I am struggling to find documentation on camelCase feature for .net 6 vs 8.0.6 of Microsoft.AspNetCore.OData
https://github.com/OData/AspNetCoreOData/issues/13#issuecomment-1013384492
The issue is when you query directly it's fine.
But when you use any functionality it breaks
Any ideas?
Code for Config in Program.cs
builder.Services.AddControllersWithViews().AddOData(options =>
{
options.Select().Filter().Expand().Count().SetMaxTop(100).OrderBy();
});
Code for Endpoint
[HttpGet]
[EnableQuery()]
public async Task<IEnumerable<Warehouse>> Warehouses()
{
return _context.Warehouses;
}
Tired
static IEdmModel GetModel()
{
var builder1 = new ODataConventionModelBuilder();
builder1.EnableLowerCamelCase();
builder1.EntitySet<Warehouse>("warehouses");
builder1.EntitySet<Company>("companies");
return builder1.GetEdmModel();
}
builder.Services.AddControllersWithViews().AddOData(options =>
{
options.Select().Filter().Expand().Count().SetMaxTop(100).OrderBy();
options.AddRouteComponents(GetModel());
});
Until this is fix here's what I did with typescript/javascript to transform the response so that it can be mapped to swagger open api objects
return this.httpClient.request<any[]>('get', `${window.origin}/odata/${path}/?${odataQuery}`,
{
headers: this.headers
}
).pipe(map((things) => {
return things.map((thing) => {
return this.util.objectToCamel(thing);
});
}));
objectToCamel(thing: any): any{
const obj: any = {};
if (!thing)
return null;
Object.keys(thing).forEach((key) => {
if (Array.isArray(thing[key])) {
thing[key] = (<any[]>thing[key]).map((thx) => {
return this.objectToCamel(thx);
});
} else if (typeof thing[key] === "object")
thing[key] = this.objectToCamel(thing[key])
obj[key.substr(0, 1).toLowerCase() + key.substr(1)] = thing[key];
});
return obj;
}

Troubleshooting SignalR websockets 400 and 503 errors in Asp.NET Core web application

I've been following a tutorial on hosting an ASP.NET Core web application (API in .NET C# and Front-end in Angular) to Heroku. I'm using the free plan and the asp.net core buildpack. I've implemented SignalR hubs in order for users to send each other live messages and know who is currently online. It worked perfectly when I was testing the app on the localhost, but when I pushed the same application to Heroku I started getting multiple 400 and 503 errors.
All the code below is also at GitHub
https://github.com/eloprey123/DatingApp
EDIT I was checking the heroku logs and noticed that all the HTTP POST and GET requests go to "http://myapp.herokuapp.com" instead of "https//myapp.herokuapp.com" which is where the application is hosted. I know making calls to http can be an issue with WebSockets. I've tried updating to a paid dyno to enable SSL, but that doesn't change anything. end of edit
The first error returned is Websocket connection to "wss://myapp.herokuapp.com/hubs/presence?id=string&access_token=string" failed: Error during WebSocket handshake: Unexpected response code: 400
Followed by: Failed to start the transport 'WebSockets': Error: There was an error with the transport.
Then I get a 400 GET error call to: https://datingapp-angular.herokuapp.com/hubs/presence?id=string&access_token=string and Failed to start the transport 'ServerSentEvents': Error: Error occurred
Lastly, I get a 503 GET error call to https://datingapp-angular.herokuapp.com/hubs/presence?id=string and Error: Connection disconnected with error 'Error: Service Unavailable'.
This error cycle goes on-and-on ad nauseum.
Are there any changes I can make to the options for SignalR and the Hubs on the API and client? Or should I make changes on Heroku? Or is it something else entirely?
This is in my C# Startup class:
public void ConfigureServices(IServiceCollection services)
{
services.AddApplicationServices(_config);
services.AddControllers();
services.AddCors();
services.AddIdentityServices(_config);
services.AddSignalR(options => {
options.EnableDetailedErrors = true;
options.KeepAliveInterval = TimeSpan.FromMinutes(1);
options.ClientTimeoutInterval = TimeSpan.FromMinutes(2);
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseMiddleware<ExceptionMiddleware>();
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors(x => x.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
.WithOrigins("https://localhost:4200"));
app.UseAuthentication();
app.UseAuthorization();
app.UseAuthorization();
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<PresenceHub>("hubs/presence");
endpoints.MapHub<MessageHub>("hubs/message");
endpoints.MapFallbackToController("Index", "Fallback");
});
}
}
I've tried using services.AddSignalR() with different options, without any success. The WithOrigins() part of app.UseCors() only comes into play during development. This because during production, both the API and client are located in the same address. From what I've seen CORS is not the issue because the application is able to make all non-hub related calls to the API all the time.
This is where I add my DB Context to the application:
services.AddDbContext<DataContext>(options =>
{
var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
string connStr;
// Depending on if in development or production, use either Heroku-provided
// connection string, or development connection string from env var.
if (env == "Development")
{
// Use connection string from file.
connStr = config.GetConnectionString("DefaultConnection");
}
else
{
// Use connection string provided at runtime by Heroku.
var connUrl = Environment.GetEnvironmentVariable("DATABASE_URL");
// Parse connection URL to connection string for Npgsql
connUrl = connUrl.Replace("postgres://", string.Empty);
var pgUserPass = connUrl.Split("#")[0];
var pgHostPortDb = connUrl.Split("#")[1];
var pgHostPort = pgHostPortDb.Split("/")[0];
var pgDb = pgHostPortDb.Split("/")[1];
var pgUser = pgUserPass.Split(":")[0];
var pgPass = pgUserPass.Split(":")[1];
var pgHost = pgHostPort.Split(":")[0];
var pgPort = pgHostPort.Split(":")[1];
connStr = $"Server={pgHost};Port={pgPort};User Id={pgUser};Password={pgPass};Database={pgDb};SSL Mode=Require;TrustServerCertificate=True";
}
// Whether the connection string came from the local development configuration file
// or from the environment variable from Heroku, use it to set up your DbContext.
options.UseNpgsql(connStr);
});
This is where the application gets the access token for the SignalR hub calls to the API.
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"])),
ValidateIssuer = false,
ValidateAudience = false
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken)
&& path.StartsWithSegments("/hubs"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
At the moment I'm using a basic Dictionary to track users instead of Redis, so here is the PresenceTracker class:
private static readonly Dictionary<string, List<string>> OnlineUsers =
new Dictionary<string, List<string>>();
public Task<bool> UserConnected(string username, string connectionId)
{
bool isOnline = false;
lock (OnlineUsers)
{
if (OnlineUsers.ContainsKey(username))
{
OnlineUsers[username].Add(connectionId);
}
else
{
OnlineUsers.Add(username, new List<string>{connectionId});
isOnline = true;
}
}
return Task.FromResult(isOnline);
}
public Task<bool> UserDisconnected(string username, string connectionId)
{
bool isOffline = false;
lock(OnlineUsers)
{
if (!OnlineUsers.ContainsKey(username))
return Task.FromResult(isOffline);
OnlineUsers[username].Remove(connectionId);
if (OnlineUsers[username].Count == 0)
{
OnlineUsers.Remove(username);
isOffline = true;
}
}
return Task.FromResult(isOffline);
}
public Task<string[]> GetOnlineUsers()
{
string[] onlineUsers;
lock(OnlineUsers)
{
onlineUsers = OnlineUsers.OrderBy(k => k.Key)
.Select(k => k.Key).ToArray();
}
return Task.FromResult(onlineUsers);
}
public Task<List<string>> GetConnectionsForUser(string username)
{
List<string> connectionIds;
lock (OnlineUsers)
{
connectionIds = OnlineUsers.GetValueOrDefault(username);
}
return Task.FromResult(connectionIds);
}
These are the hubs where I inject the PresenceTracker
(PresenceHub)
public override async Task OnConnectedAsync()
{
var isOnline = await _tracker.UserConnected(Context.User.GetUsername(), Context.ConnectionId);
if (isOnline)
await Clients.Others.SendAsync("UserIsOnline", Context.User.GetUsername());
var currentUsers = await _tracker.GetOnlineUsers();
await Clients.Caller.SendAsync("GetOnlineUsers", currentUsers);
}
public override async Task OnDisconnectedAsync(Exception exception)
{
var isOffline = await _tracker.UserDisconnected(Context.User.GetUsername(), Context.ConnectionId);
if (isOffline)
await Clients.Others.SendAsync("UserIsOffline", Context.User.GetUsername());
await base.OnDisconnectedAsync(exception);
}
(MessageHub)
public override async Task OnConnectedAsync()
{
var httpContext = Context.GetHttpContext();
var otherUser = httpContext.Request.Query["user"].ToString();
var groupName = GetGroupName(Context.User.GetUsername(), otherUser);
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
var group = await AddToGroup(groupName);
await Clients.Group(groupName).SendAsync("UpdatedGroup", group);
var messages = await _unitOfWork.MessageRepository
.GetMessageThread(Context.User.GetUsername(), otherUser);
if (_unitOfWork.HasChanges())
await _unitOfWork.Complete();
await Clients.Caller.SendAsync("ReceiveMessageThread", messages);
}
public override async Task OnDisconnectedAsync(Exception exception)
{
var group = await RemoveFromMessageGroup();
await Clients.Group(group.Name).SendAsync("UpdatedGroup", group);
await base.OnDisconnectedAsync(exception);
}
public async Task SendMessage(CreateMessageDto createMessageDto)
{
var username = Context.User.GetUsername();
if (username == createMessageDto.RecipientUsername.ToLower())
{
throw new HubException("You cannot send a message to yourself");
}
var sender = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username);
var recipient = await _unitOfWork.UserRepository.GetUserByUsernameAsync(createMessageDto.RecipientUsername);
if (recipient == null)
throw new HubException("Not found user");
var message = new Message
{
Sender = sender,
Recipient = recipient,
SenderUsername = sender.UserName,
RecipientUsername = recipient.UserName,
Content = createMessageDto.Content
};
var groupName = GetGroupName(sender.UserName, recipient.UserName);
var group = await _unitOfWork.MessageRepository.GetMessageGroup(groupName);
if (group.Connections.Any(x => x.Username == recipient.UserName))
{
message.Read = DateTime.UtcNow;
}
else
{
var connections = await _tracker.GetConnectionsForUser(recipient.UserName);
if (connections != null)
{
await _presenceHub.Clients.Clients(connections).SendAsync("NewMessageReceived",
new {username = sender.UserName, knownAs = sender.KnownAs});
}
}
_unitOfWork.MessageRepository.AddMessage(message);
if (await _unitOfWork.Complete())
{
await Clients.Group(groupName).SendAsync("NewMessage", _mapper.Map<MessageDto>(message));
}
}
private async Task<Group> AddToGroup(string groupName)
{
var group = await _unitOfWork.MessageRepository.GetMessageGroup(groupName);
var connection = new Connection(Context.ConnectionId, Context.User.GetUsername());
if (group == null)
{
group = new Group(groupName);
_unitOfWork.MessageRepository.AddGroup(group);
}
group.Connections.Add(connection);
if (await _unitOfWork.Complete())
return group;
throw new HubException("Failed to join group");
}
private async Task<Group> RemoveFromMessageGroup()
{
var group = await _unitOfWork.MessageRepository.GetGroupForConnection(Context.ConnectionId);
var connection = group.Connections.FirstOrDefault(x => x.ConnectionId == Context.ConnectionId);
_unitOfWork.MessageRepository.RemoveConnection(connection);
if (await _unitOfWork.Complete())
return group;
throw new HubException("Failed to remove from group");
}
private string GetGroupName(string caller, string other)
{
var stringCompare = string.CompareOrdinal(caller, other) < 0;
return stringCompare ? $"{caller}-{other}" : $"{other}-{caller}";
}
This is the presence.service.ts file on the client:
createHubConnection(user: User) {
this.hubConnection = new HubConnectionBuilder()
.withUrl(this.hubUrl + 'presence', {
accessTokenFactory: () => user.token
})
.withAutomaticReconnect()
.build();
this.hubConnection.start()
.catch(error => console.log(error));
this.hubConnection.on('UserIsOnline', username => {
this.onlineUsers$.pipe(take(1)).subscribe(usernames => {
this.onlineUserSource.next([...usernames, username])
});
});
this.hubConnection.on('UserIsOffline', username => {
this.onlineUsers$.pipe(take(1)).subscribe(usernames => {
this.onlineUserSource.next([...usernames.filter(x => x !== username)])
});
});
this.hubConnection.on('GetOnlineUsers', (usernames: string[]) => {
this.onlineUserSource.next(usernames);
});
this.hubConnection.on('NewMessageReceived', ({username, knownAs}) => {
this.toastr.info(knownAs + ' has sent you a message!')
.onTap.pipe(take(1)).subscribe(() => {
this.router.navigateByUrl(`/members/${username}?tab=3`)
});
})
}
stopHubConnection() {
this.hubConnection.stop()
.catch(errpr => console.log(errpr));
}
The message.service.ts file on the client:
createHubConnection(user: User, otherUsername: string) {
this.busyService.busy();
this.hubConnection = new HubConnectionBuilder()
.withUrl(this.hubUrl + 'message?user=' + otherUsername, {
accessTokenFactory: () => user.token
})
.withAutomaticReconnect()
.build();
this.hubConnection.start()
.catch(error => console.log(error))
.finally(() => this.busyService.idle());
this.hubConnection.on('ReceiveMessageThread', messages => {
this.messageThreadSource.next(messages);
});
this.hubConnection.on('NewMessage', message => {
this.messageThread$.pipe(take(1)).subscribe(messages => {
this.messageThreadSource.next([...messages, message]);
})
});
this.hubConnection.on('UpdatedGroup', (group: Group) => {
if (group.connections.some(x => x.username === otherUsername)) {
this.messageThread$.pipe(take(1)).subscribe(messages => {
messages.forEach(message => {
if (!message.read) {
message.read = new Date(Date.now())
}
})
this.messageThreadSource.next([...messages]);
})
}
})
}
stopHubConnection() {
if (this.hubConnection) {
this.messageThreadSource.next([]);
this.hubConnection.stop();
}
}
getMessages(pageNumber, pageSize, container) {
let params = getPaginationHeaders(pageNumber, pageSize);
params = params.append("Container", container);
return getPaginatedResult<Message[]>(this.baseUrl + 'messages', params, this.http);
}
getMessageThread(username: string) {
return this.http.get<Message[]>(this.baseUrl + 'messages/thread/' + username);
}
async sendMessage(username: string, content: string) {
return this.hubConnection.invoke('SendMessage',
{recipientUsername: username, content})
.catch(error => console.log(error));
}
deleteMessage(id: number) {
return this.http.delete(this.baseUrl + 'messages/' +id);
}

Actions require unique method/path combination for Swagger

I have 2 HTTP GET method in same controller and give me this error
HTTP method "GET" & path "api/DataStore" overloaded by actions - DPK.HostApi.Controllers.DataStoreController.GetByIdAsync (DPK.HostApi),DPK.HostApi.Controllers.DataStoreController.GetAllAsync (DPK.HostApi). Actions require unique method/path combination for Swagger 2.0.
My Controller :
[Route("api/[controller]")]
[ApiController]
public class DataStoreController : ApiControllerBase
{
private readonly IDataStoreService _dataStoreService;
public DataStoreController(IDataStoreService dataStoreService)
{
_dataStoreService = dataStoreService;
}
[HttpPost]
public async Task<IActionResult> PostAsync([FromBody] DataStoreCommand dataStoreCommand)
{
try
{
if (ModelState.IsValid)
{
await _dataStoreService.PostAsync(dataStoreCommand);
return Ok();
}
var errorList = ModelState.Values.SelectMany(m => m.Errors).Select(e => e.ErrorMessage).ToList();
return ValidationProblem();
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
[HttpPut]
public async Task<IActionResult> PutAsync([FromBody] DataStoreCommand dataStoreCommand)
{
try
{
if (ModelState.IsValid)
{
await _dataStoreService.PutAsync(dataStoreCommand);
return Ok();
}
var errorList = ModelState.Values.SelectMany(m => m.Errors).Select(e => e.ErrorMessage).ToList();
return ValidationProblem();
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
[HttpDelete]
public async Task<IActionResult> DeleteAsync(int id)
{
try
{
if (ModelState.IsValid)
{
var item = await _dataStoreService.GetByIdAsync(id);
await _dataStoreService.DeleteAsync(item);
return Ok();
}
var errorList = ModelState.Values.SelectMany(m => m.Errors).Select(e => e.ErrorMessage).ToList();
return ValidationProblem();
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
[HttpGet]
public async Task<DataStoreQuery> GetByIdAsync(int id)
{
try
{
return await _dataStoreService.GetByIdAsync(id);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
[HttpGet]
public async Task<IEnumerable<DataStoreQuery>> GetAllAsync(string instanceName, string dbname, string userName, string userPass, bool isActive, DateTime? startCreatedDate, DateTime? endCreatedDate, DateTime? startModifiedDate, DateTime? endModifiedDate)
{
object[] parameters = { instanceName, dbname, userName, userPass, isActive, startCreatedDate, endCreatedDate, startModifiedDate, endModifiedDate};
var parameterName = "#instanceName , #dbname , #userName , #userPass , #isActive , #startCreatedDate , #endCreatedDate , #startModifiedDate , #endModifiedDate";
try
{
return await _dataStoreService.ExecWithStoreProcedure(parameterName, parameters);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
}
My Startup :
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().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info
{
Version = "v1",
Title = " ",
Description = " ",
TermsOfService = "None",
Contact = new Contact() { Name = " ", Email = " ", Url = " " }
});
});
}
// 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();
}
app.UseMvc();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
}
}
You can resolve it as follows:
services.AddSwaggerGen (c =>
{
other configs;
c.ResolveConflictingActions (apiDescriptions => apiDescriptions.First ());
});
//in the Startup.cs class in the ConfigureServices method
or you can put routes to differentiate your methods, for example:
[HttpGet("~/getsomething")]
[HttpGet("~/getothersomething")]
I changed the controller route to following:
[Route("api/[controller]/[action]")]
or you can also define explicit route for action as well:
[Route("GetById")]
you need to map id into HttpGet.
[HttpGet("{id}")]
public async Task<DataStoreQuery> GetByIdAsync(int id)
{
try
{
return await _dataStoreService.GetByIdAsync(id);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
when you specify HttpGet by not providing template, Swashbuckle tries to use default map for both of them. hence conflict occurs.
You can also merge methods with same endpoints to one with optional parameters. Example of implementation tested in net core 5 project:
services.AddSwaggerGen(c =>
{
c.ResolveConflictingActions(apiDescriptions =>
{
var descriptions = apiDescriptions as ApiDescription[] ?? apiDescriptions.ToArray();
var first = descriptions.First(); // build relative to the 1st method
var parameters = descriptions.SelectMany(d => d.ParameterDescriptions).ToList();
first.ParameterDescriptions.Clear();
// add parameters and make them optional
foreach (var parameter in parameters)
if (first.ParameterDescriptions.All(x => x.Name != parameter.Name))
{
first.ParameterDescriptions.Add(new ApiParameterDescription
{
ModelMetadata = parameter.ModelMetadata,
Name = parameter.Name,
ParameterDescriptor = parameter.ParameterDescriptor,
Source = parameter.Source,
IsRequired = false,
DefaultValue = null
});
}
return first;
});
});
If the method name are same then change the request method with parameter.
I changed the request method to following :
[HttpGet]
public string products()
{
// add other code
// ex. (return "products()";)
}
[HttpGet("{id}")]
public string products(int id)
{
// add other code
// ex. (return "products(int id)";)
}
This is how I specified the unique routes for method name
[HttpGet("~/GetWeatherForecast")]
Keeping it above the methods
[HttpGet("~/GetWeatherForecast")]
public int Get()
{
return Random.Next(5)
}
[HttpPost("~/InsertAddition")]
public int InsertAddition(int num1, int num2)
{
return num1 + num2;
}
Try adding both Route and HttpGet.
[HttpGet]
[Route(~/GetByIdAsync/{id})]
public async Task<DataStoreQuery> GetByIdAsync(int id)
[HttpGet]
[Route(~/GetAllAsync)]
public async Task<IEnumerable<DataStoreQuery>> GetAllAsync(string instanceName, string dbname, string userName, string userPass, bool isActive, DateTime? startCreatedDate, DateTime? endCreatedDate, DateTime? startModifiedDate, DateTime? endModifiedDate)

RESTful api versioning and grouping in doc from Swagger with multiple endpoints

I am trying to implement the version options on a MVC dotnet Core app that has API endpoint on it.
The set up i am after is like this
--AiM api
|_v1
|_v2
--RMS api
|_v1
I have it mostly working but the items on v1 are not showing up on v2. The output is like so
But when we get to the version 2 on the AiM v2 endpoint I only the one item
Which is not what i was expecting
I have made a test to get each one showing on its different pages in swagger like this
In controller
[ApiVersion("2.0")]
[ApiVersion("1.0")]
[ApiExplorerSettings(GroupName = "aim_v1")]
[Route("aim/v{version:apiVersion}/write/")]
public class aimWriter_v1Controller : Controller
{
[SwaggerOperation(Tags = new[] { "AiM Departments" })]
[HttpPost("departments/delete/{id}")]
public IActionResult departments(string foo)
{
return Json(new
{
results = "edited"
});
}
[SwaggerOperation(Tags = new[] { "AiM Contacts" })]
[HttpPost("contacts/delete/{id}")]
public IActionResult contact_delete(string foo)
{
return Json(new
{
results = "edited"
});
}
[SwaggerOperation(Tags = new[] { "AiM Contacts" })]
[HttpPost("contacts/activate/{id}")]
public IActionResult contact_activate(string foo)
{
return Json(new
{
results = "edited"
});
}
}
[ApiVersion("2.0")]
[ApiExplorerSettings(GroupName = "aim_v2")]
[Route("aim/v{version:apiVersion}/write/")]
public class aimWriter_v2Controller : Controller
{
[SwaggerOperation(Tags = new[] { "AiM Contacts" })]
[HttpPost("contacts/delete/{id}")]
public IActionResult contact_delete(string foo)
{
return Json(new
{
results = "edited"
});
}
}
[ApiVersion("2.0")]
[ApiVersion("1.0")]
[ApiExplorerSettings(GroupName = "aim_v1")]
[Route("aim/v{version:apiVersion}/")]
public class aim_v1Controller : Controller
{
[SwaggerOperation(Tags = new[] { "AiM Rooms" })]
[HttpPost("rooms")]
public IActionResult rooms(string foo)
{
return Json(new
{
results = "foo"
});
}
[SwaggerOperation(Tags = new[] { "AiM Buildings" })]
[HttpPost("buildings/rooms/{id}")]
public IActionResult building_rooms(string foo)
{
return Json(new
{
results = "foo"
});
}
[SwaggerOperation(Tags = new[] { "AiM Rooms" })]
[HttpPost("rooms/{id}")]
public IActionResult room(string foo)
{
return Json(new
{
results = "foo"
});
}
}
// set up as just a new endpoint (NOTE: in different controller)
[ApiVersion("1.0")]
[ApiExplorerSettings(GroupName = "rms_v1")]
[Route("rms/v{version:apiVersion}/")]
public class rms_v1Controller : Controller
{
[SwaggerOperation(Tags = new[] { "RMS Orders" })]
[HttpPost("set_order/{id}")]
public IActionResult set_order(string foo)
{
return Json(new
{
results = "foo"
});
}
}
And in the Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting(options => options.LowercaseUrls = true);
services.AddMvc();
services.AddApiVersioning(options => {
options.AssumeDefaultVersionWhenUnspecified = true ;
options.DefaultApiVersion = new ApiVersion(new DateTime(2016, 7, 1));
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("aim_v1", new Info
{
Version = "aim/v1",
Title = "WSU HTTP API"
});
c.SwaggerDoc("aim_v2", new Info
{
Version = "aim/v2",
Title = "WSU HTTP API v2"
});
c.SwaggerDoc("rms_v1", new Info
{
Version = "rms/v1",
Title = "WSU HTTP API"
});
//Set the comments path for the swagger json and ui.
var basePath = PlatformServices.Default.Application.ApplicationBasePath;
var xmlPath = Path.Combine(basePath, "project.in.bin.def.xml");
c.IncludeXmlComments(xmlPath);
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger(o =>
{
o.PreSerializeFilters.Add((swaggerDoc, httpReq) => swaggerDoc.Host = httpReq.Host.Value);
o.RouteTemplate = "doc/{documentName}/scheme.json";
});
// Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.RoutePrefix = "docs";
c.SwaggerEndpoint("/doc/aim_v1/scheme.json", "AiM v1.0.0");
c.SwaggerEndpoint("/doc/rms_v1/scheme.json", "Rms v1.0.0");
c.SwaggerEndpoint("/doc/aim_v2/scheme.json", "AiM v2.0.0");
});
}
And in the index.html for the swagger ui doc template file has
<script type="text/javascript">
window.JSConfig = JSON.parse('{"SwaggerEndpoints":[{"Url":"/doc/aim_v1/scheme.json","Description":"AiM v1.0.0"},{"Url":"/doc/aim_v2/scheme.json","Description":"AiM v2.0.0"},{"Url":"/doc/rms_v1/scheme.json","Description":"RMS v1.0.0"}],"BooleanValues":["false","true"],"DocExpansion":"list","SupportedSubmitMethods":["get","post","put","delete","patch"],"OnCompleteScripts":[],"OnFailureScripts":[],"ShowRequestHeaders":false,"JsonEditor":false,"OAuth2ClientId":"your-client-id","OAuth2ClientSecret":"your-client-secret-if-required","OAuth2Realm":"your-realms","OAuth2AppName":"your-app-name","OAuth2ScopeSeparator":" ","OAuth2AdditionalQueryStringParams":{}}');
$(function () {
hljs.configure({
highlightSizeThreshold: 5000
});
// Pre load translate...
if(window.SwaggerTranslator) {
window.SwaggerTranslator.translate();
}
window.swaggerUi = new SwaggerUi({
url: "/doc/aim_v1/scheme.json",
dom_id: "swagger-ui-container",
supportedSubmitMethods: ['get', 'post'],
onComplete: function(swaggerApi, swaggerUi){
if(typeof initOAuth == "function") {
initOAuth({
clientId: "ffff==",
clientSecret: "bbbb",
realm: "wsu-api",
appName: "wsu-api-broker",
scopeSeparator: " ",
additionalQueryStringParams: {}
});
}
if(window.SwaggerTranslator) {
window.SwaggerTranslator.translate();
}
_.each(JSConfig.OnCompleteScripts, function (script) {
$.getScript(script);
});
},
onFailure: function(data) {
log("Unable to Load SwaggerUI");
},
docExpansion: false,
jsonEditor: false,
defaultModelRendering: 'schema',
showRequestHeaders: false
});
window.swaggerUi.load();
function log() {
if ('console' in window) {
console.log.apply(console, arguments);
}
}
});
In order to get the items on the different endpoints I used the [ApiExplorerSettings(GroupName = "aim_v1")] on the classes and matched them up in the Startup.cs and index.html files. At this point I am unsure where to make my edit to get all of the [ApiVersion("1.0")] items show on the [ApiVersion("2.0")] as I think the ApiExplorerSettings GroupName is what it locking this up.
To integrate everything smoothly, you also need to add the official API Explorer package for API Versioning. This will collate all of the API version information for you in a way that Swagger will understand. The official Swagger/Swashbuckle integration wiki topic has additional details and examples.
The setup will look like:
public void ConfigureServices( IServiceCollection services )
{
// note: this option is only necessary when versioning by url segment.
// the SubstitutionFormat property can be used to control the format of the API version
services.AddMvcCore().AddVersionedApiExplorer(
options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
} );
services.AddMvc();
services.AddApiVersioning();
services.AddSwaggerGen(
options =>
{
var provider = services.BuildServiceProvider().GetRequiredService<IApiVersionDescriptionProvider>();
foreach ( var description in provider.ApiVersionDescriptions )
{
options.SwaggerDoc( description.GroupName, CreateInfoForApiVersion( description ) );
}
options.IncludeXmlComments( XmlCommentsFilePath );
} );
}
public void Configure( IApplicationBuilder app, IHostingEnvironment env, IApiVersionDescriptionProvider provider )
{
app.UseMvc();
app.UseSwagger();
app.UseSwaggerUI(
options =>
{
foreach ( var description in provider.ApiVersionDescriptions )
{
options.SwaggerEndpoint( $"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant() );
}
} );
}
static string XmlCommentsFilePath
{
get
{
var basePath = PlatformServices.Default.Application.ApplicationBasePath;
var fileName = typeof( Startup ).GetTypeInfo().Assembly.GetName().Name + ".xml";
return Path.Combine( basePath, fileName );
}
}
static Info CreateInfoForApiVersion( ApiVersionDescription description )
{
var info = new Info()
{
Title = $"Sample API {description.ApiVersion}",
Version = description.ApiVersion.ToString(),
Description = "A sample application with Swagger, Swashbuckle, and API versioning.",
Contact = new Contact() { Name = "Bill Mei", Email = "bill.mei#somewhere.com" },
TermsOfService = "Shareware",
License = new License() { Name = "MIT", Url = "https://opensource.org/licenses/MIT" }
};
if ( description.IsDeprecated )
{
info.Description += " This API version has been deprecated.";
}
return info;
}
A full working answer is in that question:
Grouping and Versioning not working well together in swagger in asp.net core 3.1 web api
As the author said, the DocInclusionPredicate in AddSwaggerGen in the ConfigureServices is doing the trick to map the proper controller to the wanted swagger file.

Categories