Skip to content

Commit

Permalink
Cosmos: Escape invalid characters in the id value
Browse files Browse the repository at this point in the history
Also change the DateTime format to ISO 8601

Fixes #25100
  • Loading branch information
AndriySvyryd authored Aug 26, 2021
1 parent 66c690b commit 0368eda
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 3 deletions.
28 changes: 25 additions & 3 deletions src/EFCore.Cosmos/ValueGeneration/Internal/IdValueGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
81 changes: 81 additions & 0 deletions test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CustomerDateTime>().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<CustomerDateTime>().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<CustomerDateTime>().ToListAsync());
}
}

private class Customer
{
public int Id { get; set; }
Expand All @@ -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; }
Expand Down Expand Up @@ -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<CustomerDateTime>(
cb =>
{
cb.Property(c => c.Id);
cb.Property(c => c.PartitionKey).HasConversion<string>();
cb.HasPartitionKey(c => c.PartitionKey);
cb.HasKey(c => new { c.Id, c.Name });
});
}
}

[ConditionalFact]
public async Task Can_add_update_delete_with_collections()
{
Expand Down

0 comments on commit 0368eda

Please sign in to comment.