diff --git a/README.md b/README.md
index 5724cc9..2eb5c2e 100644
--- a/README.md
+++ b/README.md
@@ -151,7 +151,7 @@ Save or Upsert an item
Azure Table Storage requires both a `RowKey` and `PartitionKey`
-The base repository will set the `RowKey` if it hasn't already been set using the `NewRowKey()` method. The default implementation is `Guid.NewGuid().ToString("N")`
+The base repository will set the `RowKey` if it hasn't already been set using the `NewRowKey()` method. The default implementation is `Ulid.NewUlid().ToString()`
If `PartitionKey` hasn't been set, `RowKey` will be used.
diff --git a/src/TableStorage.Abstracts/Extensions/DateTimeExtentions.cs b/src/TableStorage.Abstracts/Extensions/DateTimeExtentions.cs
new file mode 100644
index 0000000..a09de44
--- /dev/null
+++ b/src/TableStorage.Abstracts/Extensions/DateTimeExtentions.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace TableStorage.Abstracts.Extensions;
+
+public static class DateTimeExtentions
+{
+ ///
+ /// Rounds the date to the specified time span.
+ ///
+ /// The date to round.
+ /// The time span to round to.
+ /// The rounded date
+ public static DateTime Round(this DateTime date, TimeSpan span)
+ {
+ long ticks = (date.Ticks + (span.Ticks / 2) + 1) / span.Ticks;
+ return new DateTime(ticks * span.Ticks);
+ }
+
+ ///
+ /// Rounds the date to the specified span.
+ ///
+ /// The date to round.
+ /// The time span to round to.
+ /// The rounded date
+ public static DateTimeOffset Round(this DateTimeOffset date, TimeSpan span)
+ {
+ long ticks = (date.Ticks + (span.Ticks / 2) + 1) / span.Ticks;
+ return new DateTimeOffset(ticks * span.Ticks, date.Offset);
+ }
+
+ ///
+ /// Converts to specified to its reverse chronological equivalent. DateTime.MaxValue - dateTime
+ ///
+ /// The date time offset.
+ /// A chronological reversed.
+ public static DateTime ToReverseChronological(this DateTime dateTime)
+ {
+ var targetTicks = DateTime.MaxValue.Ticks - dateTime.Ticks;
+ return new DateTime(targetTicks);
+ }
+
+ ///
+ /// Converts to specified to its reverse chronological equivalent. DateTimeOffset.MaxValue - dateTimeOffset
+ ///
+ /// The date time offset.
+ /// A chronological reversed.
+ public static DateTimeOffset ToReverseChronological(this DateTimeOffset dateTimeOffset)
+ {
+ var targetTicks = DateTimeOffset.MaxValue.Ticks - dateTimeOffset.Ticks;
+ return new DateTimeOffset(targetTicks, TimeSpan.Zero);
+ }
+}
diff --git a/src/TableStorage.Abstracts/TableRepository.cs b/src/TableStorage.Abstracts/TableRepository.cs
index 2e3cf38..715d8bd 100644
--- a/src/TableStorage.Abstracts/TableRepository.cs
+++ b/src/TableStorage.Abstracts/TableRepository.cs
@@ -61,7 +61,7 @@ public TableRepository(ILoggerFactory logFactory, TableServiceClient tableServic
protected ILogger Logger { get; }
///
- public virtual string NewRowKey() => Guid.NewGuid().ToString("N");
+ public virtual string NewRowKey() => Ulid.NewUlid().ToString();
///
public Task GetClientAsync() => _lazyTableClient.Value;
diff --git a/src/TableStorage.Abstracts/TableStorage.Abstracts.csproj b/src/TableStorage.Abstracts/TableStorage.Abstracts.csproj
index bafa556..0a727de 100644
--- a/src/TableStorage.Abstracts/TableStorage.Abstracts.csproj
+++ b/src/TableStorage.Abstracts/TableStorage.Abstracts.csproj
@@ -14,6 +14,7 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/test/TableStorage.Abstracts.Tests/CommentRepositoryTest.cs b/test/TableStorage.Abstracts.Tests/CommentRepositoryTest.cs
index 42a8243..233d4b3 100644
--- a/test/TableStorage.Abstracts.Tests/CommentRepositoryTest.cs
+++ b/test/TableStorage.Abstracts.Tests/CommentRepositoryTest.cs
@@ -17,7 +17,7 @@ public CommentRepositoryTest(ITestOutputHelper output, DatabaseFixture databaseF
public async Task FullTest()
{
var generator = new Faker()
- .RuleFor(p => p.RowKey, _ => Guid.NewGuid().ToString("N"))
+ .RuleFor(p => p.RowKey, _ => Ulid.NewUlid().ToString())
.RuleFor(p => p.PartitionKey, f => f.PickRandom(Constants.Owners))
.RuleFor(p => p.Name, f => f.Name.FullName())
.RuleFor(p => p.Description, f => f.Lorem.Sentence())
diff --git a/test/TableStorage.Abstracts.Tests/ItemRepositoryTest.cs b/test/TableStorage.Abstracts.Tests/ItemRepositoryTest.cs
index a64fa0d..d2f0299 100644
--- a/test/TableStorage.Abstracts.Tests/ItemRepositoryTest.cs
+++ b/test/TableStorage.Abstracts.Tests/ItemRepositoryTest.cs
@@ -130,7 +130,7 @@ public async Task LargeResultOne()
private static Faker- CreateGenerator()
{
return new Faker
- ()
- .RuleFor(p => p.RowKey, _ => Guid.NewGuid().ToString("N"))
+ .RuleFor(p => p.RowKey, _ => Ulid.NewUlid().ToString())
.RuleFor(p => p.PartitionKey, f => f.PickRandom(Constants.Owners))
.RuleFor(p => p.Name, f => f.Name.FullName())
.RuleFor(p => p.Description, f => f.Lorem.Sentence())
diff --git a/test/TableStorage.Abstracts.Tests/Models/LogEvent.cs b/test/TableStorage.Abstracts.Tests/Models/LogEvent.cs
new file mode 100644
index 0000000..c6de8c8
--- /dev/null
+++ b/test/TableStorage.Abstracts.Tests/Models/LogEvent.cs
@@ -0,0 +1,14 @@
+namespace TableStorage.Abstracts.Tests.Models;
+
+public class LogEvent : TableEntityBase
+{
+ public string? Level { get; set; }
+
+ public string? MessageTemplate { get; set; }
+
+ public string? RenderedMessage { get; set; }
+
+ public string? Exception { get; set; }
+
+ public string? Data { get; set; }
+}
diff --git a/test/TableStorage.Abstracts.Tests/RoleRepositoryTest.cs b/test/TableStorage.Abstracts.Tests/RoleRepositoryTest.cs
index 2ed257e..7d6af2c 100644
--- a/test/TableStorage.Abstracts.Tests/RoleRepositoryTest.cs
+++ b/test/TableStorage.Abstracts.Tests/RoleRepositoryTest.cs
@@ -18,7 +18,7 @@ public async Task CreateRole()
{
var role = new Role
{
- RowKey = Guid.NewGuid().ToString("N"),
+ RowKey = Ulid.NewUlid().ToString(),
Name = "CreateRole",
NormalizedName = "createrole",
Claims = [new Claim { Type = "Test", Value = "testing" }]
@@ -37,7 +37,7 @@ public async Task SaveRole()
{
var role = new Role
{
- RowKey = Guid.NewGuid().ToString("N"),
+ RowKey = Ulid.NewUlid().ToString(),
Name = "SaveRole",
NormalizedName = "saverole"
};
@@ -55,7 +55,7 @@ public async Task CreateUpdateRole()
{
var role = new Role
{
- RowKey = Guid.NewGuid().ToString("N"),
+ RowKey = Ulid.NewUlid().ToString(),
Name = "CreateRole",
NormalizedName = "createrole"
};
@@ -80,7 +80,7 @@ public async Task CreateReadRole()
{
var role = new Role
{
- RowKey = Guid.NewGuid().ToString("N"),
+ RowKey = Ulid.NewUlid().ToString(),
Name = "CreateReadRole",
NormalizedName = "createreadrole"
};
diff --git a/test/TableStorage.Abstracts.Tests/Services/ILogEventRepository.cs b/test/TableStorage.Abstracts.Tests/Services/ILogEventRepository.cs
new file mode 100644
index 0000000..8c933cd
--- /dev/null
+++ b/test/TableStorage.Abstracts.Tests/Services/ILogEventRepository.cs
@@ -0,0 +1,13 @@
+using TableStorage.Abstracts.Tests.Models;
+
+namespace TableStorage.Abstracts.Tests.Services;
+
+public interface ILogEventRepository : ITableRepository
+{
+ Task> QueryByDate(
+ DateOnly date,
+ string? level = null,
+ string? continuationToken = null,
+ int? pageSize = 100,
+ CancellationToken cancellationToken = default);
+}
diff --git a/test/TableStorage.Abstracts.Tests/Services/LogEventRepository.cs b/test/TableStorage.Abstracts.Tests/Services/LogEventRepository.cs
new file mode 100644
index 0000000..04f6a03
--- /dev/null
+++ b/test/TableStorage.Abstracts.Tests/Services/LogEventRepository.cs
@@ -0,0 +1,65 @@
+using Azure.Data.Tables;
+
+using Microsoft.Extensions.Logging;
+
+using TableStorage.Abstracts.Extensions;
+using TableStorage.Abstracts.Tests.Models;
+
+namespace TableStorage.Abstracts.Tests.Services;
+
+public class LogEventRepository : TableRepository, ILogEventRepository
+{
+ public LogEventRepository(ILoggerFactory logFactory, TableServiceClient tableServiceClient)
+ : base(logFactory, tableServiceClient)
+ {
+ }
+
+ public async Task> QueryByDate(
+ DateOnly date,
+ string? level = null,
+ string? continuationToken = null,
+ int? pageSize = 100,
+ CancellationToken cancellationToken = default)
+ {
+ var baseDate = date.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);
+
+ var upperDate = baseDate.ToReverseChronological();
+ var lowwerDate = baseDate.AddDays(1).ToReverseChronological();
+
+ var upper = $"{upperDate.Ticks:D19}";
+ var lower = $"{lowwerDate.Ticks:D19}";
+
+ var filter = $"(PartitionKey ge '{lower}') and (PartitionKey lt '{upper}')";
+
+ if (level.HasValue())
+ filter += $" and (Level eq '{level}')";
+
+ return await FindPageAsync(filter, continuationToken, pageSize, cancellationToken);
+ }
+
+ public override string NewRowKey()
+ {
+ // store newest log first
+ var timestamp = DateTimeOffset.UtcNow.ToReverseChronological();
+ return Ulid.NewUlid(timestamp).ToString();
+ }
+
+ protected override void BeforeSave(LogEvent entity)
+ {
+ if (entity.RowKey.IsNullOrWhiteSpace())
+ entity.RowKey = NewRowKey();
+
+ if (entity.PartitionKey.IsNullOrWhiteSpace())
+ {
+ var timespan = entity.Timestamp ?? DateTimeOffset.UtcNow;
+ var roundedDate = timespan
+ .Round(TimeSpan.FromMinutes(5))
+ .ToReverseChronological();
+
+ // create a 19 character String for reverse chronological ordering.
+ entity.PartitionKey = $"{roundedDate.Ticks:D19}";
+ }
+ }
+
+ protected override string GetTableName() => "LogEvent";
+}
diff --git a/test/TableStorage.Abstracts.Tests/TemplateRepositoryTest.cs b/test/TableStorage.Abstracts.Tests/TemplateRepositoryTest.cs
index 9ffa281..b210a88 100644
--- a/test/TableStorage.Abstracts.Tests/TemplateRepositoryTest.cs
+++ b/test/TableStorage.Abstracts.Tests/TemplateRepositoryTest.cs
@@ -17,7 +17,7 @@ public TemplateRepositoryTest(ITestOutputHelper output, DatabaseFixture database
public async Task FullTest()
{
var generator = new Faker()
- .RuleFor(p => p.RowKey, _ => Guid.NewGuid().ToString("N"))
+ .RuleFor(p => p.RowKey, _ => Ulid.NewUlid().ToString())
.RuleFor(p => p.PartitionKey, f => f.PickRandom(Constants.Owners))
.RuleFor(p => p.Name, f => f.Name.FullName())
.RuleFor(p => p.Description, f => f.Lorem.Sentence())
diff --git a/test/TableStorage.Abstracts.Tests/UlidGeneratorTests.cs b/test/TableStorage.Abstracts.Tests/UlidGeneratorTests.cs
new file mode 100644
index 0000000..bd3f9ad
--- /dev/null
+++ b/test/TableStorage.Abstracts.Tests/UlidGeneratorTests.cs
@@ -0,0 +1,33 @@
+using TableStorage.Abstracts.Extensions;
+
+namespace TableStorage.Abstracts.Tests;
+
+public class UlidGeneratorTests
+{
+ private readonly ITestOutputHelper _output;
+
+ public UlidGeneratorTests(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ [Fact]
+ public void UlidTests()
+ {
+ var timestamp = DateTimeOffset.UtcNow.ToReverseChronological();
+
+ var previousKey = Ulid.NewUlid(timestamp).ToString();
+
+ // higher dates should be lower sort
+ for (int i = 0; i < 100; i++)
+ {
+ timestamp = DateTimeOffset.UtcNow.ToReverseChronological();
+ var key = Ulid.NewUlid(timestamp).ToString();
+ key.Should().NotBeNull();
+
+ _output.WriteLine(key);
+
+ string.Compare(key, previousKey).Should().BeLessThan(0);
+ }
+ }
+}