From c2d2f9f8f5a3724c725a68bcbfb3461b7a01c254 Mon Sep 17 00:00:00 2001
From: amsga <49681949+amsga@users.noreply.github.com>
Date: Wed, 4 Mar 2026 16:44:52 +0800
Subject: [PATCH] Initial Commit.
---
.github/FUNDING.yml | 3 +
.github/dependabot.yml | 11 ++
.github/workflows/dotnet.yml | 92 +++++++++++
.github/workflows/package-release.yml | 47 ++++++
CHANGELOG.md | 9 ++
LICENSE.txt => LICENSE.md | 0
README.md | 8 +-
....Serialization.SystemTextJson.Tests.csproj | 30 ++++
.../UlidSystemTextJsonConverterTests.cs | 147 ++++++++++++++++++
...nDev.ULID.Serialization.SystemTextJson.sln | 8 +-
.../Class1.cs | 9 --
...v.ULID.Serialization.SystemTextJson.csproj | 48 +++++-
.../UlidSystemTextJsonConverter.cs | 48 ++++++
13 files changed, 448 insertions(+), 12 deletions(-)
create mode 100644 .github/FUNDING.yml
create mode 100644 .github/dependabot.yml
create mode 100644 .github/workflows/dotnet.yml
create mode 100644 .github/workflows/package-release.yml
create mode 100644 CHANGELOG.md
rename LICENSE.txt => LICENSE.md (100%)
create mode 100644 TensionDev.ULID.Serialization.SystemTextJson.Tests/TensionDev.ULID.Serialization.SystemTextJson.Tests.csproj
create mode 100644 TensionDev.ULID.Serialization.SystemTextJson.Tests/UlidSystemTextJsonConverterTests.cs
delete mode 100644 TensionDev.ULID.Serialization.SystemTextJson/Class1.cs
create mode 100644 TensionDev.ULID.Serialization.SystemTextJson/UlidSystemTextJsonConverter.cs
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..13f9d90
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+# These are supported funding model platforms
+
+ko_fi: tensiondev
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..5072385
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: "nuget" # See documentation for possible values
+ directory: "/" # Location of package manifests
+ schedule:
+ interval: "monthly"
diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
new file mode 100644
index 0000000..7681f9f
--- /dev/null
+++ b/.github/workflows/dotnet.yml
@@ -0,0 +1,92 @@
+name: .NET
+
+on:
+ push:
+ branches: [ "**" ]
+ pull_request:
+ branches: [ "master" ]
+
+jobs:
+ build:
+
+ runs-on: windows-latest
+
+ strategy:
+ matrix:
+ dotnet: [ '8.0.x' ]
+ name: .NET ${{ matrix.dotnet }}
+
+ steps:
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: 17
+ distribution: 'zulu' # Alternative distribution options are available.
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: ${{ matrix.dotnet }}
+ - name: Cache SonarQube Cloud packages
+ uses: actions/cache@v4
+ with:
+ path: ~\sonar\cache
+ key: ${{ runner.os }}-sonar
+ restore-keys: ${{ runner.os }}-sonar
+ - name: Cache SonarQube Cloud scanner
+ id: cache-sonar-scanner
+ uses: actions/cache@v4
+ with:
+ path: .\.sonar\scanner
+ key: ${{ runner.os }}-sonar-scanner
+ restore-keys: ${{ runner.os }}-sonar-scanner
+ - name: Install SonarQube Cloud scanner
+ if: steps.cache-sonar-scanner.outputs.cache-hit != 'true'
+ shell: powershell
+ run: |
+ New-Item -Path .\.sonar\scanner -ItemType Directory
+ dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner
+ - name: Restore dependencies
+ run: dotnet restore
+ - name: SonarCloudPrepare
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ run: .\.sonar\scanner\dotnet-sonarscanner begin /k:"TensionDev_ULID.Serialization.SystemTextJson" /o:"tensiondev" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.scanner.scanAll=false /d:sonar.cs.opencover.reportsPaths=**/coverage.opencover.xml
+ - name: Build
+ run: dotnet build --no-restore
+ - name: Test
+ run: dotnet test --no-build --verbosity normal --collect "XPlat Code Coverage;Format=opencover"
+ - name: SonarCloudAnalyze
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ run: .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
+
+ test:
+
+ strategy:
+ matrix:
+ dotnet: [ '8.0.x' ]
+ os: [macos-latest, ubuntu-latest, windows-latest]
+
+ runs-on: ${{ matrix.os }}
+
+ name: .NET ${{ matrix.dotnet }} on ${{ matrix.os }}
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: ${{ matrix.dotnet }}
+ - name: Restore dependencies
+ run: dotnet restore
+ - name: Build
+ run: dotnet build --no-restore
+ - name: Test
+ run: dotnet test --no-build --verbosity normal --collect "XPlat Code Coverage;Format=opencover"
diff --git a/.github/workflows/package-release.yml b/.github/workflows/package-release.yml
new file mode 100644
index 0000000..c8ecece
--- /dev/null
+++ b/.github/workflows/package-release.yml
@@ -0,0 +1,47 @@
+# This is a basic workflow to help you get started with Actions
+
+name: Package Release
+
+# Controls when the workflow will run
+on:
+ # Triggers the workflow on push events but only for the tags
+ push:
+ tags:
+ - v[0-9]+.[0-9]+.[0-9]+
+ - v[0-9]+.[0-9]+.[0-9]+-alpha
+ - v[0-9]+.[0-9]+.[0-9]+-beta
+
+# A workflow run is made up of one or more jobs that can run sequentially or in parallel
+jobs:
+ # This workflow contains a single job called "build"
+ build:
+ # The type of runner that the job will run on
+ runs-on: ubuntu-latest
+
+ # Steps represent a sequence of tasks that will be executed as part of the job
+ steps:
+ # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET 8
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: 8.0.x
+ source-url: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json
+ env:
+ NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
+
+ - name: Restore dependencies
+ run: dotnet restore
+
+ - name: Build
+ run: dotnet build --no-restore
+
+ - name: Build Release
+ run: dotnet build --configuration Release --no-restore
+
+ - name: Pack
+ run: dotnet pack --configuration Release
+
+ - name: Push NuGet
+ run: dotnet nuget push "TensionDev.ULID.Serialization.SystemTextJson/bin/Release/*.nupkg" --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_TOKEN }} --no-symbols --skip-duplicate
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..dccf8f8
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,9 @@
+# TensionDev.ULID.Serialization.SystemTextJson
+
+# Changelog
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
diff --git a/LICENSE.txt b/LICENSE.md
similarity index 100%
rename from LICENSE.txt
rename to LICENSE.md
diff --git a/README.md b/README.md
index 84d4a61..b1f02e7 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,7 @@
-# TensionDev.ULID.Serialization.SystemTextJson
\ No newline at end of file
+# TensionDev.ULID.Serialization.SystemTextJson
+
+[](https://github.com/TensionDev/ULID.Serialization.SystemTextJson/actions/workflows/dotnet.yml)
+[](https://github.com/TensionDev/ULID.Serialization.SystemTextJson/actions/workflows/package-release.yml)
+[](https://github.com/TensionDev/ULID.Serialization.SystemTextJson/actions/workflows/github-code-scanning/codeql)
+
+TensionDev.ULID.Serialization.SystemTextJson is a .NET library for serializing and deserializing with Universally Unique Lexicographically Sortable Identifiers (ULIDs) using System.Text.Json.
diff --git a/TensionDev.ULID.Serialization.SystemTextJson.Tests/TensionDev.ULID.Serialization.SystemTextJson.Tests.csproj b/TensionDev.ULID.Serialization.SystemTextJson.Tests/TensionDev.ULID.Serialization.SystemTextJson.Tests.csproj
new file mode 100644
index 0000000..ad10c63
--- /dev/null
+++ b/TensionDev.ULID.Serialization.SystemTextJson.Tests/TensionDev.ULID.Serialization.SystemTextJson.Tests.csproj
@@ -0,0 +1,30 @@
+
+
+
+ net8.0;net10.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
diff --git a/TensionDev.ULID.Serialization.SystemTextJson.Tests/UlidSystemTextJsonConverterTests.cs b/TensionDev.ULID.Serialization.SystemTextJson.Tests/UlidSystemTextJsonConverterTests.cs
new file mode 100644
index 0000000..be4ec59
--- /dev/null
+++ b/TensionDev.ULID.Serialization.SystemTextJson.Tests/UlidSystemTextJsonConverterTests.cs
@@ -0,0 +1,147 @@
+using Moq;
+using System;
+using System.Buffers;
+using System.Text;
+using System.Text.Json;
+using Xunit;
+
+namespace TensionDev.ULID.Serialization.SystemTextJson.Tests
+{
+ public class UlidSystemTextJsonConverterTests : IDisposable
+ {
+ private bool disposedValue;
+
+ private readonly UlidSystemTextJsonConverter _converter;
+
+ public UlidSystemTextJsonConverterTests()
+ {
+ _converter = new UlidSystemTextJsonConverter();
+ }
+
+ [Theory]
+ [InlineData(true, "00000000000000000000000000")]
+ [InlineData(true, "7ZZZZZZZZZZZZZZZZZZZZZZZZZ")]
+ [InlineData(true, "01ARZ3NDEKTSV4RRFFQ69G5FAV")]
+ [InlineData(false, "00000000000000000000000000")]
+ [InlineData(false, "7ZZZZZZZZZZZZZZZZZZZZZZZZZ")]
+ [InlineData(false, "01ARZ3NDEKTSV4RRFFQ69G5FAV")]
+ public void TestWrite(bool useNullOptions, string input)
+ {
+ // Arrange
+ using var ms = new MemoryStream();
+ using var writer = new Utf8JsonWriter(ms);
+ Ulid value = Ulid.Parse(input);
+ JsonSerializerOptions? options = useNullOptions ? null : new JsonSerializerOptions();
+
+ // Act
+ _converter.Write(writer, value, options);
+ writer.Flush();
+ string actual = Encoding.UTF8.GetString(ms.ToArray());
+
+ // Assert
+ string expected = JsonSerializer.Serialize(value.ToString());
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void TestReadNotString()
+ {
+ // Arrange
+ // Use a canonical all-zero ULID representation which is commonly accepted by ULID parsers.
+ const string input = "00000000000000000000000000";
+ string jsonText = "0";
+ byte[] json = Encoding.UTF8.GetBytes(jsonText);
+ var reader = new Utf8JsonReader(json);
+ reader.Read();
+
+ // Act
+ JsonException ex = null;
+ try
+ {
+ _converter.Read(ref reader, typeof(Ulid), new JsonSerializerOptions());
+ }
+ catch (JsonException caught)
+ {
+ ex = caught;
+ }
+
+ // Assert
+ Assert.NotNull(ex);
+ }
+
+ [Fact]
+ public void TestReadEmptyString()
+ {
+ // Arrange
+ // Use a canonical all-zero ULID representation which is commonly accepted by ULID parsers.
+ const string input = "00000000000000000000000000";
+ string jsonText = "\"\"";
+ byte[] json = Encoding.UTF8.GetBytes(jsonText);
+ var reader = new Utf8JsonReader(json);
+ reader.Read();
+
+ // Act
+ JsonException ex = null;
+ try
+ {
+ _converter.Read(ref reader, typeof(Ulid), new JsonSerializerOptions());
+ }
+ catch (JsonException caught)
+ {
+ ex = caught;
+ }
+
+ // Assert
+ Assert.NotNull(ex);
+ }
+
+ [Theory]
+ [InlineData("00000000000000000000000000")]
+ [InlineData("7ZZZZZZZZZZZZZZZZZZZZZZZZZ")]
+ [InlineData("01ARZ3NDEKTSV4RRFFQ69G5FAV")]
+ public void TestReadString(string input)
+ {
+ // Arrange
+ string jsonText = "\"" + input + "\"";
+ byte[] json = Encoding.UTF8.GetBytes(jsonText);
+ var reader = new Utf8JsonReader(json);
+ reader.Read();
+
+ // Act
+ Ulid result = _converter.Read(ref reader, typeof(Ulid), new JsonSerializerOptions());
+
+ // Assert
+ // Compare textual forms to avoid depending on reference equality or unknown equality semantics of Ulid.
+ Assert.Equal(input, result.ToString());
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ if (disposing)
+ {
+ // TODO: dispose managed state (managed objects)
+ }
+
+ // TODO: free unmanaged resources (unmanaged objects) and override finalizer
+ // TODO: set large fields to null
+ disposedValue = true;
+ }
+ }
+
+ // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
+ // ~UlidSystemTextJsonConverterTests()
+ // {
+ // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ // Dispose(disposing: false);
+ // }
+
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/TensionDev.ULID.Serialization.SystemTextJson.sln b/TensionDev.ULID.Serialization.SystemTextJson.sln
index d2089da..56c36d7 100644
--- a/TensionDev.ULID.Serialization.SystemTextJson.sln
+++ b/TensionDev.ULID.Serialization.SystemTextJson.sln
@@ -1,10 +1,12 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
-VisualStudioVersion = 18.3.11520.95 d18.3
+VisualStudioVersion = 18.3.11520.95
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TensionDev.ULID.Serialization.SystemTextJson", "TensionDev.ULID.Serialization.SystemTextJson\TensionDev.ULID.Serialization.SystemTextJson.csproj", "{15DBB42E-31D1-4C9A-A699-9692796EE842}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TensionDev.ULID.Serialization.SystemTextJson.Tests", "TensionDev.ULID.Serialization.SystemTextJson.Tests\TensionDev.ULID.Serialization.SystemTextJson.Tests.csproj", "{B20108AB-DFB4-4A2D-8B15-F21843F6BEEB}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -15,6 +17,10 @@ Global
{15DBB42E-31D1-4C9A-A699-9692796EE842}.Debug|Any CPU.Build.0 = Debug|Any CPU
{15DBB42E-31D1-4C9A-A699-9692796EE842}.Release|Any CPU.ActiveCfg = Release|Any CPU
{15DBB42E-31D1-4C9A-A699-9692796EE842}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B20108AB-DFB4-4A2D-8B15-F21843F6BEEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B20108AB-DFB4-4A2D-8B15-F21843F6BEEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B20108AB-DFB4-4A2D-8B15-F21843F6BEEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B20108AB-DFB4-4A2D-8B15-F21843F6BEEB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/TensionDev.ULID.Serialization.SystemTextJson/Class1.cs b/TensionDev.ULID.Serialization.SystemTextJson/Class1.cs
deleted file mode 100644
index 45160bc..0000000
--- a/TensionDev.ULID.Serialization.SystemTextJson/Class1.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using System;
-
-namespace TensionDev.ULID.Serialization.SystemTextJson
-{
- public class Class1
- {
-
- }
-}
diff --git a/TensionDev.ULID.Serialization.SystemTextJson/TensionDev.ULID.Serialization.SystemTextJson.csproj b/TensionDev.ULID.Serialization.SystemTextJson/TensionDev.ULID.Serialization.SystemTextJson.csproj
index dbdcea4..dd5c598 100644
--- a/TensionDev.ULID.Serialization.SystemTextJson/TensionDev.ULID.Serialization.SystemTextJson.csproj
+++ b/TensionDev.ULID.Serialization.SystemTextJson/TensionDev.ULID.Serialization.SystemTextJson.csproj
@@ -1,7 +1,53 @@
- netstandard2.0
+ net461;netstandard2.0;net6.0;net8.0;net10.0
+ TensionDev.ULID.Serialization.SystemTextJson
+ TensionDev.ULID.Serialization.SystemTextJson
+ all
+ True
+ True
+ true
+ true
+ TensionDev.ULID.Serialization.SystemTextJson
+ 1.0.0
+ TensionDev amsga
+ TensionDev
+ TensionDev.ULID.Serialization.SystemTextJson
+ TensionDev.ULID.Serialization.SystemTextJson is a .NET library that serializes TensionDev.Ulid objects into JSON.
+ Copyright (c) TensionDev 2026
+ Apache-2.0
+ Apache-2.0
+ https://github.com/TensionDev/ULID.Serialization.SystemTextJson
+ https://github.com/TensionDev/ULID.Serialization.SystemTextJson
+ git
+ ULID JSON
+ Initial Release.
+ en-SG
+ true
+ snupkg
+ README.md
+
+
+ True
+ \
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/TensionDev.ULID.Serialization.SystemTextJson/UlidSystemTextJsonConverter.cs b/TensionDev.ULID.Serialization.SystemTextJson/UlidSystemTextJsonConverter.cs
new file mode 100644
index 0000000..6dd1b4a
--- /dev/null
+++ b/TensionDev.ULID.Serialization.SystemTextJson/UlidSystemTextJsonConverter.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace TensionDev.ULID.Serialization.SystemTextJson
+{
+ ///
+ /// for the type that handles serialization and deserialization
+ ///
+ public class UlidSystemTextJsonConverter : JsonConverter
+ {
+ ///
+ /// Reads a JSON value and converts it to a new instance of the Ulid type.
+ ///
+ /// The JsonReader used to read the JSON value to be converted.
+ /// The type of the object to deserialize. This parameter is not used.
+ /// The options for the deserialization.
+ /// A Ulid instance parsed from the JSON value read by the reader.
+ /// Thrown when the JSON token is not a string or if parsing fails.
+ public override Ulid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.String)
+ {
+ throw new JsonException($"Unexpected token {reader.TokenType} when parsing ULID.");
+ }
+ string s = reader.GetString();
+ if (string.IsNullOrEmpty(s))
+ {
+ throw new JsonException("ULID string value was null or empty.");
+ }
+ return Ulid.Parse(s);
+ }
+
+ ///
+ /// Writes the specified to JSON by writing its string representation
+ /// to the provided .
+ /// The converter serializes the Ulid using its canonical textual form returned by
+ /// .
+ ///
+ /// The used to write the JSON value.
+ /// The instance to serialize.
+ /// The options for the serialization.
+ public override void Write(Utf8JsonWriter writer, Ulid value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value.ToString());
+ }
+ }
+}