I am trying to implement a request response pattern in C# with the ArtemisNetClient, but having a bit of trouble finding out how to do so in a more generic way in a real solution.
I was able to do something like this in two console applications based on some Java examples:
Sender
static async System.Threading.Tasks.Task Main(string[] args)
{
var connectionFactory = new ConnectionFactory();
var endpoint = Endpoint.Create("localhost", 5672, "guest", "guest");
var connection = await connectionFactory.CreateAsync(endpoint);
string guid = new Guid().ToString();
var requestAddress = "TRADE REQ1";
var responseAddress = "TRADE RESP";
Message message = new Message("BUY AMD 1000 SHARES");
message.SetCorrelationId(guid);
message.ReplyTo = responseAddress;
var producer = await connection.CreateProducerAsync(requestAddress, RoutingType.Anycast);
await producer.SendAsync(message);
var consumer = await connection.CreateConsumerAsync(responseAddress, RoutingType.Anycast);
var responseMessage = await consumer.ReceiveAsync();
Console.WriteLine(responseMessage.GetBody<string>());
}
Receiver
static async System.Threading.Tasks.Task Main(string[] args)
{
// Create connection
var connectionFactory = new ConnectionFactory();
var endpoint = Endpoint.Create("localhost", 5672, "guest", "guest");
var connection = await connectionFactory.CreateAsync(endpoint);
var requestAddress = "TRADE REQ1";
// Create consumer to receive trade request messages
var consumer = await connection.CreateConsumerAsync(requestAddress, RoutingType.Anycast);
var message = await consumer.ReceiveAsync();
Console.WriteLine($"Received message: {message.GetBody<string>()}");
// Confirm trade request and ssend response message
if (!string.IsNullOrEmpty(message.ReplyTo))
{
Message responseMessage = new Message("Confirmed trade request");
responseMessage.SetCorrelationId(message.CorrelationId);
var producer = await connection.CreateProducerAsync(message.ReplyTo);
await producer.SendAsync(responseMessage);
}
}
This worked as expected, but I'd like to have something more down the line of what is described in this article, except it doesn't have any examples of a request response pattern.
To elaborate, I currently have two services that I want to communicate across.
In service 1 I want to create and publish a message and then wait for a response to enrich the instance object and save it to a database. I currently have this, but it lacks the await response message.
public async Task<Instance> CreateInstance(Instance instance)
{
await _instanceCollection.InsertOneAsync(instance);
var #event = new InstanceCreated
{
Id = instance.Id,
SiteUrl = instance.SiteUrl
};
await _messageProducer.PublishAsync(#event);
return instance;
}
I figured I might need to setup a temporary queue/connection or something in the PublishAsync() and change it to e.g. Task<Message> to support returning a response message. But how would I go about doing that? Would I have to do a new connectionfactory + CreateConsumerAsync etc. like in the console app example?
public class MessageProducer
{
private readonly IAnonymousProducer _producer;
public MessageProducer(IAnonymousProducer producer)
{
_producer = producer;
}
public async Task PublishAsync<T>(T message, string replyTo = null, string correlationId = null)
{
var serialized = JsonSerializer.Serialize(message);
var address = typeof(T).Name;
var msg = new Message(serialized);
if (replyTo != null && correlationId != null)
{
msg.CorrelationId = correlationId;
msg.ReplyTo = replyTo;
}
await _producer.SendAsync(address, msg);
}
public async Task PublishAsync<T>(T message, string routeName, string replyTo = null, string correlationId = null)
{
var serialized = JsonSerializer.Serialize(message);
var address = routeName;
var msg = new Message(serialized);
if(replyTo != null && correlationId != null)
{
msg.CorrelationId = correlationId;
msg.ReplyTo = replyTo;
}
await _producer.SendAsync(address, msg);
}
}
In Service 2 I have a InstanceCreatedConsumer which receives messages, but again it lacks a way to return response messages.
public class InstanceCreatedConsumer : ITypedConsumer<InstanceCreated>
{
private readonly MessageProducer _messageProducer;
public InstanceCreatedConsumer(MessageProducer messageProducer)
{
_messageProducer = messageProducer;
}
public async Task ConsumeAsync(InstanceCreated message, CancellationToken cancellationToken)
{
// consume message and return response
}
}
I figured I might be able to extend the ActiveMqExtensions class with a ConsumeAsync and HandleMessage that handles the response message with a return value, but I haven't gotten as far yet.
public static IActiveMqBuilder AddTypedConsumer<TMessage, TConsumer>(this IActiveMqBuilder builder,
RoutingType routingType)
where TConsumer : class, ITypedConsumer<TMessage>
{
builder.Services.TryAddScoped<TConsumer>();
builder.AddConsumer(typeof(TMessage).Name, routingType, HandleMessage<TMessage, TConsumer>);
return builder;
}
private static async Task HandleMessage<TMessage, TConsumer>(Message message, IConsumer consumer, IServiceProvider serviceProvider, CancellationToken token)
where TConsumer : class, ITypedConsumer<TMessage>
{
try
{
var msg = JsonConvert.DeserializeObject<TMessage>(message.GetBody<string>());
using var scope = serviceProvider.CreateScope();
var typedConsumer = scope.ServiceProvider.GetService<TConsumer>();
await typedConsumer.ConsumeAsync(msg, token);
await consumer.AcceptAsync(message);
}
catch(Exception ex)
{
// todo
}
}
Am I totally wrong in what I am trying to achieve here, or is it just not possible with the ArtemisNetClient?
Maybe someone has an example or can confirm whether I am down the right path, or maybe I should be using a different framework.
I am new to this kind of communication through messages like ActiveMQ Artemis, so any guidance is appreciated.
I don't see anything in the ArtemisNetClient that would simplify the request/response pattern from your application's point of view. One might expect something akin to JMS' QueueRequestor, but I don't see anything like that in the code, and I don't see anything like that listed in the documentation.
I recommend you simply do in your application what you did in your example (i.e. manually create the consumer & producer to deal with the responses on each end respectively). The only change I would recommend is to re-use connections so you create as few as possible. A connection pool would be ideal here.
For what it's worth, it looks to me like the first release of ArtemisNetClient was just 3 months ago and according to GitHub all but 2 of the commits to the code-base came from one developer. ArtemisNetClient may grow into a very successful C# client implementation, but at this point it seems relatively immature. Even if the existing code is high quality if there isn't a solid community around the client then chances are it won't have the support necessary to get timely bug fixes, new features, etc. Only time will tell.
With version 2.7.0 ArtemisNetClient introduces IRequestReplyClient interface that can be used to implement a request-response messaging pattern. With ArtemisNetClient.Extensions.DependencyInjection this may look as follows:
Client Side:
First you need to register your typed request-reply client in DI:
public void ConfigureServices(IServiceCollection services)
{
/*...*/
var endpoints = new[] { Endpoint.Create(host: "localhost", port: 5672, "guest", "guest") };
services.AddActiveMq("bookstore-cluster", endpoints)
.AddRequestReplyClient<MyRequestReplyClient>();
/*...*/
}
MyRequestReplyClient is your custom class that expects the IRequestReplyClient to be injected via the constructor. Once you have your custom class, you can either expose the IRequestReplyClient directly or encapsulate sending logic inside of it:
public class MyRequestReplyClient
{
private readonly IRequestReplyClient _requestReplyClient;
public MyRequestReplyClient(IRequestReplyClient requestReplyClient)
{
_requestReplyClient = requestReplyClient;
}
public async Task<TResponse> SendAsync<TRequest, TResponse>(TRequest request, CancellationToken cancellationToken)
{
var serialized = JsonSerializer.Serialize(request);
var address = typeof(TRequest).Name;
var msg = new Message(serialized);
var response = await _requestReplyClient.SendAsync(address, msg, cancellationToken);
return JsonSerializer.Deserialize<TResponse>(response.GetBody<string>());
}
}
That's it regarding the client-side.
Worker side
To implement the worker side you can (as you suggested), change the ITypedConsumer interface to return the message that would be sent back, or you can provide the additional data (ReplyTo and CorrelationId headers) so you can send the response back as part of your consumer logic. I prefer the latter as it's a more flexible option in my opinion.
Modified ITypedConsumer might look like that:
public interface ITypedConsumer<in T>
{
public Task ConsumeAsync(T message, MessageContext context, CancellationToken cancellationToken);
}
Where MessageContext is just a simple dto:
public class MessageContext
{
public string ReplyTo { get; init; }
public string CorrelationId { get; init; }
}
HandleMessage extension method:
private static async Task HandleMessage<TMessage, TConsumer>(Message message, IConsumer consumer, IServiceProvider serviceProvider, CancellationToken token)
where TConsumer : class, ITypedConsumer<TMessage>
{
var msg = JsonSerializer.Deserialize<TMessage>(message.GetBody<string>());
using var scope = serviceProvider.CreateScope();
var typedConsumer = scope.ServiceProvider.GetService<TConsumer>();
var messageContext = new MessageContext
{
ReplyTo = message.ReplyTo,
CorrelationId = message.CorrelationId
};
await typedConsumer.ConsumeAsync(msg, messageContext, token);
await consumer.AcceptAsync(message);
}
MessageProducer has to be slightly changed as well, so you can explicitly pass address and CorrelationId:
public class MessageProducer
{
private readonly IAnonymousProducer _producer;
public MessageProducer(IAnonymousProducer producer)
{
_producer = producer;
}
public async Task PublishAsync<T>(string address, T message, MessageContext context, CancellationToken cancellationToken)
{
var serialized = JsonSerializer.Serialize(message);
var msg = new Message(serialized);
if (!string.IsNullOrEmpty(context.CorrelationId))
{
msg.CorrelationId = context.CorrelationId;
}
await _producer.SendAsync(address, msg, cancellationToken);
}
}
And finally, the exemplary consumer could work like that:
public class CreateBookConsumer : ITypedConsumer<CreateBook>
{
private readonly MessageProducer _messageProducer;
public CreateBookConsumer(MessageProducer messageProducer)
{
_messageProducer = messageProducer;
}
public async Task ConsumeAsync(CreateBook message, MessageContext context, CancellationToken cancellationToken)
{
var #event = new BookCreated
{
Id = Guid.NewGuid(),
Title = message.Title,
Author = message.Author,
Cost = message.Cost,
InventoryAmount = message.InventoryAmount,
UserId = message.UserId,
Timestamp = DateTime.UtcNow
};
await _messageProducer.PublishAsync(context.ReplyTo, #event, new MessageContext
{
CorrelationId = context.CorrelationId
}, cancellationToken);
}
}
Related
I am experimenting with gRPC for long-lived streaming session as I need to guarantee message ordering from server to client.
I have the following .proto:
service Subscriber {
rpc Subscribe(SubscriptionRequest) returns (stream SubscriberEvent);
}
My current service (hosted in ASP.NET / .NET 5.0) looks like this:
public class SubscriberService : Subscriber.SubscriberBase
{
private readonly ILogger<SubscriberService> _logger;
private readonly ConcurrentDictionary<string, IServerStreamWriter<SubscriberEvent>> _subscriptions = new();
private int _messageCount = 0;
private Timer _timer;
public SubscriberService(ILogger<SubscriberService> logger)
{
_logger = logger;
_timer = new Timer(o => TimerCallback(), null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
}
private void TimerCallback()
{
Broadcast($"Current time is {DateTime.UtcNow}");
}
public override Task Subscribe(SubscriptionRequest request, IServerStreamWriter<SubscriberEvent> responseStream, ServerCallContext context)
{
_subscriptions.TryAdd(request.ClientId, responseStream);
return responseStream.WriteAsync(new SubscriberEvent() {Id = 0, Message = "Subscribe successful"});
}
public void Broadcast(string message)
{
var count = ++_messageCount;
foreach (var sub in _subscriptions.Values)
{
sub.WriteAsync(new SubscriberEvent() { Id = count, Message = message });
}
_logger.LogInformation($"Broadcast message #{count}: {message}");
}
}
My client only receives the initial 'Subscribe Successful' message, but never those triggered by the timer. Not do I get any exceptions when calling WriteAsync.
Am I trying to use gRPC for something it was never designed to do (a SignalR/WebSocket substitute), or am I merely missing something obvious?
For a long-running gRPC streaming, you have to wait for a client to say the connection is closed. Something like this:
while (!context.CancellationToken.IsCancellationRequested)
{
// event-based action
responseStream.WriteAsync(new SubscriberEvent() {Id = 0, Message = "Subscribe successful"});
}
I think the previous answer is doing busy-wait. So I want to show you an async version of it.
public override Task Subscribe(RequestMessage, IServerStreamWriter<ReplyMessage> responseStream, ServerCallContext context)
{
// your event-based code here
var tcs = new TaskCompletionSource();
context.CancellationToken.Register(() => tcs.TrySetCanceled(), false);
return tcs.Task;
}
BTW, I think I have been doing a project just like yours. And I have used Observables, Subjects, and ReplaySubjects from Rx.Net. These are very helpful for event-based code.
currently I try to implement a MessagingClient, working with RabbitMQ and C#.
My idea is that every service uses its own queue for receiving messages and there is an own exchange for each message type. For example a message type could be "download/requested" or "download/started". These exchanges are of type "fanout". Now the clients, that want to listen on some message, bind their queue to the corresponding exchange. For example the download service binds its queue "download-bot" to the exchange "download/requested".
My problem is now that I can't really imagine, how to deal with different types of messages on the parsing site. I use Newtonsoft.Json for encoding/decoding the message objects. For each different message type, there is separate "handler" I want to execute. This handler should get the message (deserialized) as a parameter. My problem is, that with mulitple message types over one queue, there seems to be just one handler qer queue. How do I find out, which concrete handler to execute and how to parse the message, if I don't know the type at compile time? I know an important keyword will be "reflection", but I could not tinker something working together. I should say that i'm pretty new with C#. Could someone provide a working example of this ? Or is this even something, I should do like that?
As a workarround, currently I got the following (reduced) example (using one queue per message type and service)
namespace Lib.Message {
public class RabbitMqMessageClient : IMessageClient {
private static NLog.ILogger logger = Common.Logger.GetLogger ();
private RabbitMqConfig config;
private IModel model;
private const string DOWNLOAD_REQUESTED = "download/requested";
public RabbitMqMessageClient (RabbitMqConfig config) {
this.config = config;
var factory = new ConnectionFactory () { HostName = config.Hostname };
var connection = factory.CreateConnection ();
this.model = connection.CreateModel ();
}
public Task PublishDownloadRequested (string userRef, string id, string url) {
var message = new DownloadRequestMessage (userRef, id, url);
return this.publish (DOWNLOAD_REQUESTED, message);
}
public Task SubscribeToDownloadRequested (Action<DownloadRequestMessage> action) {
return this.subscribe<DownloadRequestMessage> (DOWNLOAD_REQUESTED, action);
}
public Task SubscribeToDownloadRequested (Func<DownloadRequestMessage, Task> action) {
return this.subscribe<DownloadRequestMessage> (DOWNLOAD_REQUESTED, action);
}
public Task Start () {
return Task.CompletedTask;
}
private Task subscribe<TPayloadType> (string topic, Action<TPayloadType> action) {
Func<TPayloadType, Task> wrappedAction = (TPayloadType args) => {
action (args);
return Task.CompletedTask;
};
return this.subscribe<TPayloadType> (topic, wrappedAction);
}
private Task subscribe<TPayloadType> (string topic, Func<TPayloadType, Task> action) {
var queue = $"{config.QueueName}--{topic}";
model.ExchangeDeclare (topic, ExchangeType.Fanout, true);
model.QueueDeclare (
queue,
durable : true,
exclusive : false,
autoDelete : false,
arguments : null);
model.QueueBind (config.QueueName, topic, "foo");
var consumer = new EventingBasicConsumer (model);
// this consumer will be shared for multiple queues right ?
// maybe even not here in this method, but in the "Start" method
consumer.Received += async (model, ea) => {
logger.Info ($"handling {ea.Exchange}");
var jsonString = Encoding.UTF8.GetString (ea.Body.Span.ToArray ());
// here I need to know how to deserialize the payload from the value of "ea.Exchange"
var message = JsonConvert.DeserializeObject<TPayloadType>(jsonString);
// I think i have to put the action in a map (?) instead and then find the concrete handler by the exchange name?
await action(message);
};
model.BasicConsume (config.QueueName, true, consumer);
return Task.CompletedTask;
}
private Task publish (string topic, object payload) {
logger.Info ($"publishing {topic}");
string message = JsonConvert.SerializeObject (payload);
var bytes = Encoding.UTF8.GetBytes (message);
model.BasicPublish (
exchange: topic,
routingKey: "foo",
basicProperties : null,
body : bytes
);
return Task.CompletedTask;
}
}
}
I'm trying to implement a Polly Timeout policy using the new .NET Core 2.1 HttpClientFactory; however, I cannot seem to get the timeout to occur.
My ConfigureServices:
// Configure polly policies
TimeoutPolicy<HttpResponseMessage> timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(5, TimeoutStrategy.Pessimistic);
// Configure platform service clients
services.AddHttpClient<IDiscoveryClient, DiscoveryClient>()
.AddPolicyHandler(timeoutPolicy);
My POST method in DiscoveryClient:
public async Task<TResponse> PostXMLAsync<TResponse, TPostData>(string url, TPostData postData)
where TResponse : ClientResponse
where TPostData : ClientPostData
{
HttpResponseMessage response = await httpClient.PostAsXmlAsync(url, postData);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsAsync<TResponse>();
}
Unfortunately, the call times out after the default 100s rather than after the 5s defined in the Polly policy.
Any thoughts on what I'm doing wrong?
First let's define a mock server which does respond with 500 after 100 seconds:
const string address = "http://localhost:9000";
var delay = TimeSpan.FromSeconds(100);
var server = WireMockServer.Start(new WireMockServerSettings { Urls = new[] { address } });
server
.Given(Request.Create().WithPath("/").UsingPost())
.RespondWith(Response.Create().WithDelay(delay).WithStatusCode(500));
I've used WireMock.Net for this.
Now, let's see the IDiscoveryClient and DiscoveryClient:
interface IDiscoveryClient
{
Task<TResponse> SendRequest<TResponse, TPostData>(string url, TPostData data);
}
class DiscoveryClient : IDiscoveryClient
{
private readonly HttpClient httpClient;
public DiscoveryClient(HttpClient httpClient) => this.httpClient = httpClient;
public async Task<TResponse> SendRequest<TResponse, TPostData>(string url, TPostData data)
{
var content = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8);
var response = await httpClient.PostAsync(url, content);
response.EnsureSuccessStatusCode();
var rawData = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<TResponse>(rawData);
}
}
class TestRequest { public string Content { get; set; } }
class TestResponse { public string Data { get; set; } }
I've used json instead of xml, but that's not imporant from the question point of view.
And finally let's wire up the DI and issue a request:
AsyncTimeoutPolicy<HttpResponseMessage> timeoutPolicy =
Policy.TimeoutAsync<HttpResponseMessage>(5, TimeoutStrategy.Pessimistic);
IServiceCollection services = new ServiceCollection();
services.AddHttpClient<IDiscoveryClient, DiscoveryClient>()
.AddPolicyHandler(timeoutPolicy);
ServiceProvider serviceProvider = services.BuildServiceProvider();
var client = serviceProvider.GetService<IDiscoveryClient>();
Stopwatch sw = Stopwatch.StartNew();
try
{
TestResponse res = await client.SendRequest<TestResponse, TestRequest>(address, new TestRequest { Content = "Test"});
}
catch (TimeoutRejectedException ex)
{
sw.Stop();
Console.WriteLine(sw.Elapsed);
}
The printed output will be something like this:
00:00:05.0296804
The good thing is that it does work with Optimistic and Pessimistic strategies as well.
I am trying to write a function which uses Task and TaskCompletion.
My problem is that after login, the result is not returned. I used similar code before and it was working. I do not know what causes for this situation.
public async Task<byte[]> Sign(byte[] documentContent)
{
var service = new SignServiceWrapper("https://example.com?wsdl");
var loginResult = await Task.Run(() => service.Login(loginRequest));
//....
}
and my SignServiceWrapper class
public class SignServiceWrapper
{
private static string _webServiceUrl;
private BrokerClientClient client;
public SignServiceWrapper(string webServiceUrl)
{
_webServiceUrl = webServiceUrl;
}
public Task<loginResponse> Login(loginRequest request)
{
var tcs = new TaskCompletionSource<loginResponse>();
ClientGenerator.WebServiceUrl = _webServiceUrl;
ClientGenerator.InitializeService();
client = ClientGenerator.ServiceClient;
client.loginCompleted += (sender, loginResult) =>
{
if (loginResult.Error != null)
tcs.SetException(loginResult.Error);
else
tcs.TrySetResult(loginResult.Result);
};
client.loginAsync(request);
return tcs.Task;
}
// ...
}
If I call my login function like that it works
var loginResult = Task.Run(() => service.Login(loginRequest));
loginResult.Wait();
I know that there is kind of a deadlock but I don't know how to solve this here and which object.
Here is a working .NET Fiddle.
I think your .Login method is trying to do too much. The first thing that I noticed (and can only imagine how it's implemented) is the static ClientGenerator, that has static mutable state. This which is alarming and a very specific code smell. I would love to see what the client itself looks like and how that is implemented as that would certainly help to better answer this question.
Based on what you shared thus far (and assuming that the client.loginAsync returns a Task<loginResponse>), I would say that you could do the following:
public class SignServiceWrapper
{
private static string _webServiceUrl;
private BrokerClientClient client;
public SignServiceWrapper(string webServiceUrl)
{
_webServiceUrl = webServiceUrl;
}
public Task<loginResponse> LoginAsync(loginRequest request)
{
ClientGenerator.WebServiceUrl = _webServiceUrl;
ClientGenerator.InitializeService();
client = ClientGenerator.ServiceClient;
return client.loginAsync(request);
}
// ...
}
You could then consume this as such:
public async Task<byte[]> Sign(byte[] documentContent)
{
var service = new SignServiceWrapper("https://example.com?wsdl");
var loginResult = await service.LoginAsync(loginRequest);
//...
}
If the client.loginAsync doesn't return what you're looking for, then you'll need to approach this doing something similar to your current approach. Or if you are locked in to the event-based async pattern, you have other considerations - like whether or not you want to support cancellation, IsBusy, progress, incremental results and if you have the ability to have the event args inherit the System.ComponentModel.AsyncCompletedEventArgs, etc...
One final consideration, if the client.loginAsync is Task returning, even if it doesn't return the loginResponse you need to await it like so:
public async Task<loginResponse> Login(loginRequest request)
{
var tcs = new TaskCompletionSource<loginResponse>();
ClientGenerator.WebServiceUrl = _webServiceUrl;
ClientGenerator.InitializeService();
client = ClientGenerator.ServiceClient;
client.loginCompleted += (sender, loginResult) =>
{
if (loginResult.Error != null)
tcs.SetException(loginResult.Error);
else
tcs.TrySetResult(loginResult.Result);
};
await client.loginAsync(request);
return tcs.Task;
}
Update
After discussion with OP this .NET Fiddle seemed to align with his needs.
Change var loginResult = await Task.Run(() =>service.Login(loginRequest));
To var loginResult = await service.Login(loginRequest);
The sample code for working with KeyVault inside a web application has the following code in it:
public static async Task<string> GetSecret(string secretId)
{
var secret = await keyVaultClient.GetSecretAsync(secretId);
return secret.Value;
}
I've incorporated the KeyVaultAccessor object included in the sample in my application in order to test it. The call is executed as part of a query to one of my web api controller methods:
var secret = KeyVaultAccessor.GetSecret("https://superSecretUri").Result;
Unfortunately, the call never returns and the query hangs indefintely...
What might be the reason, because frankly I have no clue where to start...?
This is the common deadlock issue that I describe in full on my blog. In short, the async method is attempting to return to the ASP.NET request context after the await completes, but that request only allows one thread at a time, and there is already a thread in that context (the one blocked on the call to Result). So the task is waiting for the context to be free, and the thread is blocking the context until the task completes: deadlock.
The proper solution is to use await instead of Result:
var secret = await KeyVaultAccessor.GetSecret("https://superSecretUri");
I've used the following code to override the synchronization context:
var secret = Task.Run(async () => await KeyVaultAccessor.GetSecretAsync("https://superSecretUri")).Result;
This still lets you use .Result if you're in a non-async method
Unfortunately, the call never returns and the query hangs indefinitely...
You have a classic deadlock. That's why you shouldn't block on async code. Behind the scenes, the compiler generates a state-machine and captures something called a SynchronizationContext. When you synchronously block the calling thread, the attempt to post the continuation back onto that same context causes the deadlock.
Instead of synchronously blocking with .Result, make your controller async and await on the Task returned from GetSecret:
public async IHttpActionResult FooAsync()
{
var secret = await KeyVaultAccessor.GetSecretAsync("https://superSecretUri");
return Ok();
}
Side note - Async methods should follow naming conventions and be postfixed with Async.
Use the rest api...
public class AzureKeyVaultClient
{
public string GetSecret(string name, string vault)
{
var client = new RestClient($"https://{vault}.vault.azure.net/");
client.Authenticator = new AzureAuthenticator($"https://vault.azure.net");
var request = new RestRequest($"secrets/{name}?api-version=2016-10-01");
request.Method = Method.GET;
var result = client.Execute(request);
if (result.StatusCode != HttpStatusCode.OK)
{
Trace.TraceInformation($"Unable to retrieve {name} from {vault} with response {result.Content}");
var exception = GetKeyVaultErrorFromResponse(result.Content);
throw exception;
}
else
{
return GetValueFromResponse(result.Content);
}
}
public string GetValueFromResponse(string content)
{
var result = content.FromJson<keyvaultresponse>();
return result.value;
}
public Exception GetKeyVaultErrorFromResponse(string content)
{
try
{
var result = content.FromJson<keyvautlerrorresponse>();
var exception = new Exception($"{result.error.code} {result.error.message}");
if(result.error.innererror!=null)
{
var innerException = new Exception($"{result.error.innererror.code} {result.error.innererror.message}");
}
return exception;
}
catch(Exception e)
{
return e;
}
}
class keyvaultresponse
{
public string value { get; set; }
public string contentType { get; set; }
}
class keyvautlerrorresponse
{
public keyvaulterror error {get;set;}
}
class keyvaulterror
{
public string code { get; set; }
public string message { get; set; }
public keyvaulterror innererror { get; set; }
}
class AzureAuthenticator : IAuthenticator
{
private string _authority;
private string _clientId;
private string _clientSecret;
private string _resource;
public AzureAuthenticator(string resource)
{
_authority = WebConfigurationManager.AppSettings["azure:Authority"];
_clientId = WebConfigurationManager.AppSettings["azure:ClientId"];
_clientSecret = WebConfigurationManager.AppSettings["azure:ClientSecret"];
_resource = resource;
}
public AzureAuthenticator(string resource, string tennant, string clientid, string secret)
{
//https://login.microsoftonline.com/<tennant>/oauth2/oken
_authority = authority;
//azure client id (web app or native app
_clientId = clientid;
//azure client secret
_clientSecret = secret;
//vault.azure.net
_resource = resource;
}
public void Authenticate(IRestClient client, IRestRequest request)
{
var token = GetS2SAccessTokenForProdMSA().AccessToken;
//Trace.WriteLine(String.Format("obtained bearer token {0} from ADAL and adding to rest request",token));
request.AddHeader("Authorization", String.Format("Bearer {0}", token));
}
public AuthenticationResult GetS2SAccessTokenForProdMSA()
{
return GetS2SAccessToken(_authority, _resource, _clientId, _clientSecret);
}
private AuthenticationResult GetS2SAccessToken(string authority, string resource, string clientId, string clientSecret)
{
var clientCredential = new ClientCredential(clientId, clientSecret);
AuthenticationContext context = new AuthenticationContext(authority, false);
AuthenticationResult authenticationResult = context.AcquireToken(
resource,
clientCredential);
return authenticationResult;
}
}
}
This generic method can be used to override the deadlock issue:
public static T SafeAwaitResult<T>(Func<Task<T>> f)
{
return Task.Run(async () => await f()).Result;
}
Use:
SafeAwaitResult(() => foo(param1, param2));