.NET API threading issue (DBContext) - c#

I am implementing an API and as part of it I have setup a custom .Net Middleware service extension UseRequestLoggingModdlewareExtension() that it run between the following:
app.UseHttpsRedirection();
app.UseRequestLoggingModdlewareExtension();
app.UseRouting();
The code is simple, and just logs the output of the request into a custom table.
public async Task InvokeAsync(HttpContext httpContext)
{
var stopAccess = _keyManager.getKeyValue("stopImmediateAccess");
if (!Convert.ToBoolean(stopAccess))
{
await _next(httpContext);
var loggingLevel = _keyManager.getKeyValue("loggingLevel");
if (loggingLevel != null)
{
if (loggingLevel.ToLower() == "information")
{
var userIdClaim = httpContext.User.FindFirst("userid")?.Value;
int? userId = null;
if(userIdClaim != null)
{
userId = Int32.Parse(userIdClaim);
}
var logging = new ApiRequestLogging
{
userId = userId,
remoteIp = httpContext.Connection.RemoteIpAddress.ToString() == "::1" ? "localhost" : httpContext.Connection.RemoteIpAddress.ToString(),
userAgent = httpContext.Request.Headers["User-Agent"].ToString(),
requestMethod = httpContext.Request.Method,
requestUrl = httpContext.Request.Path,
queryString = httpContext.Request.QueryString.ToString(),
requestHeaders = String.Join(",", httpContext.Request.Headers),
responseCode = httpContext.Response.StatusCode,
responseHeaders = String.Join(",", httpContext.Response.Headers),
createdDt = DateTime.Now
};
_logging.LogApiRequest(logging);
}
}
}
}
Where I am struggling is with some errors regarding some issues with the DBContext.
System.InvalidOperationException: A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
The error is appearing twice, at both lines where the _keyManager service is called. The keyManager service is simply doing the following:
public string getKeyValue(string keyName)
{
var value = _context.keyManagement.Where(k => k.keyName == keyName).Select(v => v.keyValue).FirstOrDefault();
return value;
}
I have a suspicion that it could be something to do with the 'await' and the aschronousness of the code, however I have tried multiple combinations and cannot seem to bypass this issue.

Do you implement the IMiddleware interface i.e. something like this RequestLoggingMiddleware: IMiddleware This will resolve your middleware through the IMiddlewareFactory like a scoped service and inject the other dependant services. Your middleware ctor should be something like RequestLoggingMiddleware(IKeyManagerService keyManager) This way the middleware will be activated per client request i.e. scoped and not in the normal way as a singleton. Providing scoped middleware instances per request will allow you to use short-lived ApplicationDbContext in the middleware or its dependent services:
public RequestLoggingMiddleware(ApplicationDbContext db)
{
_db = db;
}
or in your case more like
public class RequestLoggingMiddleware: IMiddleware
{
public RequestLoggingMiddleware(IKeyManagerService keyManager)
{
_keyManager = keyManager;
}
}
public KeyManagerService(ApplicationDbContext db)
{
_db = db;
}
services.AddScoped<IKeyManagerService, KeyManagerService>()
This way the ApplicationDbContext used by keyManager service will be created per request and disposed of after the request is completed. Of course, the IKeyManagerService should be registered as a scoped service as well.

Thats why i like to use IDisposable interface with DbContext.
public string getKeyValue(string keyName)
{
string value = null;
using(var _cnx = new DbContext())
{
value = _cnx.keyManagement.Where(k => k.keyName == keyName).Select(v => v.keyValue).FirstOrDefault();
}
return value;
}

Related

create an API with .NET Minimal APIs that require session api key

This video is really nice and shows how to create Minimal APIs using .net 6:
https://www.youtube.com/watch?v=eRJFNGIsJEo
It is amazing how it uses dependency injection to get mostly everything that you need inside your endpoints. For example if I need the value of a custom header I would have this:
app.MapGet("/get-custom-header", ([FromHeader(Name = "User-Agent")] string data) =>
{
return $"User again is: {data}";
});
I can have another endpoint where I have access to the entire httpContext like this:
app.MapGet("/foo", (Microsoft.AspNetCore.Http.HttpContext c) =>
{
var path = c.Request.Path;
return path;
});
I can even register my own classes with this code: builder.Services.AddTransient<TheClassIWantToRegister>()
If I register my custom classes I will be able to create an instance of that class every time I need it on and endpoint (app.MapGet("...)
Anyways back to the question. When a user logs in I send him this:
{
"ApiKey": "1234",
"ExpirationDate": blabla bla
.....
}
The user must send the 1234 token to use the API. How can I avoid repeating my code like this:
app.MapGet("/getCustomers", ([FromHeader(Name = "API-KEY")] string apiToken) =>
{
// validate apiToken agains DB
if(validationPasses)
return Database.Customers.ToList();
else
// return unauthorized
});
I have tried creating a custom class RequiresApiTokenKey and registering that class as builder.Services.AddTransient<RequiresApiTokenKey>() so that my API knows how to create an instance of that class when needed but how can I access the current http context inside that class for example? How can I avoid having to repeat having to check if the header API-KEY header is valid in every method that requires it?
Gave this a test based on my comments.
This would call the method Invoke in the middleware on each request and you can do checks here.
Probably a better way would be to use the AuthenticationHandler. using this would mean you can attribute individual endpoints to have the API key check done instead of all incoming requests
But, I thought this was still useful, middleware can be used for anything you'd like to perform on every request
Using Middleware
Program.cs:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
//our custom middleware extension to call UseMiddleware
app.UseAPIKeyCheckMiddleware();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.MapGet("/", () => "Hello World!");
app.Run();
APIKeyCheckMiddleware.cs
using Microsoft.Extensions.Primitives;
internal class APIKeyCheckMiddleware
{
private readonly RequestDelegate _next;
public APIKeyCheckMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
//we could inject here our database context to do checks against the db
if (httpContext.Request.Headers.TryGetValue("API-KEY", out StringValues value))
{
//do the checks on key
var apikey = value;
}
else
{
//return 403
httpContext.Response.StatusCode = 403;
}
await _next(httpContext);
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class APIKeyCheckMiddlewareExtensions
{
public static IApplicationBuilder UseAPIKeyCheckMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<APIKeyCheckMiddleware>();
}
}
I used SmithMart's answer but had to change things in the Invoke method and used DI in the constructor.
Here's my version:
internal class ApiKeyCheckMiddleware
{
public static string ApiKeyHeaderName = "X-ApiKey";
private readonly RequestDelegate _next;
private readonly ILogger<ApiKeyCheckMiddleware> _logger;
private readonly IApiKeyService _apiKeyService;
public ApiKeyCheckMiddleware(RequestDelegate next, ILogger<ApiKeyCheckMiddleware> logger, IApiKeyService apiKeyService)
{
_next = next;
_logger = logger;
_apiKeyService = apiKeyService;
}
public async Task InvokeAsync(HttpContext httpContext)
{
var request = httpContext.Request;
var hasApiKeyHeader = request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKeyValue);
if (hasApiKeyHeader)
{
_logger.LogDebug("Found the header {ApiKeyHeader}. Starting API Key validation", ApiKeyHeaderName);
if (apiKeyValue.Count != 0 && !string.IsNullOrWhiteSpace(apiKeyValue))
{
if (Guid.TryParse(apiKeyValue, out Guid apiKey))
{
var allowed = await _apiKeyService.Validate(apiKey);
if (allowed)
{
_logger.LogDebug("Client successfully logged in with key {ApiKey}", apiKeyValue);
var apiKeyClaim = new Claim("ApiKey", apiKeyValue);
var allowedSiteIdsClaim = new Claim("SiteIds", string.Join(",", allowedSiteIds));
var principal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim> { apiKeyClaim, allowedSiteIdsClaim }, "ApiKey"));
httpContext.User = principal;
await _next(httpContext);
return;
}
}
_logger.LogWarning("Client with ApiKey {ApiKey} is not authorized", apiKeyValue);
}
else
{
_logger.LogWarning("{HeaderName} header found, but api key was null or empty", ApiKeyHeaderName);
}
}
else
{
_logger.LogWarning("No ApiKey header found.");
}
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
}
}

How to retrieve current IServiceProvider scope (MS DI) in MassTransit on SendContext filter

I'm trying to find information about a way to get DI scope, that were created for message Consumer in SendContext/PublishContext filter.
I was able to find some hints how to get this done for ConsumeContext middleware in MassTransit by using MassTransit.ExtensionsDependencyInjectionIntegration.ScopeProviders.DependencyInjectionConsumerScopeProvider
public static void UseUserServiceMiddleware(this IPipeConfigurator<ConsumeContext> configurator, IServiceProvider serviceProvider)
{
// create PayLoad to retrieve current scope in Middleware
var scopeProvider = new DependencyInjectionConsumerScopeProvider(serviceProvider);
configurator.AddPipeSpecification(new FilterPipeSpecification<ConsumeContext>(new ScopeFilter(scopeProvider)));
// add specific filter/middleware to set context of IUserService
configurator.AddPipeSpecification(new UserServiceMiddlewareSpecification<ConsumeContext>());
}
And in IFliter implementation
public class UserServiceMiddlewareFilter<T> :
IFilter<T>
where T : class, ConsumeContext
{
public void Probe(ProbeContext context)
{
}
public async Task Send(T context, IPipe<T> next)
{
try
{
// current Consumer scope
if (context.TryGetPayload(out IServiceScope scope))
{
if (context.Headers.TryGetHeader("clientIp", out var clientIp) &&
context.Headers.TryGetHeader("subId", out var subId))
{
UserIdentityRetrievalSetter(scope, clientIp?.ToString(), subId?.ToString());
}
}
await next.Send(context).ConfigureAwait(false);
}
catch (Exception ex)
{
// propagate the exception up the call stack
throw;
}
}
private void UserIdentityRetrievalSetter(IServiceScope scope, string clientIp, string subId)
{
if (!string.IsNullOrWhiteSpace(clientIp) && !string.IsNullOrWhiteSpace(subId))
{
var userIdentityRetrieval =
scope.ServiceProvider.GetRequiredService<UserIdentityProvider.RequestHandlers.UserIdentityRetrieval>();
userIdentityRetrieval.SetIpAddress(clientIp);
userIdentityRetrieval.SetUserSubId(subId);
}
}
}
By using context.TryGetPayload(out IServiceScope scope) I'm able to get into scope created for message consumer.
The problem that I'm facing is how to get current DI scope on SendContext/PublishContext before message is sent into RabbitMq, to be able to resolve scoped implementations, that I'm able to set/reconfigure in ConsumeContext middleware.
The perfect solution for me is, that sender of message and Consumer are not aware about some part of logic, and there is a need of adding some additional information into messages (I found headers to be perfect for this), that are not part of them.
I was able to write something like this
cfg.ConfigureSend(exec => exec.UseExecute(context =>
{
if (context.GetType().IsGenericType &&
context.GetType().GetGenericTypeDefinition() ==
typeof(MassTransit.RabbitMqTransport.Contexts.BasicPublishRabbitMqSendContext<>))
{
if (context.GetType().GetProperty("Message")?.GetValue(context, null) is
Core.MessageBus.Models.IBusDrivenCommunication message)
{
SendContext sendContext;
if (context.TryGetPayload(out ConsumeContext _))
{
var userIdentityScoped = provider.GetService<UserIdentityProvider.RequestHandlers.UserIdentityRetrieval>();
var clientIpAddress = userIdentityScoped.GetIpAddress();
var subId = userIdentityScoped.GetUserSubGuidClaim();
if (!context.Headers.TryGetHeader("clientIp", out object cip) &&
!context.Headers.TryGetHeader("subId", out object sid))
{
context.Headers.Set("clientIp", clientIpAddress);
context.Headers.Set("subId", subId);
}
}
if (context.TryGetPayload(out sendContext))
{
var userIdentityScoped = provider.GetService<UserIdentityProvider.RequestHandlers.UserIdentityRetrieval>();
var clientIpAddress = userIdentityScoped.GetIpAddress();
var subId = userIdentityScoped.GetUserSubGuidClaim();
if (!context.Headers.TryGetHeader("clientIp", out object cip) &&
!context.Headers.TryGetHeader("subId", out object sid))
{
context.Headers.Set("clientIp", clientIpAddress);
context.Headers.Set("subId", subId);
}
}
}
}
}));
Provider is delivered from IServiceCollectionConfigurator.AddBus, but there is no way to resolve scoped services.
I've tried to get Consumer scope
var consumerScope = context.GetPayload<IConsumerScopeContext>();
var consumeContext = context.GetPayload<ConsumeContext>();
var consumerScopeProvider = provider.GetRequiredService<IConsumerScopeProvider>();
using (var scopeMain = consumerScopeProvider.GetScope(consumeContext))
{
var serviceScope = scopeMain.Context.GetPayload<IServiceScope>();
var userIdentityScoped =
serviceScope.ServiceProvider.GetRequiredService<UserIdentityProvider.RequestHandlers.UserIdentityRetrieval>();
}
return;
But it's a blind spot, I've tried to find any hint in SendContext about PayLoad or, something, to be able to get current scope of IServiceProvider.
In the last resort, I can move logic into Command/Event model of message from scoped services and then change ConsumeContext to get this data from it, not from headers, but I prefer to seperate logic from models.
In documentation of MassTrasnit there is Audit functionality http://masstransit-project.com/MassTransit/advanced/audit/, but as far as I understand, Observers are not used to modify data.

How to ensure that the WCF service will not communicate to the database in parallel

I have a WCF service, which should return objects from database, but each entity should be returned only once. I would like to avoid scenario, where many clients are using service, and they can get same Request entity.
public Request GetChangeRequest()
{
using (var context = new Data.Core.Context())
{
var request = context.Requests
.Where(r => r.IsAvaible)
.FirstOrDefault();
if (request != null)
{
request.IsAvaible = false;
context.SaveChanges();
}
return request;
}
}
I am really wondering if there is a reason to give additional security like locking database. To do this I have managed something like this:
public Request GetChangeRequest()
{
using (var context = new Data.Core.Context())
{
context.OnLock<Request>(context.GetTableName<Request>(), () =>
{
var request = context.Requests
.Where(r => r.IsAvaible)
.FirstOrDefault();
if (request != null)
{
request.IsAvaible = false;
context.SaveChanges();
}
return request;
});
}
}
public static class DBContextExtensions
{
public static string GetTableName<T>(this DbContext context) where T : class
{
var type = typeof(T);
var entityName = (context as System.Data.Entity.Infrastructure.IObjectContextAdapter).ObjectContext.CreateObjectSet<T>().EntitySet.Name;
var tableAttribute = type.GetCustomAttributes(false).OfType<System.ComponentModel.DataAnnotations.Schema.TableAttribute>().FirstOrDefault();
return tableAttribute == null ? entityName : tableAttribute.Name;
}
public static T OnLock<T>(this DbContext context, string tableName, Func<T> action)
{
T res;
using (DbContextTransaction scope = context.Database.BeginTransaction())
{
context.Database.ExecuteSqlCommand($"SELECT TOP 1 Id FROM {tableName} WITH (TABLOCKX, HOLDLOCK)");
res = action.Invoke();
scope.Commit();
}
return res;
}
}
I couldn't reproduce scenerio, when two request entity are returned to two different clients. Does that mean, that WCF service performs requests sequentially?
Instead of implementing a locking mechanism by yourself, one possible solution would be running the service as a singleton and not allowing parallel requests.
You can achieve this by setting your WCF Service properties InstanceContextMode and ConcurrencyMode to Single.
For more information about Sessions, Instancing, and Concurrency see here.
You should be able to use concurrency checking to make sure that only one entity is returned, without blocking the WCF to one query at a time.
You need a special field in you entity class with an attribute [Timestamp] and then catch the DbUpdateConcurrencyException when saving, which will let you know that someone else has already returned that record, so you should get another one.
public class Request
{
...
[Timestamp]
public byte[] RowVersion { get; set; }
}
public Request GetChangeRequest()
{
using (var context = new Data.Core.Context())
{
while (true)
{
try
{
var request = context.Requests
.Where(r => r.IsAvaible)
.FirstOrDefault();
request.IsAvaible = false;
context.SaveChanges();
return request;
}
catch (DbUpdateConcurrencyException)
{
}
}
}
}
See here for more details

A second operation started on this context before a previous asynchronous operation completed with UnitofWork and async

A second operation started on this context before a previous asynchronous operation completed. Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. Any instance members are not guaranteed to be thread safe.
My unitofwork code
public class UnitOfWork : IUnitOfWork
{
private readonly CAMSDbEntities _context;
private bool _disposed;
public Dictionary<Type, object> repositories = new Dictionary<Type, object>();
private Guid _objectId;
public UnitOfWork(IContextFactory contextFactory)
{
_context = contextFactory.DbContext as CAMSDbEntities;
_objectId = Guid.NewGuid();
}
public IGenericRepository<T> Repository<T>() where T : class
{
if (repositories.Keys.Contains(typeof(T)) == true)
{
return repositories[typeof(T)] as GenericRepository<T>;
}
GenericRepository<T> repo = new GenericRepository<T>(_context);
repositories.Add(typeof(T), repo);
return repo;
}
My unity config
container.RegisterType<IHttpContext, HttpContextObject>();
container.RegisterType<IDataBaseManager, DataBaseManager>();
container.RegisterType<IContextFactory, ContextFactory>();
container.RegisterType(typeof(IGenericRepository<>), typeof(GenericRepository<>));
container.RegisterType<IUnitOfWork, UnitOfWork>();
container.RegisterType<IAnalytics, DashbordService>();
GlobalConfiguration.Configuration.DependencyResolver = new UnityDependencyResolver(container);
webApi Controller
public class DashbordController : ApiController
{
private static IAnalytics _analytics;
public DashbordController(IAnalytics dashbordService)
{
_analytics = dashbordService;
}
[HttpGet]
[Route("GetStudentAssessmentHistory")]
public IHttpActionResult GetStudentAssessmentHistory(int studentID)
{
var result = _analytics.GetStudentAssessmentHistoryGraphData(studentID);
return Ok(result);
}
[HttpGet]
[Route("GetStudentFeePaymentHistory")]
public async Task<IHttpActionResult> GetStudentFeePaymentData(int studentID)
{
var result = await _analytics.GetStudentFeePaymentData(studentID);
return Ok(result);
}
[HttpGet]
[Route("GetLedgerHitoryByDepartment")]
public async Task<IHttpActionResult> GetLedgerHitoryByDepartment(int schoolID, int departmentId)
{
var result = await _analytics.GetLedgerHitory(schoolID, departmentId);
return Ok(result);
}
[HttpGet]
[Route("GetLedgerExpenseTrendByDepartment")]
public async Task<IHttpActionResult> GetLedgerExpenseTrendByDepartment(int schoolID)
{
var result = await _analytics.GetLedgerExpenseTrend(schoolID);
return Ok(result);
}
dashboardservice Code
public async Task<List<LedgerExpense>> GetLedgerExpenseTrend(int schoolId)
{
try
{
var ledgerExpenses = new List<LedgerExpense>();
var currentDate = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, INDIAN_ZONE);
DateTime previoYearDate = currentDate.AddYears(-1);
var ledgerPayments = await _unitOfWork.Repository<LedgerDetail>().GetManyAsync(x => x.SchoolID == schoolId && x.PaymentDate <= currentDate
&& x.PaymentDate >= previoYearDate);
foreach (var ledgerPayment in ledgerPayments.OrderBy(x => x.PaymentDate).GroupBy(y => y.DepartmentID))
{
var department = await _unitOfWork.Repository<DeptartmentType>().GetAsync(x => x.ID == ledgerPayment.Key);
var ledgerData = new LedgerExpense
{
Department = department.DepartmentName,
TotalLedgerExpense = 0
};
foreach (var departmentPayment in ledgerPayment)
{
ledgerData.TotalLedgerExpense += departmentPayment.TotalPaidAmount;
}
ledgerExpenses.Add(ledgerData);
}
return ledgerExpenses;
}
catch (Exception ex)
{
logger.Log("An error occurred while fetching ledger expenses");
return null;
}
}
I have similar type of asynchronous metods implemented in my dashboardservice code. whenever I request a dashboard UI all request comes to the same controller at the same time and creates the new object for unitofwork and dbcontext for each request one by one. it works perfectly sometimes but Sometimes I think unitofwork and dbcontext object flows with the wrong thread and throws this error. I think somehow its picking wrong dbcontext which is already busy with someother api request from dashboard service.
Please remove the static keyword in your controller from this code:
private static IAnalytics _analytics;`
Once that has been created, it will never be created again unless the application pool is recycled (manual or IIS restart etc.) Since you are using the same instance for all requests, you are getting that error at random. If a request finishes before the next one arrives, it will NOT result in an error. Otherwise it will. Hence the reason for not always getting the error (as you mention in your question).
Please read about how static affects the design in a web scenario (or server).
Try and think of web requests as a single transaction, all classes are created for each request and then thrown away after the request has been served. That means if you have static or any other mechanism which is for sharing, it will be shared between requests.

Create EF Core DBContext in controller for multi-threading?

In one of my controllers, I have an async method that has to make a few db calls simultaneously, so I set it up like this:
public xxxController(IConfiguration configuration, xxxContext xxxContext, xxx2Context xxx2Context)
: base(xxxContext)
I store the contexts that are injected. In the particular method:
var v = await Task.WhenAll(... )
Inside of the WhenAll, I need to use the xxxContext for each item, so I get the non-thread safe exception.
What is the correct way to create a new DbContext? Right now I'm doing:
var v = new DbContextOptionsBuilder<xxxContext>();
v.UseSqlServer(_xxxContext.Database.GetDbConnection().ConnectionString);
xxContext e = new xxContext(v.Options);
So I'm getting the connection string from the existing context that was injected and use that to create a new one.
The connection strings are stored in appSettings.json. In the "ConnectionStrings" section.
Is there a cleaner way to create the contexts for multi-threading?
For this purpose I created something like factory class which can provide context per call. For your case it can be
public class AppDependencyResolver
{
private static AppDependencyResolver _resolver;
public static AppDependencyResolver Current
{
get
{
if (_resolver == null)
throw new Exception("AppDependencyResolver not initialized. You should initialize it in Startup class");
return _resolver;
}
}
public static void Init(IServiceProvider services)
{
_resolver = new AppDependencyResolver(services);
}
private readonly IServiceProvider _serviceProvider;
private AppDependencyResolver(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public xxxContext CreatexxxContextinCurrentThread()
{
var scopeResolver = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope();
return scopeResolver.ServiceProvider.GetRequiredService<xxxContext>();
}
}
Than you should call Init method in Startup
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
AppDependencyResolver.Init(app.ApplicationServices);
//other configure code
}
You can look my approach for this on github
I afraid you executing your asynchronous methods in this way
var task = Task.Run(() => MyAsynchronosMethod());
DbContext operations are Input-Output operations you don't need to execute them on different thread to work "simultaneously". You can get it working without new threads.
public MyController(IConfiguration configuration, AContext aContext, BContext bContext)
: base(aContext)
{
_aContext = aContext;
_bContext = bContext;
}
public Task<IActionResult> GetSomething(int id)
{
var customerTask = aContext.Customers
.FirstOrDefaultAsync(customer => customer.Id == id);
var sellerTask = aContext.Sellers
.FirstOrDefaultAsync(seller => seller.CustomerId == id);
var ordersTask = bContext.Orders
.Where(order => order.CustomerId == id)
.ToListAsync();
var invoicesTask = bContext.Invoices
.Where(invoice => invoice.CustomerId == id)
.ToListAsync();
var allTasks = new[] { customerTask, sellerTask, ordersTask, invoicesTask};
await Task.WhenAll(allTasks);
// Do stuff with result of completed tasks
Return OK(calculatedResult);
}
In method above you will send all queries almost simultaneously without waiting for response. You start waiting for responses only after all queries were send.
After all query results arrived you can process data - all operations will be executed on one thread.

Categories