Skip to content

Commit

Permalink
Re-introduce hybrid repo, add some tests for it
Browse files Browse the repository at this point in the history
  • Loading branch information
NeilMountford committed Aug 8, 2023
1 parent 24e6efa commit 1e1a445
Show file tree
Hide file tree
Showing 11 changed files with 264 additions and 45 deletions.
35 changes: 0 additions & 35 deletions sample/CustomerApi.Tests/MovieController/GetMovieTests.cs

This file was deleted.

82 changes: 82 additions & 0 deletions sample/CustomerApi.Tests/MovieController/MovieTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System.Net;
using System.Net.Http.Json;
using CustomerApi.Controllers.Movies;
using CustomerApi.Uris;
using FluentAssertions;
using Xunit;

namespace CustomerApi.Tests.MovieController;

public class MovieTests
{
[Fact]
public async Task Valid_ReturnsOkWhenUsingEventStream()
{
using var customerApi = new TestCustomerApi();
await customerApi.Given.AnExistingMovie(MovieUri.Parse("/movies/ExistingMovie"), "The Matrix");

var client = customerApi.CreateClient();

var response = await client.GetAsync("/movies/ExistingMovie/by-event");

response.StatusCode.Should().Be(HttpStatusCode.OK);
}

[Fact]
public async Task Valid_ReturnsNotFoundWhenUsingSnapshotThatIsNotEnabled()
{
using var customerApi = new TestCustomerApi();
await customerApi.Given.AnExistingMovie(MovieUri.Parse("/movies/ExistingMovie"), "The Matrix");

var client = customerApi.CreateClient();

var response = await client.GetAsync("/movies/ExistingMovie/by-snapshot");

response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}

[Fact]
public async Task Valid_ReturnsOkWhenUsingHybridAndSnapshotIsNotEnabled()
{
using var customerApi = new TestCustomerApi();
await customerApi.Given.AnExistingMovie(MovieUri.Parse("/movies/ExistingMovie"), "The Matrix");

var client = customerApi.CreateClient();

var response = await client.GetAsync("/movies/ExistingMovie/by-event");

response.StatusCode.Should().Be(HttpStatusCode.OK);
}

[Fact]
public async Task Valid_SavesSnapshotWhenUsingHybridRepoToApply()
{
using var customerApi = new TestCustomerApi();
var client = customerApi.CreateClient();

var response = await client.PostAsJsonAsync("/movies/create-hybrid", new { name = "Hot Fuzz" });

response.StatusCode.Should().Be(HttpStatusCode.Created);
response.Headers.Location.Should().NotBeNull();
var movieUri = MovieUri.Parse(response.Headers.Location!.ToString());
await customerApi.Then.TheMovieSnapshotShouldMatch(movieUri, snapshot => snapshot.Name == "Hot Fuzz");
}

[Fact]
public async Task Valid_ReturnsOkWhenUsingHybridToQueryLatestChanges()
{
using var customerApi = new TestCustomerApi();
var movieUri = MovieUri.Parse("/movies/ExistingMovie");
await customerApi.Given.AnExistingMovieWithAProjectedSnapshot(movieUri, "The Matrix");
await customerApi.Given.AnExistingMovieNameIsChangedButNotProjected(movieUri, "The Matrix Reloaded");

var client = customerApi.CreateClient();

var response = await client.GetAsync("/movies/ExistingMovie/by-hybrid-query");

response.StatusCode.Should().Be(HttpStatusCode.OK);
var movie = await response.Content.ReadFromJsonAsync<MovieQueryResponse>();
movie.Should().NotBeNull();
movie!.Name.Should().Be("The Matrix Reloaded");
}
}
10 changes: 10 additions & 0 deletions sample/CustomerApi.Tests/TestApi/GivenSteps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ public async Task<MovieReadModel> AnExistingMovie(MovieUri movieUri, Discretiona
return await _movieStore.GivenAnExistingMovie(movieUri, name);
}

public async Task<MovieReadModel> AnExistingMovieWithAProjectedSnapshot(MovieUri movieUri, Discretionary<string> name = default)
{
return await _movieStore.GivenAnExistingMovieSnapshot(movieUri, name);
}

public async Task<MovieReadModel> AnExistingMovieNameIsChangedButNotProjected(MovieUri movieUri, string newName)
{
return await _movieStore.GivenAnExistingMovieNameIsChanged(movieUri, newName);
}

public async Task TheCustomerIsDeleted(CustomerUri customerUri)
{
await _customerStore.GivenTheCustomerIsDeleted(customerUri);
Expand Down
46 changes: 44 additions & 2 deletions sample/CustomerApi.Tests/TestApi/MovieStore.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using CustomerApi.Events.Movies;
using System.Linq.Expressions;
using CustomerApi.Events.Movies;
using CustomerApi.Uris;
using FluentAssertions;
using LogOtter.CosmosDb.EventStore;

// ReSharper disable UnusedMethodReturnValue.Local
Expand All @@ -9,10 +11,18 @@ namespace CustomerApi.Tests;
public class MovieStore
{
private readonly EventRepository<MovieEvent, MovieReadModel> _movieEventRepository;
private readonly SnapshotRepository<MovieEvent, MovieReadModel> _movieSnapshotRepository;
private readonly HybridRepository<MovieEvent, MovieReadModel> _movieHybridRepository;

public MovieStore(EventRepository<MovieEvent, MovieReadModel> movieEventRepository)
public MovieStore(
EventRepository<MovieEvent, MovieReadModel> movieEventRepository,
SnapshotRepository<MovieEvent, MovieReadModel> movieSnapshotRepository,
HybridRepository<MovieEvent, MovieReadModel> movieHybridRepository
)
{
_movieEventRepository = movieEventRepository;
_movieSnapshotRepository = movieSnapshotRepository;
_movieHybridRepository = movieHybridRepository;
}

public async Task<MovieReadModel> GivenAnExistingMovie(MovieUri movieUri, Discretionary<string> name)
Expand All @@ -27,4 +37,36 @@ public async Task<MovieReadModel> GivenAnExistingMovie(MovieUri movieUri, Discre

return await _movieEventRepository.ApplyEvents(movieUri.Uri, 0, movieAdded);
}

public async Task<MovieReadModel> GivenAnExistingMovieSnapshot(MovieUri movieUri, Discretionary<string> name)
{
var movieReadModel = await _movieEventRepository.Get(movieUri.Uri);
if (movieReadModel != null)
{
return movieReadModel;
}

var movieAdded = new MovieAdded(movieUri, name.GetValueOrDefault("Dwayne Dibley in the Duke of Dork"));

return await _movieHybridRepository.ApplyEventsAndUpdateSnapshotImmediately(movieUri.Uri, 0, CancellationToken.None, movieAdded);
}

public async Task<MovieReadModel> GivenAnExistingMovieNameIsChanged(MovieUri movieUri, string newName)
{
var movieReadModel = await _movieEventRepository.Get(movieUri.Uri);
if (movieReadModel == null)
{
throw new Exception("Movie not found, make sure you called a setup method to create it first");
}

var movieNameChanged = new MovieNameChanged(movieUri, newName);
return await _movieEventRepository.ApplyEvents(movieUri.Uri, movieReadModel.Revision, movieNameChanged);
}

public async Task ThenTheMovieSnapshotShouldMatch(MovieUri movieUri, Expression<Func<MovieReadModel, bool>> matchFunc)
{
var movie = await _movieSnapshotRepository.GetSnapshot(movieUri.Uri, MovieReadModel.StaticPartitionKey);
movie.Should().NotBeNull();
movie.Should().Match(matchFunc);
}
}
10 changes: 9 additions & 1 deletion sample/CustomerApi.Tests/TestApi/ThenSteps.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Linq.Expressions;
using CustomerApi.Events.Customers;
using CustomerApi.Events.Movies;
using CustomerApi.NonEventSourcedData.CustomerInterests;
using CustomerApi.Uris;

Expand All @@ -8,12 +9,14 @@ namespace CustomerApi.Tests;
public class ThenSteps
{
private readonly CustomerStore _customerStore;
private readonly MovieStore _movieStore;
private readonly SearchableInterestStore _searchableInterestStore;

public ThenSteps(CustomerStore customerStore, SearchableInterestStore searchableInterestStore)
public ThenSteps(CustomerStore customerStore, SearchableInterestStore searchableInterestStore, MovieStore movieStore)
{
_customerStore = customerStore;
_searchableInterestStore = searchableInterestStore;
_movieStore = movieStore;
}

public async Task TheCustomerShouldBeDeleted(CustomerUri customerUri)
Expand All @@ -31,6 +34,11 @@ public async Task TheMovieShouldMatch(MovieUri movieUri, Expression<Func<Movie,
await _customerStore.ThenTheMovieShouldMatch(movieUri, matchFunc);
}

public async Task TheMovieSnapshotShouldMatch(MovieUri movieUri, Expression<Func<MovieReadModel, bool>> matchFunc)
{
await _movieStore.ThenTheMovieSnapshotShouldMatch(movieUri, matchFunc);
}

public async Task TheSongShouldMatch(SongUri songUri, Expression<Func<Song, bool>> matchFunc)
{
await _customerStore.ThenTheSongShouldMatch(songUri, matchFunc);
Expand Down
3 changes: 3 additions & 0 deletions sample/CustomerApi/Controllers/Movies/CreateMovieRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace CustomerApi.Controllers.Movies;

public record CreateMovieRequest(string Name);
62 changes: 58 additions & 4 deletions sample/CustomerApi/Controllers/Movies/MovieController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,25 @@
namespace CustomerApi.Controllers.Movies;

[ApiController]
[Route("movies/{movieId}")]
[Route("movies")]
public class MovieController : ControllerBase
{
private readonly EventRepository<MovieEvent, MovieReadModel> _movieEventRepository;
private readonly SnapshotRepository<MovieEvent, MovieReadModel> _movieSnapshotRepository;
private readonly HybridRepository<MovieEvent, MovieReadModel> _movieHybridRepository;

public MovieController(
EventRepository<MovieEvent, MovieReadModel> movieEventRepository,
SnapshotRepository<MovieEvent, MovieReadModel> movieSnapshotRepository
SnapshotRepository<MovieEvent, MovieReadModel> movieSnapshotRepository,
HybridRepository<MovieEvent, MovieReadModel> movieHybridRepository
)
{
_movieEventRepository = movieEventRepository;
_movieSnapshotRepository = movieSnapshotRepository;
_movieHybridRepository = movieHybridRepository;
}

[HttpGet("by-event")]
[HttpGet("{movieId}/by-event")]
public async Task<IActionResult> GetByEvent(string movieId, CancellationToken cancellationToken)
{
var movieUri = new MovieUri(movieId);
Expand All @@ -35,7 +38,7 @@ public async Task<IActionResult> GetByEvent(string movieId, CancellationToken ca
return NotFound();
}

[HttpGet("by-snapshot")]
[HttpGet("{movieId}/by-snapshot")]
public async Task<IActionResult> GetBySnapshot(string movieId, CancellationToken cancellationToken)
{
var movieUri = new MovieUri(movieId);
Expand All @@ -48,4 +51,55 @@ public async Task<IActionResult> GetBySnapshot(string movieId, CancellationToken

return NotFound();
}

[HttpGet("{movieId}/by-hybrid")]
public async Task<IActionResult> GetByHybrid(string movieId, CancellationToken cancellationToken)
{
var movieUri = new MovieUri(movieId);
var movie = await _movieHybridRepository.GetSnapshotWithCatchupExpensivelyAsync(
movieUri.Uri,
MovieReadModel.StaticPartitionKey,
cancellationToken: cancellationToken
);

if (movie != null)
{
return Ok(movie);
}

return NotFound();
}

[HttpGet("{movieId}/by-hybrid-query")]
public async Task<IActionResult> GetByHybridQuery(string movieId, CancellationToken cancellationToken)
{
var movieUri = new MovieUri(movieId);

var movies = await _movieHybridRepository
.QuerySnapshotsWithCatchupExpensivelyAsync(
MovieReadModel.StaticPartitionKey,
q => q.Where(m => m.MovieUri.Uri == movieUri.Uri),
cancellationToken: cancellationToken
)
.ToListAsync();

var movie = movies.FirstOrDefault();
if (movie != null)
{
return Ok(new MovieQueryResponse(movie.MovieUri, movie.Name));
}

return NotFound();
}

[HttpPost("create-hybrid")]
public async Task<IActionResult> CreateUsingHybrid(CreateMovieRequest request, CancellationToken cancellationToken)
{
var movieUri = MovieUri.Generate();

var movieCreated = new MovieAdded(movieUri, request.Name);
var movie = await _movieHybridRepository.ApplyEventsAndUpdateSnapshotImmediately(movieUri.Uri, 0, cancellationToken, movieCreated);

return Created(movieUri.Uri, movie);
}
}
5 changes: 5 additions & 0 deletions sample/CustomerApi/Controllers/Movies/MovieQueryResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using CustomerApi.Uris;

namespace CustomerApi.Controllers.Movies;

public record MovieQueryResponse(MovieUri MovieUri, string Name);
24 changes: 24 additions & 0 deletions sample/CustomerApi/Events/Movies/MovieNameChanged.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using CustomerApi.Uris;

namespace CustomerApi.Events.Movies;

public class MovieNameChanged : MovieEvent
{
public string Name { get; }

public MovieNameChanged(MovieUri movieUri, string name, DateTimeOffset? timestamp = null)
: base(movieUri, timestamp)
{
Name = name;
}

public override void Apply(MovieReadModel model)
{
model.Name = Name;
}

public override string GetDescription()
{
return $"Movie {MovieUri} name changed to {Name}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public EventSourcingBuilder AddEventSource<TBaseEvent>(string containerName, Act

var eventRepository = typeof(EventRepository<,>);
var snapshotRepository = typeof(SnapshotRepository<,>);
var hybridRepository = typeof(HybridRepository<,>);
foreach (var projection in config.Projections)
{
Services.AddSingleton(eventRepository.MakeGenericType(typeof(TBaseEvent), projection.ProjectionType));
Expand All @@ -77,6 +78,7 @@ public EventSourcingBuilder AddEventSource<TBaseEvent>(string containerName, Act
);

Services.AddSingleton(snapshotRepository.MakeGenericType(typeof(TBaseEvent), projection.ProjectionType));
Services.AddSingleton(hybridRepository.MakeGenericType(typeof(TBaseEvent), projection.ProjectionType));
}
}

Expand Down
Loading

0 comments on commit 1e1a445

Please sign in to comment.