Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ jobs:
name: Analyze
runs-on: ubuntu-latest

env:
PROJECT_PATHS: >- # List of projects to analyze
src/Ulid/Impl.csproj
src/EFCore/EFCore.Impl.csproj
src/LinqToDB/LinqToDB.Impl.csproj

permissions:
actions: read # Allow GitHub Actions to read workflow files
contents: read # Allow access to repository contents
Expand Down Expand Up @@ -51,11 +57,17 @@ jobs:

- name: Build the code
run: |
# Restore dependencies and build the C# project
dotnet restore src/Ulid/Impl.csproj -p:Configuration=CI-Release
dotnet restore src/EFCore/EFCore.Impl.csproj -p:Configuration=CI-Release
dotnet build src/Ulid/Impl.csproj --configuration CI-Release --no-incremental
dotnet build src/EFCore/EFCore.Impl.csproj --configuration CI-Release --no-incremental
# Loop for restoring dependencies
for project in ${{ env.PROJECT_PATHS }}; do
echo "Restoring: $project"
dotnet restore "$project" -p:Configuration=CI-Release
done

# Loop for building the projects
for project in ${{ env.PROJECT_PATHS }}; do
echo "Building: $project"
dotnet build "$project" --configuration CI-Release --no-incremental
done

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
Expand Down
41 changes: 39 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ ULID addresses this by design, mandating strict lexicographical sortability and

### Extension Packages
* **[ByteAether.Ulid.EntityFrameworkCore](#ef-core-integration--byteaetherulidentityframeworkcore)**: Dedicated Entity Framework Core integration providing specialized storage formats (`String`, `Binary`, `Guid`, and `SqlServerGuid`).
* **[ByteAether.Ulid.linq2db](#linqtodb-integration--byteaetherulidlinq2db)**: Official LinqToDB integration supporting global type mappings and optimized storage schemes (`String`, `Binary`, `Guid`, and `SqlServerGuid`).

These features collectively make **ByteAether.Ulid** a robust and efficient choice for managing unique identifiers in your .NET applications.

Expand Down Expand Up @@ -292,8 +293,9 @@ Supports seamless integration as a route or query parameter with built-in `TypeC

Includes a `JsonConverter` for easy serialization and deserialization.

### EF Core Integration – ByteAether.Ulid.EntityFrameworkCore
### [EF Core](https://github.com/dotnet/efcore) Integration – ByteAether.Ulid.EntityFrameworkCore
[![License](https://img.shields.io/github/license/ByteAether/Ulid?logo=github&label=License)](https://github.com/ByteAether/Ulid/blob/main/LICENSE)
![Entity Framework Core 6.0.0+](https://img.shields.io/badge/Entity_Framework_Core-6.0.0+-orange)
[![NuGet Version](https://img.shields.io/nuget/v/ByteAether.Ulid.EntityFrameworkCore?logo=nuget&label=Version)](https://www.nuget.org/packages/ByteAether.Ulid.EntityFrameworkCore/)
[![NuGet Downloads](https://img.shields.io/nuget/dt/ByteAether.Ulid.EntityFrameworkCore?logo=nuget&label=Downloads)](https://www.nuget.org/packages/ByteAether.Ulid.EntityFrameworkCore/)

Expand All @@ -304,7 +306,7 @@ Includes a `JsonConverter` for easy serialization and deserialization.
![.NET 7.0](https://img.shields.io/badge/.NET-7.0-green)
![.NET 6.0](https://img.shields.io/badge/.NET-6.0-green)

To seamlessly use ULIDs with Entity Framework Core, install the specialized extension package:
To seamlessly use ULIDs with [Entity Framework Core](https://github.com/dotnet/efcore), install the specialized extension package:

```sh
dotnet add package ByteAether.Ulid.EntityFrameworkCore
Expand Down Expand Up @@ -350,6 +352,41 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.HasConversion<UlidToSqlServerGuidConverter>();
}
```

### [LinqToDB](https://github.com/linq2db/linq2db) Integration – ByteAether.Ulid.linq2db

[![License](https://img.shields.io/github/license/ByteAether/Ulid?logo=github&label=License)](https://github.com/ByteAether/Ulid/blob/main/LICENSE)
![LinqToDB 6.0.0+](https://img.shields.io/badge/LinqToDB-6.0.0+-orange)
[![NuGet Version](https://img.shields.io/nuget/v/ByteAether.Ulid.linq2db?logo=nuget&label=Version)](https://www.nuget.org/packages/ByteAether.Ulid.linq2db/)
[![NuGet Downloads](https://img.shields.io/nuget/dt/ByteAether.Ulid.linq2db?logo=nuget&label=Downloads)](https://www.nuget.org/packages/ByteAether.Ulid.linq2db/)

![.NET AOT Ready](https://img.shields.io/badge/.NET-AOT_Ready-blue)
![.NET 10.0](https://img.shields.io/badge/.NET-10.0-brightgreen)
![.NET 9.0](https://img.shields.io/badge/.NET-9.0-brightgreen)
![.NET 8.0](https://img.shields.io/badge/.NET-8.0-brightgreen)
![.NET 7.0](https://img.shields.io/badge/.NET-7.0-green)
![.NET 6.0](https://img.shields.io/badge/.NET-6.0-green)

To integrate with [LinqToDB](https://github.com/linq2db/linq2db), install the specialized extension package:

```sh
dotnet add package ByteAether.Ulid.linq2db
```

Register the ULID conventions for your `DataOptions` instance using your preferred storage backend format (`String`, `Binary`, `Guid`, or `SqlServerGuid`):

```csharp
using LinqToDB;
using ByteAether.Ulid.LinqToDB;

var options = new DataOptions()
.UseSQLite()
.UseConnectionString(connectionString)
// Registers mapping for both Ulid and Ulid? types.
// Supports: UlidStorageFormat.String (Default), Binary, Guid, and SqlServerGuid
.RegisterUlid(UlidStorageFormat.Binary);
```

### Dapper Integration
To use ULIDs with Dapper, you can create a custom **TypeHandler** to convert between `Ulid` and `byte[]`. Here's how to set it up:

Expand Down
11 changes: 9 additions & 2 deletions src/ByteAether.Ulid.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@
<BuildType Name="CI-Debug" />
<BuildType Name="CI-Release" />
</Configurations>
<Folder Name="/EFCore/">
<Folder Name="/DB/">
<Project Path="DB.Shared.Tests/DB.Shared.Tests.csproj" />
<Project Path="DB.Shared/DB.Shared.csproj" />
</Folder>
<Folder Name="/DB/EFCore/">
<Project Path="EFCore.IntegrationTests/EFCore.IntegrationTests.csproj" />
<Project Path="EFCore.Tests/EFCore.Tests.csproj" />
<Project Path="EFCore/EFCore.Impl.csproj" />
</Folder>
<Folder Name="/DB/LinqToDB/">
<Project Path="LinqToDB\LinqToDB.Impl.csproj" />
<Project Path="LinqToDB.IntegrationTests\LinqToDB.IntegrationTests.csproj" />
</Folder>
<Folder Name="/Solution Items/">
<File Path="../README.md" />
<File Path=".editorconfig" />
Expand Down
36 changes: 36 additions & 0 deletions src/DB.Shared.Tests/DB.Shared.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsTestProject>true</IsTestProject>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

<UseDataCollector/>

<RootNamespace>ByteAether.Ulid.DB.Shared.Tests</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"/>
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

<PackageReference Include="xunit" Version="2.9.3"/>
</ItemGroup>

<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DB.Shared\DB.Shared.csproj" />
<ProjectReference Include="..\Ulid\Impl.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System.Data.SqlTypes; // For SqlGuid testing
using System.Data.SqlTypes;

namespace ByteAether.Ulid.EntityFrameworkCore.Tests;
namespace ByteAether.Ulid.DB.Shared.Tests;

public class SqlServerGuidConverterTests
public class MsSqlUlidShufflerTests
{
[Fact]
public void Converter_ShouldBePerfectRoundTrip()
Expand All @@ -11,8 +11,8 @@ public void Converter_ShouldBePerfectRoundTrip()
var originalUlid = Ulid.New();

// Act
var sqlGuid = UlidToSqlServerGuidConverter.ToSqlServerGuid(originalUlid);
var roundTrippedUlid = UlidToSqlServerGuidConverter.FromSqlServerGuid(sqlGuid);
var sqlGuid = MsSqlUlidShuffler.ToSqlServerGuid(originalUlid);
var roundTrippedUlid = MsSqlUlidShuffler.FromSqlServerGuid(sqlGuid);

// Assert
Assert.Equal(originalUlid, roundTrippedUlid);
Expand All @@ -26,8 +26,8 @@ public void ToSqlServerGuid_ShouldSortChronologicallyInSqlServer()
var secondUlid = Ulid.New(DateTimeOffset.UtcNow);

// Act - Convert using our custom shuffler
var firstGuid = UlidToSqlServerGuidConverter.ToSqlServerGuid(firstUlid);
var secondGuid = UlidToSqlServerGuidConverter.ToSqlServerGuid(secondUlid);
var firstGuid = MsSqlUlidShuffler.ToSqlServerGuid(firstUlid);
var secondGuid = MsSqlUlidShuffler.ToSqlServerGuid(secondUlid);

// Wrap them in .NET's SqlGuid, which uses the exact same sorting rules as SQL Server engine
var sqlGuid1 = new SqlGuid(firstGuid);
Expand Down
16 changes: 16 additions & 0 deletions src/DB.Shared/DB.Shared.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net10.0;net9.0;net8.0;net7.0;net6.0</TargetFrameworks>
<OutputType>library</OutputType>
<IsAotCompatible>true</IsAotCompatible>

<RootNamespace>ByteAether.Ulid.DB.Shared</RootNamespace>
<AssemblyName>ByteAether.Ulid.DB.Shared</AssemblyName>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Ulid\Impl.csproj" />
</ItemGroup>

</Project>
35 changes: 35 additions & 0 deletions src/DB.Shared/MsSqlUlidShuffler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace ByteAether.Ulid.DB.Shared;

public static class MsSqlUlidShuffler
{
public static Guid ToSqlServerGuid(Ulid ulid)
{
var source = ulid.AsByteSpan();
Span<byte> shuffled = stackalloc byte[16];

// MSSQL sorts uniqueidentifier values from right to left across byte groups (bytes 10-15 highest).
// Move the 6-byte ULID timestamp (bytes 0-5) to the end (bytes 10-15).
source[0..6].CopyTo(shuffled[10..16]);

// Move the 10-byte random/increment part to bytes 0-9.
source[6..16].CopyTo(shuffled[0..10]);

return Ulid.New(shuffled).ToGuid();
}

public static Ulid FromSqlServerGuid(Guid guid)
{
var shuffledUlid = Ulid.New(guid);
var shuffledBytes = shuffledUlid.AsByteSpan();

Span<byte> originalBytes = stackalloc byte[16];

// Reverse shuffling: move timestamp from bytes 10-15 back to 0-5.
shuffledBytes[10..16].CopyTo(originalBytes[0..6]);

// Move randomness from bytes 0-9 back to 6-15.
shuffledBytes[0..10].CopyTo(originalBytes[6..16]);

return Ulid.New(originalBytes);
}
}
1 change: 1 addition & 0 deletions src/EFCore/EFCore.Impl.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\DB.Shared\DB.Shared.csproj" PrivateAssets="All" />
<ProjectReference Include="..\Ulid\Impl.csproj" PrivateAssets="None" />
</ItemGroup>

Expand Down
6 changes: 4 additions & 2 deletions src/EFCore/PACKAGE.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# ULID Entity Framework Core Integration
# ULID [Entity Framework Core](https://github.com/dotnet/efcore) Integration
*from ByteAether*

[![License](https://img.shields.io/github/license/ByteAether/Ulid?logo=github&label=License)](https://github.com/ByteAether/Ulid/blob/main/LICENSE)
![Entity Framework Core 6.0.0+](https://img.shields.io/badge/Entity_Framework_Core-6.0.0+-orange)
[![NuGet Version](https://img.shields.io/nuget/v/ByteAether.Ulid.EntityFrameworkCore?logo=nuget&label=Version)](https://www.nuget.org/packages/ByteAether.Ulid.EntityFrameworkCore/)
[![NuGet Downloads](https://img.shields.io/nuget/dt/ByteAether.Ulid.EntityFrameworkCore?logo=nuget&label=Downloads)](https://www.nuget.org/packages/ByteAether.Ulid.EntityFrameworkCore/)

An official extension package for `ByteAether.Ulid`, providing seamless integration with Entity Framework Core. It enables effortless mapping of `Ulid` and `Ulid?` properties to database columns using customizable persistence strategies.
An official extension package for `ByteAether.Ulid`, providing seamless integration with [Entity Framework Core](https://github.com/dotnet/efcore). It enables effortless mapping of `Ulid` and `Ulid?` properties to database columns using customizable persistence strategies.

For the core library and full details, visit our [GitHub repository](https://github.com/ByteAether/Ulid).

Expand All @@ -17,6 +18,7 @@ For the core library and full details, visit our [GitHub repository](https://git
![.NET 7.0](https://img.shields.io/badge/.NET-7.0-green)
![.NET 6.0](https://img.shields.io/badge/.NET-6.0-green)

- **Version Support**: Fully compatible with **[Entity Framework Core](https://github.com/dotnet/efcore) versions 6.0.0 and newer**.
- **Automated Configuration**: Register mappings globally for both nullable and non-nullable `Ulid` types using a single extension method.
- **Flexible Storage Strategies**: Choose how your identifiers are persisted based on your database engine constraints:
- `String`: 26-character [Crockford's Base32](https://www.crockford.com/base32.html) string (e.g., `CHAR(26)`). **(Default)**
Expand Down
39 changes: 3 additions & 36 deletions src/EFCore/UlidConverters.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using ByteAether.Ulid.DB.Shared;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;

namespace ByteAether.Ulid.EntityFrameworkCore;
Expand Down Expand Up @@ -36,44 +37,10 @@ public class UlidToGuidConverter() : ValueConverter<Ulid, Guid>(

/// <inheritdoc />
public class UlidToSqlServerGuidConverter() : ValueConverter<Ulid, Guid>(
ulid => ToSqlServerGuid(ulid),
guid => FromSqlServerGuid(guid),
ulid => MsSqlUlidShuffler.ToSqlServerGuid(ulid),
guid => MsSqlUlidShuffler.FromSqlServerGuid(guid),
_defaultHints
)
{
private static readonly ConverterMappingHints _defaultHints = new(size: 16);

internal static Guid ToSqlServerGuid(Ulid ulid)
{
var source = ulid.AsByteSpan();
Span<byte> shuffled = stackalloc byte[16];

// MSSQL compares uniqueidentifier values from right to left across byte groups,
// with bytes 10-15 having the highest sorting priority.
// We move the 6-byte ULID timestamp (bytes 0-5) to the end (bytes 10-15).
source[0..6].CopyTo(shuffled[10..16]);

// Move the 10-byte random/increment part to bytes 0-9.
source[6..16].CopyTo(shuffled[0..10]);

// Hand over the shuffled structure to the core library to produce a compliant .NET Guid.
return Ulid.New(shuffled).ToGuid();
}

internal static Ulid FromSqlServerGuid(Guid guid)
{
// Materialize the Guid back into a Big-Endian byte layout via the core library.
var shuffledUlid = Ulid.New(guid);
var shuffledBytes = shuffledUlid.AsByteSpan();

Span<byte> originalBytes = stackalloc byte[16];

// Reverse the shuffling: move timestamp from bytes 10-15 back to 0-5.
shuffledBytes[10..16].CopyTo(originalBytes[0..6]);

// Move randomness from bytes 0-9 back to 6-15.
shuffledBytes[0..10].CopyTo(originalBytes[6..16]);

return Ulid.New(originalBytes);
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net10.0;net9.0;net8.0;net7.0;net6.0</TargetFrameworks>
<NoWarn>NU1903</NoWarn>

<IsTestProject>true</IsTestProject>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

<UseDataCollector/>

<RootNamespace>ByteAether.Ulid.EntityFrameworkCore.Tests</RootNamespace>
<RootNamespace>ByteAether.Ulid.LinqToDB.IntegrationTests</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" Condition="'$(TargetFramework)' == 'net6.0' Or '$(TargetFramework)' == 'net7.0'"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" Condition="'$(TargetFramework)' != 'net6.0' And '$(TargetFramework)' != 'net7.0'"/>
<PackageReference Include="Xunit.Combinatorial" Version="1.6.24" />

<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2" Condition="'$(TargetFramework)' == 'net6.0' Or '$(TargetFramework)' == 'net7.0'">
<PrivateAssets>all</PrivateAssets>
Expand All @@ -30,15 +20,14 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

<PackageReference Include="xunit" Version="2.9.3"/>
</ItemGroup>
<PackageReference Include="xunit" Version="2.9.3" />

<ItemGroup>
<ProjectReference Include="..\EFCore\EFCore.Impl.csproj" />
<PackageReference Include="linq2db" Version="6.3.0" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.9" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit"/>
<ProjectReference Include="..\LinqToDB\LinqToDB.Impl.csproj" />
</ItemGroup>

</Project>
Loading