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