I am creating a WebApi server with integrated SignalR Hubs. For simplicity's sake I am using a Controller which is operating on a List.
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
public static List<string> Source { get; set; } = new List<string>();
public static int counter = 0;
private IHubContext<ValuesHub, IValuesClient> hubContext;
public ValuesController(IHubContext<ValuesHub, IValuesClient> hub)
{
Source.Add("bla" + counter);
counter++;
Source.Add("bla" + counter);
counter++;
this.hubContext = hub;
}
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return Source;
}
// GET api/values/x
[HttpGet("{id}")]
public ActionResult<string> Get(int id)
{
return Source[id];
}
// POST api/values
[HttpPost]
public void Post([FromBody] string value)
{
Source.Add(value);
}
// PUT api/values/x
[HttpPut("{id}")]
public void Put(int id, [FromBody] string value)
{
Source[id] = value;
}
// DELETE api/values/x
[HttpDelete("{id}")]
public void Delete(int id)
{
var item = Source[id];
Source.Remove(item);
Console.WriteLine("Outgoing message!");
hubContext.Clients.All.ReceiveMessage("Message incoming", "Blaaaaa");
}
}
}
My Hub doesn't do anything special yet:
public interface IValuesClient
{
Task ReceiveMessage(string value, string message);
Task ReceiveMessage(string message);
}
public class ValuesHub : Hub<IValuesClient>
{
// private static ValuesController ctrl = Glo
public override async Task OnConnectedAsync()
{
await Groups.AddToGroupAsync(Context.ConnectionId, "Users");
Console.WriteLine("Client connected - Client-Id: {0}", Context.ConnectionId);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, "Users");
Console.WriteLine("Client disconnected - Client-Id: {0}", Context.ConnectionId);
Console.WriteLine("Disconnection due to: {0}", exception);
await base.OnDisconnectedAsync(exception);
}
public async Task MessageToAll(string user, string message)
{
Console.WriteLine("SendMessage - User: {0} - Message: {1}", user, message);
await Clients.All.ReceiveMessage(user, message);
}
public async Task MessageToCaller(string message)
{
Console.WriteLine("SendMessageToCaller: {0}", message);
await Clients.Caller.ReceiveMessage(message);
}
}
}
Also for simplicity's sake I will not go into detail why I want to achieve this, but I want the server to wait for a certain amount of time and then delete the according value, after a disconnection is detected. Let's say I want to simply delete the first element in my Source list.
How would I access the according Controller-functions from inside my OnDisconnectedAsync function?
One idea I came up with is to create a HttpClient inside my Hub and let the Hub act as a client here by calling e. g. DELETE: http://localhost:5000/api/values/0. I have to admit this sounds like a rather horrible approach, though.
So If I understand your problem is that you are having is that you want to access the methods on the controller from your hubs?
If this is the case - It seems to me that you have a fundamental design flaw. I would create a service that handles all the things your controller is doing, and then inject this service directly into the hub. Then you can use that service directly in the hub on the overrides and operate on your list . If this is unclear I can Provide an example.
public class ValuesHub : Hub<IValuesClient>
{
IListService _listService;
public ValuesHub (IListService listService)
{
_listService = listService;
}
public override async Task OnConnectedAsync()
{
await Groups.AddToGroupAsync(Context.ConnectionId, "Users");
Console.WriteLine("Client connected - Client-Id: {0}", Context.ConnectionId);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, "Users");
Console.WriteLine("Client disconnected - Client-Id: {0}", Context.ConnectionId);
Console.WriteLine("Disconnection due to: {0}", exception);
//Call your methods here.
_listService.RemoveFirstElement();
await base.OnDisconnectedAsync(exception);
}
public async Task MessageToAll(string user, string message)
{
Console.WriteLine("SendMessage - User: {0} - Message: {1}", user, message);
await Clients.All.ReceiveMessage(user, message);
}
public async Task MessageToCaller(string message)
{
Console.WriteLine("SendMessageToCaller: {0}", message);
await Clients.Caller.ReceiveMessage(message);
}
}
}
Thats your hub - See service example below
public class ListService : IListService
{
public void RemoveFirstElement()
{
//Delete Your Element here
}
}
public interface IListService
{
void RemoveFirstElement();
}
And then your startup.cs
services.AddSingleton<IListService,ListService>();
Related
I'm using signalr with .Net Core 3.0 and Angular 8
public class ConnectionHub : Hub
{
public ConnectionHub()
{
}
public override async Task OnConnectedAsync()
{
//get userName, token and other stuff
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await base.OnDisconnectedAsync(exception);
}
}
Added in startup.cs:
app.UseSignalR(options =>
{
options.MapHub<ConnectionHub>("/ConnectionHub");
});
In angular
//configure
private connection: any = new signalR.HubConnectionBuilder().withUrl("http://localhost:6002/ConnectionHub?userName='abc'")
.configureLogging(signalR.LogLevel.Information)
.build();
//start
this.connection.start()
I want to access my custom data sent in query params, but in OnConnectedAsync, the context is following:
How can I access query params in OnConnectedAsync() method.
Documentation says I can use Context.Request, but in OnConnectedAsync it says 'HubCallerContext' does not contain a definition for 'Request'
I found the solution
Inherit ConnectionHub with Hub<IHubClients>
get request object by using Context.GetHttpContext().Request
so the ConnectionHub class will look like this:
public class ConnectionHub: Hub<IHubClients>
{
public ConnectionHub()
{
}
public override async Task OnConnectedAsync()
{
//get userName, token and other stuff
var userName = Context.GetHttpContext().Request.Query["userName"];
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await base.OnDisconnectedAsync(exception);
}
}
I had an app in .NET Framework in which I implemented OAuthAuthorizationServer. Now I want to upgrade my app to .NET Core 2.1, so I did some R&D and decided to use ASOS. Now the issue is I have implemented ASOS and it is working fine but I have some chunks that I can't figure out how to convert.
private Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var identity = new ClaimsIdentity(new GenericIdentity(context.UserName, OAuthDefaults.AuthenticationType),
context.Scope.Select(x => new Claim("claim", x)));
context.Validated(identity);
return Task.FromResult(0);
}
private Task GrantClientCredetails(OAuthGrantClientCredentialsContext context)
{
var identity = new ClaimsIdentity(new GenericIdentity(context.ClientId, OAuthDefaults.AuthenticationType),
context.Scope.Select(x => new Claim("claim", x)));
context.Validated(identity);
return Task.FromResult(0);
}
private readonly ConcurrentDictionary<string, string> _authenticationCodes =
new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
private void CreateAuthenticationCode(AuthenticationTokenCreateContext context)
{
context.SetToken(Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
_authenticationCodes[context.Token] = context.SerializeTicket();
}
private void ReceiveAuthenticationCode(AuthenticationTokenReceiveContext context)
{
string value;
if (_authenticationCodes.TryRemove(context.Token, out value))
{
context.DeserializeTicket(value);
}
}
private void CreateRefreshToken(AuthenticationTokenCreateContext context)
{
context.SetToken(context.SerializeTicket());
}
private void ReceiveRefreshToken(AuthenticationTokenReceiveContext context)
{
context.DeserializeTicket(context.Token);
}
Now I have couple of question:
Client Credentials and Resource owner password grant types are two different grant types so how can we differentiate in them using ASOS?
GrantResourceOwnerCredentials takes OAuthGrantResourceOwnerCredentialsContext as a param and GrantClientCredentials takes OAuthGrantClientCredentialsContext as a param. Both these contexts contains scope which is not available in ASOS.
How can I serialize and deserialize access and refresh tokens like I was doing OAuthAuthorizationProvider?
How do we handle refresh tokens in ASOS? I can see refresh tokens in response but I haven't write any logic for refresh token my self.
Client Credentials and Resource owner password grant types are two different grant types so how can we differentiate in them using ASOS?
public override async Task HandleTokenRequest(HandleTokenRequestContext context)
{
if (context.Request.IsClientCredentialsGrantType())
{
// ...
}
else if (context.Request.IsPasswordGrantType())
{
// ...
}
else
{
throw new NotSupportedException();
}
}
GrantResourceOwnerCredentials takes OAuthGrantResourceOwnerCredentialsContext as a param and GrantClientCredetails takes OAuthGrantClientCredentialsContext as a param. Both these contexts contains scope which is not available in ASOS
public override async Task HandleTokenRequest(HandleTokenRequestContext context)
{
var scopes = context.Request.GetScopes();
// ...
}
How can I serialize and deserialize access and refresh tokens like I was doing OAuthAUthorizationProvider?
By using the OnSerializeAccessToken/OnDeserializeAccessToken and OnSerializeRefreshToken/OnDeserializeRefreshToken events.
How do we handle refresh tokens in ASOS? I can see refresh tokens in response but I haven't write any logic for refresh token my self.
Unlike Katana's OAuth server middleware, ASOS provides default logic for generating authorization codes and refresh tokens. If you want to use implement things like token revocation, you can do that in the events I mentioned. Read AspNet.Security.OpenIdConnect.Server. Refresh tokens for more information.
Here's an example that returns GUID refresh tokens and stores the associated (encrypted) payload in a database:
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace AuthorizationServer
{
public class MyToken
{
public string Id { get; set; }
public string Payload { get; set; }
}
public class MyDbContext : DbContext
{
public MyDbContext(DbContextOptions<MyDbContext> options)
: base(options) { }
public DbSet<MyToken> Tokens { get; set; }
}
public class MyProvider : OpenIdConnectServerProvider
{
private readonly MyDbContext _database;
public MyProvider(MyDbContext database)
{
_database = database;
}
public override Task ValidateTokenRequest(ValidateTokenRequestContext context)
{
if (!context.Request.IsPasswordGrantType() && !context.Request.IsRefreshTokenGrantType())
{
context.Reject(error: OpenIdConnectConstants.Errors.UnsupportedGrantType);
}
else
{
// Don't enforce client authentication.
context.Skip();
}
return Task.CompletedTask;
}
public override async Task HandleTokenRequest(HandleTokenRequestContext context)
{
if (context.Request.IsPasswordGrantType())
{
if (context.Request.Username == "bob" && context.Request.Password == "bob")
{
var identity = new ClaimsIdentity(context.Scheme.Name);
identity.AddClaim(new Claim(OpenIdConnectConstants.Claims.Subject, "Bob"));
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), identity.AuthenticationType);
ticket.SetScopes(OpenIdConnectConstants.Scopes.OfflineAccess);
context.Validate(ticket);
}
else
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The username/password couple is invalid.");
}
}
else
{
var token = await _database.Tokens.FindAsync(context.Request.RefreshToken);
_database.Tokens.Remove(token);
await _database.SaveChangesAsync();
context.Validate(context.Ticket);
}
}
public override async Task SerializeRefreshToken(SerializeRefreshTokenContext context)
{
context.RefreshToken = Guid.NewGuid().ToString();
_database.Tokens.Add(new MyToken
{
Id = context.RefreshToken,
Payload = context.Options.RefreshTokenFormat.Protect(context.Ticket)
});
await _database.SaveChangesAsync();
}
public override async Task DeserializeRefreshToken(DeserializeRefreshTokenContext context)
{
context.HandleDeserialization();
var token = await _database.Tokens.FindAsync(context.RefreshToken);
if (token == null)
{
return;
}
context.Ticket = context.Options.RefreshTokenFormat.Unprotect(token.Payload);
}
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<MyDbContext>(options =>
{
options.UseInMemoryDatabase(nameof(MyDbContext));
});
services.AddAuthentication()
.AddOpenIdConnectServer(options =>
{
options.TokenEndpointPath = "/token";
options.ProviderType = typeof(MyProvider);
options.AllowInsecureHttp = true;
})
.AddOAuthValidation();
services.AddMvc();
services.AddScoped<MyProvider>();
}
public void Configure(IApplicationBuilder app)
{
app.UseAuthentication();
app.UseMvcWithDefaultRoute();
}
}
}
I've been trying to fetch exception text from backend (ASP.NET Core) in Angular app.
I've seen examples where the controller action's return type is JsonResult or ActionResult.
In this case we can do the following:
[HttpGet]
public ActionResult GetSomething()
{
...
}
catch (Exception ex)
{
return Json(new { error = $"{ex.GetType().FullName}: '{ex.Message}'" }, JsonRequestBehavior.AllowGet);
}
}
All controller actions I have return DTOs, e.g.
public async Task<List<OrderDto>> GetMany(long clientId)
{
....
Since I'm returning DTO - I can't seem to return Json, so the approach above doesn't work.
I wonder if there's a way to pass exception description other than via Json(...).
Does anyone have an idea of how to handle this?
You can create a middleware:
public class ExceptionHandleMiddleware
{
private readonly RequestDelegate next;
public ExceptionHandleMiddleware(RequestDelegate next)
{
this.next = next ?? throw new ArgumentNullException(nameof(next));
}
public async Task Invoke(HttpContext context)
{
try
{
await next(context);
}
catch (Exception ex)
{
context.Response.Clear();
context.Response.ContentType = #"application/json";
await context.Response.WriteAsync(JsonConvert.SerializeObject(new { error = $"{ex.GetType().FullName}: '{ex.Message}'"}));
}
}
}
And then add it to application Builder in Configure method:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
....
app.UseMiddleware<ExceptionHandleMiddleware>();
}
Pay attention that with this middleware you shouldn't catch your exception on Controller level.
Below is my SignalR Hub class code.
public class ChatHub : Hub
{
public void Send(string name, string message)
{
// Call the addNewMessageToPage method to update clients.
Clients.All.addNewMessageToPage(name, message);
}
public async void webAPIRequest()
{
HttpClient client = new HttpClient();
HttpResponseMessage response = await client.GetAsync("https://jsonplaceholder.typicode.com/posts");
//Clients.All.addWebAPIResponseToPage(response);
Clients.Caller.addWebAPIResponseToPage(response);
await Task.Delay(1000);
response = await client.GetAsync("http://www.google.com");
Clients.Caller.addWebAPIResponseToPage(response);
//Clients.All.addWebAPIResponseToPage(response);
await Task.Delay(1000);
response = await client.GetAsync("https://jsonplaceholder.typicode.com/posts?userId=1");
//Clients.All.addWebAPIResponseToPage(response);
Clients.Caller.addWebAPIResponseToPage(response);
}
}
As per my understanding ,
Clients.Caller.addWebAPIResponseToPage(response);
sends message only to caller client , whereas
Clients.All.addWebAPIResponseToPage(response);
sends the message to all the clients.
Is my understanding correct ?
If No , then what method needs to be called to send message only to caller client.
Yes your understanding is correct. Read it here
https://learn.microsoft.com/en-us/aspnet/signalr/overview/guide-to-the-api/hubs-api-guide-server#selectingclients
You can use caller, you can provide current user connection id and send message to that or I have seen a group called self in some places which keeps user logged in from various devices and send message to that.
For example if you are logged in on a desktop and on mobile as well then you will have two connection IDs but you are same user. You can add this user to a self_username_unique_group_name kind of group and then send a message to that group which will be sent to all devices where user is connected.
You can also manage connection IDs for a single user in a separate table and send message to all of those connection IDs if you want.
Too much flexibility and magic
Enjoy
I found this to work quite well where ConnectionMapping is described in https://learn.microsoft.com/en-us/aspnet/signalr/overview/guide-to-the-api/mapping-users-to-connections
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<SomeService>();
services.AddScoped<SessionService>();
services.AddScoped<ProgressHub>();
}
}
public class SomeService
{
ProgressHub _hub;
public SomeService(ProgressHub hub)
{
_hub = hub;
}
private async Task UpdateProgressT(T value)
{
_hub.Send(value);
}
}
public class ProgressHub : Hub
{
private readonly static ConnectionMapping<string> _connections = new ConnectionMapping<string>();
private readonly IHubContext<ProgressHub> _context;
private readonly SessionService _session;
public ProgressHub(IHubContext<ProgressHub> context, SessionService session)
{
_context = context;
_session = session;
}
public override Task OnConnectedAsync()
{
_connections.Add(_session.SiteId, Context.ConnectionId);
return base.OnConnectedAsync();
}
public override Task OnDisconnectedAsync(Exception exception)
{
_connections.Remove(_session.SiteId, Context.ConnectionId);
return base.OnDisconnectedAsync(exception);
}
public async Task Send(object data)
{
foreach (var connectionId in _connections.GetConnections(_session.SiteId))
{
await _context.Clients.Client(connectionId).SendAsync("Message", data);
}
}
}
public class SessionService
{
private readonly ISession _session;
public SessionService(IHttpContextAccessor accessor)
{
_session = accessor.HttpContext.Session;
if (_session == null) throw new ArgumentNullException("session");
}
public string SiteId
{
get => _session.GetString("SiteId");
set => _session.SetString("SiteId", value);
}
}
I use the code below to throttle my ASP.NET Web Api:
public class Throttle : ActionFilterAttribute
{
public override async Task OnActionExecutingAsync(HttpActionContext context, CancellationToken cancellationToken)
{
// ...
if (throttle)
{
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Conflict));
}
}
}
However, I cannot return error code 429, because it's not in HttpStatusCode enum. Is there a way to return a custom error code?
I found this over here.
var response = new HttpResponseMessage
{
StatusCode = (HttpStatusCode)429,
ReasonPhrase = "Too Many Requests",
Content = new StringContent(string.Format(CultureInfo.InvariantCulture, "Rate limit reached. Reset in {0} seconds.", data.ResetSeconds))
};
response.Headers.Add("Retry-After", data.ResetSeconds.ToString(CultureInfo.InvariantCulture));
actionContext.Response = response;
Hope this helps
This is what I did based on another response on StackOverflow.
Create Class (in controller file worked for me)
public class TooManyRequests : IHttpActionResult
{
public TooManyRequests()
{
}
public TooManyRequests(string message)
{
Message = message;
}
public string Message { get; private set; }
public HttpResponseMessage Execute()
{
HttpResponseMessage response = new HttpResponseMessage((HttpStatusCode)429);
if (!string.IsNullOrEmpty(Message))
{
response.Content = new StringContent(Message); // Put the message in the response body (text/plain content).
}
return response;
}
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
return Task.FromResult(Execute());
}
}
Use in controller
public IHttpActionResult Get()
{
// with message
return new TooManyRequests("Limited to 5 request per day. Come back tomorrow.");
// without message
// return new TooManyRequests();
}