Recently started with ReactiveUI and so far so good. While trying to figure out stuff I came across a HttpClient / Refit problem. I have a long list and every item in that list have to make a request to a API as follows.
This is a ReactiveObject
public SearchResult()
{
LoadStats = ReactiveCommand.CreateAsyncTask(_ => DownloadMultipleCalls(this.IdText, this.DisplayText));
LoadStats
.ThrownExceptions
.SubscribeOn(RxApp.MainThreadScheduler)
.Subscribe(ex => UserError.Throw("Couldn't load stats", ex));
_avaText = LoadStats.Select(x => x.Ava).ToProperty(this, x => x.AvaText, string.Empty);
_perText = LoadStats.Select(x => x.Per).ToProperty(this, x => x.PerText, string.Empty);
this.WhenAnyValue(x => x.IdText)
.Where(x => !string.IsNullOrWhiteSpace(x))
//.Select(_ => Observable.Timer(DateTimeOffset.MinValue, TimeSpan.FromSeconds(1), RxApp.MainThreadScheduler))
.Retry(5)
.InvokeCommand(LoadStats);
}
In DownloadMultipleCalls the following is happening.
async static Task<AvaPer> DownloadMultipleCalls(string id, string displaytext)
{
AvaPer response = new AvaPer();
var block = new ActionBlock<string>(async service =>
{
response = await DownloadCall(service, displaytext);
},
new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 4 });
block.Post(id);
block.Complete();
await block.Completion;
return response;
}
async static Task<AvaPer> DownloadCall(string id, string displaytext)
{
System.Diagnostics.Debug.WriteLine($"Starting download at {DateTimeOffset.Now}");
var client = new HttpClient(NetCache.UserInitiated)
{
BaseAddress = new Uri("https://api.adres.com"),
};
//client.Timeout = TimeSpan.FromMilliseconds(10);
var token = GetToken;
client.DefaultRequestHeaders.Add("authToken", token);
var api = RestService.For<IMyApi>(client);
AvaPer idEntries = new AvaPer();
try
{
var searchResult = await api.GetAvaPerStatusRaw(id); // <== Here is the error (400 - Bad Request)
//var parsedEntries = Task.Run(() => {
idEntries = new AvaPer
{
Id = searchResult.Id.ToString() ?? string.Empty,
display = displaytext ?? string.Empty,
Ava = searchResult.AvailabilityStatus ?? string.Empty,
Per = searchResult.PerformanceStatus ?? string.Empty,
Updated = DateTimeOffset.Now.ToLocalTime().ToString() ?? string.Empty
};
//}).ConfigureAwait(false);
//return idEntries;
}
catch (ApiException ex)
{
var statusCode = ex.StatusCode;
var error = ex.GetContentAs<Models.ServiceModel.ErrorResponse>();
}
return idEntries;
}
Finally it goes all wrong with this line of code var searchResult = await api.GetAvaPerStatusRaw(id); The app crashes with a response "400 Bad Request" and if I look into the error it sais that the there is no id value inplace of the GET request. But when I check the initial value of id there is a value so it should not respond with that. Can anyone help me with this? If you have any question, please do not hesitate to ask. Thank you very much.
Kind regards,
Fernando
Related
I'm working on an application based on .NET Framework 4.8. I'm using Microsoft Batching API. The below are code snippets
public async Task<List<BatchResponse>> UpdateEventsInBatchAsync(string accessToken, Dictionary<int, Tuple<string, OfficeEvent>> absEvents)
{
var httpMethod = new HttpMethod("PATCH");
var batches = GetUpdateRequestBatches(absEvents, httpMethod);
var graphClient = GetGraphClient(accessToken);
var batchResponses = new List<BatchResponse>();
foreach (var batch in batches)
{
try
{
var batchResponseList = await ExecuteBatchRequestAsync(graphClient, batch).ConfigureAwait(false);
batchResponses.AddRange(batchResponseList);
}
catch (ClientException exc)
{
_logService.LogException("Error while processing update batch", exc);
batchResponses.Add(new BatchResponse
{ StatusCode = HttpStatusCode.InternalServerError, ReasonPhrase = exc.Message });
}
catch (Exception exc)
{
_logService.LogException("Error while processing update batch", exc);
batchResponses.Add(new BatchResponse { StatusCode = HttpStatusCode.InternalServerError, ReasonPhrase = exc.Message });
}
}
return batchResponses;
}
The respective methods used in the above code are mentioned below in respective order-
GetUpdateRequestBatches
private IEnumerable<BatchRequestContent> GetUpdateRequestBatches(Dictionary<int, Tuple<string, OfficeEvent>> absEvents, HttpMethod httpMethod)
{
var batches = new List<BatchRequestContent>();
var batchRequestContent = new BatchRequestContent();
const int maxNoBatchItems = 20;
var batchItemsCount = 0;
foreach (var kvp in absEvents)
{
System.Diagnostics.Debug.Write($"{kvp.Key} --- ");
System.Diagnostics.Debug.WriteLine(_serializer.SerializeObject(kvp.Value.Item2));
var requestUri = $"{_msOfficeBaseApiUrl}/me/events/{kvp.Value.Item1}";
var httpRequestMessage = new HttpRequestMessage(httpMethod, requestUri)
{
Content = _serializer.SerializeAsJsonContent(kvp.Value.Item2)
};
var requestStep = new BatchRequestStep(kvp.Key.ToString(), httpRequestMessage);
batchRequestContent.AddBatchRequestStep(requestStep);
batchItemsCount++;
// Max number of 20 request per batch. So we need to send out multiple batches.
if (batchItemsCount > 0 && batchItemsCount % maxNoBatchItems == 0)
{
batches.Add(batchRequestContent);
batchRequestContent = new BatchRequestContent();
batchItemsCount = 0;
}
}
if (batchRequestContent.BatchRequestSteps.Count < maxNoBatchItems)
{
batches.Add(batchRequestContent);
}
if (batches.Count == 0)
{
batches.Add(batchRequestContent);
}
return batches;
}
GetGraphClient
private static GraphServiceClient GetGraphClient(string accessToken)
{
var graphClient = new GraphServiceClient(new DelegateAuthenticationProvider(requestMessage =>
{
requestMessage
.Headers
.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
return Task.FromResult(0);
}));
return graphClient;
}
ExecuteBatchRequestAsync
private async Task<List<BatchResponse>> ExecuteBatchRequestAsync(IBaseClient graphClient, BatchRequestContent batch)
{
BatchResponseContent response = await graphClient.Batch.Request().PostAsync(batch);
Dictionary<string, HttpResponseMessage> responses = await response.GetResponsesAsync();
var batchResponses = new List<BatchResponse>();
var failedReqKeys = new Dictionary<string, TimeSpan>();
foreach (var key in responses.Keys)
{
using (HttpResponseMessage httpResponseMsg = await response.GetResponseByIdAsync(key))
{
var responseContent = await httpResponseMsg.Content.ReadAsStringAsync();
string eventId = null;
var reasonPhrase = httpResponseMsg.ReasonPhrase;
if (!string.IsNullOrWhiteSpace(responseContent))
{
var eventResponse = JObject.Parse(responseContent);
eventId = (string)eventResponse["id"];
// If still null, then might error have occurred
if (eventId == null)
{
var errorResponse = _serializer.DeserializeObject<ErrorResponse>(responseContent);
var error = errorResponse?.Error;
if (error != null)
{
if (httpResponseMsg.StatusCode == (HttpStatusCode)429)
{
System.Diagnostics.Debug.WriteLine($"{httpResponseMsg.StatusCode} {httpResponseMsg.Content}");
var executionDelay = httpResponseMsg.Headers.RetryAfter.Delta ?? TimeSpan.FromSeconds(5);
failedReqKeys.Add(key, executionDelay);
continue;
}
reasonPhrase = $"{error.Code} - {error.Message}";
}
}
}
var batchResponse = new BatchResponse
{
Key = key,
EventId = eventId,
StatusCode = httpResponseMsg.StatusCode,
ReasonPhrase = reasonPhrase
};
batchResponses.Add(batchResponse);
}
}
if (failedReqKeys.Count == 0) return batchResponses;
return await HandleFailedRequestsAsync(graphClient, failedReqKeys, batch, batchResponses).ConfigureAwait(false);
}
HandleFailedRequestsAsync
private async Task<List<BatchResponse>> HandleFailedRequestsAsync(IBaseClient graphClient, Dictionary<string, TimeSpan> failedReqKeys, BatchRequestContent batch, List<BatchResponse> batchResponses)
{
// Sleep for the duration as suggested in RetryAfter
var sleepDuration = failedReqKeys.Values.Max();
Thread.Sleep(sleepDuration);
var failedBatchRequests = batch.BatchRequestSteps.Where(b => failedReqKeys.Keys.Contains(b.Key)).ToList();
var failedBatch = new BatchRequestContent();
foreach (var kvp in failedBatchRequests)
{
failedBatch.AddBatchRequestStep(kvp.Value);
}
var failedBatchResponses = await ExecuteBatchRequestAsync(graphClient, failedBatch);
batchResponses.AddRange(failedBatchResponses);
return batchResponses;
}
I'm getting an error as on the first line in method ExecuteBatchRequestAsync as
Microsoft.Graph.ClientException: Code: invalidRequest
Message: Unable to deserialize content.
---> System.ObjectDisposedException: Cannot access a closed Stream.
Can anyone nudge me where I'm doing wrong?
I am getting Azure AD users into a list to be used in a dropdown, but it takes about at least 8/9 seconds to do the call... I know this can probably be reduced... So I will place my code here, hoping that someone can give me a better idea of how to change the code to a better one.
public async Task<List<Microsoft.Graph.User>> getAzureUsers()
{
IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create("***")
.WithTenantId("***")
.WithClientSecret("***")
.Build();
ClientCredentialProvider authProvider = new ClientCredentialProvider(confidentialClientApplication);
GraphServiceClient graphClient = new GraphServiceClient(authProvider);
List<Microsoft.Graph.User> utilizadores = new List<Microsoft.Graph.User>();
var user = await graphClient.Users
.Request()
.Select(p => new {
p.DisplayName,
p.UserPrincipalName,
p.Id
})
.GetAsync();
utilizadores.AddRange(user.CurrentPage);
while (user.NextPageRequest != null)
{
user = await user.NextPageRequest.GetAsync();
utilizadores.AddRange(user.CurrentPage);
}
return utilizadores;
}
public async Task<ViewModelColaboradores> GetVM()
{
bool colExist = false;
List<Microsoft.Graph.User> utilizadores = new List<Microsoft.Graph.User>();
List<UserAD> nomes = new List<UserAD>();
utilizadores = await getAzureUsers();
ViewModelColaboradores vm = new ViewModelColaboradores();
foreach (var n in utilizadores)
{
foreach (var col in _context.RH_Colaboradores)
{
if (n.DisplayName == col.Nome)
{
colExist = true;
}
}
if (!colExist)
{
nomes.Add(new UserAD { DisplayName = n.DisplayName, Id = n.Id, Email = n.UserPrincipalName });
}
colExist = false;
}
vm.usersAd = nomes;
return vm;
}
and then I just call GETVM to get the viewmodel with the users inside. Any ideas?
You can use pagination to improve the performance.
You can have a search-based dropdown(non-paginated) or scroll based dropdown (paginated - that fetches new items on scrolling down and fetches scroll up results from cache).
C# sample code:
public async Task<AzureUserDto> GetUsersPage(int pageSize, string skipToken)
{
var filterString = $"startswith(givenName, '{firstName}')";
var queryOptions = new List<QueryOption>
{
new QueryOption("$top", pageSize)
};
if (!string.IsNullOrEmpty(skipToken))
{
queryOptions.Add(new QueryOption("$skiptoken", skipToken));
}
var azureUsers = await GraphServiceClient.Users
.Request(queryOptions)
.Filter(filterString)
.Select(x => new
{
x.Id,
x.DisplayName,
x.GivenName,
x.Surname,
x.UserPrincipalName,
x.AccountEnabled,
x.Identities,
x.BusinessPhones,
x.JobTitle,
x.MobilePhone,
x.OfficeLocation,
x.PreferredLanguage,
x.Mail
})
.GetAsync();
// Get SkipToken, if exists
var skipToken = azureUsers
.NextPageRequest?
.QueryOptions?
.FirstOrDefault(
x => string.Equals("$skiptoken", x.Name, StringComparison.InvariantCultureIgnoreCase))?
.Value;
var azureUserDto = new AzureUserDto
{
// Map the azureUsers to AzureUsersDto or something similar and return
// don't forget to include the SkipToken
};
return azureUsers;
}
For more details on how to implement the latter method, you can visit Paging Microsoft Graph data in your app and Microsoft Graph API: C# / Filtering / Pagination.
I'm trying to update some fields by script. In Postman I'm sending the following request:
https://search.test.com/items_example/item/en_01_2/_update
the request's body:
{
"script": {
"inline": "ctx._source.Title = params.Title; ctx._source.Desc = params.Desc",
"params": {
"Title": "New Title",
"Desc": "New Desc"
}
}
}
But I have no idea how I can send this request using NEST. Can someone please help me?
Elasticsearch 5.4.1 version, NEST 5.6.1
Update it with your index settings & query
var elasticClient = new ElasticClient(settings);
var scriptParams = new Dictionary<string, object>
{
{"Title", "New Title"},
{"Desc", "New Desc"}
};
var response = elasticClient
.UpdateByQuery<dynamic>(q => q.Query(rq => rq.Term(....))
.Script(script =>
script.Inline(
$"ctx._source.Title = params.Title;" +
$"ctx._source.Desc = params.Desc ;"
)
.Params(scriptParams));
Edit: If you're looking for just Update, just change the syntax to
var response = elasticClient.Update<dynamic>(
"items_example/item/en_01_2" //This is your document path
, request => request.Script(
script =>
script.Inline(
$"ctx._source.Title = params.Title;" +
$"ctx._source.Desc = params.Desc ;"
)
.Params(scriptParams)));
If someone is here looking for the solution with NEST 6x version, please see below
public async Task UpdateId(MetaData request)
{
try
{
var scriptParams = new Dictionary<string, object>
{
{ "Id",request.Id}
};
var script = $"ctx._source.Id= params.Id;";
var indexResponse =
await EsClient.UpdateByQueryAsync<Doc>(
qd => qd.Index(request.IndexName)
.Conflicts(Conflicts.Proceed)
.Query(
rq => rq.Term("_id", request._id))
.Script(
s => s.Source(script)
.Params(scriptParams)
)
)
);
if (!indexResponse.IsValid)
{
}
}
catch (Exception ex)
{
}
}
[ElasticsearchType(Name = "_doc")]
public class Doc
{
}
I am adding a new web API call to existing functionality. I want to make this API call async but looks like it is causing deadlock. I have to make a lot more changes if I want to make entire code channel async which is not possible.
Questions I have are:
Is it possible to call async method from regular method?
What am I missing here? OR What is the correct approach here?
Code:
// Exisitng Method
public Tuple<RestaurantDeliveryProvider, DeliveryHubResult, Task<DeliveryManagerQuoteResponse>> CreateDeliveryRequest(OrderContextDTO orderContextDto)
{
var provider = RestaurantBl.GetDeliveryProviderInformationByRestaurantId(orderContextDto.RestaurantId ?? 0);
var deliveryHubResult = RestaurantBl.GetDeliveryHubResult(orderContextDto.OrderId ?? 0);;
// New Call which always comes back with "Not Yet Computed" result
Task<DeliveryManagerQuoteResponse> deliveryManagerQuoteResponse = _deliveryManager.CreateQuoteRequestAsync(orderContextDto, orderInfo);
return Tuple.Create(provider, deliveryHubResult, deliveryManagerQuoteResponse);
}
Async Methods:
public async Task<DeliveryManagerQuoteResponse> CreateQuoteRequestAsync(OrderContextDTO orderContextDto, OrderInfoDTO orderInfo)
{
DeliveryManagerQuoteResponse deliveryManagerQuoteResponse = null;
try
{
var restaurantInfo = RestaurantApi.GetRestaurant(orderInfo.RestaurantId);
var quoteRequest = new DeliveryManagerQuoteRequest
{
DeliveryProvider = null,
Country = orderContextDto.DeliveryEstimateRequestDto.RequestedDeliveryAddress.Country,
Concept = "BK",
StoreName = "BK-TEST-US-4",
OrderId = orderInfo.OrderId.ToString(),
AllowCash = false,
PaymentType = OrderPaymentType.Prepaid_Credit,
Note = orderInfo.DeliveryInstructions,
};
deliveryManagerQuoteResponse = await Quote(quoteRequest);
}
catch (Exception ex)
{
Log.ErrorFormat("Get Delivery Manager Quote failed: Error: {0}, OrderId: {1}", ex.Message, orderContextDto.OrderId);
}
return deliveryManagerQuoteResponse;
}
public async Task<DeliveryManagerQuoteResponse> Quote(DeliveryManagerQuoteRequest quoteRequest)
{
DeliveryManagerQuoteResponse deliveryManagerQuoteResponse;
var client = HttpClientFactory.GetClient();
var content = HttpClientFactory.JsonContentFactory.CreateJsonContent(quoteRequest);
var response = await client.PostAsync("https://myUrl", content);
if (response.IsSuccessStatusCode)
{
var data = await response.Content.ReadAsStringAsync();
deliveryManagerQuoteResponse = JsonConvert.DeserializeObject<DeliveryManagerQuoteResponse>(data);
}
else
{
throw new Exception((int)response.StatusCode + "-" + response.StatusCode);
}
return deliveryManagerQuoteResponse;
}
I tried following as well but same result:
public async Task<DeliveryManagerQuoteResponse> Quote(DeliveryManagerQuoteRequest quoteRequest)
{
DeliveryManagerQuoteResponse deliveryManagerQuoteResponse;
using (var client = new HttpClient())
{
var content = HttpClientFactory.JsonContentFactory.CreateJsonContent(quoteRequest);
var response = await client.PostAsync("https://myUrl", content);
if (response.IsSuccessStatusCode)
{
var data = await response.Content.ReadAsStringAsync();
deliveryManagerQuoteResponse = JsonConvert.DeserializeObject<DeliveryManagerQuoteResponse>(data);
}
else
{
throw new Exception((int)response.StatusCode + "-" + response.StatusCode);
}
}
return deliveryManagerQuoteResponse;
}
Output (sorry for the blurry output, if you click on it, you will see clear result):
don't
don't
Basically, there is no good or workable way to call an async method from a sync method and wait for the answer. There's "sync over async", but that's an anti-pattern and should be aggressively avoided.
So either:
rewrite the caller to be async
implement a synchronous version of the API
Yesterday I've found out how to create several async http requests without async/await. But today I need to do it in a loop: if some of responses don't satisfy some condition - I need to change a request for them and send these requests again. It may be repeated several times.
I've tried this code:
do
{
var loadingCoordinatesTasks = new List<Task<Terminal>>();
var totalCountOfTerminals = terminalPresetNode.ChildNodes.Count;
var uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
foreach (var terminal in terminals.Except(_terminalsWithCoordinates))
{
var address = terminal.GetNextAddress();
var webRequest = (HttpWebRequest)WebRequest.Create(GeoCoder.GeoCodeUrl + address);
var webRequestTask = Task.Factory.FromAsync<WebResponse>(webRequest.BeginGetResponse,
webRequest.EndGetResponse,
terminal);
var parsingTask = webRequestTask.ContinueWith(antecedent =>
{
// Parse the response
});
loadingCoordinatesTasks.Add(parsingTask);
}
Task.Factory.ContinueWhenAll(loadingCoordinatesTasks.ToArray(), antecedents =>
{
foreach (var antecedent in antecedents)
{
var terminalWithCoordinates = antecedent.Result;
if (antecedent.Status == TaskStatus.RanToCompletion &&
!terminalWithCoordinates.Coordinates.AreUnknown)
{
_terminalsWithCoordinates.Add(terminalWithCoordinates);
_countOfProcessedTerminals++;
}
}
});
} while (_countOfProcessedTerminals < totalCountOfTerminals);
but is it possible to check the condition in while just after every single set of requests executed?
You can perform the check after increasing the count:
_countOfProcessedTerminals++;
if (_countOfProcessedTerminals >= totalCountOfTerminals)
{
break;
}
Is _countOfProcessedTerminals thread-safe though?
I manage to do it using recursion:
public void RunAgainFailedTasks(IEnumerable<Task<Terminal>> tasks)
{
Task.Factory.ContinueWhenAll(tasks.ToArray(), antecedents =>
{
var failedTasks = new List<Task<Terminal>>();
foreach (var antecedent in antecedents)
{
var terminal = antecedent.Result;
// Previous request was failed
if (terminal.Coordinates.AreUnknown)
{
string address;
try
{
address = terminal.GetNextAddress();
}
catch (FormatException) // No versions more
{
continue;
}
var getCoordinatesTask = CreateGetCoordinatesTask(terminal, address);
failedTasks.Add(getCoordinatesTask);
}
else
{
_terminalsWithCoordinates.Add(terminal);
}
}
if (failedTasks.Any())
{
RunAgainFailedTasks(failedTasks);
}
else
{
// Display a map
}
}, CancellationToken.None,
TaskContinuationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
}
private Task<Terminal> CreateGetCoordinatesTask(Terminal terminal, string address)
{
var webRequest = (HttpWebRequest)WebRequest.Create(GeoCoder.GeoCodeUrl + address);
webRequest.KeepAlive = false;
webRequest.ProtocolVersion = HttpVersion.Version10;
var webRequestTask = Task.Factory.FromAsync<WebResponse>(webRequest.BeginGetResponse,
webRequest.EndGetResponse,
terminal);
var parsingTask = webRequestTask.ContinueWith(webReqTask =>
{
// Parse the response
});
return parsingTask;
}