diff --git a/src/CommunityToolkit.Datasync.Server.EntityFrameworkCore/EntityTableRepository.cs b/src/CommunityToolkit.Datasync.Server.EntityFrameworkCore/EntityTableRepository.cs index b1724af..6443001 100644 --- a/src/CommunityToolkit.Datasync.Server.EntityFrameworkCore/EntityTableRepository.cs +++ b/src/CommunityToolkit.Datasync.Server.EntityFrameworkCore/EntityTableRepository.cs @@ -60,10 +60,14 @@ public EntityTableRepository(DbContext context) } /// - /// Creates a new ID for an entity when one is not provided. + /// The mechanism by which an Id is generated when one is not provided. /// - /// A globally unique identifier for the entity. - protected string CreateId() => Guid.NewGuid().ToString(); + public Func IdGenerator { get; set; } = _ => Guid.NewGuid().ToString("N"); + + /// + /// The mechanism by which a new version byte array is generated. + /// + public Func VersionGenerator { get; set; } = () => Guid.NewGuid().ToByteArray(); /// /// Retrieves an untracked version of an entity from the database. @@ -87,7 +91,7 @@ internal void UpdateManagedProperties(TEntity entity) if (this.shouldUpdateVersion) { - entity.Version = Guid.NewGuid().ToByteArray(); + entity.Version = VersionGenerator.Invoke(); } } @@ -128,7 +132,7 @@ public virtual async ValueTask CreateAsync(TEntity entity, CancellationToken can { if (string.IsNullOrEmpty(entity.Id)) { - entity.Id = CreateId(); + entity.Id = IdGenerator.Invoke(entity); } await WrapExceptionAsync(entity.Id, async () => @@ -192,7 +196,7 @@ public virtual async ValueTask ReplaceAsync(TEntity entity, byte[]? version = nu await WrapExceptionAsync(entity.Id, async () => { - TEntity storedEntity = await DataSet.FindAsync(new object[] { entity.Id }, cancellationToken).ConfigureAwait(false) + TEntity storedEntity = await DataSet.FindAsync([entity.Id], cancellationToken).ConfigureAwait(false) ?? throw new HttpException((int)HttpStatusCode.NotFound); if (version?.Length > 0 && !storedEntity.Version.SequenceEqual(version)) diff --git a/src/CommunityToolkit.Datasync.Server.InMemory/InMemoryRepository.cs b/src/CommunityToolkit.Datasync.Server.InMemory/InMemoryRepository.cs index e66706c..d3c9baa 100644 --- a/src/CommunityToolkit.Datasync.Server.InMemory/InMemoryRepository.cs +++ b/src/CommunityToolkit.Datasync.Server.InMemory/InMemoryRepository.cs @@ -34,11 +34,21 @@ public InMemoryRepository(IEnumerable entities) { foreach (TEntity entity in entities) { - entity.Id ??= Guid.NewGuid().ToString(); + entity.Id ??= IdGenerator.Invoke(entity); StoreEntity(entity); } } + /// + /// The mechanism by which an Id is generated when one is not provided. + /// + public Func IdGenerator { get; set; } = _ => Guid.NewGuid().ToString("N"); + + /// + /// The mechanism by which a new version byte array is generated. + /// + public Func VersionGenerator { get; set; } = () => Guid.NewGuid().ToByteArray(); + #region Internal properties and methods for testing. /// /// If set, the repository will throw this exception when any method is called. @@ -95,7 +105,7 @@ internal void RemoveEntity(string id) internal void StoreEntity(TEntity entity) { entity.UpdatedAt = DateTimeOffset.UtcNow; - entity.Version = Guid.NewGuid().ToByteArray(); + entity.Version = VersionGenerator.Invoke(); this._entities[entity.Id] = Disconnect(entity); } @@ -124,7 +134,7 @@ public virtual ValueTask CreateAsync(TEntity entity, CancellationToken cancellat ThrowExceptionIfSet(); if (string.IsNullOrEmpty(entity.Id)) { - entity.Id = Guid.NewGuid().ToString(); + entity.Id = IdGenerator.Invoke(entity); } if (this._entities.TryGetValue(entity.Id, out TEntity? storedEntity)) diff --git a/src/CommunityToolkit.Datasync.Server.LiteDb/LiteDbRepository.cs b/src/CommunityToolkit.Datasync.Server.LiteDb/LiteDbRepository.cs index 5225c71..cfa7675 100644 --- a/src/CommunityToolkit.Datasync.Server.LiteDb/LiteDbRepository.cs +++ b/src/CommunityToolkit.Datasync.Server.LiteDb/LiteDbRepository.cs @@ -50,14 +50,24 @@ public LiteDbRepository(LiteDatabase dbConnection, string collectionName) /// public virtual ILiteCollection Collection { get; } + /// + /// The mechanism by which an Id is generated when one is not provided. + /// + public Func IdGenerator { get; set; } = _ => Guid.NewGuid().ToString("N"); + + /// + /// The mechanism by which a new version byte array is generated. + /// + public Func VersionGenerator { get; set; } = () => Guid.NewGuid().ToByteArray(); + /// /// Updates the system properties for the provided entity on write. /// /// The entity to update. - protected static void UpdateEntity(TEntity entity) + protected void UpdateEntity(TEntity entity) { entity.UpdatedAt = DateTimeOffset.UtcNow; - entity.Version = Guid.NewGuid().ToByteArray(); + entity.Version = VersionGenerator.Invoke(); } /// @@ -101,7 +111,7 @@ public virtual async ValueTask CreateAsync(TEntity entity, CancellationToken can { if (string.IsNullOrEmpty(entity.Id)) { - entity.Id = Guid.NewGuid().ToString(); + entity.Id = IdGenerator.Invoke(entity); } await ExecuteOnLockedCollectionAsync(() => diff --git a/src/CommunityToolkit.Datasync.Server.NSwag/DatasyncOperationProcessor.cs b/src/CommunityToolkit.Datasync.Server.NSwag/DatasyncOperationProcessor.cs index e0662b7..3f873b6 100644 --- a/src/CommunityToolkit.Datasync.Server.NSwag/DatasyncOperationProcessor.cs +++ b/src/CommunityToolkit.Datasync.Server.NSwag/DatasyncOperationProcessor.cs @@ -110,7 +110,7 @@ private static void ProcessDatasyncOperation(OperationProcessorContext context) } } - private static void AddMissingSchemaProperties(JsonSchema? schema) + internal static void AddMissingSchemaProperties(JsonSchema? schema) { if (schema is null) { diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 702711e..cac365e 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,7 @@ - Microsoft.Toolkit,dotnetfoundation,Community Toolkit + Microsoft.Toolkit,dotnetfoundation .NET Foundation (c) .NET Foundation and Contributors. All rights reserved. https://github.com/CommunityToolkit/Datasync/blob/main/LICENSE.md diff --git a/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test.csproj b/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test.csproj index f4840f4..a99feff 100644 --- a/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test.csproj +++ b/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test.csproj @@ -1,4 +1,7 @@ + + + diff --git a/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/RepositoryControlledEntityTableRepository_Tests.cs b/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/RepositoryControlledEntityTableRepository_Tests.cs index 4a7adb5..950dc51 100644 --- a/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/RepositoryControlledEntityTableRepository_Tests.cs +++ b/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/RepositoryControlledEntityTableRepository_Tests.cs @@ -4,9 +4,12 @@ using CommunityToolkit.Datasync.TestCommon; using CommunityToolkit.Datasync.TestCommon.Databases; +using CommunityToolkit.Datasync.TestCommon.Models; using Microsoft.EntityFrameworkCore; using Xunit.Abstractions; +using TestData = CommunityToolkit.Datasync.TestCommon.TestData; + namespace CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test; [ExcludeFromCodeCoverage] @@ -37,4 +40,51 @@ protected override Task> GetPopulat protected override Task GetRandomEntityIdAsync(bool exists) => Task.FromResult(exists ? this.movies[this.random.Next(Context.Movies.Count())].Id : Guid.NewGuid().ToString()); #endregion + + [SkippableFact] + public async Task IdGenerator_Ulid_CanCreate() + { + Skip.IfNot(CanRunLiveTests()); + + IRepository repository = await GetPopulatedRepositoryAsync(); + string generatedId = string.Empty; + ((EntityTableRepository)repository).IdGenerator = _ => { generatedId = Ulid.NewUlid().ToString(); return generatedId; }; + + RepositoryControlledEntityMovie addition = TestData.Movies.OfType(TestData.Movies.BlackPanther); + addition.Id = null; + RepositoryControlledEntityMovie sut = addition.Clone(); + await repository.CreateAsync(sut); + RepositoryControlledEntityMovie actual = await GetEntityAsync(sut.Id); + + actual.Should().BeEquivalentTo(addition); + actual.UpdatedAt.Should().BeAfter(StartTime); + generatedId.Should().NotBeNullOrEmpty(); + actual.Id.Should().Be(generatedId); + } + + [SkippableFact] + public async Task VersionGenerator_Ticks_CanCreate() + { + Skip.IfNot(CanRunLiveTests()); + + IRepository repository = await GetPopulatedRepositoryAsync(); + byte[] generatedVersion = []; + ((EntityTableRepository)repository).VersionGenerator = () => + { + DateTimeOffset offset = DateTimeOffset.UtcNow; + generatedVersion = BitConverter.GetBytes(offset.Ticks); + return generatedVersion; + }; + + RepositoryControlledEntityMovie addition = TestData.Movies.OfType(TestData.Movies.BlackPanther); + addition.Id = null; + RepositoryControlledEntityMovie sut = addition.Clone(); + await repository.CreateAsync(sut); + RepositoryControlledEntityMovie actual = await GetEntityAsync(sut.Id); + + actual.Should().BeEquivalentTo(addition); + actual.UpdatedAt.Should().BeAfter(StartTime); + generatedVersion.Should().NotBeNullOrEmpty(); + actual.Version.Should().BeEquivalentTo(generatedVersion); + } } diff --git a/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/SqliteEntityTableRepository_Tests.cs b/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/SqliteEntityTableRepository_Tests.cs index 26878ad..8e04bfc 100644 --- a/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/SqliteEntityTableRepository_Tests.cs +++ b/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/SqliteEntityTableRepository_Tests.cs @@ -4,9 +4,12 @@ using CommunityToolkit.Datasync.TestCommon; using CommunityToolkit.Datasync.TestCommon.Databases; +using CommunityToolkit.Datasync.TestCommon.Models; using Microsoft.EntityFrameworkCore; using Xunit.Abstractions; +using TestData = CommunityToolkit.Datasync.TestCommon.TestData; + namespace CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test; [ExcludeFromCodeCoverage] @@ -38,6 +41,27 @@ protected override Task GetRandomEntityIdAsync(bool exists) => Task.FromResult(exists ? this.movies[this.random.Next(Context.Movies.Count())].Id : Guid.NewGuid().ToString()); #endregion + [SkippableFact] + public async Task IdGenerator_Ulid_CanCreate() + { + Skip.IfNot(CanRunLiveTests()); + + IRepository repository = await GetPopulatedRepositoryAsync(); + string generatedId = string.Empty; + ((EntityTableRepository) repository).IdGenerator = _ => { generatedId = Ulid.NewUlid().ToString(); return generatedId; }; + + SqliteEntityMovie addition = TestData.Movies.OfType(TestData.Movies.BlackPanther); + addition.Id = null; + SqliteEntityMovie sut = addition.Clone(); + await repository.CreateAsync(sut); + SqliteEntityMovie actual = await GetEntityAsync(sut.Id); + + actual.Should().BeEquivalentTo(addition); + actual.UpdatedAt.Should().BeAfter(StartTime); + generatedId.Should().NotBeNullOrEmpty(); + actual.Id.Should().Be(generatedId); + } + [Fact] public void EntityTableRepository_BadDbSet_Throws() { diff --git a/tests/CommunityToolkit.Datasync.Server.InMemory.Test/CommunityToolkit.Datasync.Server.InMemory.Test.csproj b/tests/CommunityToolkit.Datasync.Server.InMemory.Test/CommunityToolkit.Datasync.Server.InMemory.Test.csproj index 14c7429..5d6cb0e 100644 --- a/tests/CommunityToolkit.Datasync.Server.InMemory.Test/CommunityToolkit.Datasync.Server.InMemory.Test.csproj +++ b/tests/CommunityToolkit.Datasync.Server.InMemory.Test/CommunityToolkit.Datasync.Server.InMemory.Test.csproj @@ -1,4 +1,7 @@ + + + diff --git a/tests/CommunityToolkit.Datasync.Server.InMemory.Test/InMemoryRepository_Tests.cs b/tests/CommunityToolkit.Datasync.Server.InMemory.Test/InMemoryRepository_Tests.cs index 69e1603..9573cd3 100644 --- a/tests/CommunityToolkit.Datasync.Server.InMemory.Test/InMemoryRepository_Tests.cs +++ b/tests/CommunityToolkit.Datasync.Server.InMemory.Test/InMemoryRepository_Tests.cs @@ -4,6 +4,7 @@ using CommunityToolkit.Datasync.TestCommon; using CommunityToolkit.Datasync.TestCommon.Databases; +using CommunityToolkit.Datasync.TestCommon.Models; using TestData = CommunityToolkit.Datasync.TestCommon.TestData; namespace CommunityToolkit.Datasync.Server.InMemory.Test; @@ -124,4 +125,51 @@ public async Task ReplaceAsync_Throws_OnForcedException(string id) await act.Should().ThrowAsync(); } + + [SkippableFact] + public async Task IdGenerator_Ulid_CanCreate() + { + Skip.IfNot(CanRunLiveTests()); + + IRepository repository = await GetPopulatedRepositoryAsync(); + string generatedId = string.Empty; + ((InMemoryRepository)repository).IdGenerator = _ => { generatedId = Ulid.NewUlid().ToString(); return generatedId; }; + + InMemoryMovie addition = TestData.Movies.OfType(TestData.Movies.BlackPanther); + addition.Id = null; + InMemoryMovie sut = addition.Clone(); + await repository.CreateAsync(sut); + InMemoryMovie actual = await GetEntityAsync(sut.Id); + + actual.Should().BeEquivalentTo(addition); + actual.UpdatedAt.Should().BeAfter(StartTime); + generatedId.Should().NotBeNullOrEmpty(); + actual.Id.Should().Be(generatedId); + } + + [SkippableFact] + public async Task VersionGenerator_Ticks_CanCreate() + { + Skip.IfNot(CanRunLiveTests()); + + IRepository repository = await GetPopulatedRepositoryAsync(); + byte[] generatedVersion = []; + ((InMemoryRepository)repository).VersionGenerator = () => + { + DateTimeOffset offset = DateTimeOffset.UtcNow; + generatedVersion = BitConverter.GetBytes(offset.Ticks); + return generatedVersion; + }; + + InMemoryMovie addition = TestData.Movies.OfType(TestData.Movies.BlackPanther); + addition.Id = null; + InMemoryMovie sut = addition.Clone(); + await repository.CreateAsync(sut); + InMemoryMovie actual = await GetEntityAsync(sut.Id); + + actual.Should().BeEquivalentTo(addition); + actual.UpdatedAt.Should().BeAfter(StartTime); + generatedVersion.Should().NotBeNullOrEmpty(); + actual.Version.Should().BeEquivalentTo(generatedVersion); + } } diff --git a/tests/CommunityToolkit.Datasync.Server.LiteDb.Test/CommunityToolkit.Datasync.Server.LiteDb.Test.csproj b/tests/CommunityToolkit.Datasync.Server.LiteDb.Test/CommunityToolkit.Datasync.Server.LiteDb.Test.csproj index be19e87..b298fac 100644 --- a/tests/CommunityToolkit.Datasync.Server.LiteDb.Test/CommunityToolkit.Datasync.Server.LiteDb.Test.csproj +++ b/tests/CommunityToolkit.Datasync.Server.LiteDb.Test/CommunityToolkit.Datasync.Server.LiteDb.Test.csproj @@ -1,4 +1,7 @@ + + + diff --git a/tests/CommunityToolkit.Datasync.Server.LiteDb.Test/LiteDbRepository_Tests.cs b/tests/CommunityToolkit.Datasync.Server.LiteDb.Test/LiteDbRepository_Tests.cs index 4a0d11b..b5cfd17 100644 --- a/tests/CommunityToolkit.Datasync.Server.LiteDb.Test/LiteDbRepository_Tests.cs +++ b/tests/CommunityToolkit.Datasync.Server.LiteDb.Test/LiteDbRepository_Tests.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using CommunityToolkit.Datasync.TestCommon; +using CommunityToolkit.Datasync.TestCommon.Models; using LiteDB; using TestData = CommunityToolkit.Datasync.TestCommon.TestData; @@ -67,4 +68,51 @@ protected virtual void Dispose(bool disposing) } } #endregion + + [SkippableFact] + public async Task IdGenerator_Ulid_CanCreate() + { + Skip.IfNot(CanRunLiveTests()); + + IRepository repository = await GetPopulatedRepositoryAsync(); + string generatedId = string.Empty; + ((LiteDbRepository)repository).IdGenerator = _ => { generatedId = Ulid.NewUlid().ToString(); return generatedId; }; + + LiteDbMovie addition = TestData.Movies.OfType(TestData.Movies.BlackPanther); + addition.Id = null; + LiteDbMovie sut = addition.Clone(); + await repository.CreateAsync(sut); + LiteDbMovie actual = await GetEntityAsync(sut.Id); + + actual.Should().BeEquivalentTo(addition); + actual.UpdatedAt.Should().BeAfter(StartTime); + generatedId.Should().NotBeNullOrEmpty(); + actual.Id.Should().Be(generatedId); + } + + [SkippableFact] + public async Task VersionGenerator_Ticks_CanCreate() + { + Skip.IfNot(CanRunLiveTests()); + + IRepository repository = await GetPopulatedRepositoryAsync(); + byte[] generatedVersion = []; + ((LiteDbRepository)repository).VersionGenerator = () => + { + DateTimeOffset offset = DateTimeOffset.UtcNow; + generatedVersion = BitConverter.GetBytes(offset.Ticks); + return generatedVersion; + }; + + LiteDbMovie addition = TestData.Movies.OfType(TestData.Movies.BlackPanther); + addition.Id = null; + LiteDbMovie sut = addition.Clone(); + await repository.CreateAsync(sut); + LiteDbMovie actual = await GetEntityAsync(sut.Id); + + actual.Should().BeEquivalentTo(addition); + actual.UpdatedAt.Should().BeAfter(StartTime); + generatedVersion.Should().NotBeNullOrEmpty(); + actual.Version.Should().BeEquivalentTo(generatedVersion); + } } diff --git a/tests/CommunityToolkit.Datasync.Server.NSwag.Test/NSwag_Tests.cs b/tests/CommunityToolkit.Datasync.Server.NSwag.Test/NSwag_Tests.cs index ec3d5c4..c9a1e3f 100644 --- a/tests/CommunityToolkit.Datasync.Server.NSwag.Test/NSwag_Tests.cs +++ b/tests/CommunityToolkit.Datasync.Server.NSwag.Test/NSwag_Tests.cs @@ -49,6 +49,13 @@ public async Task NSwag_GeneratesSwagger() actualContent.Should().Be(expectedContent); } + [Fact] + public void NSwag_AddMissingSchemaProperties_CornerCase() + { + Action act = () => DatasyncOperationProcessor.AddMissingSchemaProperties(null); + act.Should().NotThrow(); + } + [Fact] public void ContainsRequestHeader_ReturnsFalse_WhenQueryParam() { diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index ca82773..d1949f4 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -1,7 +1,7 @@ - Microsoft.Toolkit,dotnetfoundation,Community Toolkit + Microsoft.Toolkit,dotnetfoundation Community Toolkit (c) .NET Foundation and Contributors. All rights reserved. A test package for supporting the Datasync Toolkit.