diff --git a/Cosmonaut.Console/Program.cs b/Cosmonaut.Console/Program.cs index 0e5cbcd..8e9a4ca 100644 --- a/Cosmonaut.Console/Program.cs +++ b/Cosmonaut.Console/Program.cs @@ -32,12 +32,14 @@ static void Main(string[] args) var provider = serviceCollection.BuildServiceProvider(); var cosmoStore = provider.GetService>(); + cosmoStore.RemoveAsync(x => true).GetAwaiter().GetResult(); + var books = new List(); for (int i = 0; i < 50; i++) { books.Add(new Book { - CosmosId = Guid.NewGuid().ToString(), + Id = Guid.NewGuid().ToString(), Name = "Test " + i }); } diff --git a/Cosmonaut.Models/Book.cs b/Cosmonaut.Models/Book.cs index d3a2c78..2b9edb6 100644 --- a/Cosmonaut.Models/Book.cs +++ b/Cosmonaut.Models/Book.cs @@ -4,12 +4,13 @@ namespace Cosmonaut.Models { [CosmosCollection(Throughput = 1000)] - public class Book : ICosmosEntity + public class Book { public string Name { get; set; } public TestUser Author { get; set; } - public string CosmosId { get; set; } + [JsonProperty("id")] + public string Id { get; set; } } } \ No newline at end of file diff --git a/Cosmonaut.Tests/Cosmonaut.Tests.csproj b/Cosmonaut.Tests/Cosmonaut.Tests.csproj index bad93ae..27bd8a3 100644 --- a/Cosmonaut.Tests/Cosmonaut.Tests.csproj +++ b/Cosmonaut.Tests/Cosmonaut.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/Cosmonaut.Tests/CosmosAddTests.cs b/Cosmonaut.Tests/CosmosAddTests.cs new file mode 100644 index 0000000..f2d16ce --- /dev/null +++ b/Cosmonaut.Tests/CosmosAddTests.cs @@ -0,0 +1,72 @@ +using System; +using System.Threading.Tasks; +using Cosmonaut.Exceptions; +using Cosmonaut.Response; +using Xunit; + +namespace Cosmonaut.Tests +{ + public class CosmosAddTests + { + private readonly ICosmosStore _dummyStore; + + public CosmosAddTests() + { + _dummyStore = new InMemoryCosmosStore(); + } + + [Fact] + public async Task AddValidObjectSuccess() + { + // Arrange + var id = Guid.NewGuid().ToString(); + var dummy = new Dummy + { + Id = id, + Name = "Nick" + }; + + // Act + var expectedResponse = new CosmosResponse(dummy, CosmosOperationStatus.Success); + var result = await _dummyStore.AddAsync(dummy); + + //Assert + Assert.Equal(expectedResponse.Entity, result.Entity); + Assert.Equal(expectedResponse.IsSuccess, result.IsSuccess); + } + + [Fact] + public async Task AddEntityWithoutIdEmptyGeneratedId() + { + // Arrange + var dummy = new Dummy + { + Name = "Nick" + }; + + // Act + var result = await _dummyStore.AddAsync(dummy); + + //Assert + var isGuid = Guid.TryParse(result.Entity.Id, out var guid); + Assert.True(isGuid); + Assert.NotEqual(Guid.Empty, guid); + } + + [Fact] + public async Task AddingEntityWithoutIdThrowsException() + { + // Arrange + var dummy = new + { + Name = "Name" + }; + + // Act + var addTask = new InMemoryCosmosStore().AddAsync(dummy); + + //Assert + await Assert.ThrowsAsync>(() => addTask); + } + } +} \ No newline at end of file diff --git a/Cosmonaut.Tests/CosmosCrudTests.cs b/Cosmonaut.Tests/CosmosCrudTests.cs deleted file mode 100644 index 476efad..0000000 --- a/Cosmonaut.Tests/CosmosCrudTests.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Threading.Tasks; -using Cosmonaut.Response; -using Xunit; - -namespace Cosmonaut.Tests -{ - public class CosmosCrudTests - { - private readonly ICosmosStore _dummyStore; - - public CosmosCrudTests() - { - _dummyStore = new InMemoryCosmosStore(); - } - - [Fact] - public async Task AddValidObjectSuccess() - { - // Arrange - var id = Guid.NewGuid().ToString(); - var dummy = new Dummy - { - Id = id, - Name = "Nick" - }; - - // Act - var expectedResponse = new CosmosResponse(dummy, CosmosOperationStatus.Success); - var result = await _dummyStore.AddAsync(dummy); - - //Assert - Assert.Equal(expectedResponse.Entity, result.Entity); - Assert.Equal(expectedResponse.IsSuccess, result.IsSuccess); - } - } -} \ No newline at end of file diff --git a/Cosmonaut.Tests/CosmosRemoveTests.cs b/Cosmonaut.Tests/CosmosRemoveTests.cs new file mode 100644 index 0000000..393c170 --- /dev/null +++ b/Cosmonaut.Tests/CosmosRemoveTests.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Cosmonaut.Response; +using Xunit; + +namespace Cosmonaut.Tests +{ + public class CosmosRemoveTests + { + private readonly ICosmosStore _dummyStore; + + public CosmosRemoveTests() + { + _dummyStore = new InMemoryCosmosStore(); + } + + [Fact] + public async Task RemoveEntityRemoves() + { + // Assign + var id = Guid.NewGuid().ToString(); + var addedDummy = new Dummy + { + Id = id, + Name = "Test" + }; + await _dummyStore.AddAsync(addedDummy); + + // Act + var result = await _dummyStore.RemoveAsync(addedDummy); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(CosmosOperationStatus.Success, result.CosmosOperationStatus); + } + + [Fact] + public async Task RemoveByIdRemoves() + { + // Assign + var id = Guid.NewGuid().ToString(); + var addedDummy = new Dummy + { + Id = id, + Name = "Test" + }; + await _dummyStore.AddAsync(addedDummy); + + // Act + var result = await _dummyStore.RemoveByIdAsync(id); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(CosmosOperationStatus.Success, result.CosmosOperationStatus); + } + + [Fact] + public async Task RemoveByExpressionRemoves() + { + // Assign + foreach (var i in Enumerable.Range(0, 10)) + { + var id = Guid.NewGuid().ToString(); + var addedDummy = new Dummy + { + Id = id, + Name = "Test " + i + }; + await _dummyStore.AddAsync(addedDummy); + } + + // Act + var result = await _dummyStore.RemoveAsync(x => x.Name.Contains("Test")); + + // Assert + Assert.True(result.IsSuccess); + Assert.Empty(result.FailedEntities); + } + + [Fact] + public async Task RemoveRangeRemoves() + { + // Assign + var addedList = new List(); + foreach (var i in Enumerable.Range(0, 10)) + { + var id = Guid.NewGuid().ToString(); + var addedDummy = new Dummy + { + Id = id, + Name = "Test " + i + }; + await _dummyStore.AddAsync(addedDummy); + addedList.Add(addedDummy); + } + + // Act + var result = await _dummyStore.RemoveRangeAsync(addedList); + + // Assert + Assert.True(result.IsSuccess); + Assert.Empty(result.FailedEntities); + } + } +} \ No newline at end of file diff --git a/Cosmonaut.Tests/CosmosUpdateTests.cs b/Cosmonaut.Tests/CosmosUpdateTests.cs new file mode 100644 index 0000000..c2ed39d --- /dev/null +++ b/Cosmonaut.Tests/CosmosUpdateTests.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Cosmonaut.Response; +using Xunit; + +namespace Cosmonaut.Tests +{ + public class CosmosUpdateTests + { + private readonly ICosmosStore _dummyStore; + + public CosmosUpdateTests() + { + _dummyStore = new InMemoryCosmosStore(); + } + + [Fact] + public async Task UpdateEntityUpdates() + { + // Arrange + var id = Guid.NewGuid().ToString(); + var addedDummy = new Dummy + { + Id = id, + Name = "Test" + }; + var expectedName = "NewTest"; + await _dummyStore.AddAsync(addedDummy); + + // Act + addedDummy.Name = expectedName; + var result = await _dummyStore.UpdateAsync(addedDummy); + + // Assert + Assert.Equal(expectedName, result.Entity.Name); + } + + [Fact] + public async Task UpdateRangeUpdatesEntities() + { + // Arrange + var addedEntities = new List(); + for (int i = 0; i < 10; i++) + { + var id = Guid.NewGuid().ToString(); + var addedDummy = new Dummy + { + Id = id, + Name = "UpdateMe" + }; + var added = await _dummyStore.AddAsync(addedDummy); + addedEntities.Add(added.Entity); + } + + // Act + var result = await _dummyStore.UpdateRangeAsync(); + + // Assert + Assert.True(result.IsSuccess); + Assert.Empty(result.FailedEntities); + } + + [Fact] + public async Task UpdateEntityThatHasIdChangedFails() + { + // Arrange + var id = Guid.NewGuid().ToString(); + var addedDummy = new Dummy + { + Id = id, + Name = "Test" + }; + await _dummyStore.AddAsync(addedDummy); + + // Act + addedDummy.Id = Guid.NewGuid().ToString(); + var result = await _dummyStore.UpdateAsync(addedDummy); + + // Assert + Assert.False(result.IsSuccess); + Assert.Equal(CosmosOperationStatus.ResourceNotFound, result.CosmosOperationStatus); + } + } +} \ No newline at end of file diff --git a/Cosmonaut.Tests/Dummy.cs b/Cosmonaut.Tests/Dummy.cs index c218785..0e8f4ec 100644 --- a/Cosmonaut.Tests/Dummy.cs +++ b/Cosmonaut.Tests/Dummy.cs @@ -1,4 +1,6 @@ -namespace Cosmonaut.Tests +using Newtonsoft.Json; + +namespace Cosmonaut.Tests { public class Dummy { @@ -6,4 +8,50 @@ public class Dummy public string Name { get; set; } } + + public class DummyImplEntity : ICosmosEntity + { + public string Name { get; set; } + + public string CosmosId { get; set; } + } + + public class DummyImplEntityWithAttr : ICosmosEntity + { + [JsonProperty("id")] + public string Id { get; set; } + + public string Name { get; set; } + + public string CosmosId { get; set; } + } + + public class DummyWithIdAndWithAttr + { + [JsonProperty("id")] + public string ActualyId { get; set; } + + public string Name { get; set; } + + public string Id { get; set; } + } + + public class DummyWithMultipleAttr + { + [JsonProperty("id")] + public string ActualyId { get; set; } + + public string Name { get; set; } + + [JsonProperty("id")] + public string Id { get; set; } + } + + public class DummyWithIdAttrOnId + { + public string Name { get; set; } + + [JsonProperty("id")] + public string Id { get; set; } + } } \ No newline at end of file diff --git a/Cosmonaut.Tests/EntityValidatorTests.cs b/Cosmonaut.Tests/EntityValidatorTests.cs new file mode 100644 index 0000000..5db2d79 --- /dev/null +++ b/Cosmonaut.Tests/EntityValidatorTests.cs @@ -0,0 +1,138 @@ +using System; +using Cosmonaut.Exceptions; +using Xunit; + +namespace Cosmonaut.Tests +{ + public class EntityValidatorTests + { + [Fact] + public void ObjectWithPropertyNamedIdAssignsCosmosId() + { + // Arrange + var id = Guid.NewGuid().ToString(); + var processor = new CosmosDocumentProcessor(); + var dummy = new Dummy + { + Id = id, + Name = "Test" + }; + + // Act + processor.ValidateEntityForCosmosDb(dummy); + var idFromDocument = processor.GetDocumentId(dummy); + + // Assert + Assert.Equal(id, idFromDocument); + } + + [Fact] + public void ObjectImplimentingCosmosEntityAssignsCosmosId() + { + // Arrange + var id = Guid.NewGuid().ToString(); + var processor = new CosmosDocumentProcessor(); + var dummy = new DummyImplEntity() + { + CosmosId = id, + Name = "Test" + }; + + // Act + processor.ValidateEntityForCosmosDb(dummy); + var idFromDocument = processor.GetDocumentId(dummy); + + // Assert + Assert.Equal(id, idFromDocument); + } + + [Fact] + public void ObjectImplimentingCosmosEntityWithExistingIdThrowsException() + { + // Arrange + var id = Guid.NewGuid().ToString(); + var processor = new CosmosDocumentProcessor(); + var dummy = new DummyImplEntityWithAttr() + { + CosmosId = id, + Name = "Test", + Id = id + }; + + // Act & Assert + Assert.Throws(() => processor.ValidateEntityForCosmosDb(dummy)); + } + + [Fact] + public void ObjectWithIdAndAttributeThrowsException() + { + // Arrange + var id = Guid.NewGuid().ToString(); + var processor = new CosmosDocumentProcessor(); + var dummy = new DummyWithIdAndWithAttr() + { + ActualyId = id, + Name = "Test", + Id = id + }; + + // Act & Assert + Assert.Throws(() => processor.ValidateEntityForCosmosDb(dummy)); + } + + [Fact] + public void ObjectMultipleAttributesThrowsException() + { + // Arrange + var id = Guid.NewGuid().ToString(); + var processor = new CosmosDocumentProcessor(); + var dummy = new DummyWithMultipleAttr() + { + ActualyId = id, + Name = "Test", + Id = id + }; + + // Act & Assert + Assert.Throws(() => processor.ValidateEntityForCosmosDb(dummy)); + } + + [Fact] + public void ObjectWithIdPropertyAndAttributeOnThatPropertyAssigns() + { + // Arrange + var id = Guid.NewGuid().ToString(); + var processor = new CosmosDocumentProcessor(); + var dummy = new DummyWithIdAttrOnId() + { + Id = id, + Name = "Test" + }; + + // Act + processor.ValidateEntityForCosmosDb(dummy); + var idFromDocument = processor.GetDocumentId(dummy); + + // Assert + Assert.Equal(id, idFromDocument); + } + + [Fact] + public void ObjectWithoutAnyIdThrowsException() + { + // Arrange + var processor = new CosmosDocumentProcessor(); + var dummy = new + { + Name = "Test" + }; + + // Act & Assert + Assert.Throws>(() => + { + processor.ValidateEntityForCosmosDb(dummy); + processor.GetDocumentId(dummy); + }); + } + } +} \ No newline at end of file diff --git a/Cosmonaut.Tests/InMemoryCosmosStore.cs b/Cosmonaut.Tests/InMemoryCosmosStore.cs index 2418da9..109f8f9 100644 --- a/Cosmonaut.Tests/InMemoryCosmosStore.cs +++ b/Cosmonaut.Tests/InMemoryCosmosStore.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Cosmonaut.Response; using Microsoft.Azure.Documents; -using Microsoft.Azure.Documents.Client; namespace Cosmonaut.Tests { @@ -23,11 +22,15 @@ public InMemoryCosmosStore() public async Task> AddAsync(TEntity entity) { - _documentProcessor.ValidateEntityForCosmosDb(entity); - var id = _documentProcessor.GetDocumentId(entity); - if(_store.TryAdd(id, entity)) - return new CosmosResponse(entity, CosmosOperationStatus.Success); - return new CosmosResponse(entity, CosmosOperationStatus.ResourceWithIdAlreadyExists); + return await Task.Run(() => + { + _documentProcessor.ValidateEntityForCosmosDb(entity); + var id = _documentProcessor.GetDocumentId(entity); + if (_store.TryAdd(id, entity)) + return new CosmosResponse(entity, CosmosOperationStatus.Success); + return new CosmosResponse(entity, CosmosOperationStatus.ResourceWithIdAlreadyExists); + } + ); } public async Task> AddRangeAsync(params TEntity[] entities) @@ -49,15 +52,17 @@ public async Task> AddRangeAsync(IEnumerable> UpdateAsync(TEntity entity) { - _documentProcessor.ValidateEntityForCosmosDb(entity); - var id = _documentProcessor.GetDocumentId(entity); - var exists = _store[id]; - if (exists == null) - return new CosmosResponse(CosmosOperationStatus.ResourceNotFound); + return await Task.Run(() => + { + _documentProcessor.ValidateEntityForCosmosDb(entity); + var id = _documentProcessor.GetDocumentId(entity); - _store[id] = entity; + if (!_store.ContainsKey(id)) + return new CosmosResponse(CosmosOperationStatus.ResourceNotFound); - return new CosmosResponse(entity, CosmosOperationStatus.Success); + _store[id] = entity; + return new CosmosResponse(entity, CosmosOperationStatus.Success); + }); } public async Task> UpdateRangeAsync(params TEntity[] entities) @@ -106,10 +111,13 @@ public async Task> RemoveRangeAsync(IEnumerable< public async Task> RemoveByIdAsync(string id) { - if (_store.TryRemove(id, out var outEntity)) - return new CosmosResponse(outEntity, CosmosOperationStatus.Success); + return await Task.Run(() => + { + if (_store.TryRemove(id, out var outEntity)) + return new CosmosResponse(outEntity, CosmosOperationStatus.Success); - return new CosmosResponse(CosmosOperationStatus.ResourceNotFound); + return new CosmosResponse(CosmosOperationStatus.ResourceNotFound); + }); } public async Task> RemoveAsync(Func predicate) @@ -129,10 +137,14 @@ public async Task> RemoveAsync(Func> ToListAsync(Func predicate = null) { - if (predicate == null) - predicate = entity => true; + return await Task.Run(() => + { - return _store.Values.Where(predicate).ToList(); + if (predicate == null) + predicate = entity => true; + + return _store.Values.Where(predicate).ToList(); + }); } public Task> QueryableAsync() @@ -140,14 +152,18 @@ public Task> QueryableAsync() throw new NotImplementedException(); } - public Task> WhereAsync(Expression> predicate) + public async Task> WhereAsync(Expression> predicate) { - throw new NotImplementedException(); + return (IQueryable) await Task.Run(() => + { + var pred = predicate.Compile(); + return _store.Values.Where(pred); + }); } - public Task FirstOrDefaultAsync(Func predicate) + public async Task FirstOrDefaultAsync(Func predicate) { - throw new NotImplementedException(); + return await Task.Run(() => _store.Values.FirstOrDefault(predicate)); } } } \ No newline at end of file diff --git a/Cosmonaut/Cosmonaut.csproj b/Cosmonaut/Cosmonaut.csproj index a592d36..4367802 100644 --- a/Cosmonaut/Cosmonaut.csproj +++ b/Cosmonaut/Cosmonaut.csproj @@ -12,7 +12,7 @@ https://github.com/Elfocrash/Cosmonaut cosmosdb azure cosmos entitystore entity db Initial release of Cosmonaut. Please report any issues on Github. - 1.0.1 + 1.0.2 diff --git a/Cosmonaut/CosmosDocumentProcessor.cs b/Cosmonaut/CosmosDocumentProcessor.cs index 82eec04..d4c88a7 100644 --- a/Cosmonaut/CosmosDocumentProcessor.cs +++ b/Cosmonaut/CosmosDocumentProcessor.cs @@ -48,10 +48,7 @@ internal TEntity ValidateEntityForCosmosDb(TEntity entity) return entity; } - if (idProperty == null || containsJsonAttributeIdCount == 1) - return entity; - - if (idProperty.GetValue(entity) == null) + if (idProperty != null && idProperty.GetValue(entity) == null) idProperty.SetValue(entity, Guid.NewGuid().ToString()); return entity; @@ -77,8 +74,8 @@ internal string GetDocumentId(TEntity entity) if (propertyNamedId != null && !string.IsNullOrEmpty(propertyNamedId.GetValue(entity)?.ToString())) return propertyNamedId.GetValue(entity).ToString(); - var potentialCosmosEntityId = entity.GetType().GetInterface(nameof(ICosmosEntity)) - .GetProperties().SingleOrDefault(x => + var potentialCosmosEntityId = entity.GetType().GetInterface(nameof(ICosmosEntity))? + .GetProperties()?.SingleOrDefault(x => x.GetCustomAttribute()?.PropertyName == "id"); if (potentialCosmosEntityId != null && !string.IsNullOrEmpty(potentialCosmosEntityId.GetValue(entity)?.ToString())) diff --git a/Cosmonaut/CosmosStore.cs b/Cosmonaut/CosmosStore.cs index 3d1d986..028ea33 100644 --- a/Cosmonaut/CosmosStore.cs +++ b/Cosmonaut/CosmosStore.cs @@ -123,7 +123,8 @@ public async Task> UpdateAsync(TEntity entity) if (documentExists == null) return new CosmosResponse(entity, CosmosOperationStatus.ResourceNotFound); - var result = await DocumentClient.UpsertDocumentAsync((await _collection).DocumentsLink, entity); + var document = _documentProcessor.GetCosmosDbFriendlyEntity(entity); + var result = await DocumentClient.UpsertDocumentAsync((await _collection).DocumentsLink, document); return new CosmosResponse(entity, result); } catch (DocumentClientException exception) diff --git a/Cosmonaut/Response/CosmosResponse.cs b/Cosmonaut/Response/CosmosResponse.cs index cc2e7f2..770d2e2 100644 --- a/Cosmonaut/Response/CosmosResponse.cs +++ b/Cosmonaut/Response/CosmosResponse.cs @@ -5,10 +5,11 @@ namespace Cosmonaut.Response { public class CosmosResponse where TEntity : class { - public bool IsSuccess => ResourceResponse != null && + public bool IsSuccess => (ResourceResponse != null && (int)ResourceResponse.StatusCode >= 200 && (int)ResourceResponse.StatusCode <= 299 && - CosmosOperationStatus == CosmosOperationStatus.Success; + CosmosOperationStatus == CosmosOperationStatus.Success) || + (ResourceResponse == null && CosmosOperationStatus == CosmosOperationStatus.Success); public CosmosOperationStatus CosmosOperationStatus { get; set; } = CosmosOperationStatus.Success;