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