ServiceStack keep a long-live connection and send response asynchronously - servicestack

I have a client app which monitors the changes in real-time by establishing a long-live HTTP connection to server.
In ASP.NET WebAPI, the server can take use PushStreamContent to keep the connection for a long time and send response once there is an update.
But in ServiceStack, seems there is no similar stuff.
I looked at the sample code of Different ways of returning an ImageStream
IStreamWriter.WriteTo method is only called once, and I can't use async IO operation to avoid blocking server thread.
Is there a way to send progressive response to client asynchronously?
here is sample code in WebAPI which does the job
public static async Task Monitor(Stream stream, HttpContent httpContent, TransportContext transportContext)
{
ConcurrentQueue<SessionChangeEvent> queue = new ConcurrentQueue<SessionChangeEvent>();
TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
Action<SessionChangeEvent> callback = (evt) =>
{
queue.Enqueue(evt);
tcs.TrySetResult(null);
};
OnSessionChanged += callback;
try
{
using (StreamWriter sw = new StreamWriter(stream, new UTF8Encoding(false)))
{
await sw.WriteLineAsync(string.Empty);
await sw.FlushAsync();
await stream.FlushAsync();
for (; ; )
{
Task task = tcs.Task;
await Task.WhenAny(task, Task.Delay(15000));
if (task.Status == TaskStatus.RanToCompletion)
{
tcs = new TaskCompletionSource<object>();
SessionChangeEvent e;
while (queue.TryDequeue(out e))
{
string json = JsonConvert.SerializeObject(e);
await sw.WriteLineAsync(json);
}
task.Dispose();
}
else
{
// write an empty line to keep the connection alive
await sw.WriteLineAsync(string.Empty);
}
await sw.FlushAsync();
await stream.FlushAsync();
}
}
}
catch (CommunicationException ce)
{
}
finally
{
OnSessionChanged -= callback;
}
}

Writing to a long-running connection is exactly what Server Events does. You can look at the implementation for ServerEventsHandler or ServerEventsHeartbeatHandler to see it's implemented in ServiceStack.
Basically it just uses a custom ASP.NET IHttpAsyncHandler which can be registered at the start of ServiceStack's Request Pipeline with:
appHost.RawHttpHandlers.Add(req => req.PathInfo.EndsWith("/my-stream")
? new MyStreamHttpHandler()
: null);
Where MyStreamHttpHandler is a custom HttpAsyncTaskHandler, e.g:
public class MyStreamHttpHandler : HttpAsyncTaskHandler
{
public override bool RunAsAsync() { return true; }
public override Task ProcessRequestAsync(
IRequest req, IResponse res, string operationName)
{
//Write any custom request filters and registered headers
if (HostContext.ApplyCustomHandlerRequestFilters(req, res))
return EmptyTask;
res.ApplyGlobalResponseHeaders();
//Write to response output stream here, either by:
res.OuputStream.Write(...);
//or if need access to write to underlying ASP.NET Response
var aspRes = (HttpResponseBase)res.OriginalResponse;
aspRes.OutputStream...
//After you've finished end the request with
res.EndHttpHandlerRequest(skipHeaders: true);
return EmptyTask;
}
}
The ApplyCustomHandlerRequestFilters() and ApplyGlobalResponseHeaders() at the start gives other plugins a chance to validate/terminate the request or add any HTTP Headers (e.g. CorsFeature).

Have a look at ServerEvents. If I understood you right, this is what you are looking for.

Related

Solace can't SEND while in WaitingForDNS

I'm trying send a message on a topic queue but the API returns
OperationError: Cannot perform operation SEND while in state WaitingForDNS
The URL can be pinged and it resolves to an IP address. I feel this message is misleading.
FWIW, I'm using Solace's Javascript API calling it from my Typescript code.
Below is code snippet. I've tried to remove irrelevant items such as the console logging. In short, it tests for a session already having been created, and if not, creates it using the Solace factory, then sets some event handlers which I think are irrelevant to this item
so I didn't include them. Because I don't catch any errors at connect() time, I presume it connected. Then when I try to send(), it suddenly is still waiting on the DNS. Or, at least, the error implies this.
class TopicPublisher {
private getSession() {
if (this.session == null) {
try {
this.log('Creating session for url=' + this.hosturl+' vpn='+this.vpn+' username='+this.username);
this.session = this.solace.SolclientFactory.createSession({
// solace.SessionProperties
url: this.hosturl,
vpnName: this.vpn,
userName: this.username,
password: this.pass,
});
// Set session event handlers
try {
this.session.connect();
} catch(error : any) {
this.log('Could not make connection to existing session. Error: '+error.toString());
this.session = null;
}
} catch (error: any) {
this.log(error.toString());
}
}
return this.session;
}
public publish(messageContent: any) {
var solaceMessage = this.getSolaceMessage(messageContent);
this.log('Publishing message "' + messageContent + '" to topic "' + this.topicName + '"...');
try {
// *************************
// This is where SEND fails
// *************************
this.getSession().send(solaceMessage);
this.log('Message published.');
} catch (error: any) {
this.log(error.toString());
}
};
}
An OperationError is thrown when you are attempting to execute a command on the session before it's actually up. What you can do here is only return this.session on session.on(solace.SessionEventCode.UP_NOTICE)
You can also see more info on this in the API docs and check out the note in the docs
Note: Before the session's state transitions to 'connected', a client
application cannot use the session; any attempt to call functions will
throw solace.OperationError.

Why a 400 response from Azure endpoint for Postman but works fine with the Alexa console?

I am new to developing Alexa skills so I am using a sample I found on the web as a C# endpoint hosted on Azure. It works correctly with the Alexa console but when I try to test the same endpoint with the Postman app, I get a 400 error.
When I use the Alexa console, it displays the JSON input that it sends to the endpoint and the JSON output that it receives from the endpoint. If I copy the JSON input and paste it into Postman and send it to the same endpoint, I get a 400 error. Obviously, I am missing something.
The following are my two source files and the JSON input.
RollTheDice.cs
public static class RollTheDice
{
[FunctionName("RollTheDice")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]HttpRequestMessage req, TraceWriter log)
{
log.Info("C# HTTP trigger function processed a request.");
var speechlet = new RollTheDiceSpeechlet();
return await speechlet.GetResponseAsync(req);
}
}
RollTheDiceSpeechlet.cs
public class RollTheDiceSpeechlet : SpeechletBase, ISpeechletWithContext
{
public SpeechletResponse OnIntent(IntentRequest intentRequest, Session session, Context context)
{
try
{
// Default to 6 sides if not specified
if (!int.TryParse(intentRequest.Intent.Slots["DiceType"].Value, out int numSides))
numSides = 6;
var rollResults = new Random().Next(Math.Max(1, numSides - 1)) + 1; // Account for random returning '0'
return new SpeechletResponse
{
ShouldEndSession = false,
OutputSpeech = new PlainTextOutputSpeech { Text = $"I rolled a {numSides} sided die and got a {rollResults}." }
};
}
catch (Exception ex)
{
return new SpeechletResponse
{
ShouldEndSession = false,
OutputSpeech = new PlainTextOutputSpeech { Text = ex.Message }
};
}
}
public SpeechletResponse OnLaunch(LaunchRequest launchRequest, Session session, Context context)
{
return new SpeechletResponse
{
ShouldEndSession = false,
OutputSpeech = new PlainTextOutputSpeech { Text = "Welcome to the Roll the Dice. Ask me to roll the dice." }
};
}
public void OnSessionEnded(SessionEndedRequest sessionEndedRequest, Session session, Context context)
{
return;
}
public void OnSessionStarted(SessionStartedRequest sessionStartedRequest, Session session, Context context)
{
return;
}
}
JSON Input
Again, everything works fine but when I test it with Postman I get a 404 error.
The endpoint is C# serverless function that I developed in Visual Studio 201.
When I run it locally, I copy/paste the URL in the Postman app and send a post. See attached screenshots.
As the error suggest you are missing Signature and SignatureCertChainUrl headers. These helps to protect your endpoint and verify that incoming requests were sent by Alexa. Any requests coming from other sources should be rejected. When you test it via Test Console these headers are included and you get successful response.
Headers:
Signature
SignatureCertChainUrl
There are two parts to validating incoming requests:
Check the request signature to verify the authenticity of the request.
Check the request timestamp to ensure that the request is not an old request.
More information on verifying that the request was sent by Alexa here

ServiceStack MQ: how to populate data in RequestContext

I'm developing a JWT-based multi-tenancy system using ServiceStack. The JWT token contains shard information, and I use JwtAuthProvider to translate the JWT token to session object following instructions at http://docs.servicestack.net/jwt-authprovider.
Now, I want to use ServiceStack MQ for asynchronous processing. The MQ request needs to be aware of the shard information, so I populate the request context before executing it as follow
mqServer.RegisterHandler<EmployeeAssignedToProject>(m =>
{
var req = new BasicRequest { Verb = HttpMethods.Post };
var sessionKey = SessionFeature.GetSessionKey(m.GetBody().SessionId);
var session = HostContext.TryResolve<ICacheClient>().Get<Context>(sessionKey);
req.Items[Keywords.Session] = session;
var response = ExecuteMessage(m, req);
return response;
});
Here, Context is my custom session class. This technique is stemmed from the instruction at http://docs.servicestack.net/messaging#authenticated-requests-via-mq. Since I execute the message within the context of req, I reckon that I should then be able to resolve Context as follow
container.AddScoped<Context>(c =>
{
var webRequest = HostContext.TryGetCurrentRequest();
if (webRequest != null)
{
return webRequest.SessionAs<Context>();
} else
{
return HostContext.RequestContext.Items[Keywords.Session] as Context;
}
});
However, HostContext.RequestContext.Items is always empty. So the question is, how to populate HostContext.RequestContext.Items from within message handler registration code?
I've tried to dig a little bit into ServiceStack code and found that the ExecuteMessage(IMessage dto, IRequest req) in ServiceController doesn't seem to populate data in RequestContext. For my case, it is a bit too late to get session inside service instance, as a service instance depends on some DB connections whose shard info is kept in session.
The same Request Context instance can't be resolved from the IOC. The Request Context instance is created in the MQ's RegisterHandler<T>() where you can add custom data in the IRequest.Items property, e.g:
mqServer.RegisterHandler<EmployeeAssignedToProject>(m =>
{
var req = new BasicRequest { Verb = HttpMethods.Post };
req.Items[MyKey] = MyValue; //Inject custom per-request data
//...
var response = ExecuteMessage(m, req);
return response;
});
This IRequest instance is available throughout the Request pipeline and from base.Request in your Services. It's not available from your IOC registrations so you will need to pass it in as an argument when calling your dependency, e.g:
public class MyServices : Service
{
public IDependency MyDep { get; set; }
public object Any(MyRequest request) => MyDep.Method(base.Request, request.Id);
}

Azure service bus message delivery count is not increasing or is reset when topic subscription disabling/enabling

I have the following workflow:
Service bus receives messages.
Azure function triggers and tries to deliver this messages via HTTP to some service.
If delivery failed - function throws exception (custom) and disables topic subscription via code below:
The other function in parallel pings special health check endpoint of the service, and if it gets 200 - it tries to enable subscription and make the flow work again.
The steps could be reproduced N times, cause health check will return 200, thus the delivery url of point 2 - 4xx code.
After the next attempt to enable subscription and deliver the message, I expect that delivery count will be increased and in the end (after 10 deliveries attempt) it will get to dead-letter.
Actual - it equals 1.
I assume, that it may reset when I call CreateOrUpdate with status changed.
If yes - what is the other way to manage subscription status instead of Microsoft.Azure.Management package so that the messages delivery count will not be reset?
UPDATE: Function code
public static class ESBTESTSubscriptionTrigger
{
private static readonly HttpClient Client = new HttpClient();
private static IDatabase redisCache;
[FunctionName("ESBTESTSubscriptionTrigger")]
[Singleton]
public static async Task Run([ServiceBusTrigger("Notifications", "ESBTEST", AccessRights.Listen, Connection = "NotificationsBusConnectionString")]BrokeredMessage serviceBusMessage, TraceWriter log, [Inject]IKeyVaultSecretsManager keyVaultSecretsManager)
{
var logicAppUrl = await keyVaultSecretsManager.GetSecretAsync("NotificationsLogicAppUrl");
if (redisCache == null)
{
redisCache = RedisCacheConnectionManager.GetRedisCacheConnection(
keyVaultSecretsManager.GetSecretAsync("RedisCacheConnectionString").GetAwaiter().GetResult());
}
if (string.IsNullOrWhiteSpace(logicAppUrl))
{
log.Error("Logic App URL should be provided in Application settings of function App.");
throw new ParameterIsMissingException("Logic App URL should be provided in Application settings of function App.");
}
var applicaitonId = serviceBusMessage.Properties["applicationId"].ToString();
var eventName = serviceBusMessage.Properties.ContainsKey("Event-Name") ? serviceBusMessage.Properties["Event-Name"].ToString() : string.Empty;
if (string.IsNullOrWhiteSpace(applicaitonId))
{
log.Error("ApplicationId should be present in service bus message properties.");
throw new ParameterIsMissingException("Application id is missing in service bus message.");
}
Stream stream = serviceBusMessage.GetBody<Stream>();
StreamReader reader = new StreamReader(stream);
string s = reader.ReadToEnd();
var content = new StringContent(s, Encoding.UTF8, "application/json");
content.Headers.Add("ApplicationId", applicaitonId);
HttpResponseMessage response;
try
{
response = await Client.PostAsync(logicAppUrl, content);
}
catch (HttpRequestException e)
{
log.Error($"Logic App responded with {e.Message}");
throw new LogicAppBadRequestException($"Logic App responded with {e.Message}", e);
}
if (!response.IsSuccessStatusCode)
{
log.Error($"Logic App responded with {response.StatusCode}");
var serviceBusSubscriptionsSwitcherUrl = await keyVaultSecretsManager.GetSecretAsync("ServiceBusTopicSubscriptionSwitcherUri");
var sbSubscriptionSwitcherResponse = await Client.SendAsync(
new HttpRequestMessage(HttpMethod.Post, serviceBusSubscriptionsSwitcherUrl)
{
Content =
new
StringContent(
$"{{\"Action\":\"Disable\",\"SubscriptionName\":\"{applicaitonId}\"}}",
Encoding.UTF8,
"application/json")
});
if (sbSubscriptionSwitcherResponse.IsSuccessStatusCode == false)
{
throw new FunctionNotAvailableException($"ServiceBusTopicSubscriptionSwitcher responded with {sbSubscriptionSwitcherResponse.StatusCode}");
}
throw new LogicAppBadRequestException($"Logic App responded with {response.StatusCode}");
}
if (!string.IsNullOrWhiteSpace(eventName))
{
redisCache.KeyDelete($"{applicaitonId}{eventName}DeliveryErrorEmailSent");
}
}
}

Using JsConfig.BeginScope with Async Client Methods in ServiceStack

I'm trying to set an option on JsConfig for a single async method on JsonServiceClient by using JsConfigScope, but it does not seem to work. What am I doing wrong? Is there another way to do this?
var client = new JsonServiceClient(baseUrl);
using (var scope = JsConfig.BeginScope())
{
scope.EmitCamelCaseNames = true;
return client.PostAsync<SomeResponse>(url, request);
}
You can't use a using scope with an async request since the scope will be disposed before the async service has completed. You would need to await the response, i.e:
using (JsConfig.With(new Config { TextCase = TextCase.CamelCase }))
{
return await client.PostAsync<SomeResponse>(url, request);
}
You can use the lower-level HTTP Utils to split the request serialization and response deserialization outside of the async call which is the approach used in the StripeGateway.
Otherwise you could potentially use the Response filter to dispose of the scoped configuration, e.g:
var scope = JsConfig.With(new Config { TextCase = TextCase.CamelCase });
var client = new JsonServiceClient(BaseUrl) {
ResponseFilter = httpRes => scope.Dispose()
};
return client.PostAsync<SomeResponse>(url, request);

Resources