From b0bb4cc55110d096c640e077b4441745dfb9700a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20P=C3=B6chtrager?= Date: Mon, 7 Apr 2025 08:24:20 +0200 Subject: [PATCH 1/3] Remove unneeded using --- CloudConvert.Test/TestJobs.cs | 1 - CloudConvert.Test/TestSignedUrl.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/CloudConvert.Test/TestJobs.cs b/CloudConvert.Test/TestJobs.cs index 835b729..db2db3c 100644 --- a/CloudConvert.Test/TestJobs.cs +++ b/CloudConvert.Test/TestJobs.cs @@ -1,7 +1,6 @@ using System; using System.IO; using System.Text.Json; -using System.Text.Json.Serialization; using System.Threading.Tasks; using CloudConvert.API; using CloudConvert.API.Models; diff --git a/CloudConvert.Test/TestSignedUrl.cs b/CloudConvert.Test/TestSignedUrl.cs index 7c0353b..389c1f7 100644 --- a/CloudConvert.Test/TestSignedUrl.cs +++ b/CloudConvert.Test/TestSignedUrl.cs @@ -1,4 +1,3 @@ - using CloudConvert.API; using CloudConvert.API.Models.JobModels; using CloudConvert.API.Models.TaskOperations; From f0cc85000476ca235f1dbb5d6cf608e3a369a8cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20P=C3=B6chtrager?= Date: Mon, 7 Apr 2025 09:29:02 +0200 Subject: [PATCH 2/3] Add CancellationToken support --- CloudConvert.API/CloudConvertAPI.cs | 71 +++++++++++++++++++---------- CloudConvert.API/RestHelper.cs | 13 +++--- CloudConvert.API/WebApiHandler.cs | 6 +-- CloudConvert.Test/TestJobs.cs | 10 ++-- CloudConvert.Test/TestTasks.cs | 14 +++--- 5 files changed, 69 insertions(+), 45 deletions(-) diff --git a/CloudConvert.API/CloudConvertAPI.cs b/CloudConvert.API/CloudConvertAPI.cs index 340958b..b329c9a 100644 --- a/CloudConvert.API/CloudConvertAPI.cs +++ b/CloudConvert.API/CloudConvertAPI.cs @@ -10,29 +10,30 @@ using CloudConvert.API.Models.JobModels; using CloudConvert.API.Models.TaskModels; using CloudConvert.API.Models; +using System.Threading; namespace CloudConvert.API { public interface ICloudConvertAPI { #region Jobs - Task> GetAllJobsAsync(JobListFilter jobFilter); - Task> CreateJobAsync(JobCreateRequest request); - Task> GetJobAsync(string id); - Task> WaitJobAsync(string id); - Task DeleteJobAsync(string id); + Task> GetAllJobsAsync(JobListFilter jobFilter, CancellationToken cancellationToken = default); + Task> CreateJobAsync(JobCreateRequest request, CancellationToken cancellationToken = default); + Task> GetJobAsync(string id, CancellationToken cancellationToken = default); + Task> WaitJobAsync(string id, CancellationToken cancellationToken = default); + Task DeleteJobAsync(string id, CancellationToken cancellationToken = default); #endregion #region Tasks - Task> GetAllTasksAsync(TaskListFilter jobFilter); - Task> CreateTaskAsync(string operation, T request); - Task> GetTaskAsync(string id, string include = null); - Task> WaitTaskAsync(string id); - Task DeleteTaskAsync(string id); + Task> GetAllTasksAsync(TaskListFilter jobFilter, CancellationToken cancellationToken = default); + Task> CreateTaskAsync(string operation, T request, CancellationToken cancellationToken = default); + Task> GetTaskAsync(string id, string include = null, CancellationToken cancellationToken = default); + Task> WaitTaskAsync(string id, CancellationToken cancellationToken = default); + Task DeleteTaskAsync(string id, CancellationToken cancellationToken = default); #endregion - Task UploadAsync(string url, byte[] file, string fileName, object parameters); - Task UploadAsync(string url, Stream file, string fileName, object parameters); + Task UploadAsync(string url, byte[] file, string fileName, object parameters, CancellationToken cancellationToken = default); + Task UploadAsync(string url, Stream file, string fileName, object parameters, CancellationToken cancellationToken = default); bool ValidateWebhookSignatures(string payloadString, string signature, string signingSecret); string CreateSignedUrl(string baseUrl, string signingSecret, JobCreateRequest job, string cacheKey = null); } @@ -114,26 +115,32 @@ private HttpRequestMessage GetMultipartFormDataRequest(string endpoint, HttpMeth /// List all jobs. Requires the task.read scope. /// /// + /// /// /// The list of jobs. You can find details about the job model response in the documentation about the show jobs endpoint. /// - public Task> GetAllJobsAsync(JobListFilter jobFilter) => _restHelper.RequestAsync>(GetRequest($"{_apiUrl}/jobs?filter[status]={jobFilter.Status}&filter[tag]={jobFilter.Tag}&include={jobFilter.Include}&per_page={jobFilter.PerPage}&page={jobFilter.Page}", HttpMethod.Get)); + public Task> GetAllJobsAsync(JobListFilter jobFilter, CancellationToken cancellationToken = default) + => _restHelper.RequestAsync>(GetRequest($"{_apiUrl}/jobs?filter[status]={jobFilter.Status}&filter[tag]={jobFilter.Tag}&include={jobFilter.Include}&per_page={jobFilter.PerPage}&page={jobFilter.Page}", HttpMethod.Get), cancellationToken); /// /// Create a job with one ore more tasks. Requires the task.write scope. /// /// + /// /// /// The created job. You can find details about the job model response in the documentation about the show jobs endpoint. /// - public Task> CreateJobAsync(JobCreateRequest model) => _restHelper.RequestAsync>(GetRequest($"{_apiUrl}/jobs", HttpMethod.Post, model)); + public Task> CreateJobAsync(JobCreateRequest model, CancellationToken cancellationToken = default) + => _restHelper.RequestAsync>(GetRequest($"{_apiUrl}/jobs", HttpMethod.Post, model), cancellationToken); /// /// Show a job. Requires the task.read scope. /// /// + /// /// - public Task> GetJobAsync(string id) => _restHelper.RequestAsync>(GetRequest($"{_apiUrl}/jobs/{id}", HttpMethod.Get)); + public Task> GetJobAsync(string id, CancellationToken cancellationToken = default) + => _restHelper.RequestAsync>(GetRequest($"{_apiUrl}/jobs/{id}", HttpMethod.Get), cancellationToken); /// /// Wait until the job status is finished or error. This makes the request block until the job has been completed. Requires the task.read scope. @@ -145,20 +152,24 @@ private HttpRequestMessage GetMultipartFormDataRequest(string endpoint, HttpMeth /// Using an asynchronous approach with webhooks is beneficial in such cases. /// /// + /// /// /// The finished or failed job, including tasks. You can find details about the job model response in the documentation about the show job endpoint. /// - public Task> WaitJobAsync(string id) => _restHelper.RequestAsync>(GetRequest($"{_apiSyncUrl}/jobs/{id}", HttpMethod.Get)); + public Task> WaitJobAsync(string id, CancellationToken cancellationToken = default) + => _restHelper.RequestAsync>(GetRequest($"{_apiSyncUrl}/jobs/{id}", HttpMethod.Get), cancellationToken); /// /// Delete a job, including all tasks and data. Requires the task.write scope. /// Jobs are deleted automatically 24 hours after they have ended. /// /// + /// /// /// An empty response with HTTP Code 204. /// - public Task DeleteJobAsync(string id) => _restHelper.RequestAsync(GetRequest($"{_apiUrl}/jobs/{id}", HttpMethod.Delete)); + public Task DeleteJobAsync(string id, CancellationToken cancellationToken = default) + => _restHelper.RequestAsync(GetRequest($"{_apiUrl}/jobs/{id}", HttpMethod.Delete), cancellationToken); #endregion @@ -168,28 +179,34 @@ private HttpRequestMessage GetMultipartFormDataRequest(string endpoint, HttpMeth /// List all tasks with their status, payload and result. Requires the task.read scope. /// /// + /// /// /// The list of tasks. You can find details about the task model response in the documentation about the show tasks endpoint. /// - public Task> GetAllTasksAsync(TaskListFilter taskFilter) => _restHelper.RequestAsync>(GetRequest($"{_apiUrl}/tasks?filter[job_id]={taskFilter.JobId}&filter[status]={taskFilter.Status}&filter[operation]={taskFilter.Operation}&include={taskFilter.Include}&per_page={taskFilter.PerPage}&page={taskFilter.Page}", HttpMethod.Get)); + public Task> GetAllTasksAsync(TaskListFilter taskFilter, CancellationToken cancellationToken = default) + => _restHelper.RequestAsync>(GetRequest($"{_apiUrl}/tasks?filter[job_id]={taskFilter.JobId}&filter[status]={taskFilter.Status}&filter[operation]={taskFilter.Operation}&include={taskFilter.Include}&per_page={taskFilter.PerPage}&page={taskFilter.Page}", HttpMethod.Get), cancellationToken); /// /// Create task. /// /// /// + /// /// /// The created task. You can find details about the task model response in the documentation about the show tasks endpoint. /// - public Task> CreateTaskAsync(string operation, T model) => _restHelper.RequestAsync>(GetRequest($"{_apiUrl}/{operation}", HttpMethod.Post, model)); + public Task> CreateTaskAsync(string operation, T model, CancellationToken cancellationToken = default) + => _restHelper.RequestAsync>(GetRequest($"{_apiUrl}/{operation}", HttpMethod.Post, model), cancellationToken); /// /// Show a task. Requires the task.read scope. /// /// /// + /// /// - public Task> GetTaskAsync(string id, string include = null) => _restHelper.RequestAsync>(GetRequest($"{_apiUrl}/tasks/{id}?include={include}", HttpMethod.Get)); + public Task> GetTaskAsync(string id, string include = null, CancellationToken cancellationToken = default) + => _restHelper.RequestAsync>(GetRequest($"{_apiUrl}/tasks/{id}?include={include}", HttpMethod.Get), cancellationToken); /// /// Wait until the task status is finished or error. This makes the request block until the task has been completed. Requires the task.read scope. @@ -201,26 +218,32 @@ private HttpRequestMessage GetMultipartFormDataRequest(string endpoint, HttpMeth /// Using an asynchronous approach with webhooks is beneficial in such cases. /// /// + /// /// /// The finished or failed task. You can find details about the task model response in the documentation about the show tasks endpoint. /// - public Task> WaitTaskAsync(string id) => _restHelper.RequestAsync>(GetRequest($"{_apiSyncUrl}/tasks/{id}", HttpMethod.Get)); + public Task> WaitTaskAsync(string id, CancellationToken cancellationToken = default) + => _restHelper.RequestAsync>(GetRequest($"{_apiSyncUrl}/tasks/{id}", HttpMethod.Get), cancellationToken); /// /// Delete a task, including all data. Requires the task.write scope. /// Tasks are deleted automatically 24 hours after they have ended. /// /// + /// /// /// An empty response with HTTP Code 204. /// - public Task DeleteTaskAsync(string id) => _restHelper.RequestAsync(GetRequest($"{_apiUrl}/tasks/{id}", HttpMethod.Delete)); + public Task DeleteTaskAsync(string id, CancellationToken cancellationToken = default) + => _restHelper.RequestAsync(GetRequest($"{_apiUrl}/tasks/{id}", HttpMethod.Delete), cancellationToken); #endregion - public Task UploadAsync(string url, byte[] file, string fileName, object parameters) => _restHelper.RequestAsync(GetMultipartFormDataRequest(url, HttpMethod.Post, new ByteArrayContent(file), fileName, GetParameters(parameters))); + public Task UploadAsync(string url, byte[] file, string fileName, object parameters, CancellationToken cancellationToken) + => _restHelper.RequestAsync(GetMultipartFormDataRequest(url, HttpMethod.Post, new ByteArrayContent(file), fileName, GetParameters(parameters)), cancellationToken); - public Task UploadAsync(string url, Stream stream, string fileName, object parameters) => _restHelper.RequestAsync(GetMultipartFormDataRequest(url, HttpMethod.Post, new StreamContent(stream), fileName, GetParameters(parameters))); + public Task UploadAsync(string url, Stream stream, string fileName, object parameters, CancellationToken cancellationToken = default) + => _restHelper.RequestAsync(GetMultipartFormDataRequest(url, HttpMethod.Post, new StreamContent(stream), fileName, GetParameters(parameters)), cancellationToken); public string CreateSignedUrl(string baseUrl, string signingSecret, JobCreateRequest job, string cacheKey = null) { diff --git a/CloudConvert.API/RestHelper.cs b/CloudConvert.API/RestHelper.cs index 8379f9e..5bff460 100644 --- a/CloudConvert.API/RestHelper.cs +++ b/CloudConvert.API/RestHelper.cs @@ -1,5 +1,6 @@ using System.Net.Http; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; namespace CloudConvert.API @@ -19,18 +20,18 @@ internal RestHelper(HttpClient httpClient) _httpClient = httpClient; } - public async Task RequestAsync(HttpRequestMessage request) + public async Task RequestAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - var response = await _httpClient.SendAsync(request); - var responseRaw = await response.Content.ReadAsStringAsync(); + var response = await _httpClient.SendAsync(request, cancellationToken); + var responseRaw = await response.Content.ReadAsStringAsync(cancellationToken); return JsonSerializer.Deserialize(responseRaw, DefaultJsonSerializerOptions.SerializerOptions); } - public async Task RequestAsync(HttpRequestMessage request) + public async Task RequestAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - var response = await _httpClient.SendAsync(request); - return await response.Content.ReadAsStringAsync(); + var response = await _httpClient.SendAsync(request, cancellationToken); + return await response.Content.ReadAsStringAsync(cancellationToken); } } } diff --git a/CloudConvert.API/WebApiHandler.cs b/CloudConvert.API/WebApiHandler.cs index 3ad5410..47cb0ca 100644 --- a/CloudConvert.API/WebApiHandler.cs +++ b/CloudConvert.API/WebApiHandler.cs @@ -25,19 +25,19 @@ protected override async Task SendAsync(HttpRequestMessage { if (writeLog) { - var requestString = request.Content != null ? await request.Content.ReadAsStringAsync() : string.Empty; + var requestString = request.Content != null ? await request.Content.ReadAsStringAsync(cancellationToken) : string.Empty; } var response = await base.SendAsync(request, cancellationToken); if (writeLog) { - string responseString = (await response.Content.ReadAsStringAsync()).TrimLengthWithEllipsis(20000); + string responseString = (await response.Content.ReadAsStringAsync(cancellationToken)).TrimLengthWithEllipsis(20000); } if ((int)response.StatusCode >= 400) { - throw new WebApiException((await response.Content.ReadAsStringAsync()).TrimLengthWithEllipsis(20000)); + throw new WebApiException((await response.Content.ReadAsStringAsync(cancellationToken)).TrimLengthWithEllipsis(20000)); } return response; diff --git a/CloudConvert.Test/TestJobs.cs b/CloudConvert.Test/TestJobs.cs index db2db3c..4b063ed 100644 --- a/CloudConvert.Test/TestJobs.cs +++ b/CloudConvert.Test/TestJobs.cs @@ -23,7 +23,7 @@ public async Task GetAllJobs() var path = @"Responses/jobs.json"; string json = File.ReadAllText(path); - _cloudConvertAPI.Setup(cc => cc.GetAllJobsAsync(filter)) + _cloudConvertAPI.Setup(cc => cc.GetAllJobsAsync(filter, default)) .ReturnsAsync(JsonSerializer.Deserialize>(json, DefaultJsonSerializerOptions.SerializerOptions)); var jobs = await _cloudConvertAPI.Object.GetAllJobsAsync(filter); @@ -55,7 +55,7 @@ public async Task CreateJob() var path = AppDomain.CurrentDomain.BaseDirectory + @"Responses/job_created.json"; string json = File.ReadAllText(path); - _cloudConvertAPI.Setup(cc => cc.CreateJobAsync(req)) + _cloudConvertAPI.Setup(cc => cc.CreateJobAsync(req, default)) .ReturnsAsync(JsonSerializer.Deserialize>(json, DefaultJsonSerializerOptions.SerializerOptions)); var job = await _cloudConvertAPI.Object.CreateJobAsync(req); @@ -72,7 +72,7 @@ public async Task GetJob() var path = AppDomain.CurrentDomain.BaseDirectory + @"Responses/job.json"; string json = File.ReadAllText(path); - _cloudConvertAPI.Setup(cc => cc.GetJobAsync(id)) + _cloudConvertAPI.Setup(cc => cc.GetJobAsync(id, default)) .ReturnsAsync(JsonSerializer.Deserialize>(json, DefaultJsonSerializerOptions.SerializerOptions)); var job = await _cloudConvertAPI.Object.GetJobAsync(id); @@ -89,7 +89,7 @@ public async Task WaitJob() var path = AppDomain.CurrentDomain.BaseDirectory + @"Responses/job_finished.json"; string json = File.ReadAllText(path); - _cloudConvertAPI.Setup(cc => cc.WaitJobAsync(id)) + _cloudConvertAPI.Setup(cc => cc.WaitJobAsync(id, default)) .ReturnsAsync(JsonSerializer.Deserialize>(json, DefaultJsonSerializerOptions.SerializerOptions)); var job = await _cloudConvertAPI.Object.WaitJobAsync(id); @@ -104,7 +104,7 @@ public async Task DeleteJob() { string id = "cd82535b-0614-4b23-bbba-b24ab0e892f7"; - _cloudConvertAPI.Setup(cc => cc.DeleteJobAsync(id)); + _cloudConvertAPI.Setup(cc => cc.DeleteJobAsync(id, default)); await _cloudConvertAPI.Object.DeleteJobAsync(id); } diff --git a/CloudConvert.Test/TestTasks.cs b/CloudConvert.Test/TestTasks.cs index c978851..19a4826 100644 --- a/CloudConvert.Test/TestTasks.cs +++ b/CloudConvert.Test/TestTasks.cs @@ -28,7 +28,7 @@ public async Task GetAllTasks() string json = File.ReadAllText(path); var cloudConvertApi = new Mock(); - cloudConvertApi.Setup(cc => cc.GetAllTasksAsync(filter)) + cloudConvertApi.Setup(cc => cc.GetAllTasksAsync(filter, default)) .ReturnsAsync(JsonSerializer.Deserialize>(json, DefaultJsonSerializerOptions.SerializerOptions)); var tasks = await cloudConvertApi.Object.GetAllTasksAsync(filter); @@ -68,7 +68,7 @@ public async Task CreateTask() string json = File.ReadAllText(path); var cloudConvertApi = new Mock(); - cloudConvertApi.Setup(cc => cc.CreateTaskAsync(req.Operation, req)) + cloudConvertApi.Setup(cc => cc.CreateTaskAsync(req.Operation, req, default)) .ReturnsAsync(JsonSerializer.Deserialize>(json, DefaultJsonSerializerOptions.SerializerOptions)); var task = await cloudConvertApi.Object.CreateTaskAsync(req.Operation, req); @@ -86,7 +86,7 @@ public async Task GetTask() string json = File.ReadAllText(path); var _cloudConvertAPI = new Mock(); - _cloudConvertAPI.Setup(cc => cc.GetTaskAsync(id, null)) + _cloudConvertAPI.Setup(cc => cc.GetTaskAsync(id, null, default)) .ReturnsAsync(JsonSerializer.Deserialize>(json, DefaultJsonSerializerOptions.SerializerOptions)); var task = await _cloudConvertAPI.Object.GetTaskAsync("9de1a620-952c-4482-9d44-681ae28d72a1"); @@ -104,7 +104,7 @@ public async Task WaitTask() string json = File.ReadAllText(path); var cloudConvertApi = new Mock(); - cloudConvertApi.Setup(cc => cc.WaitTaskAsync(id)) + cloudConvertApi.Setup(cc => cc.WaitTaskAsync(id, default)) .ReturnsAsync(JsonSerializer.Deserialize>(json, DefaultJsonSerializerOptions.SerializerOptions)); var task = await cloudConvertApi.Object.WaitTaskAsync(id); @@ -120,7 +120,7 @@ public async Task DeleteTask() string id = "9de1a620-952c-4482-9d44-681ae28d72a1"; var cloudConvertApi = new Mock(); - cloudConvertApi.Setup(cc => cc.DeleteTaskAsync(id)); + cloudConvertApi.Setup(cc => cc.DeleteTaskAsync(id, default)); await cloudConvertApi.Object.DeleteTaskAsync("c8a8da46-3758-45bf-b983-2510e3170acb"); } @@ -133,7 +133,7 @@ public async Task Upload() var path = AppDomain.CurrentDomain.BaseDirectory + @"Responses/upload_task_created.json"; string json = File.ReadAllText(path); - cloudConvertApi.Setup(cc => cc.CreateTaskAsync(req.Operation, req)) + cloudConvertApi.Setup(cc => cc.CreateTaskAsync(req.Operation, req, default)) .ReturnsAsync(JsonSerializer.Deserialize>(json, DefaultJsonSerializerOptions.SerializerOptions)); var task = await cloudConvertApi.Object.CreateTaskAsync(req.Operation, req); @@ -144,7 +144,7 @@ public async Task Upload() byte[] file = File.ReadAllBytes(pathFile); string fileName = "input.pdf"; - cloudConvertApi.Setup(cc => cc.UploadAsync(task.Data.Result.Form.Url.ToString(), file, fileName, task.Data.Result.Form.Parameters)); + cloudConvertApi.Setup(cc => cc.UploadAsync(task.Data.Result.Form.Url.ToString(), file, fileName, task.Data.Result.Form.Parameters, default)); await cloudConvertApi.Object.UploadAsync(task.Data.Result.Form.Url.ToString(), file, fileName, task.Data.Result.Form.Parameters); } From d4e4fa3f0012e997e066e050473c2ef8a678582c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20P=C3=B6chtrager?= Date: Tue, 8 Apr 2025 09:32:51 +0200 Subject: [PATCH 3/3] Add test for CancellationToken --- CloudConvert.Test/IntegrationTests.cs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CloudConvert.Test/IntegrationTests.cs b/CloudConvert.Test/IntegrationTests.cs index caed31a..7953055 100644 --- a/CloudConvert.Test/IntegrationTests.cs +++ b/CloudConvert.Test/IntegrationTests.cs @@ -12,6 +12,7 @@ using System.Collections.Generic; using System.Dynamic; using System.Text.Json; +using System.Threading; namespace CloudConvert.Test { @@ -27,6 +28,28 @@ public void Setup() _cloudConvertAPI = new CloudConvertAPI(apiKey, true); } + [TestCase("cancellationtoken")] + public async Task CancellationToken(string streamingMethod) + { + using var cts = new CancellationTokenSource(); + var token = cts.Token; + + cts.Cancel(); + + try + { + await _cloudConvertAPI.CreateJobAsync(new JobCreateRequest + { + Tasks = new() + }, token).ConfigureAwait(false); + + Assert.Fail("Expected TaskCanceledException"); + } + catch (WebApiException ex) when (ex.InnerException is TaskCanceledException) + { + } + } + [TestCase("stream")] [TestCase("bytes")] public async Task CreateJob(string streamingMethod)