Skip to content

Commit

Permalink
Add cancellation token support (ardalis#23)
Browse files Browse the repository at this point in the history
* feat(lib): Add cancellation token support for async endpoints

* feat(tests): Add unit tests for cancellation token

* chore(Sample): Update formatting from CodeMaid run

* chore(Tests): Fix type in method names
  • Loading branch information
JoeyMckenzie authored Jul 14, 2020
1 parent ae36b23 commit 0bd2823
Show file tree
Hide file tree
Showing 15 changed files with 174 additions and 52 deletions.
22 changes: 22 additions & 0 deletions sample/Sample.FunctionalTests/AuthorEndpoints/CreateEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
using SampleEndpointApp.Authors;
using SampleEndpointApp.DataAccess;
using SampleEndpointApp.DomainModel;
using System;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

Expand Down Expand Up @@ -44,5 +46,25 @@ public async Task CreatesANewAuthor()
Assert.Equal(result.PluralsightUrl, newAuthor.PluralsightUrl);
Assert.Equal(result.TwitterAlias, newAuthor.TwitterAlias);
}

[Fact]
public async Task GivenLongRunningCreateRequest_WhenTokenSourceCallsForCancellation_RequestIsTerminated()
{
// Arrange, generate a token source that times out instantly
var tokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(0));
var lastAuthor = SeedData.Authors().Last();
var newAuthor = new CreateAuthorCommand()
{
Name = "James Eastham",
PluralsightUrl = "https://app.pluralsight.com",
TwitterAlias = "jeasthamdev",
};

// Act
var request = _client.PostAsync("/authors", new StringContent(JsonConvert.SerializeObject(newAuthor), Encoding.UTF8, "application/json"), tokenSource.Token);

// Assert
await Assert.ThrowsAsync<OperationCanceledException>(async () => await request);
}
}
}
15 changes: 15 additions & 0 deletions sample/Sample.FunctionalTests/AuthorEndpoints/DeleteEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
using SampleEndpointApp.Authors;
using SampleEndpointApp.DataAccess;
using SampleEndpointApp.DomainModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

Expand Down Expand Up @@ -40,5 +42,18 @@ public async Task DeleteAnExistingAuthor()
Assert.Equal(2, result.DeletedAuthorId);
Assert.True(listResult.Count() <= 2);
}

[Fact]
public async Task GivenLongRunningDeleteRequest_WhenTokenSourceCallsForCancellation_RequestIsTerminated()
{
// Arrange, generate a token source that times out instantly
var tokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(0));

// Act
var request = _client.DeleteAsync("/authors/2", tokenSource.Token);

// Assert
await Assert.ThrowsAsync<OperationCanceledException>(async () => await request);
}
}
}
16 changes: 16 additions & 0 deletions sample/Sample.FunctionalTests/AuthorEndpoints/GetEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
using SampleEndpointApp;
using SampleEndpointApp.DataAccess;
using SampleEndpointApp.DomainModel;
using System;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

Expand Down Expand Up @@ -34,5 +36,19 @@ public async Task ReturnsAuthorById()
Assert.Equal(firstAuthor.PluralsightUrl, result.PluralsightUrl);
Assert.Equal(firstAuthor.TwitterAlias, result.TwitterAlias);
}

[Fact]
public async Task GivenLongRunningGetRequest_WhenTokenSourceCallsForCancellation_RequestIsTerminated()
{
// Arrange, generate a token source that times out instantly
var tokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(0));
var firstAuthor = SeedData.Authors().First();

// Act
var request = _client.GetAsync($"/authors/{firstAuthor}", tokenSource.Token);

// Assert
await Assert.ThrowsAsync<OperationCanceledException>(async () => await request);
}
}
}
15 changes: 15 additions & 0 deletions sample/Sample.FunctionalTests/AuthorEndpoints/ListEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
using SampleEndpointApp;
using SampleEndpointApp.DataAccess;
using SampleEndpointApp.DomainModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

Expand All @@ -30,5 +32,18 @@ public async Task ReturnsTwoGivenTwoAuthors()
Assert.NotNull(result);
Assert.Equal(SeedData.Authors().Count(), result.Count());
}

[Fact]
public async Task GivenLongRunningListRequest_WhenTokenSourceCallsForCancellation_RequestIsTerminated()
{
// Arrange, generate a token source that times out instantly
var tokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(0));

// Act
var request = _client.GetAsync("/authors", tokenSource.Token);

// Assert
var response = await Assert.ThrowsAsync<OperationCanceledException>(async () => await request);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
using SampleEndpointApp;
using SampleEndpointApp.DataAccess;
using SampleEndpointApp.DomainModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

Expand All @@ -28,7 +30,20 @@ public async Task Page1PerPage1_ShouldReturnFirstAuthor()
var result = JsonConvert.DeserializeObject<IEnumerable<Author>>(stringResponse);

Assert.NotNull(result);
Assert.Equal(1, result.Count());
Assert.Single(result);
}

[Fact]
public async Task GivenLongRunningPaginatedListRequest_WhenTokenSourceCallsForCancellation_RequestIsTerminated()
{
// Arrange, generate a token source that times out instantly
var tokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(0));

// Act
var request = _client.GetAsync("/authors?perPage=1&page=1", tokenSource.Token);

// Assert
var response = await Assert.ThrowsAsync<OperationCanceledException>(async () => await request);
}
}
}
21 changes: 21 additions & 0 deletions sample/Sample.FunctionalTests/AuthorEndpoints/UpdateEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
using SampleEndpointApp.Authors;
using SampleEndpointApp.DataAccess;
using SampleEndpointApp.DomainModel;
using System;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

Expand Down Expand Up @@ -44,5 +46,24 @@ public async Task UpdateAnExistingAuthor()
Assert.Equal(result.PluralsightUrl, authorPreUpdate.PluralsightUrl);
Assert.Equal(result.TwitterAlias, authorPreUpdate.TwitterAlias);
}

[Fact]
public async Task GivenLongRunningUpdateRequest_WhenTokenSourceCallsForCancellation_RequestIsTerminated()
{
// Arrange, generate a token source that times out instantly
var tokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(0));
var authorPreUpdate = SeedData.Authors().FirstOrDefault(p => p.Id == 2);
var updatedAuthor = new UpdateAuthorCommand()
{
Id = 2,
Name = "James Eastham",
};

// Act
var request = _client.PutAsync("/authors", new StringContent(JsonConvert.SerializeObject(updatedAuthor), Encoding.UTF8, "application/json"), tokenSource.Token);

// Assert
await Assert.ThrowsAsync<OperationCanceledException>(async () => await request);
}
}
}
5 changes: 3 additions & 2 deletions sample/SampleEndpointApp/AuthorEndpoints/Create.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.AspNetCore.Mvc;
using SampleEndpointApp.DomainModel;
using Swashbuckle.AspNetCore.Annotations;
using System.Threading;
using System.Threading.Tasks;

namespace SampleEndpointApp.Authors
Expand All @@ -26,11 +27,11 @@ public Create(IAsyncRepository<Author> repository,
OperationId = "Author.Create",
Tags = new[] { "AuthorEndpoint" })
]
public override async Task<ActionResult<CreateAuthorResult>> HandleAsync([FromBody]CreateAuthorCommand request)
public override async Task<ActionResult<CreateAuthorResult>> HandleAsync([FromBody]CreateAuthorCommand request, CancellationToken cancellationToken)
{
var author = new Author();
_mapper.Map(request, author);
await _repository.AddAsync(author);
await _repository.AddAsync(author, cancellationToken);

var result = _mapper.Map<CreateAuthorResult>(author);
return Ok(result);
Expand Down
14 changes: 10 additions & 4 deletions sample/SampleEndpointApp/AuthorEndpoints/Delete.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Threading.Tasks;

using Swashbuckle.AspNetCore.Annotations;
using System.Threading;

namespace SampleEndpointApp.Authors
{
Expand All @@ -23,11 +24,16 @@ public Delete(IAsyncRepository<Author> repository)
OperationId = "Author.Delete",
Tags = new[] { "AuthorEndpoint" })
]
public override async Task<ActionResult<DeletedAuthorResult>> HandleAsync(int id)
public override async Task<ActionResult<DeletedAuthorResult>> HandleAsync(int id, CancellationToken cancellationToken)
{
var author = await _repository.GetByIdAsync(id);
if (author == null) return NotFound(id);
await _repository.DeleteAsync(author);
var author = await _repository.GetByIdAsync(id, cancellationToken);

if (author is null)
{
return NotFound(id);
}

await _repository.DeleteAsync(author, cancellationToken);

return Ok(new DeletedAuthorResult { DeletedAuthorId = id });
}
Expand Down
7 changes: 3 additions & 4 deletions sample/SampleEndpointApp/AuthorEndpoints/Get.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
using Microsoft.AspNetCore.Mvc;
using SampleEndpointApp.DomainModel;
using System.Threading.Tasks;

using Swashbuckle.AspNetCore.Annotations;
using System.Threading;

namespace SampleEndpointApp.Authors
{
Expand All @@ -21,16 +21,15 @@ public Get(IAsyncRepository<Author> repository,
}

[HttpGet("/authors/{id}")]

[SwaggerOperation(
Summary = "Get a specific Author",
Description = "Get a specific Author",
OperationId = "Author.Get",
Tags = new[] { "AuthorEndpoint" })
]
public override async Task<ActionResult<AuthorResult>> HandleAsync(int id)
public override async Task<ActionResult<AuthorResult>> HandleAsync(int id, CancellationToken cancellationToken)
{
var author = await _repository.GetByIdAsync(id);
var author = await _repository.GetByIdAsync(id, cancellationToken);

var result = _mapper.Map<AuthorResult>(author);

Expand Down
6 changes: 3 additions & 3 deletions sample/SampleEndpointApp/AuthorEndpoints/List.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using SampleEndpointApp.DomainModel;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using Swashbuckle.AspNetCore.Annotations;
using System.Threading;

namespace SampleEndpointApp.Authors
{
Expand All @@ -29,9 +29,9 @@ public List(IAsyncRepository<Author> repository,
OperationId = "Author.List",
Tags = new[] { "AuthorEndpoint" })
]
public async Task<ActionResult> HandleAsync([FromQuery] int page = 1, int perPage = 10)
public async Task<ActionResult> HandleAsync([FromQuery] int page = 1, int perPage = 10, CancellationToken cancellationToken = default)
{
var result = (await _repository.ListAllAsync(perPage, page))
var result = (await _repository.ListAllAsync(perPage, page, cancellationToken))
.Select(i => _mapper.Map<AuthorListResult>(i));

return Ok(result);
Expand Down
7 changes: 4 additions & 3 deletions sample/SampleEndpointApp/AuthorEndpoints/Update.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading.Tasks;

using Swashbuckle.AspNetCore.Annotations;
using System.Threading;

namespace SampleEndpointApp.Authors
{
Expand All @@ -28,11 +29,11 @@ public Update(IAsyncRepository<Author> repository,
OperationId = "Author.Update",
Tags = new[] { "AuthorEndpoint" })
]
public override async Task<ActionResult<UpdatedAuthorResult>> HandleAsync([FromBody]UpdateAuthorCommand request)
public override async Task<ActionResult<UpdatedAuthorResult>> HandleAsync([FromBody]UpdateAuthorCommand request, CancellationToken cancellationToken)
{
var author = await _repository.GetByIdAsync(request.Id);
var author = await _repository.GetByIdAsync(request.Id, cancellationToken);
_mapper.Map(request, author);
await _repository.UpdateAsync(author);
await _repository.UpdateAsync(author, cancellationToken);

var result = _mapper.Map<UpdatedAuthorResult>(author);
return Ok(result);
Expand Down
28 changes: 15 additions & 13 deletions sample/SampleEndpointApp/DataAccess/EfRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using SampleEndpointApp.DomainModel;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace SampleEndpointApp.DataAccess
Expand All @@ -19,42 +20,43 @@ public EfRepository(AppDbContext dbContext)
_dbContext = dbContext;
}

public virtual async Task<T> GetByIdAsync(int id)
public virtual async Task<T> GetByIdAsync(int id, CancellationToken cancellationToken)
{
return await _dbContext.Set<T>().FindAsync(id);
return await _dbContext.Set<T>().FirstOrDefaultAsync(a => a.Id == id, cancellationToken);
}

public async Task<IReadOnlyList<T>> ListAllAsync()
public async Task<IReadOnlyList<T>> ListAllAsync(CancellationToken cancellationToken)
{
return await _dbContext.Set<T>().ToListAsync();
return await _dbContext.Set<T>().ToListAsync(cancellationToken);
}

/// <inheritdoc />
public async Task<IReadOnlyList<T>> ListAllAsync(
int perPage,
int page)
int page,
CancellationToken cancellationToken)
{
return await this._dbContext.Set<T>().Skip(perPage * (page - 1)).Take(perPage).ToListAsync();
return await _dbContext.Set<T>().Skip(perPage * (page - 1)).Take(perPage).ToListAsync(cancellationToken);
}

public async Task<T> AddAsync(T entity)
public async Task<T> AddAsync(T entity, CancellationToken cancellationToken)
{
await _dbContext.Set<T>().AddAsync(entity);
await _dbContext.SaveChangesAsync();
await _dbContext.Set<T>().AddAsync(entity, cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken);

return entity;
}

public async Task UpdateAsync(T entity)
public async Task UpdateAsync(T entity, CancellationToken cancellationToken)
{
_dbContext.Entry(entity).State = EntityState.Modified;
await _dbContext.SaveChangesAsync();
await _dbContext.SaveChangesAsync(cancellationToken);
}

public async Task DeleteAsync(T entity)
public async Task DeleteAsync(T entity, CancellationToken cancellationToken)
{
_dbContext.Set<T>().Remove(entity);
await _dbContext.SaveChangesAsync();
await _dbContext.SaveChangesAsync(cancellationToken);
}
}
}
Loading

0 comments on commit 0bd2823

Please sign in to comment.