I am trying to use SimpleInjector in a WPF Application (.NET Framework). We use it in exactly the same way in many of our Services but for some reason when I am attempting to implement the same logic in this WPF Application, the call to the HttpClient().GetAsync is hanging. We think it is because for some reason the Task is not executing.
I am registering the objects from the OnStartUp element of App.xaml.cs as below. Inside the SetupService Constructor we call a SetupService URL (set in the SetupConfiguration Section of the App.Config) to get the SetupResponse to use in the app.
It is ultimately hanging in the ServiceClient.GetAsync method, I have tried to show the flow below:
All classes appear to have been injected correctly, and the ServiceClient is populated in exactly the same way as the same point in one of our working services. We're at a loss as to what is happening, and how to fix this.
Finally, SetupService is being injected in other Classes - so I would rather get it working like this, rather than remove the call from the SimpleInjector mechanism.
Any help is very much appreciated.
public partial class App : Application
{
private static readonly Container _container = new Container();
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
RegisterDependencies();
_container.Verify();
}
private void RegisterDependencies()
{
var serviceConfigSection = ServiceConfigurationSection.Get();
_container.RegisterSingle<ILoggingProvider, LoggingProvider>();
_container.RegisterSingle<IServiceClient>(() => new ServiceClient(_container.GetInstance<ILoggingProvider>()));
_container.RegisterSingle<IConfigurationSection>(() => SetupConfigurationSection.Get());
_container.RegisterSingle<ISetupService, SetupService>();
}
}
public class SetupService: ISetupService
{
private static readonly Dictionary<string, string> AcceptType = new Dictionary<string, string>
{
{"Accept", "application/xml"}
};
private const string AuthenticationType = "Basic";
private readonly IServiceClient _serviceClient;
private readonly ILoggingProvider _logger;
private readonly IConfigurationSection _configuration;
public SetupService(IConfigurationSection configuration, IServiceClient serviceClient, ILoggingProvider logger)
{
_serviceClient = serviceClient;
_logger = logger;
_configuration = kmsConfiguration;
RefreshSetup();
}
public void RefreshSetup()
{
try
{
var token = BuildIdentityToken();
var authHeaderClear = string.Format("IDENTITY_TOKEN:{0}", token);
var authenticationHeaderValue =
new AuthenticationHeaderValue(AuthenticationType, Convert.ToBase64String(Encoding.ASCII.GetBytes(authHeaderClear)));
_serviceClient.Url = _configuration.Url;
var httpResponse = _serviceClient.GetAsync(string.Empty, authenticationHeaderValue, AcceptType).Result;
var responseString = httpResponse.Content.ReadAsStringAsync().Result;
_response = responseString.FromXML<SetupResponse>();
}
catch (Exception e)
{
throw
}
}
public class ServiceClient : IServiceClient
{
private const string ContentType = "application/json";
private string _userAgent;
private ILoggingProvider _logger;
public string Url { get; set; }
public string ProxyAddress { get; set; }
public int TimeoutForRequestAndResponseMs { get; set; }
public int HttpCode { get; private set; }
public ServiceClient(ILoggingProvider logger = null)
{
_logger = logger;
}
public async Task<HttpResponseMessage> GetAsync(string endpoint, AuthenticationHeaderValue authenticationHeaderValue = null, IDictionary<string, string> additionalData = null, IDictionary<string, string> additionalParams = null)
{
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(Url);
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(ContentType));
if (authenticationHeaderValue != null)
client.DefaultRequestHeaders.Authorization = authenticationHeaderValue;
ProcessHeader(client.DefaultRequestHeaders, additionalData);
var paramsQueryString = ProcessParams(additionalParams);
if (!string.IsNullOrEmpty(paramsQueryString))
endpoint = $"{endpoint}?{paramsQueryString}";
return await client.GetAsync(endpoint); **// HANGS ON THIS LINE!**
}
}
}
}
If you block on asynchronous code from a UI thread, then you can expect deadlocks. I explain this fully on my blog. In this case, the cause of the deadlock is Result. There's a couple of solutions.
The one I recommend is to rethink your user experience. Your UI shouldn't be blocking on an HTTP call to complete before it shows anything; instead, immediately (and synchronously) display a UI (i.e., some "loading..." screen), and then update that UI when the HTTP call completes.
The other is to block during startup. There's a few patterns for this. None of them work in all situations, but one that usually works is to wrap the asynchronous work in Task.Run and then block on that, e.g., var httpResponse = Task.Run(() => _serviceClient.GetAsync(string.Empty, authenticationHeaderValue, AcceptType)).GetAwaiter().GetResult(); and similar for other blocking calls.
Blocking before showing a UI is generally considered a bad UX. App stores generally disallow it. So that's why I recommend changing the UX. You may find an approach like this helpful.
Thanks for your Responses, I just wanted to sync the solution I've gone for.
It was risky for me to change the code in SetupService to remove the .Result, even though this was probably the correct solution, as I did not want to affect the other working Services using the SetupService library already there.
I ended up moving the regsitrations off the UI Thread by embedding the SimpleInjector code in a Code library, Creating a Program.cs and Main() and setting that as my Entry point.
static class Program
{
public static readonly Container _container = new Container();
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
public static void Main(){
var app = new MyApp.App();
Register();
app.Run(_container.GetInstance<MainWindow>());
}
static void Register()
{
_container.Register<MainWindow>();
MySimpleInjector.Register(_container);
_container.Verify();
}
}
and then, in a Separate .dll project, MyApp.Common
public class MySimpleInjector
{
private readonly Container _container;
public static void Register(Container container)
{
var injector = new MySimpleInjector(container);
}
private void RegisterDependencies()
{
var serviceConfigSection = ServiceConfigurationSection.Get();
_container.RegisterSingle<ILoggingProvider, LoggingProvider>();
_container.RegisterSingle<IServiceClient>(() => new ServiceClient(_container.GetInstance<ILoggingProvider>()));
_container.RegisterSingle<IConfigurationSection>(() => SetupConfigurationSection.Get());
_container.RegisterSingle<ISetupService, SetupService>();
}
}
I appreciate that this may not be the ideal solution - but it suits my purposes.
Again, thanks for your help and comments!
Andrew.
Related
Currently i'm designing a logger service to log HttpRequests made by HttpClient.
This logger service is Singleton and i want to have scoped contexts inside it.
Here's my logger:
public Logger
{
public LogContext Context { get; set; }
public void LogRequest(HttpRequestLog log)
{
context.AddLog(log);
}
}
I'm using the logger inside a DelegatingHandler:
private readonly Logger logger;
public LoggingDelegatingHandler(Logger logger)
{
this.logger = logger;
}
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
await base.SendAsync(request, cancellationToken);
this.logger.LogRequest(new HttpRequestog());
}
Then when i make some request using HttpClient, i want to have the logs for this specific call:
private void InvokeApi()
{
var logContext = new LogContext();
this.Logger.LogContext = logContext;
var httpClient = httpClientFactory.CreateClient(CustomClientName);
await httpClient.GetAsync($"http://localhost:11111");
var httpRequestLogs = logContext.Logs;
}
The problem is, it works but it's not thread safe. If i have parallel executions of InvokeApi, the context will not be the correct.
How can i have a attached context for each execution properly?
I'm registering the HttpClient like this:
services.AddSingleton<Logger>();
services.AddHttpClient(CentaurusHttpClient)
.ConfigurePrimaryHttpMessageHandler((c) => new HttpClientHandler()
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
})
.AddHttpMessageHandler(sp => new LoggingDelegatingHandler(sp.GetRequiredService<Logger>()));
I'm testing this piece of code using this:
public void Test_Parallel_Logging()
{
Random random = new Random();
Action test = async () =>
{
await RunScopedAsync(async scope =>
{
IServiceProvider sp = scope.ServiceProvider;
var httpClientFactory = sp.GetRequiredService<IHttpClientFactory>();
using (var context = new HttpRequestContext(sp.GetRequiredService<Logger>()))
{
try
{
var httpClient = httpClientFactory.CreateClient();
await httpClient.GetAsync($"http://localhost:{random.Next(11111, 55555)}");
}
catch (HttpRequestException ex)
{
Output.WriteLine("count: " + context?.Logs?.Count);
}
}
});
};
Parallel.Invoke(new Action[] { test, test, test });
}
This logger service is Singleton and i want to have scoped contexts inside it.
The Logger has a singleton lifetime, so its LogContext property also has a singleton lifetime.
For the kind of scoped data you're wanting, AsyncLocal<T> is an appropriate solution. I tend to prefer following a "provider"/"consumer" pattern similar to React's Context, although the more common name in the .NET world for "consumer" is "accessor". In this case, you could make the Logger itself into the provider, though I generally try to keep it a separate type.
IMO this is most cleanly done by providing your own explicit scope, and not tying into the "scope" lifetime of your DI container. It's possible to do it with DI container scopes but you end up with some weird code like resolving producers and then doing nothing with them - and if a future maintainer removes (what appears to be) unused injected types, then the accessors break.
So I recommend your own scope, as such:
public Logger
{
private readonly AsyncLocal<LogContext> _context = new();
public void LogRequest(HttpRequestLog log)
{
var context = _context.Value;
if (context == null)
throw new InvalidOperationException("LogRequest was called without anyone calling SetContext");
context.AddLog(log);
}
public IDisposable SetContext(LogContext context)
{
var oldValue = _context.Value;
_context.Value = context;
// I use Nito.Disposables; replace with whatever Action-Disposable type you have.
return new Disposable(() => _context.Value = oldValue);
}
}
Usage (note the explicit scoping provided by using):
private void InvokeApi()
{
var logContext = new LogContext();
using (this.Logger.SetContext(logContext))
{
var httpClient = httpClientFactory.CreateClient(CustomClientName);
await httpClient.GetAsync($"http://localhost:11111");
var httpRequestLogs = logContext.Logs;
}
}
I am trying to write Xamarin.Forms UI tests using Moq to mock my authentication interface: [previous question][1]. I have refactored my application so that my SignIn(string username, string password) method is inside a class that implements the IAuthService. I am now having issues with mocking the IAuthService to essentially 'replace' the actual sign in verification that occurs when clicking the Sign In button. In my CloudAuthService class (which implements IAuthService), I am authenticating to Amazon Cognito, but I want to mock this result within the UI test so it is not calling the cloud service.
EDIT: after many suggestions, I have decided to include my current implementation below. This still doesn't appear to fully work despite the
output from Console.WriteLine(App.AuthApi.IsMockService()); within the BeforeEachTest() method results in true (as expected).
However, running the same thing within the App() constructor method results in false. So it doesn't appear to be running before the app actually starts, is there a way to have UITest code that runs before the app initializes?
LoginPage
[XamlCompilation(XamlCompilationOptions.Compile)]
public sealed partial class LoginPage
{
private readonly IBiometricAuthentication _bioInterface;
private static readonly Lazy<LoginPage>
Lazy =
new Lazy<LoginPage>
(() => new LoginPage(App.AuthApi));
public static LoginPage Instance => Lazy.Value;
private string _username;
private string _password;
private LoginPageViewModel _viewModel;
private IAuthService _authService;
public LoginPage(IAuthService authService)
{
InitializeComponent();
_authService = authService;
_viewModel = new LoginPageViewModel();
BindingContext = _viewModel;
}
private void LoginButtonClicked(object sender, EventArgs args)
{
_username = UsernameEntry.Text;
_password = PasswordEntry.Text;
LoginToApplication();
}
public async void LoginToApplication()
{
AuthenticationContext context = await _authService.SignIn(_username, _password);
}
}
App Class
public partial class App
{
public static IAuthService AuthApi { get; set; } = new AWSCognito()
public App()
{
Console.WriteLine(AuthApi.IsMockService())
// AuthApi = new AWSCognito(); // AWSCognito implements IAuthService
InitializeComponent();
MainPage = new NavigationPage(new LoginPage(AuthApi));
}
}
Test Class
class LoginPageTest
{
IApp app;
readonly Platform platform;
public LoginPageTest(Platform platform)
{
this.platform = platform;
}
[SetUp]
public void BeforeEachTest()
{
var mocker = new Mock<IAuthService>();
var response = new AuthenticationContext(CognitoResult.Ok)
{
IdToken = "SUCCESS_TOKEN"
};
mocker.Setup(x => x.SignIn(It.IsAny<string>(), It.IsAny<string>())).Returns(() => new MockAuthService().SignIn("a", "a"));
mocker.Setup(x => x.IsMockService()).Returns(() => new MockAuthService().IsMockService());
App.AuthApi = mocker.Object;
Console.WriteLine(App.AuthApi.IsMockService());
app = AppInitializer.StartApp(platform);
}
[Test]
public void ClickingLoginWithUsernameAndPasswordStartsLoading()
{
app.WaitForElement(c => c.Marked("Welcome"));
app.EnterText(c => c.Marked("Username"), new string('a', 1));
app.EnterText(c => c.Marked("Password"), new string('a', 1));
app.Tap("Login");
bool state = app.Query(c => c.Class("ProgressBar")).FirstOrDefault().Enabled;
Assert.IsTrue(state);
}
}
Your problem seems to be that you've injected the mock after you run through the test. This means when it's executing it's using the original AuthService. If we rearrange the code to move the injection before anything gets executed we should see the result we expect:
// let's bring this mock injection up here
var mocker = new Mock<IAuthService>();
mocker.Setup(x => x.SignIn(It.IsAny<string>(), It.IsAny<string>())).Returns(Task.FromResult(response)).Verifiable();
App.AuthApi = mocker.Object;
// now we try to login, which should call the mock methods of the auth service
app.WaitForElement(c => c.Marked("Welcome to Manuly!"));
app.EnterText(c => c.Marked("Username"), new string('a', 1));
app.EnterText(c => c.Marked("Password"), new string('a', 1));
app.Tap("Login");
var response = new AuthenticationContext(CognitoResult.Ok)
{
IdToken = "SUCCESS_TOKEN",
};
bool state = app.Query(c => c.Class("ProgressBar")).FirstOrDefault().Enabled;
Assert.IsTrue(state);
Now try executing it, and it should do as you desire.
EDIT:
As pointed out in the comments by Nkosi the static Auth service is set in the constructor preventing this.
SO this will need to be changed too:
public partial class App
{
public static IAuthService AuthApi { get; set; } =new AWSCognito(); // assign it here statically
public App()
{
// AuthApi = new AWSCognito(); <-- remove this
InitializeComponent();
MainPage = new NavigationPage(new LoginPage(AuthApi));
}
}
After reading this blog post and thisofficial note on www.asp.net:
HttpClient is intended to be instantiated once and re-used throughout
the life of an application. Especially in server applications,
creating a new HttpClient instance for every request will exhaust the
number of sockets available under heavy loads. This will result in
SocketException errors.
I discovered that our code was disposing the HttpClient on each call. I'm updating our code so that we reuse the HttClient, but I'm concerned our implement but not thread-safe.
Here is the current draft of new code:
For Unit Testing, we implemented an wrapper for HttpClient, the consumers call the wrapper:
public class HttpClientWrapper : IHttpClient
{
private readonly HttpClient _client;
public Uri BaseAddress
{
get
{
return _client.BaseAddress;
}
set
{
_client.BaseAddress = value;
}
}
public HttpRequestHeaders DefaultRequestHeaders
{
get
{
return _client.DefaultRequestHeaders;
}
}
public HttpClientWrapper()
{
_client = new HttpClient();
}
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, String userOrProcessName)
{
IUnityContainer container = UnityCommon.GetContainer();
ILogService logService = container.Resolve<ILogService>();
logService.Log(ApplicationLogTypes.Debug, JsonConvert.SerializeObject(request), userOrProcessName);
return _client.SendAsync(request);
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing && _client != null)
{
_client.Dispose();
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(true);
}
#endregion
}
Here is a service that calls:
public class EnterpriseApiService : IEnterpriseApiService
{
private static IHttpClient _client;
static EnterpriseApiService()
{
IUnityContainer container = UnityCommon.GetContainer();
IApplicationSettingService appSettingService = container.Resolve<IApplicationSettingService>();
_client = container.Resolve<IHttpClient>();
}
public EnterpriseApiService() { }
public Task<HttpResponseMessage> CallApiAsync(Uri uri, HttpMethod method, HttpContent content, HttpRequestHeaders requestHeaders, bool addJsonMimeAccept = true)
{
IUnityContainer container = UnityCommon.GetContainer();
HttpRequestMessage request;
_client.BaseAddress = new Uri(uri.GetLeftPart(UriPartial.Authority));
if (addJsonMimeAccept)
_client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request = new HttpRequestMessage(method, uri.AbsoluteUri);
// Removed logic that built request with content, requestHeaders and method
return _client.SendAsync(request, UserOrProcessName);
}
}
My questions:
Is this an appropriate approach to reuse the HttpClient object?
Is the static _httpClient field (populated with the static constructor) shared for all instances of EnterpriseApiService? I wanted to confirm since is being called by instance methods.
When CallApiAsync() is called, when that makes changes to the static HttpClient, such as the "_client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"))" could those values be overwriten by another process before the last line "_client.SendAsync" is called? I'm concerned that halfway through processing CallApiAsync() the static instance is updated.
Since it is calling SendAsync(), are we guaranteed the response is mapped back to the correct caller? I want to confirm the response doesn't go to another caller.
Update:
Since I've removed the USING statements, and the Garage Collection doesn't call Dispose, I'm going to go with the safer approach of creating a new instance within the method. To reuse an instance of HttpClient even within the thread lifetime, it would require a significant reworking of the logic because the method sets HttpClient properties per call.
Do you really want one instance?
I don't think you want one instance application-wide. You want one instance per thread. Otherwise you won't get very good performance! Also, this will resolve your questions #3 and #4, since no two threads will be accessing the same HttpClient at the same time.
You don't need a singleton
Just use Container.Resolve with the PerThreadLifetimeManager.
For those lucky enough to be using .NET Core this is fairly straightforward.
As John Wu so eloquently stated, you don't want a singleton per se, but rather a singleton per request. As such, the AddScoped<TService>() method is what you're after.
In your ConfigureServices(IServiceCollection services) method:
services.AddScoped<HttpClient>();
To consume:
public class HomeController
{
readonly HttpClient client;
public HomeController (HttpClient client)
{
this.client = client;
}
//rest of controller code
}
Since it is calling SendAsync(), are we guaranteed the response is mapped back to the correct caller? I want to confirm the response doesn't go to another caller.
This will be handled via callback pointers. It has nothing to do with using HttpClient as singleton. More details here - https://stackoverflow.com/a/42307650/895724
This is what i use
public abstract class BaseClient : IDisposable
{
private static object locker = new object();
private static volatile HttpClient httpClient;
protected static HttpClient Client
{
get
{
if (httpClient == null)
{
lock (locker)
{
if (httpClient == null)
{
httpClient = new HttpClient();
}
}
}
return httpClient;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (httpClient != null)
{
httpClient.Dispose();
}
httpClient = null;
}
}
}
Its used in the extension method like this:
public static Task<HttpResponseMessage> PostAsJsonAsync<T>(
this HttpClient httpClient, string url, T data, string token, IDictionary<string, string> dsCustomHeaders = null)
{
ThrowExceptionIf.Argument.IsNull(httpClient, nameof(httpClient));
var dataAsString = JsonConvert.SerializeObject(data);
var httpReqPostMsg = new HttpRequestMessage(HttpMethod.Post, url)
{
Content = new StringContent(dataAsString, Encoding.UTF8, "application/json")
};
httpReqPostMsg.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
httpReqPostMsg.Headers.Add(Constants.TelemetryCorrelationKey, Utilities.GetRequestCorrelationId());
if (dsCustomHeaders != null) {
foreach (var keyValue in dsCustomHeaders)
{
httpReqPostMsg.Headers.Add(keyValue.Key, keyValue.Value);
}
}
return httpClient.SendAsync(httpReqPostMsg);
}
I do requests to Github Api so I have async methods, these do this job. Before it, I always called they in method, that calls from command(actually DelegateCommand). But now I wanna do request in ViewModel because I need to display list on page. I am using Prism to wire view and viewmodel.
Because I can't make viewmodel async, I can't use await word, so I tried to do something like gets result from task, or task.wait. But with this I have the same result. My app stop works with white display when it did request. I read some info about that and I understood that call async method in not async method is bad, and it causes deadlock, but I don't know what to do with this. And I think deadlock causes that app stop works.
Here is method where app die:
public async Task<IEnumerable<RepositoryModel>> GetRepositoriesAsync()
{
try
{
var reposRequest = new RepositoryRequest { Sort = RepositorySort.FullName };
var gitHubRepos = await _gitHubClient.Repository.GetAllForCurrent(reposRequest); //async request, don't say about name convention, it is not my method.
var gitRemoteRepos = new List<RepositoryModel>();
foreach ( var repository in gitHubRepos )
{
var repos = new RepositoryModel();
repos.RepositoryTypeIcon = GetRepositoryTypeIcon(repository);
gitRemoteRepos.Add(repos);
}
return gitRemoteRepos;
}
catch ( WebException ex )
{
throw new Exception("Something wrong with internet connection, try to On Internet " + ex.Message);
}
catch ( Exception ex )
{
throw new Exception("Getting repos from github failed! " + ex.Message);
}
}
And here is viewmodel:
public class RepositoriesPageViewModel : BindableBase
{
private INavigationService _navigationService;
private readonly Session _session;
public ObservableCollection<RepositoryModel> Repositories { get; }
private readonly RepositoriesManager _repositoriesManager;
public RepositoriesPageViewModel(INavigationService navigationService, ISecuredDataProvider securedDataProvider)
{
_navigationService = navigationService;
var token = securedDataProvider.Retreive(ConstantsService.ProviderName, UserManager.GetLastUser());
_session = new Session(UserManager.GetLastUser(), token.Properties.First().Value);
var navigationParameters = new NavigationParameters { { "Session", _session } };
_repositoriesManager = new RepositoriesManager(_session);
var task = _repositoriesManager.GetRepositoriesAsync();
//task.Wait();
Repositories = task.Result as ObservableCollection<RepositoryModel>;
}
}
I recommend using my NotifyTask<T> type, which provides a data-bindable wrapper around Task<T>. I explain this pattern more completely in my article on async MVVM data binding.
public class RepositoriesPageViewModel : BindableBase
{
private INavigationService _navigationService;
private readonly Session _session;
public NotifyTask<ObservableCollection<RepositoryModel>> Repositories { get; }
private readonly RepositoriesManager _repositoriesManager;
public RepositoriesPageViewModel(INavigationService navigationService, ISecuredDataProvider securedDataProvider)
{
_navigationService = navigationService;
var token = securedDataProvider.Retreive(ConstantsService.ProviderName, UserManager.GetLastUser());
_session = new Session(UserManager.GetLastUser(), token.Properties.First().Value);
var navigationParameters = new NavigationParameters { { "Session", _session } };
_repositoriesManager = new RepositoriesManager(_session);
Repositories = NotifyTask.Create(GetRepositoriesAsync());
}
}
private async Task<ObservableCollection<RepositoryModel>> GetRepositoriesAsync()
{
return new ObservableCollection<RepositoryModel>(await _repositoriesManager.GetRepositoriesAsync());
}
Note that with this approach, your data binding would use Repositories.Result to access the actual collection. Other properties are also available, most notably Repositories.IsCompleted and Respositories.IsNotCompleted for showing/hiding busy spinners.
I'm writing a class that uses HttpClient to access an API and I want to throttle the number of concurrent calls that can be made to a certain function in this class. The trick is though that the limit is per tenant and multiple tenants might be using their own instance of the class at a time.
My Tenant class is just a container for read-only context information.
public class Tenant
{
public string Name { get; }
public string ApiKey { get; }
}
Here's the ApiClient:
public class ApiClient
{
private readonly Tenant tenant;
public ApiClient(Tenant tenant)
{
this.tenant = tenant;
}
public async Task<string> DoSomething()
{
var response = await this.SendCoreAsync();
return response.ToString();
}
private Task<XElement> SendCore()
{
using (var httpClient = new HttpClient())
{
var httpRequest = this.BuildHttpRequest();
var httpResponse = await httpClient.SendAsync(httpRequest);
return XElement.Parse(await httpResponse.Content.ReadAsStringAsync());
}
}
}
What I want to do is throttle the SendCore method and limit it to two concurrent requests per tenant. I've read suggestions of using TPL or SemaphoreSlim to do basic throttling (such as here: Throttling asynchronous tasks), but I'm not clear on how to add in the further complication of the tenant.
Thanks for the suggestions.
UPDATE
I've attempted to use a set of SemaphoreSlim objects (one per tenant) contained in a ConcurrentDictionary. This seems to work, but I'm not sure if this is ideal. The new code is:
public class ApiClient
{
private static readonly ConcurrentDictionary<string, SemaphoreSlim> Semaphores = new ConcurrentDictionary<string, SemaphoreSlim>();
private readonly Tenant tenant;
private readonly SemaphoreSlim semaphore;
public ApiClient(Tenant tenant)
{
this.tenant = tenant;
this.semaphore = Semaphores.GetOrAdd(this.tenant.Name, k => new SemaphoreSlim(2));
}
public async Task<string> DoSomething()
{
var response = await this.SendCoreAsync);
return response.ToString();
}
private Task<XElement> SendCore()
{
await this.semaphore.WaitAsync();
try
{
using (var httpClient = new HttpClient())
{
var httpRequest = this.BuildHttpRequest();
var httpResponse = await httpClient.SendAsync(httpRequest);
return XElement.Parse(await httpResponse.Content.ReadAsStringAsync());
}
}
finally
{
this.semaphore.Release();
}
}
}
Your SemaphoreSlim approach seems mostly reasonable to me.
One potential issue is that if Tenants can come and go over the lifetime of the application, then you'll be keeping semaphores even for Tenants that don't exist anymore.
A solution to that would be to use ConditionalWeakTable<Tenant, SemaphoreSlim> instead of your ConcurrentDictionary, which makes sure its keys can be garbage collected and when they are, it releases the value.