in src/tools/restapi-async-proxy/AsyncController.cs [16:146]
public class AsyncController(
ILogger logger,
HttpClient serviceHttpClient,
BackgroundWorkerQueue bgQueue,
IAsyncOperationsManager opManager) : Controller
{
private readonly BackgroundWorkerQueue bgQueue = bgQueue;
private readonly ILogger logger = logger;
private HttpClient serviceHttpClient = serviceHttpClient;
private IAsyncOperationsManager opManager = opManager;
[HttpPost]
[HttpPut]
[Route("/{**catchAll}")]
public async Task<IActionResult> PerformAction(
[FromRoute] string catchAll,
CancellationToken cancellationToken,
[FromBody] JsonObject? resource = null)
{
var path = this.HttpContext.GetResourceRelativeUri().Replace("%2F", "/");
var requestMessage = new HttpRequestMessage
{
Method = new HttpMethod(this.HttpContext.Request.Method),
RequestUri = new Uri(path, UriKind.Relative)
};
if (resource != null)
{
requestMessage.Content = new StringContent(
resource.ToString(),
Encoding.UTF8,
"application/json");
}
// Following:
// https://github.com/microsoft/api-guidelines/blob/vNext/azure/ConsiderationsForServiceDesign.md#long-running-action-operations
requestMessage.CopyHeaders(this.HttpContext.Request.Headers);
var operationId = requestMessage.AddOperationStatusHeader();
// Record operation start before seeing the response else there can be a race condition
// where in service tries to update the operation before recording was done. We
// can make a spurious entry if operation never got started.
var cts = new CancellationTokenSource();
await this.opManager.RecordOperationStart(
operationId,
requestMessage.Method.ToString(),
path,
cts);
this.bgQueue.QueueBackgroundWorkItem(async () =>
{
JsonObject? result = null;
ODataError? error = null;
string status;
int statusCode;
try
{
// Passing cts.Token so that if the /cancel API gets invoked then this request
// will get cancelled. As these calls can be long running its up to the
// server on how to react to a client side cancellation. The server should get
// a connection aborted and handle that as appropriate.
var response = await this.serviceHttpClient.SendAsync(requestMessage, cts.Token);
await response.ValidateStatusCodeAsync(this.logger);
var content = await response.Content.ReadAsStringAsync();
if (!string.IsNullOrEmpty(content))
{
result = JsonNode.Parse(content)!.AsObject();
}
status = nameof(OperationState.Succeeded);
statusCode = (int)response.StatusCode;
}
catch (Exception e)
{
status = nameof(OperationState.Failed);
(statusCode, error) = ODataError.FromException(e);
}
await this.opManager.RecordOperationEnd(operationId, status, statusCode, result, error);
});
this.HttpContext.AddOperationStatusResponseHeader(operationId);
var response = new JsonObject
{
["id"] = operationId
};
return this.Accepted(response);
}
[HttpGet]
[Route("/operations/{operationId}")]
public async Task<IActionResult> GetOperationStatusAsync([FromRoute] string operationId)
{
OperationStatus? opStatus = await this.opManager.GetOperationStatus(operationId);
if (opStatus == null)
{
return this.NotFound();
}
return this.Ok(opStatus);
}
[HttpPost]
[Route("/operations/{operationId}/cancel")]
public async Task<IActionResult> CancelOpeation([FromRoute] string operationId)
{
bool cancelled = await this.opManager.CancelOperation(operationId);
return this.Ok(cancelled);
}
[HttpGet]
[Route("/show")]
public IActionResult Show()
{
var view = new WsConfig
{
EnvironmentVariables = Environment.GetEnvironmentVariables(),
TargetEndpoint = this.serviceHttpClient.BaseAddress!.ToString()
};
return this.Ok(view);
}
public class WsConfig
{
public System.Collections.IDictionary EnvironmentVariables { get; set; } = default!;
public string TargetEndpoint { get; set; } = default!;
}
}