How to send Azure SignalR messages to clients on all instances of a multi-instance Azure Application - azure

We are evaluating how to send messages to connected clients via SignalR. Our application is published in Azure, and has multiple instances. We are able to successfully pass messages to clients connected to the same instance, but not other instances.
We initially were looking at ServiceBus, but we (perhaps mistakenly) found out that AzureSignalR should basically be a service bus that handles all of the backend stuff for us.
We set up signalR in Startup.cs such as:
public void ConfigureServices(IServiceCollection services)
{
var signalRConnString = Configuration.GetConnectionString("AxiomSignalRPrimaryEndPoint");
services.AddSignalR()
.AddAzureSignalR(signalRConnString)
.AddJsonProtocol(options =>
{
options.PayloadSerializerSettings.ContractResolver = new DefaultContractResolver();
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAzureSignalR(routes =>
{
routes.MapHub<CallRegistrationHub>("/callRegistrationHub");
routes.MapHub<CaseHeaderHub>("/caseHeaderHub");
routes.MapHub<EmployeesHub>("/employeesHub");
});
}
Issue
We have to store some objects that should probably be on the service bus, and not stored in an individual instance; However, I am unsure of how to tell the hub that the objects should be on the bus and not internal to that specific instance of the hub, as below:
public class EmployeesHub : Hub
{
private static volatile List<Tuple<string, string, string,string, int>> UpdateList = new List<Tuple<string, string, string,string,int>>();
private static volatile List<Tuple<string, int>> ConnectedClients = new List<Tuple<string, int>>();
}
We have functions that need to send messages to all connected clients that are looking at the current record regardless of in what instance they reside:
public async void LockField(string fieldName, string value, string userName, int IdRec)
{
var clients = ConnectedClients.Where(x => x.Item1 != Context.ConnectionId && x.Item2 == IdRec).Select(x => x.Item1).Distinct().ToList();
clients.ForEach(async x =>
{
await Clients.Client(x).SendAsync("LockField", fieldName, value, userName, true);
});
if (!UpdateList.Any(x=> x.Item1 == Context.ConnectionId && x.Item3 == fieldName && x.Item5 == IdRec))
{
UpdateList.Add(new Tuple<string, string, string,string,int>(Context.ConnectionId,userName, fieldName, value, IdRec));
}
}
This is not working for different instances (which makes sense, because each instance will have its own objects.. However, we were hoping that by using AzureSignalR instead of SignalR (AzureSignalR conn string has an endpoint to the Azure service) that it would handle the service bus functionality for us. We are not sure what steps to take to get this functioning correctly.
Thanks.

The reason for this issue is that I was preemptively attempting to limit message traffic. I was attempting to only send messages to clients that were looking at the same record. However, because my objects were instance-specific, it would only grab the connection IDs from the current instance's object.
Further testing (using ARR affinity) proves that on a Clients.All() call, all clients, including those in different instances, receive the message.
So, our AzureSignalR setup appears to be correct.
Current POC Solution - currently testing
-When a client registers, we will broadcast to all connected clients "What field do you have locked for this Id?"
-If client is on a different Id, it will ignore the message.
-If client does not have any fields locked, it will ignore the message.
-If client has a field locked, it will respond to the message with required info.
-AzureSignalR will then rebroadcast the data required to perform a lock.
This increases message count, but not significantly. But it will resolve the multiple instances holding different connected ClientIds issue.

Just a thought, but have you tried using SignalR Groups? https://learn.microsoft.com/en-us/aspnet/core/signalr/groups?view=aspnetcore-2.2#groups-in-signalr
You could try creating a group for each combination of IdRec and fieldName and then just broadcast messages to the group. This is the gist of how I think your LockField function might look:
public async void LockField(string fieldName, string value, string userName, int IdRec)
{
string groupName = GetGroupName(IdRec, fieldName);
await Clients.Group(groupName).SendAsync("LockField", fieldName, value, userName, true);
await this.Groups.AddToGroupAsync(Context.ConnectionId, groupName);
}
You could implement the GetGroupName method however you please, so long as it produces unique strings. A simple solution might be something like
public string GetGroupName(int IdRec, string fieldName)
{
return $"{IdRec} - {fieldName}";
}

Related

Messages not coming thru to Azure SignalR Service

I'm implementing Azure SignalR service in my ASP.NET Core 2.2 app with React front-end. When I send a message, I'm NOT getting any errors but my messages are not reaching the Azure SignalR service.
To be specific, this is a private chat application so when a message reaches the hub, I only need to send it to participants in that particular chat and NOT to all connections.
When I send a message, it hits my hub but I see no indication that the message is making it to the Azure Service.
For security, I use Auth0 JWT Token authentication. In my hub, I correctly see the authorized user claims so I don't think there's any issues with security. As I mentioned, the fact that I'm able to hit the hub tells me that the frontend and security are working fine.
In the Azure portal however, I see no indication of any messages but if I'm reading the data correctly, I do see 2 client connections which is correct in my tests i.e. two open browsers I'm using for testing. Here's a screen shot:
Here's my Startup.cs code:
public void ConfigureServices(IServiceCollection services)
{
// Omitted for brevity
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(jwtOptions => {
jwtOptions.Authority = authority;
jwtOptions.Audience = audience;
jwtOptions.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// Check to see if the message is coming into chat
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/im")))
{
context.Token = accessToken;
}
return System.Threading.Tasks.Task.CompletedTask;
}
};
});
// Add SignalR
services.AddSignalR(hubOptions => {
hubOptions.KeepAliveInterval = TimeSpan.FromSeconds(10);
}).AddAzureSignalR(Configuration["AzureSignalR:ConnectionString"]);
}
And here's the Configure() method:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Omitted for brevity
app.UseSignalRQueryStringAuth();
app.UseAzureSignalR(routes =>
{
routes.MapHub<Hubs.IngridMessaging>("/im");
});
}
Here's the method I use to map a user's connectionId to the userName:
public override async Task OnConnectedAsync()
{
// Get connectionId
var connectionId = Context.ConnectionId;
// Get current userId
var userId = Utils.GetUserId(Context.User);
// Add connection
var connections = await _myServices.AddHubConnection(userId, connectionId);
await Groups.AddToGroupAsync(connectionId, "Online Users");
await base.OnConnectedAsync();
}
Here's one of my hub methods. Please note that I'm aware a user may have multiple connections simultaneously. I just simplified the code here to make it easier to digest. My actual code accounts for users having multiple connections:
[Authorize]
public async Task CreateConversation(Conversation conversation)
{
// Get sender
var user = Context.User;
var connectionId = Context.ConnectionId;
// Send message to all participants of this chat
foreach(var person in conversation.Participants)
{
var userConnectionId = Utils.GetUserConnectionId(user.Id);
await Clients.User(userConnectionId.ToString()).SendAsync("new_conversation", conversation.Message);
}
}
Any idea what I'm doing wrong that prevents messages from reaching the Azure SignalR service?
It might be caused by misspelled method, incorrect method signature, incorrect hub name, duplicate method name on the client, or missing JSON parser on the client, as it might fail silently on the server.
Taken from Calling methods between the client and server silently fails
:
Misspelled method, incorrect method signature, or incorrect hub name
If the name or signature of a called method does not exactly match an appropriate method on the client, the call will fail. Verify that the method name called by the server matches the name of the method on the client. Also, SignalR creates the hub proxy using camel-cased methods, as is appropriate in JavaScript, so a method called SendMessage on the server would be called sendMessage in the client proxy. If you use the HubName attribute in your server-side code, verify that the name used matches the name used to create the hub on the client. If you do not use the HubName attribute, verify that the name of the hub in a JavaScript client is camel-cased, such as chatHub instead of ChatHub.
Duplicate method name on client
Verify that you do not have a duplicate method on the client that differs only by case. If your client application has a method called sendMessage, verify that there isn't also a method called SendMessage as well.
Missing JSON parser on the client
SignalR requires a JSON parser to be present to serialize calls between the server and the client. If your client doesn't have a built-in JSON parser (such as Internet Explorer 7), you'll need to include one in your application.
Update
In response to your comments, I would suggest you try one of the Azure SignalR samples, such as
Get Started with SignalR: a Chat Room Example to see if you get the same behavior.
Hope it helps!

Subscribing to Service Fabric cluster level events

I am trying to create a service that will update an external list of Service Endpoints for applications running in my service fabric cluster. (Basically I need to replicate the Azure Load Balancer in my on premises F5 Load Balancer.)
During last month's Service Fabric Q&A, the team pointed me at RegisterServiceNotificationFilterAsync.
I made a stateless service using this method, and deployed it to my development cluster. I then made a new service by running the ASP.NET Core Stateless service template.
I expected that when I deployed the second service, the break point would hit in my first service, indicating that a service had been added. But no breakpoint was hit.
I have found very little in the way of examples for this kind of thing on the internet, so I am asking here hopping that someone else has done this and can tell me where I went wrong.
Here is the code for my service that is trying to catch the application changes:
protected override async Task RunAsync(CancellationToken cancellationToken)
{
var fabricClient = new FabricClient();
long? filterId = null;
try
{
var filterDescription = new ServiceNotificationFilterDescription
{
Name = new Uri("fabric:")
};
fabricClient.ServiceManager.ServiceNotificationFilterMatched += ServiceManager_ServiceNotificationFilterMatched;
filterId = await fabricClient.ServiceManager.RegisterServiceNotificationFilterAsync(filterDescription);
long iterations = 0;
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
ServiceEventSource.Current.ServiceMessage(this.Context, "Working-{0}", ++iterations);
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
}
finally
{
if (filterId != null)
await fabricClient.ServiceManager.UnregisterServiceNotificationFilterAsync(filterId.Value);
}
}
private void ServiceManager_ServiceNotificationFilterMatched(object sender, EventArgs e)
{
Debug.WriteLine("Change Occured");
}
If you have any tips on how to get this going, I would love to see them.
You need to set the MatchNamePrefix to true, like this:
var filterDescription = new ServiceNotificationFilterDescription
{
Name = new Uri("fabric:"),
MatchNamePrefix = true
};
otherwise it will only match specific services. In my application I can catch cluster wide events when this parameter is set to true.

How to add more entries in customDimensions in to Application Insights Telemetry in Azure Function

I am trying to integrate Azure App Insights with an Azure Function App (HttpTriggered). I want to add my own keys and values in the "customDimensions" object of the requests table. Right now it only shows the following:
On query
requests
| where iKey == "449470fb-****" and id == "5e17e23e-****"
I get this:
LogLevel: Information
Category: Host.Results
FullName: Functions.FTAID
StartTime: 2017-07-14T14:24:10.9410000Z
param__context: ****
HttpMethod: POST
param__req: Method: POST, Uri: ****
Succeeded: True
TriggerReason: This function was programmatically called via the host APIs.
EndTime: 2017-07-14T14:24:11.6080000Z
I want to add more key values such as:
EnvironmentName: Development
ServiceLine: Business
Based on this answer, I implemented the ITelemetryInitializer interface as follows:
public class CustomTelemetry : ITelemetryInitializer
{
public void Initialize(ITelemetry telemetry)
{
var requestTelemetry = telemetry as RequestTelemetry;
if (requestTelemetry == null) return;
requestTelemetry.Context.Properties.Add("EnvironmentName", "Development");
}
}
Here is how the run.csx code for the Azure Function App looks like:
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, ExecutionContext context, TraceWriter log)
{
// Initialize the App Insights Telemetry
TelemetryConfiguration.Active.InstrumentationKey = System.Environment.GetEnvironmentVariable("APPINSIGHTS_INSTRUMENTATIONKEY", EnvironmentVariableTarget.Process);
TelemetryConfiguration.Active.TelemetryInitializers.Add(new CustomTelemetry());
TelemetryClient telemetry = new TelemetryClient();
var jsonBody = await req.Content.ReadAsStringAsync();
GetIoItemID obj = new GetIoItemID();
JArray output = obj.GetResponseJson(jsonBody, log, telemetry);
var response = req.CreateResponse(HttpStatusCode.OK);
response.Content = new StringContent(output.ToString(), System.Text.Encoding.UTF8, "application/json");
return response;
}
But this did not work...
I believe, since you're creating the TelemetryClient yourself in this example, you don't need to bother with the telemetry initializer, you could just do
var telemetry = new TelemetryClient();
telemetry.Context.Properties["EnvironmentName"] = "Development";
directly, and everything sent by that instance of that telemetry client will have those properties set.
You'd need that telemetry initializer if you don't have control over who's creating the telemetry client and want to touch every item of telemetry created wherever?
I don't know how that TelemetryClient instance gets used downstream in azure functions though, so i'm not entirely positive, though.
Edit: from azure functions post about this, it says:
We’ll be working hard to get Application Insights ready for production
workloads. We’re also listening for any feedback you have. Please file
it on our GitHub. We’ll be adding some new features like better
sampling controls and automatic dependency tracking soon. We hope
you’ll give it a try and start to gain more insight into how your
Functions are behaving. You can read more about how it works at
https://aka.ms/func-ai
and the example from that func-ai link has a couple things:
1) it creates the telemetry client statically up front once (instead of in each call to the function)
private static TelemetryClient telemetry = new TelemetryClient();
private static string key = TelemetryConfiguration.Active.InstrumentationKey = System.Environment.GetEnvironmentVariable("APPINSIGHTS_INSTRUMENTATIONKEY", EnvironmentVariableTarget.Process);
and inside the function it is doing:
telemetry.Context.Operation.Id = context.InvocationId.ToString();
to properly do correlation with events you might create with your telemetry client so you might want to do that too.
2) it appears that the telemetry client you create you can use, but they create their own telemetry client and send data there, so anything you touch in your telemetry client's context isn't seen by azure functions itself.
so, to me that leads me to something you can try:
add a static constructor in your class, and in that static constructor, do the telemetry initializer thing you were doing above. possibly this gets your telemetry initializer added to the context before azure functions starts creating its request and calling your method?
If that doesn't work, you might need to post on their GitHub or email the person listed in the article for more details on how to do this?

Triggering WebJob Method Based on Message Property

I have an Azure WebJobs project which handles a number of time-consuming tasks triggered by website actions. It works fine.
But the mapping from message to method call uses magic strings:
public class SomeClass
{
public async Task ProcessMessage(
[ QueueTrigger( "%" + nameof( ContainerQueueConstants.FilteredVoterFiles ) + "%" ) ] AgencyOutreachMessage
msg,
TextWriter azureLogWriter
)
{
PhaseNames.SetNames( "Exporting Data", "Job Completed" );
await ExecuteFromMessage( msg, azureLogWriter, Launch );
}
}
public class ContainerQueueConstants
{
public const string ImportFile = "import-file";
public const string VoterTraits = "voter-traits";
public const string Voter = "voter";
public const string FilteredVoterFiles = "filtered-voter-files";
}
I'd like to get away from using hard-coded strings for queue names. Ideally, I'd like to be able to route a message to a particular method based on the value of a property contained in the message.
But I'm not sure if that's even possible, at least in the 1.1.x version of the WebJobs SDK.
Suggestions or advice appreciated.
I suggest using N CloudQueue instances to monitor N different Storage Queues. Since you're doing this in a WebJob, you will probably do this as a continuous webjob and have to perform the polling for each queue yourself. You will also have to take responsibility for removing successfully processed messages.
The QueueTriggerAttribute has built-in support for deadlettering. I do not believe that there is automatic deadlettering support if you do not use the QueueTriggerAttribute.

Change database in connectionstring from app

I have created a Xamarin where I want to query one database on a Azure SQL Server and regarding which result I get back I want to query one of X numbers of certain databases.
So far I have been able to create two different APIs where the first API gets information from the first database.
And I have hardcoded (in the TableController) to use one specific database (in the same SQL Server).
string dbString = "database2";
myContextClass context = new myContextClass(dbString);
This works like a charm. However. I would like to be able to pass which database I want to connect to from my app.
I.e. when calling my mobileservice all I do is this:
this.client = new MobileServiceClient(
Constants.DatabaseURL);
Is there something I can add to this methodcall that will set the database connectionstring in the controller?
You should not be connecting to the database from the mobile App, you should have a backend REST service built for example using Web API and hosted in Azure API Service, this is where the connection to the database will occur, and the connection string will be added to the web.config file which can be easily changed from the Azure portal.
I think you need 2 dbContext instances. Here is an example in the controller how to initialize 1 dbContext. So, I guess, you could have the parameter you want to specify which db you want, and use the main controller or call another controller which is initialized with the other dbContext. (or try overwriting the DomainManager with different context)
public class TodoItemController : TableController<TodoItem>
{
protected override void Initialize(HttpControllerContext controllerContext)
{
base.Initialize(controllerContext);
AppContext context = new AppContext();
DomainManager = new EntityDomainManager<TodoItem>(context, Request);
}
// GET tables/TodoItem
public IQueryable<TodoItem> GetAllTodoItems()
{
return Query();
}
Then you have 2 dbContext instances like this:
public class AppContext : DbContext
{
private const string connectionStringName = "Name=MS_TableConnectionString";
public NeptuneAppContext() : base(connectionStringName)
{
}
where you can specify different connectionStrings.
Hope it helps

Resources