Skip to content

Commit

Permalink
add KeyGenerator
Browse files Browse the repository at this point in the history
  • Loading branch information
pwelter34 committed Apr 26, 2024
1 parent 1512b57 commit 170c819
Show file tree
Hide file tree
Showing 9 changed files with 322 additions and 34 deletions.
5 changes: 1 addition & 4 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,7 @@ jobs:
- name: Install .NET Core
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
6.0.x
7.0.x
8.0.x
dotnet-version: 8.0.x

- name: Restore Dependencies
run: dotnet restore
Expand Down
18 changes: 10 additions & 8 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project>

<PropertyGroup Label="Package">
<Description>The Azure Table Storage Abstracts library defines abstract base classes for repository pattern.</Description>
Expand All @@ -17,14 +16,17 @@
<PublishRepositoryUrl>true</PublishRepositoryUrl>
</PropertyGroup>

<PropertyGroup>
<DebugType>portable</DebugType>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PropertyGroup Label="Debug">
<DebugType>embedded</DebugType>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>

<PropertyGroup>
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

<PropertyGroup Label="Options">
<DefaultLanguage>en-US</DefaultLanguage>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>1591</NoWarn>
Expand Down
149 changes: 149 additions & 0 deletions src/TableStorage.Abstracts/KeyGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
using Azure.Data.Tables;

using TableStorage.Abstracts.Extensions;

namespace TableStorage.Abstracts;

/// <summary>
/// Key generation helper methods
/// </summary>
public static class KeyGenerator
{
private const string PartitionKeyName = nameof(ITableEntity.PartitionKey);

/// <summary>
/// Generates the PartitionKey based on the specified <paramref name="eventTime"/> timestamp
/// </summary>
/// <param name="eventTime">The event time.</param>
/// <param name="roundSpan">The round span.</param>
/// <returns>
/// The Generated PartitionKey
/// </returns>
/// <remarks>
/// The partition key based on the Timestamp rounded to the nearest 5 min
/// </remarks>
public static string GeneratePartitionKey(DateTimeOffset eventTime, TimeSpan? roundSpan = null)
{
var span = roundSpan ?? TimeSpan.FromMinutes(5);
var dateTime = eventTime.ToUniversalTime();
var roundedEvent = dateTime.Round(span);

// create a 19 character String for reverse chronological ordering.
return $"{DateTimeOffset.MaxValue.Ticks - roundedEvent.Ticks:D19}";
}

/// <summary>
/// Generates the PartitionKey based on the specified <paramref name="eventTime"/> timestamp
/// </summary>
/// <param name="eventTime">The event time.</param>
/// <param name="roundSpan">The round span.</param>
/// <returns>
/// The Generated PartitionKey
/// </returns>
/// <remarks>
/// The partition key based on the Timestamp rounded to the nearest 5 min
/// </remarks>
public static string GeneratePartitionKey(DateTime eventTime, TimeSpan? roundSpan = null)
{
var dateTime = eventTime.ToUniversalTime();
var dateTimeOffset = new DateTimeOffset(dateTime, TimeSpan.Zero);

return GeneratePartitionKey(dateTimeOffset, roundSpan);
}


/// <summary>
/// Generates the RowKey using a reverse chronological ordering date, newest logs sorted first
/// </summary>
/// <param name="eventTime">The event time.</param>
/// <returns>
/// The generated RowKey
/// </returns>
public static string GenerateRowKey(DateTimeOffset eventTime)
{
var dateTime = eventTime.ToUniversalTime();

// create a reverse chronological ordering date, newest logs sorted first
var timestamp = dateTime.ToReverseChronological();

// use Ulid for speed and efficiency
return Ulid.NewUlid(timestamp).ToString();
}

/// <summary>
/// Generates the RowKey using a reverse chronological ordering date, newest logs sorted first
/// </summary>
/// <param name="eventTime">The event time.</param>
/// <returns>
/// The generated RowKey
/// </returns>
public static string GenerateRowKey(DateTime eventTime)
{
var dateTime = eventTime.ToUniversalTime();
var dateTimeOffset = new DateTimeOffset(dateTime, TimeSpan.Zero);

return GenerateRowKey(dateTimeOffset);
}


#if NET6_0_OR_GREATER
/// <summary>
/// Generates the partition key query using the specified <paramref name="date"/>.
/// </summary>
/// <param name="date">The date to use for query.</param>
/// <param name="offset">The date's offset from Coordinated Universal Time (UTC).</param>
/// <returns>An Azure Table partiion key query.</returns>
public static string GeneratePartitionKeyQuery(DateOnly date, TimeSpan offset)
{
// date is assumed to be in local time, will be converted to UTC
var eventTime = new DateTimeOffset(date.Year, date.Month, date.Day, 0, 0, 0, offset);
return GeneratePartitionKeyQuery(eventTime);
}

/// <summary>
/// Generates the partition key query using the specified <paramref name="date"/>.
/// </summary>
/// <param name="date">The date to use for query.</param>
/// <param name="zone">The time zone the date is in.</param>
/// <returns>An Azure Table partiion key query.</returns>
public static string GeneratePartitionKeyQuery(DateOnly date, TimeZoneInfo? zone = null)
{
// date is assumed to be in local time, will be converted to UTC
zone ??= TimeZoneInfo.Local;

var dateTime = date.ToDateTime(TimeOnly.MinValue);
var offset = zone.GetUtcOffset(dateTime);

var eventTime = new DateTimeOffset(dateTime, offset);
return GeneratePartitionKeyQuery(eventTime);
}
#endif

/// <summary>
/// Generates the partition key query using the specified <paramref name="eventTime"/>.
/// </summary>
/// <param name="eventTime">The date to use for query.</param>
/// <returns>An Azure Table partiion key query.</returns>
public static string GeneratePartitionKeyQuery(DateTime eventTime)
{
var dateTime = eventTime.ToUniversalTime();
var dateTimeOffset = new DateTimeOffset(dateTime, TimeSpan.Zero);

return GeneratePartitionKeyQuery(dateTimeOffset);
}

/// <summary>
/// Generates the partition key query using the specified <paramref name="eventTime"/>.
/// </summary>
/// <param name="eventTime">The date to use for query.</param>
/// <returns>An Azure Table partiion key query.</returns>
public static string GeneratePartitionKeyQuery(DateTimeOffset eventTime)
{
var dateTime = eventTime.ToUniversalTime();

var upper = dateTime.ToReverseChronological().Ticks.ToString("D19");
var lower = dateTime.AddDays(1).ToReverseChronological().Ticks.ToString("D19");

return $"({PartitionKeyName} ge '{lower}') and ({PartitionKeyName} lt '{upper}')";
}
}
1 change: 1 addition & 0 deletions test/TableStorage.Abstracts.Tests/DatabaseFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ protected override void ConfigureApplication(HostApplicationBuilder builder)
services.AddTableStorageRepository(StorageConnectionName);

services.TryAddSingleton<IUserRepository, UserRepository>();
services.TryAddSingleton<ILogEventRepository, LogEventRepository>();
}
}
128 changes: 128 additions & 0 deletions test/TableStorage.Abstracts.Tests/KeyGeneratorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using TableStorage.Abstracts.Extensions;

namespace TableStorage.Abstracts.Tests;

public class KeyGeneratorTests
{
private readonly ITestOutputHelper _output;

public KeyGeneratorTests(ITestOutputHelper output)
{
_output = output;
}


[Fact]
public void GenerateRowKeyDateTimeOffsetNow()
{
var dateTime = new DateTimeOffset(2024, 4, 1, 23, 0, 0, TimeSpan.FromHours(-5));

var rowKey = KeyGenerator.GenerateRowKey(dateTime);
rowKey.Should().NotBeNull();

var parsed = Ulid.TryParse(rowKey, out var ulid);
parsed.Should().BeTrue();
ulid.Should().NotBeNull();

var reversed = dateTime.ToUniversalTime().ToReverseChronological();
var ulidDate = ulid.Time;

ulidDate.Year.Should().Be(reversed.Year);
ulidDate.Month.Should().Be(reversed.Month);
ulidDate.Day.Should().Be(reversed.Day);
ulidDate.Hour.Should().Be(reversed.Hour);
ulidDate.Minute.Should().Be(reversed.Minute);
}


[Fact]
public void GeneratePartitionKeyDateTimeOffsetNow()
{
var dateTime = new DateTimeOffset(2024, 4, 1, 23, 0, 0, TimeSpan.FromHours(-5));

var partitionKey = KeyGenerator.GeneratePartitionKey(dateTime);
partitionKey.Should().NotBeNull();
partitionKey.Should().Be("2516902703999999999");
}

[Fact]
public void GeneratePartitionKeyDateTimeNow()
{
var dateTime = new DateTimeOffset(2024, 4, 1, 23, 0, 0, TimeSpan.FromHours(-5));
var eventTime = dateTime.UtcDateTime;

var partitionKey = KeyGenerator.GeneratePartitionKey(eventTime);
partitionKey.Should().NotBeNull();
partitionKey.Should().Be("2516902703999999999");
}

[Theory]
[MemberData(nameof(GetDateRounding))]
public void GeneratePartitionKeyDateTimeNowRound(DateTimeOffset dateTime, string expected)
{
var partitionKey = KeyGenerator.GeneratePartitionKey(dateTime);
partitionKey.Should().NotBeNull();
partitionKey.Should().Be(expected);
}

public static IEnumerable<object[]> GetDateRounding()
{
yield return new object[]
{
new DateTimeOffset(2024, 4, 1, 23, 1, 0, TimeSpan.FromHours(-5)),
"2516902703999999999"
};
yield return new object[]
{
new DateTimeOffset(2024, 4, 1, 23, 2, 55, TimeSpan.FromHours(-5)),
"2516902700999999999"
};
yield return new object[]
{
new DateTimeOffset(2024, 4, 1, 23, 3, 5, TimeSpan.FromHours(-5)),
"2516902700999999999"
};
yield return new object[]
{
new DateTimeOffset(2024, 4, 1, 23, 4, 11, TimeSpan.FromHours(-5)),
"2516902700999999999"
};
yield return new object[]
{
new DateTimeOffset(2024, 4, 1, 23, 4, 43, TimeSpan.FromHours(-5)),
"2516902700999999999"
};
}


[Fact]
public void GeneratePartitionKeyQueryDateOnly()
{
var date = new DateOnly(2024, 4, 1);

var partitionKeyQuery = KeyGenerator.GeneratePartitionKeyQuery(date, TimeSpan.FromHours(-5));
partitionKeyQuery.Should().NotBeNull();
partitionKeyQuery.Should().Be("(PartitionKey ge '2516902667999999999') and (PartitionKey lt '2516903531999999999')");
}

[Fact]
public void GeneratePartitionKeyQueryDateTime()
{
var dateTime = new DateTimeOffset(2024, 4, 1, 0, 0, 0, TimeSpan.FromHours(-5));
var eventTime = dateTime.UtcDateTime;

var partitionKeyQuery = KeyGenerator.GeneratePartitionKeyQuery(eventTime);
partitionKeyQuery.Should().NotBeNull();
partitionKeyQuery.Should().Be("(PartitionKey ge '2516902667999999999') and (PartitionKey lt '2516903531999999999')");
}

[Fact]
public void GeneratePartitionKeyQueryDateTimeOffset()
{
var dateTime = new DateTimeOffset(2024, 4, 1, 0, 0, 0, TimeSpan.FromHours(-5));

var partitionKeyQuery = KeyGenerator.GeneratePartitionKeyQuery(dateTime);
partitionKeyQuery.Should().NotBeNull();
partitionKeyQuery.Should().Be("(PartitionKey ge '2516902667999999999') and (PartitionKey lt '2516903531999999999')");
}
}
25 changes: 25 additions & 0 deletions test/TableStorage.Abstracts.Tests/LogEventRepositoryTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Microsoft.Extensions.DependencyInjection;

using TableStorage.Abstracts.Tests.Services;

namespace TableStorage.Abstracts.Tests;

public class LogEventRepositoryTest : DatabaseTestBase
{
public LogEventRepositoryTest(ITestOutputHelper output, DatabaseFixture databaseFixture)
: base(output, databaseFixture)
{
}

[Fact]
public async void QueryTest()
{
var repository = Services.GetRequiredService<ILogEventRepository>();
repository.Should().NotBeNull();

var today = DateOnly.FromDateTime(DateTime.Now);

var result = await repository.Query(today);
result.Should().NotBeNull();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace TableStorage.Abstracts.Tests.Services;

public interface ILogEventRepository : ITableRepository<LogEvent>
{
Task<PagedResult<LogEvent>> QueryByDate(
Task<PagedResult<LogEvent>> Query(
DateOnly date,
string? level = null,
string? continuationToken = null,
Expand Down

0 comments on commit 170c819

Please sign in to comment.