diff --git a/src/EFCore.Cosmos/ValueGeneration/Internal/IdValueGenerator.cs b/src/EFCore.Cosmos/ValueGeneration/Internal/IdValueGenerator.cs index f7c9a54ce39..961105da797 100644 --- a/src/EFCore.Cosmos/ValueGeneration/Internal/IdValueGenerator.cs +++ b/src/EFCore.Cosmos/ValueGeneration/Internal/IdValueGenerator.cs @@ -87,20 +87,42 @@ private void AppendString(StringBuilder builder, object? propertyValue) switch (propertyValue) { case string stringValue: - builder.Append(stringValue.Replace("|", "^|")); + AppendEscape(builder, stringValue); return; case IEnumerable enumerable: foreach (var item in enumerable) { - builder.Append(item.ToString()!.Replace("|", "^|")); + AppendEscape(builder, item.ToString()!); builder.Append('|'); } + return; + case DateTime dateTime: + AppendEscape(builder, dateTime.ToString("O")); return; default: - builder.Append(propertyValue == null ? "null" : propertyValue.ToString()!.Replace("|", "^|")); + if (propertyValue == null) + { + builder.Append("null"); + } else + { + AppendEscape(builder, propertyValue.ToString()!); + } return; } } + + private static StringBuilder AppendEscape(StringBuilder builder, string stringValue) + { + var startingIndex = builder.Length; + return builder.Append(stringValue) + // We need this to avoid collissions with the value separator + .Replace("|", "^|", startingIndex, builder.Length - startingIndex) + // These are invalid characters, see https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.documents.resource.id + .Replace("/", "^2F", startingIndex, builder.Length - startingIndex) + .Replace("\\", "^5C", startingIndex, builder.Length - startingIndex) + .Replace("?", "^3F", startingIndex, builder.Length - startingIndex) + .Replace("#", "^23", startingIndex, builder.Length - startingIndex); + } } } diff --git a/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs index e06029a4b5c..fe5f3eb1ce9 100644 --- a/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs @@ -456,6 +456,59 @@ public async Task Can_add_update_delete_end_to_end_with_Guid_async() } } + [ConditionalFact] + public async Task Can_add_update_delete_end_to_end_with_DateTime_async() + { + var options = Fixture.CreateOptions(); + + var customer = new CustomerDateTime + { + Id = DateTime.MinValue, + Name = "Theon/\\#\\\\?", + PartitionKey = 42 + }; + + using (var context = new CustomerContextDateTime(options)) + { + await context.Database.EnsureCreatedAsync(); + + var entry = context.Add(customer); + + Assert.Equal("CustomerDateTime|0001-01-01T00:00:00.0000000|Theon^2F^5C^23^5C^5C^3F", entry.CurrentValues["__id"]); + + await context.SaveChangesAsync(); + } + + using (var context = new CustomerContextDateTime(options)) + { + var customerFromStore = await context.Set().SingleAsync(); + + Assert.Equal(customer.Id, customerFromStore.Id); + Assert.Equal("Theon/\\#\\\\?", customerFromStore.Name); + + customerFromStore.Value = 23; + + await context.SaveChangesAsync(); + } + + using (var context = new CustomerContextDateTime(options)) + { + var customerFromStore = await context.Set().SingleAsync(); + + Assert.Equal(customer.Id, customerFromStore.Id); + Assert.Equal(23, customerFromStore.Value); + + context.Remove(customerFromStore); + + await context.SaveChangesAsync(); + } + + using (var context = new CustomerContextDateTime(options)) + { + Assert.Empty(await context.Set().ToListAsync()); + } + } + private class Customer { public int Id { get; set; } @@ -477,6 +530,14 @@ private class CustomerGuid public int PartitionKey { get; set; } } + private class CustomerDateTime + { + public DateTime Id { get; set; } + public string Name { get; set; } + public int PartitionKey { get; set; } + public int Value { get; set; } + } + private class CustomerNoPartitionKey { public int Id { get; set; } @@ -515,6 +576,26 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } } + private class CustomerContextDateTime : DbContext + { + public CustomerContextDateTime(DbContextOptions dbContextOptions) + : base(dbContextOptions) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity( + cb => + { + cb.Property(c => c.Id); + cb.Property(c => c.PartitionKey).HasConversion(); + cb.HasPartitionKey(c => c.PartitionKey); + cb.HasKey(c => new { c.Id, c.Name }); + }); + } + } + [ConditionalFact] public async Task Can_add_update_delete_with_collections() {