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.