From 313d668bfec86d4314439f04816f3b8cfea77e55 Mon Sep 17 00:00:00 2001 From: Robert Marshall Date: Sun, 21 Jun 2020 22:39:10 +0100 Subject: [PATCH] Add code for using MongoDB --- src/Robware.Api.Blog.sln | 12 +++ src/Robware.Api.Blog/Robware.Api.Blog.csproj | 2 + .../ItemNotFoundException.cs | 2 +- .../BlogRepositoryTests.cs | 54 +++++++++++++ .../Robware.Data.MongoDB.Tests.csproj | 24 ++++++ src/Robware.Data.MongoDB/BlogRepository.cs | 77 +++++++++++++++++++ .../Robware.Data.MongoDB.csproj | 15 ++++ .../State/BlogPostState.cs | 33 ++++++++ 8 files changed, 218 insertions(+), 1 deletion(-) rename src/{Robware.Data => Robware.Blog}/ItemNotFoundException.cs (91%) create mode 100644 src/Robware.Data.MongoDB.Tests/BlogRepositoryTests.cs create mode 100644 src/Robware.Data.MongoDB.Tests/Robware.Data.MongoDB.Tests.csproj create mode 100644 src/Robware.Data.MongoDB/BlogRepository.cs create mode 100644 src/Robware.Data.MongoDB/Robware.Data.MongoDB.csproj create mode 100644 src/Robware.Data.MongoDB/State/BlogPostState.cs diff --git a/src/Robware.Api.Blog.sln b/src/Robware.Api.Blog.sln index a8953ef..ab12437 100644 --- a/src/Robware.Api.Blog.sln +++ b/src/Robware.Api.Blog.sln @@ -20,6 +20,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robware.Api.Blog.Tests", "R EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "..\build\_build.csproj", "{9F55FA01-FEC9-4326-8338-4EF014E127A4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robware.Data.MongoDB", "Robware.Data.MongoDB\Robware.Data.MongoDB.csproj", "{D48723D5-053A-4654-B37D-CC2FA62625C3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robware.Data.MongoDB.Tests", "Robware.Data.MongoDB.Tests\Robware.Data.MongoDB.Tests.csproj", "{E4361445-DD13-4DB3-A278-4198F4DEE00A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -48,6 +52,14 @@ Global {54DEC274-5F28-42FB-9B60-1E4BAF3D7BF9}.Debug|Any CPU.Build.0 = Debug|Any CPU {54DEC274-5F28-42FB-9B60-1E4BAF3D7BF9}.Release|Any CPU.ActiveCfg = Release|Any CPU {54DEC274-5F28-42FB-9B60-1E4BAF3D7BF9}.Release|Any CPU.Build.0 = Release|Any CPU + {D48723D5-053A-4654-B37D-CC2FA62625C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D48723D5-053A-4654-B37D-CC2FA62625C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D48723D5-053A-4654-B37D-CC2FA62625C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D48723D5-053A-4654-B37D-CC2FA62625C3}.Release|Any CPU.Build.0 = Release|Any CPU + {E4361445-DD13-4DB3-A278-4198F4DEE00A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E4361445-DD13-4DB3-A278-4198F4DEE00A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4361445-DD13-4DB3-A278-4198F4DEE00A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E4361445-DD13-4DB3-A278-4198F4DEE00A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Robware.Api.Blog/Robware.Api.Blog.csproj b/src/Robware.Api.Blog/Robware.Api.Blog.csproj index 0a85bab..7d4a1dc 100644 --- a/src/Robware.Api.Blog/Robware.Api.Blog.csproj +++ b/src/Robware.Api.Blog/Robware.Api.Blog.csproj @@ -5,12 +5,14 @@ + + diff --git a/src/Robware.Data/ItemNotFoundException.cs b/src/Robware.Blog/ItemNotFoundException.cs similarity index 91% rename from src/Robware.Data/ItemNotFoundException.cs rename to src/Robware.Blog/ItemNotFoundException.cs index c097c95..f3ea74e 100644 --- a/src/Robware.Data/ItemNotFoundException.cs +++ b/src/Robware.Blog/ItemNotFoundException.cs @@ -1,6 +1,6 @@ using System; -namespace Robware.Data { +namespace Robware.Blog { public class ItemNotFoundException:Exception { public object Parameters { get; } diff --git a/src/Robware.Data.MongoDB.Tests/BlogRepositoryTests.cs b/src/Robware.Data.MongoDB.Tests/BlogRepositoryTests.cs new file mode 100644 index 0000000..4a8529f --- /dev/null +++ b/src/Robware.Data.MongoDB.Tests/BlogRepositoryTests.cs @@ -0,0 +1,54 @@ +using FluentAssertions; +using MongoDB.Driver; +using NSubstitute; +using Robware.Blog; +using Robware.Blog.Data.MongoDB.State; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Robware.Data.MongoDB.Tests { + public class BlogRepositoryTests { + [Fact] + public async Task SavePost_WithNewPost_GetsNewIdAndSetsTimestampAndSavesAsync() { + var collection = Substitute.For>(); + collection.CountDocumentsAsync(Arg.Any>()).Returns(10); + var database = Substitute.For(); + database.GetCollection("blog").Returns(collection); + + var repo = new BlogRepository(database); + var post = new BlogPost(); + await repo.SavePost(post); + post.Id.Should().Be(11); + post.Timestamp.Should().BeCloseTo(DateTime.Now); + await collection.Received(1).InsertOneAsync(Arg.Any()); + } + + [Fact] + public async Task SavePost_WithExistingPost_DoesNotSetTimestampAndSavesAsync() { + var collection = Substitute.For>(); + var database = Substitute.For(); + database.GetCollection("blog").Returns(collection); + + var repo = new BlogRepository(database); + var post = new BlogPost() { + Id = 1 + }; + await repo.SavePost(post); + post.Id.Should().Be(1); + post.Timestamp.Should().Be(new DateTime()); + await collection.Received(1).InsertOneAsync(Arg.Any()); + } + + [Fact] + public async Task GetCountAsync_ReturnsCountFromDatabase() { + var collection = Substitute.For>(); + collection.CountDocumentsAsync(Arg.Any>()).Returns(10); + var database = Substitute.For(); + database.GetCollection("blog").Returns(collection); + + var repo = new BlogRepository(database); + (await repo.GetCountAsync()).Should().Be(10); + } + } +} diff --git a/src/Robware.Data.MongoDB.Tests/Robware.Data.MongoDB.Tests.csproj b/src/Robware.Data.MongoDB.Tests/Robware.Data.MongoDB.Tests.csproj new file mode 100644 index 0000000..ff7b67b --- /dev/null +++ b/src/Robware.Data.MongoDB.Tests/Robware.Data.MongoDB.Tests.csproj @@ -0,0 +1,24 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + + + + + diff --git a/src/Robware.Data.MongoDB/BlogRepository.cs b/src/Robware.Data.MongoDB/BlogRepository.cs new file mode 100644 index 0000000..d8faa77 --- /dev/null +++ b/src/Robware.Data.MongoDB/BlogRepository.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using MongoDB.Driver; +using Robware.Blog; +using Robware.Blog.Data.MongoDB.State; + +namespace Robware.Data.MongoDB { + public class BlogRepository : IBlogRepository { + private readonly IMongoCollection _collection; + + public BlogRepository(IMongoDatabase database) { + _collection = database.GetCollection("blog"); + } + + private BlogPost MapStateToPost(BlogPostState state) => + new BlogPost { + Id = state.Id, + Title = state.Title, + Content = state.Content, + Timestamp = state.Timestamp, + Draft = state.Draft, + Url = state.Url, + UserId = state.UserId, + }; + + private IEnumerable MapStateToPost(IEnumerable states) => states.Select(MapStateToPost); + + public async Task DeletePostAsync(int id) { + var update = Builders.Update.Set(nameof(BlogPostState.Deleted), true); + await _collection.UpdateOneAsync(post => post.Id == id, update); + } + + public async Task> GetAllPostsAsync() => MapStateToPost((await _collection.FindAsync(post => true)).ToEnumerable()); + + public async Task GetCountAsync() => (int)await _collection.CountDocumentsAsync(post => !post.Deleted); + + public async Task GetLatestPostAsync() => (await GetLatestPostsAsync(1)).FirstOrDefault(); + + public async Task> GetLatestPostsAsync(int limit, int offset = 0) { + var filter = Builders.Filter.Eq(nameof(BlogPostState.Deleted), false); + var sort = Builders.Sort.Descending(nameof(BlogPostState.Timestamp)); + var options = new FindOptions { + Sort = sort, + Limit = limit, + Skip = offset + }; + var states = (await _collection.FindAsync(filter, options)).ToEnumerable(); + return MapStateToPost(states); + } + + private async Task GetPostAsync(Expression> filter) { + var result = (await _collection.FindAsync(filter)).FirstOrDefault(); + if (result == null) + throw new ItemNotFoundException(nameof(GetPostAsync), null); + return MapStateToPost(result); + } + + public async Task GetPostByIdAsync(int id) => await GetPostAsync(post => post.Id == id && !post.Deleted); + + public async Task GetPostByUrlAsync(string url) => await GetPostAsync(post => post.Url == url && !post.Deleted); + + public async Task SavePost(BlogPost post) { + if (post.Id == 0) { + post.Id = (await GetCountAsync()) + 1; + post.Timestamp = DateTime.Now; + } + + var mongoPost = new BlogPostState(post); + await _collection.InsertOneAsync(mongoPost); + + return post; + } + } +} diff --git a/src/Robware.Data.MongoDB/Robware.Data.MongoDB.csproj b/src/Robware.Data.MongoDB/Robware.Data.MongoDB.csproj new file mode 100644 index 0000000..fe591c3 --- /dev/null +++ b/src/Robware.Data.MongoDB/Robware.Data.MongoDB.csproj @@ -0,0 +1,15 @@ + + + + + + + + + + + + netcoreapp3.1 + + + diff --git a/src/Robware.Data.MongoDB/State/BlogPostState.cs b/src/Robware.Data.MongoDB/State/BlogPostState.cs new file mode 100644 index 0000000..4ecfd41 --- /dev/null +++ b/src/Robware.Data.MongoDB/State/BlogPostState.cs @@ -0,0 +1,33 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System; + +namespace Robware.Blog.Data.MongoDB.State { + public class BlogPostState { + public BlogPostState() + { + + } + + public BlogPostState(BlogPost basePost) { + Id = basePost.Id; + Title = basePost.Title; + Content = basePost.Content; + Timestamp = basePost.Timestamp; + Draft = basePost.Draft; + Url = basePost.Url; + UserId = basePost.UserId; + Deleted = false; + } + + [BsonId] + public int Id { get; set; } + public string Title { get; private set; } + public string Content { get; private set; } + public DateTime Timestamp { get; private set; } + public string Draft { get; private set; } + public string Url { get; private set; } + public int UserId { get; private set; } + public bool Deleted { get; private set; } + } +} \ No newline at end of file