SignalR Serverless Report Progress - signalr

I am trying to build a test app so that my HTML page can send a message to an long running method on an Azure function via SignalR, and the long running function can use SignalR to report progress back to the HTML page.
Does anyone know how to do this? I am using .NET Core 3.1 for the function and the Azure SignalR service in serverless mode. I have looked around on the web but it all seems to be about chat whereas I would have thought this is quite a common requirement.
This is what I have so far...
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
namespace AzureSignalRFunctionTest
{
public static class Function1
{
// Input Binding
[FunctionName("negotiate")]
public static SignalRConnectionInfo Negotiate(
[HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequest req,
[SignalRConnectionInfo(HubName = "TestHub")] SignalRConnectionInfo connectionInfo)
{
return connectionInfo;
}
// Trigger Binding
[FunctionName("longrunningtask")]
public static void LongRunningTask([SignalRTrigger("longrunningtask", "messages", "SendMessage")] InvocationContext invocationContext, [SignalRParameter] string message, ILogger logger)
{
logger.LogInformation($"Receive {message} from {invocationContext.ConnectionId}.");
// what to put here?
//var clients = invocationContext.GetClientsAsync().Result;
//ReportProgress(invocationContext.ConnectionId, "Progress", ...);
// Simulate Long running task
System.Threading.Thread.Sleep(30000);
// ReportProgress etc
}
// Output Binding
[FunctionName("reportprogress")]
public static Task ReportProgress(string connectionId, string message,
[SignalR(HubName = "TestHub")] IAsyncCollector<SignalRMessage> signalRMessages)
{
return signalRMessages.AddAsync(
new SignalRMessage
{
ConnectionId = connectionId,
Target = "reportProgress",
Arguments = new[] { message }
});
}
}
}

The answer was to do this:
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace MyAzureFunction
{
public class TestHub : ServerlessHub
{
// Input Binding
[FunctionName("negotiate")]
public SignalRConnectionInfo Negotiate(
[HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequest req,
[SignalRConnectionInfo(HubName="testhub")]SignalRConnectionInfo connectionInfo, ILogger logger)
{
logger.LogInformation($"ConnectionInfo: {connectionInfo.Url} {connectionInfo.AccessToken} {req.Path} {req.Query}");
return connectionInfo;
}
// Trigger Binding
[FunctionName("longrunningtask")]
public async Task LongRunningTask(
[SignalRTrigger] InvocationContext invocationContext,
string message, ILogger logger)
{
logger.LogInformation($"Receive {message} from {invocationContext.ConnectionId}.");
await Clients.Client(invocationContext.ConnectionId).SendAsync("reportProgress", message + " has started.");
System.Threading.Thread.Sleep(10000);
await Clients.Client(invocationContext.ConnectionId).SendAsync("reportProgress", message + " has ended.");
}
}
}
The Html looked like this:
<h2>SignalR</h2>
<div id="messages"></div>
<form>
<button type="submit" id="submitbtn">Submit</button>
</form>
#section Scripts {
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/3.1.19/signalr.min.js"></script>
<script>
let messages = document.querySelector('#messages');
const apiBaseUrl = 'https://myazurefunction.azurewebsites.net';
const connection = new signalR.HubConnectionBuilder()
.withUrl(apiBaseUrl + '/api')
.configureLogging(signalR.LogLevel.Information)
.build();
connection.on('reportProgress', (message) => {
document.getElementById("messages").innerHTML = message;
});
function onConnected(connection) {
document.getElementById('submitbtn').addEventListener('click', function (event) {
connection.send('longrunningtask', 'this is my message');
event.preventDefault();
});
}
connection.start()
.then(function () {
onConnected(connection)
})
.catch(console.error);
</script>
}
If you move away from the calling page, and return while the function app is running, it does not show the end result. If this is a requirement rather than just a simple progress bar, then I think the answer is to authentication via a user id, and then send messages to that user id.

Related

Signlar client not executing connection.on method

I have a Signaler client in worker service. developed Signaler server in .net core web API.
Here everything is working but when I am calling.
await _hub.Clients.All.SendAsync("calldscclient", desUserList.FirstOrDefault().DscConnectionId);
From the server, it's not sending a message to the client
connection.On<string>("calldscclient", (message) => {}
I am writing my client code here.
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using WorkerService1.Properties;
namespace WorkerService1
{
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
public HubConnection connection;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
//while (!stoppingToken.IsCancellationRequested)
{
using (StreamWriter writer = new
StreamWriter(#"C:\Users\devma\source\repos\WorkerService1\WorkerService1\TextFile1.txt"))
{
writer.WriteLine("bigin");
}
var ip = GetLocalIPAddress();
ip = "localhost";
connection = new HubConnectionBuilder()
//.WithUrl(new Uri("https://localhost:5001/dschub"))
//.WithAutomaticReconnect(new RandomRetryPolicy())
//.Build();
.WithUrl("https://localhost:44339/dschub"
,
options =>
{
options.WebSocketConfiguration = conf =>
{
conf.RemoteCertificateValidationCallback = (message, cert, chain, errors) =>
{ return true; };
};
options.HttpMessageHandlerFactory = factory => new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
{ return true; }
};
}
)
.Build();
await connection.StartAsync();
await connection.InvokeAsync("dscRegister", ip);
connection.On<string>("calldscclient", (message) =>
{
using (StreamWriter writer = new
StreamWriter(#"C:\Users\devma\source\repos\WorkerService1\WorkerService1\TextFile1.txt"))
{
writer.WriteLine("Monica Rathbun");
}
});
}
}
public static string GetLocalIPAddress()
{
var host = Dns.GetHostEntry(Dns.GetHostName());
foreach (var ip in host.AddressList)
{
if (ip.AddressFamily == AddressFamily.InterNetwork)
{
return ip.ToString();
}
}
throw new Exception("No network adapters with an IPv4 address in the system!");
}
}
}
For calling
connection.On<string>("calldscclient", (message) => {}
I am using web API on the server.
My WebAPI code is here.
[HttpGet("servedsc")]
public async Task<ActionResult> ServeDsc()
{
var remoteIpAddress = HttpContext.Request.Host.Host;
var desUserList = await _dscService.GetDscUser(remoteIpAddress);
await _hub.Clients.All.SendAsync("calldscclient",
desUserList.FirstOrDefault().DscConnectionId);
//await
_hub.Clients.User(desUserList.FirstOrDefault().DscConnectionId).SendAsync("calldscclient");
return Ok(desUserList);
}
Here everything is working fine like Signaler Hub is executing property when the client making a connection.
But the problem is when the server is sending messages to the client it's not getting.
You should generally register your .On method handlers before starting the connection. In your case, you're awaiting InvokeAsync which will invoke the server method and wait for the server method to finish. Then you are registering your .On method which will be too late at that point.

Unauthorized when accessing Azure cognitive services from netcore3.1 console app

I have a netcore console app which is accessing the Azure's Text analysis API's using the Client library from the Microsoft.Azure.CognitiveServices.Language.TextAnalytics Nuget package.
When trying to access the API, I receive the following HttpException:
Unauthorized. Access token is missing, invalid, audience is incorrect (https://cognitiveservices.azure.com), or have expired.
Unhandled exception. System.AggregateException: One or more errors occurred. (Operation returned an invalid status code 'Unauthorized')
When accessing the same API using exactly the same code which is hosted on Azure Functions - everything works as expected. I was unable to find any info in the docs or anywhere else.
Try the .net core console app code below to use TextAnalytics SDK :
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.CognitiveServices.Language.TextAnalytics;
using Microsoft.Azure.CognitiveServices.Language.TextAnalytics.Models;
using Microsoft.Rest;
namespace TextAnalysis
{
class Program
{
private static readonly string key = "<your text analyisis service key>";
private static readonly string endpoint = "<your text analyisis service endpoint>";
static void Main(string[] args)
{
var client = authenticateClient();
sentimentAnalysisExample(client);
languageDetectionExample(client);
entityRecognitionExample(client);
keyPhraseExtractionExample(client);
Console.Write("Press any key to exit.");
Console.ReadKey();
}
static TextAnalyticsClient authenticateClient()
{
ApiKeyServiceClientCredentials credentials = new ApiKeyServiceClientCredentials(key);
TextAnalyticsClient client = new TextAnalyticsClient(credentials)
{
Endpoint = endpoint
};
return client;
}
class ApiKeyServiceClientCredentials : ServiceClientCredentials
{
private readonly string apiKey;
public ApiKeyServiceClientCredentials(string apiKey)
{
this.apiKey = apiKey;
}
public override Task ProcessHttpRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request == null)
{
throw new ArgumentNullException("request");
}
request.Headers.Add("Ocp-Apim-Subscription-Key", this.apiKey);
return base.ProcessHttpRequestAsync(request, cancellationToken);
}
}
static void sentimentAnalysisExample(ITextAnalyticsClient client)
{
var result = client.Sentiment("I had the best day of my life.", "en");
Console.WriteLine($"Sentiment Score: {result.Score:0.00}");
}
static void languageDetectionExample(ITextAnalyticsClient client)
{
var result = client.DetectLanguage("This is a document written in English.","us");
Console.WriteLine($"Language: {result.DetectedLanguages[0].Name}");
}
static void entityRecognitionExample(ITextAnalyticsClient client)
{
var result = client.Entities("Microsoft was founded by Bill Gates and Paul Allen on April 4, 1975, to develop and sell BASIC interpreters for the Altair 8800.");
Console.WriteLine("Entities:");
foreach (var entity in result.Entities)
{
Console.WriteLine($"\tName: {entity.Name},\tType: {entity.Type ?? "N/A"},\tSub-Type: {entity.SubType ?? "N/A"}");
foreach (var match in entity.Matches)
{
Console.WriteLine($"\t\tOffset: {match.Offset},\tLength: {match.Length},\tScore: {match.EntityTypeScore:F3}");
}
}
}
static void keyPhraseExtractionExample(TextAnalyticsClient client)
{
var result = client.KeyPhrases("My cat might need to see a veterinarian.");
// Printing key phrases
Console.WriteLine("Key phrases:");
foreach (string keyphrase in result.KeyPhrases)
{
Console.WriteLine($"\t{keyphrase}");
}
}
}
}
You can find your key and endpoint here on Azure portal :
Result :

Azure Function SignalR Negotiate function works but Send function fails

i have a xamarin app that is trying to talk to use SignalR in Azure functions.
i have 2 azure functions as per the documentation.
public static class NegotiateFunction
{
[FunctionName("negotiate")]
public static SignalRConnectionInfo GetSignalRInfo(
[HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest req,
[SignalRConnectionInfo(HubName = "chat")] SignalRConnectionInfo connectionInfo)
//, UserId = "{headers.x-ms-client-principal-id}"
{
return connectionInfo;
}
}
and
public static class SendMessageFunction
{
[FunctionName("Send")]
public static Task SendMessage(
[HttpTrigger(AuthorizationLevel.Anonymous, "post")]object message,
[SignalR(HubName = "chat")]IAsyncCollector<SignalRMessage> signalRMessages)
{
// var chatObj = (ChatObject)(message);
return signalRMessages.AddAsync(
new SignalRMessage
{
// the message will only be sent to this user ID
// UserId = chatObj.ReciversId,
Target = "Send",
Arguments = new[] { message }
});
}
}
in my xamarin client i am connecting like this.
try
{
_connection = new HubConnectionBuilder()
.WithUrl("http://192.168.1.66:7071/api")
.Build();
_connection.On<string>("Send", (message) =>
{
AppendMessage(message);
});
await _connection.StartAsync();
}
I send message using this code in one of the pages of Xamarin app page.
try
{
await _connection.SendAsync("Send", MessageEntry.Text);
MessageEntry.Text = "";
}
connection code works it hits "negotiate" function properly but when i call SendAsync it does not hit break-point in [FunctionName("Send")] and nothing happens. It doesn't give me any exception as well.
local settings are like this
Update
i also tried Invoke. it didnt worked.
Should i try making a POST call to [FunctionName("Send")] ?
The way SignalR SaaS works in Functions is slightly different to using the NuGet package in a .NET Application.
You can't invoke a function using the SignalR library, as you can see on the attribute in your function, it's expecting a Http trigger so you have to do a POST to this endpoint instead of invoking it as you normally would.
[HttpTrigger(AuthorizationLevel.Anonymous, "post")]
You still want to listen to the Send target as normal.

ASP.NET Core - getting a message back from AuthenticationHandler

I have implemented a subclass of AuthenticationHandler. It returns AuthenticationResult.Fail("This is why you can't log in");
I would have expected this message to end up in the body, or at least in the HTTP status text, but instead I get a blank 401 response.
Is there any way to provide additional information for failed authentication attempts in ASP.NET core?
Override HandleChallengeAsync:
In the example below the failReason is a private field in my implementation of AuthenticationHandler.
I don't know if this is the best way to pass the reason for failure. But the AuthenticationProperties on the AuthenticateResult.Fail method did not make it through to HandleChallengeAsync in my test.
public class CustomAuthenticationHandler<TOptions> : AuthenticationHandler<TOptions> where TOptions : AuthenticationSchemeOptions, new()
{
private string failReason;
public CustomAuthenticationHandler(IOptionsMonitor<TOptions> options
, ILoggerFactory logger
, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
failReason = "Reason for auth fail";
return AuthenticateResult.Fail(failReason);
}
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.StatusCode = 401;
if (failReason != null)
{
Response.HttpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase = failReason;
}
return Task.CompletedTask;
}
}
From the docs: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1?view=aspnetcore-2.2
Override this method to deal with 401 challenge concerns, if an authentication scheme in question deals an authentication interaction as part of it's request flow. (like adding a response header, or changing the 401 result to 302 of a login page or external sign-in location.)
Source:
https://github.com/aspnet/Security/blob/master/src/Microsoft.AspNetCore.Authentication/AuthenticationHandler.cs#L201
I used this code in my custom Middleware to return problemDetails response.
public async Task Invoke(HttpContext httpContext)
{
await this.Next(httpContext);
if (httpContext.Response.StatusCode == StatusCodes.Status401Unauthorized)
{
var authenticateResult = await httpContext.AuthenticateAsync();
if (authenticateResult.Failure != null)
{
var routeData = httpContext.GetRouteData() ?? new RouteData();
var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor());
var problemDetails = this.ProblemDetailsFactory.CreateProblemDetails(httpContext,
statusCode: httpContext.Response.StatusCode,
detail: authenticateResult.Failure.Message);
var result = new ObjectResult(problemDetails)
{
ContentTypes = new MediaTypeCollection(),
StatusCode = problemDetails.Status,
DeclaredType = problemDetails.GetType()
};
await this.Executor.ExecuteAsync(actionContext, result);
}
}
}
For changing the body or Http status, you could try Context.Response.
Here is a demo code:
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace TestIdentity
{
public class CustomAuthenticationHandler<TOptions> : AuthenticationHandler<TOptions> where TOptions : AuthenticationSchemeOptions, new()
{
public CustomAuthenticationHandler(IOptionsMonitor<TOptions> options
, ILoggerFactory logger
, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
await Context.Response.WriteAsync("This is why you can't log in");
return AuthenticateResult.Fail("This is why you can't log in");
}
}
}

Can't call javascript client method in signalr

The following code works fine in IIS Express, but failed in IIS10.
The weird thing is serverside method can successfully be invoked, however clientside method can't.
JavaScript
var hub = $.connection.liveRoomHub;
hub.client.addMessageToPage = function(data){
debugger;//here, this method never gets invoked
console.log(JSON.stringify(data));
};
$.connection.hub.start()
.done(function() {
hub.server.join('room1')
.done(function(){
debugger; //code can run into here
hub.server.sendMessage('user','test','room1');
})
});
C#
public class LiveRoomHub : Microsoft.AspNet.SignalR.Hub
{
public ILogger Logger { get; set; }
public async Task SendMessage(string name, string message, string roomName)
{
await Clients.Group(roomName)
.addMessageToPage(new
{
Name = name,
Message = message
});
Logger.Info($"{name}send msg:{message}in room:{roomName},");//logged
}
public async Task Join(string roomName)
{
await Groups.Add(Context.ConnectionId, roomName);
Logger.Info($"{Context.ConnectionId} enter room: {roomName}");//logged
}
}
All right, problem solved.
I'm using aspnetboilerplate, and abp.signalr.js automatically calls the hub connection before my JavaScript code is loaded.
Obviously, at that time, my hub.client.addMessageToPage isn't registered yet.
That's the common Connection started before subscriptions are added error.

Resources