OData ASPNetCore CamelCase - c#

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;
}

Related

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);
}

simple ASP.NET Core routing gone wrong

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

401 (Unauthorized) request on Angular 7, .netcore application

Iam getting a 401 unauthorized error while authorizing an user to consume an api on .net core and angular 7 application.
My angular service has a function :-
getUserProfile() {
var tokenHeader = new HttpHeaders({'Authorization':'Bearer ' + localStorage.getItem('token')});
console.log(tokenHeader)
return this.http.get('/api/ApplicationUser/UserProfile', { headers: tokenHeader});
}
on tokenHeader I am sending the user jwt token.
My api is
[HttpGet]
[Authorize]
[Route("UserProfile")]
//'api/userProfile'
public async Task<Object> GetUserProfile()
{
string userId = User.Claims.First(c => c.Type == "UserID").Value;
var user = await _userManager.FindByIdAsync(userId);
return new { user.fullName, user.Email, user.UserName };
}
I have tried some answers from other questions but nothing helps.
Any helps appreciated.
Your code should be like this
const httpOptions = {
headers: new HttpHeaders({
'Authorization': `Bearer ${localStorage.getItem('token')}`
})
};
return this.http.get('/api/ApplicationUser/UserProfile', httpOptions);
Also make sure you have this line in your controller
[Authorize(AuthenticationSchemes = "Bearer")]
Can you share your request from network tab.
Also I recommend to use interceptors for make it global
#Injectable()
export class TokenInterceptor implements HttpInterceptor {
private isRefreshing = false;
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
constructor(public authService: AuthService) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (this.authService.getJwtToken()) {
request = this.addToken(request, this.authService.getJwtToken());
}
return next.handle(request).pipe(catchError(error => {
if (error instanceof HttpErrorResponse && error.status === 401) {
return this.handle401Error(request, next);
} else {
return throwError(error);
}
}));
}
private addToken(request: HttpRequest<any>, token: string) {
return request.clone({
setHeaders: {
'Authorization': `Bearer ${token}`
}
});
}
private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(null);
return this.authService.refreshToken().pipe(
switchMap((token: any) => {
this.isRefreshing = false;
this.refreshTokenSubject.next(token.jwt);
return next.handle(this.addToken(request, token.jwt));
}));
} else {
return this.refreshTokenSubject.pipe(
filter(token => token != null),
take(1),
switchMap(jwt => {
return next.handle(this.addToken(request, jwt));
}));
}
}
}
Full example https://github.com/bartosz-io/jwt-auth-angular/blob/master/src/app/auth/token.interceptor.ts

How to make a post and get works in a MVC project

I have an ASP.NET MVC app and I'm struggling with the connection between the typescript and the C#.
I can see that the C# is giving the response in the Inspect, the value is there but I don't know how to treat in Typescript.
C# Code:
namespace TEST.Controllers
{
[Route("api/[controller]")]
public class TestController : Controller
{
// GET api/GetTest
[HttpGet("GetTest")]
public IEnumerable<string> GetTest()
{
return new string[] { "Teste1", "Teste2" };
}
}
}
TypeScript SERVICE Code:
public getTest(): Observable<any> {
return this.dataService.get(this.baseUrl + '/GetTest')
.map((response: Response) => <any>response.json())
// .do(data => console.log("All: " + JSON.stringify(data)))
.catch(this.handleError);
}
Data Service Code (TypeScript):
public get<T>(url: string, params?: any): Observable<T> {
const options = new DataServiceOptions();
options.method = RequestMethod.Get;
options.url = url;
options.params = params;
return this.request(options);
}
private request(options: DataServiceOptions): Observable<any> {
options.method = (options.method || RequestMethod.Get);
options.url = (options.url || '');
options.headers = (options.headers || {});
options.params = (options.params || {});
options.data = (options.data || {});
this.interpolateUrl(options);
this.addXsrfToken(options);
this.addContentType(options);
this.addAuthToken(options);
// this.addCors(options);
const requestOptions = new RequestOptions();
requestOptions.method = options.method;
requestOptions.url = options.url;
requestOptions.headers = options.headers;
requestOptions.search = this.buildUrlSearchParams(options.params);
requestOptions.body = JSON.stringify(options.data);
this.pendingCommandsSubject.next(++this.pendingCommandCount);
const stream = this.http.request(options.url, requestOptions)
.catch((error: any) => {
this.handleErrors(error);
return Observable.throw(error);
})
.map(this.unwrapHttpValue)
.catch((error: any) => {
return Observable.throw(this.unwrapHttpError(error));
})
.finally(() => {
this.pendingCommandsSubject.next(--this.pendingCommandCount);
});
return stream;
}
The Calling:
private getDataBase() {
this.service.getTest().subscribe((res) => {
console.log(res);
this._proceduresImportData = res;
});
}
OBS: I also can console the observable, but I cannot treat it.
The best way to approach this is to have a generic request service and encapsulate your service calls, then inject that in where you need it. Taking get for an example (this can be expanded upon)
request.service.ts
import { Injectable } from "#angular/core";
import { Http, Response } from "#angular/http";
import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/map";
import { WindowRef } from "./window.service";
#Injectable()
export class RequestService {
private baseUrl: string;
constructor(private http: Http, private windowRef: WindowRef) {
this.baseUrl = this.getBaseUrl();
}
public get<T>(resource: string): Observable<T> {
return this.http.get(this.baseUrl + resource)
.map<Response, T>(this.extractData);
}
private extractData(response: Response) {
return response.json();
}
private getBaseUrl(): string {
if (this.windowRef.getNativeWindow().location.hostname === "localhost") {
return "http://localhostAddress/api/";
} else if (this.windowRef.getNativeWindow().location.hostname === "anotherEnviroment") {
return "https://anotherAddress/api/";
}
}
}
window.service.ts
import { Injectable } from "#angular/core";
#Injectable()
export class WindowRef {
public getNativeWindow(): any {
return window;
}
}
This then returns an observable of the object you are expecting, used with a resolver or onInit it can be subscribed to where needed.
get-stuff.service.ts
import { Injectable } from "#angular/core";
import { Observable } from "rxjs/Observable";
import { RequestService } from "../common/request.service";
#Injectable()
export class Service {
constructor(private requestService: RequestService) { }
public getTestService(): void {
let requestedStuff: Observable<string[]> = this.requestService.get<string[]>(`GetTest`);
requestedStuff.subscribe(stuff: string[]) => {
//do stuff with your string
}
}
}
Then subscribe and use your data
Hope that helps

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