From 0806572cbdfc18149bd30dd6f50fcba9f1613ba6 Mon Sep 17 00:00:00 2001 From: Mostafa Shraief Date: Mon, 13 Apr 2026 18:10:43 +0300 Subject: [PATCH 1/9] feat: add CI github action to build and test project --- .github/workflows/dotnet.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/dotnet.yml diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..7fcd6dc --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,28 @@ +# This workflow will build a .NET project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net + +name: .NET + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal From a88d4d6fcf9fe8453c52d54f104088d44b1d835b Mon Sep 17 00:00:00 2001 From: Mostafa Shraief Date: Mon, 13 Apr 2026 19:36:22 +0300 Subject: [PATCH 2/9] feat: extend string class to call SlugGenerator.Generate using ToSlug method --- SlugGeneratorLibrary/SlugGenerator.cs | 8 ++++++++ SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/SlugGeneratorLibrary/SlugGenerator.cs b/SlugGeneratorLibrary/SlugGenerator.cs index 726f0eb..704f5fe 100644 --- a/SlugGeneratorLibrary/SlugGenerator.cs +++ b/SlugGeneratorLibrary/SlugGenerator.cs @@ -16,4 +16,12 @@ public static string Generate(string text) return text; } } + + public static class StringExtensions + { + public static string ToSlug(this string text) + { + return SlugGenerator.Generate(text); + } + } } \ No newline at end of file diff --git a/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs b/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs index f2af7c6..5e453c1 100644 --- a/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs +++ b/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs @@ -1,4 +1,5 @@ using SlugGeneratorLibrary; +using System.Net.Security; using Xunit; namespace SlugGeneratorUnitTesting @@ -67,5 +68,11 @@ public void InputWithArabicCharacters() { Assert.Equal("مرحبا-بالعالم", SlugGenerator.Generate("مرحبا بالعالم")); } + + [Fact] + public void SlugStringExtention() + { + Assert.Equal("hello-world", "hello world".ToSlug()); + } } } From 9835c3bfca375f3c39599908a8d3db678b36b11b Mon Sep 17 00:00:00 2001 From: Mostafa Shraief Date: Mon, 13 Apr 2026 20:06:14 +0300 Subject: [PATCH 3/9] feat: ability to use a custom separator character --- SlugGeneratorLibrary/SlugGenerator.cs | 17 +++++- .../SlugAlgorithmUnitTest.cs | 59 +++++++++++-------- 2 files changed, 50 insertions(+), 26 deletions(-) diff --git a/SlugGeneratorLibrary/SlugGenerator.cs b/SlugGeneratorLibrary/SlugGenerator.cs index 704f5fe..e9d25c9 100644 --- a/SlugGeneratorLibrary/SlugGenerator.cs +++ b/SlugGeneratorLibrary/SlugGenerator.cs @@ -5,23 +5,34 @@ namespace SlugGeneratorLibrary { public static class SlugGenerator { - public static string Generate(string text) + public static string CustomGenerate(string text, char separator) { if (text is null) throw new ArgumentNullException(nameof(text)); text = text.Trim() .ToLowerInvariant(); text = Regex.Replace(text, @"[+()^*%#@!/\\.,|`~]+", string.Empty); - text = Regex.Replace(text, @"[\s_-]+", "-"); + text = Regex.Replace(text, @"[\s_-]+", separator.ToString()); return text; } + + public static string GenerateHyphens(string text) + { + return CustomGenerate(text, '-'); + } + + public static string GenerateUnderscores(string text) + { + return CustomGenerate(text, '_'); + } } public static class StringExtensions { + /// Convert string to slug separated by hyphens. public static string ToSlug(this string text) { - return SlugGenerator.Generate(text); + return SlugGenerator.GenerateHyphens(text); } } } \ No newline at end of file diff --git a/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs b/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs index 5e453c1..25df18a 100644 --- a/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs +++ b/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs @@ -9,64 +9,64 @@ public class SlugAlgorithmUnitTest [Fact] public void RemoveWhiteSpaces() { - Assert.Equal("hello", SlugGenerator.Generate("Hello")); - Assert.Equal("hello", SlugGenerator.Generate(" Hello ")); + Assert.Equal("hello", SlugGenerator.GenerateHyphens("Hello")); + Assert.Equal("hello", SlugGenerator.GenerateHyphens(" Hello ")); } [Fact] public void ConvertToLowerCase() { - Assert.Equal("hello", SlugGenerator.Generate("hElLo")); + Assert.Equal("hello", SlugGenerator.GenerateHyphens("hElLo")); } [Fact] public void NullInput() { - Assert.Throws(() => SlugGenerator.Generate(null)); + Assert.Throws(() => SlugGenerator.GenerateHyphens(null)); } [Fact] public void EmptyInput() { - Assert.Equal(string.Empty, SlugGenerator.Generate(string.Empty)); + Assert.Equal(string.Empty, SlugGenerator.GenerateHyphens(string.Empty)); } [Fact] public void InputWithOnlyWhiteSpaces() { - Assert.Equal(string.Empty, SlugGenerator.Generate(" ")); + Assert.Equal(string.Empty, SlugGenerator.GenerateHyphens(" ")); } [Fact] public void InputWithSpecialCharacters() { - Assert.Equal("hello-world", SlugGenerator.Generate("Hello World")); - Assert.Equal("hello-world", SlugGenerator.Generate("Hello-World")); - Assert.Equal("hello-world", SlugGenerator.Generate("Hello_World")); - Assert.Equal("hello-world", SlugGenerator.Generate("Hello____World")); - Assert.Equal("hello-world", SlugGenerator.Generate("Hello _ World")); - Assert.Equal("hello-world", SlugGenerator.Generate("Hello - _ - World")); - Assert.Equal("hello-world", SlugGenerator.Generate("Hello -_World")); + Assert.Equal("hello-world", SlugGenerator.GenerateHyphens("Hello World")); + Assert.Equal("hello-world", SlugGenerator.GenerateHyphens("Hello-World")); + Assert.Equal("hello-world", SlugGenerator.GenerateHyphens("Hello_World")); + Assert.Equal("hello-world", SlugGenerator.GenerateHyphens("Hello____World")); + Assert.Equal("hello-world", SlugGenerator.GenerateHyphens("Hello _ World")); + Assert.Equal("hello-world", SlugGenerator.GenerateHyphens("Hello - _ - World")); + Assert.Equal("hello-world", SlugGenerator.GenerateHyphens("Hello -_World")); } [Fact] public void InputWithSpecialCharactersToRemove() { - Assert.Equal("hello", SlugGenerator.Generate("Hello!")); - Assert.Equal("hello", SlugGenerator.Generate("Hello@")); - Assert.Equal("hello", SlugGenerator.Generate("Hello#")); - Assert.Equal("hello", SlugGenerator.Generate("Hello%")); - Assert.Equal("hello", SlugGenerator.Generate("Hello^")); - Assert.Equal("hello", SlugGenerator.Generate("Hello*")); - Assert.Equal("hello", SlugGenerator.Generate("Hello()")); - Assert.Equal("helloworld", SlugGenerator.Generate("Hello()!@#%^*+/\\.|`~,world")); - Assert.Equal("hello-world", SlugGenerator.Generate("Hello_- ()!@#%^*+/\\.|`~,- world")); + Assert.Equal("hello", SlugGenerator.GenerateHyphens("Hello!")); + Assert.Equal("hello", SlugGenerator.GenerateHyphens("Hello@")); + Assert.Equal("hello", SlugGenerator.GenerateHyphens("Hello#")); + Assert.Equal("hello", SlugGenerator.GenerateHyphens("Hello%")); + Assert.Equal("hello", SlugGenerator.GenerateHyphens("Hello^")); + Assert.Equal("hello", SlugGenerator.GenerateHyphens("Hello*")); + Assert.Equal("hello", SlugGenerator.GenerateHyphens("Hello()")); + Assert.Equal("helloworld", SlugGenerator.GenerateHyphens("Hello()!@#%^*+/\\.|`~,world")); + Assert.Equal("hello-world", SlugGenerator.GenerateHyphens("Hello_- ()!@#%^*+/\\.|`~,- world")); } [Fact] public void InputWithArabicCharacters() { - Assert.Equal("مرحبا-بالعالم", SlugGenerator.Generate("مرحبا بالعالم")); + Assert.Equal("مرحبا-بالعالم", SlugGenerator.GenerateHyphens("مرحبا بالعالم")); } [Fact] @@ -74,5 +74,18 @@ public void SlugStringExtention() { Assert.Equal("hello-world", "hello world".ToSlug()); } + + [Fact] + public void SlugWithUnderscores() + { + Assert.Equal("hello_world", SlugGenerator.GenerateUnderscores("hello world")); + } + + [Fact] + public void CustomGenerate() + { + Assert.Equal("hello.world", SlugGenerator.CustomGenerate("hello world", '.')); + Assert.Equal("hello*world", SlugGenerator.CustomGenerate("hello world", '*')); + } } } From 304053b3bd0e2564969b12537584953a897ccc45 Mon Sep 17 00:00:00 2001 From: Mostafa Shraief Date: Mon, 13 Apr 2026 20:56:45 +0300 Subject: [PATCH 4/9] feat: prevent collision through implement SlugGenerator.GenerateUnique method --- SlugGeneratorLibrary/SlugGenerator.cs | 10 ++++++++++ SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/SlugGeneratorLibrary/SlugGenerator.cs b/SlugGeneratorLibrary/SlugGenerator.cs index e9d25c9..ae00f9c 100644 --- a/SlugGeneratorLibrary/SlugGenerator.cs +++ b/SlugGeneratorLibrary/SlugGenerator.cs @@ -1,4 +1,5 @@ using System; +using System.Security.Cryptography; using System.Text.RegularExpressions; namespace SlugGeneratorLibrary @@ -25,6 +26,15 @@ public static string GenerateUnderscores(string text) { return CustomGenerate(text, '_'); } + + public static string GenerateUnique(string text) + { + // append text with current milliseconds and a random number to ensure uniqueness + string millisecondsText = text + '-' + DateTimeOffset.UtcNow. + ToUnixTimeMilliseconds().ToString() + '-' + + RandomNumberGenerator.GetInt32(0, 100).ToString(); + return GenerateHyphens(millisecondsText); + } } public static class StringExtensions diff --git a/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs b/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs index 25df18a..14312c2 100644 --- a/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs +++ b/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs @@ -87,5 +87,12 @@ public void CustomGenerate() Assert.Equal("hello.world", SlugGenerator.CustomGenerate("hello world", '.')); Assert.Equal("hello*world", SlugGenerator.CustomGenerate("hello world", '*')); } + + [Fact] + public void UniqueSlug() + { + Assert.NotEqual(SlugGenerator.GenerateUnique("شقة في الجميلية"), + SlugGenerator.GenerateUnique("شقة في الجميلية")); + } } } From 333a12bc66dcf60f0eb95dfc41cebd73a9d3b92d Mon Sep 17 00:00:00 2001 From: Mostafa Shraief Date: Mon, 13 Apr 2026 23:26:46 +0300 Subject: [PATCH 5/9] fix: null validation, clean code and improve slug uniqueness --- SlugGeneratorLibrary/SlugGenerator.cs | 14 ++++++-------- SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs | 9 ++++----- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/SlugGeneratorLibrary/SlugGenerator.cs b/SlugGeneratorLibrary/SlugGenerator.cs index ae00f9c..0f1346e 100644 --- a/SlugGeneratorLibrary/SlugGenerator.cs +++ b/SlugGeneratorLibrary/SlugGenerator.cs @@ -1,6 +1,4 @@ -using System; -using System.Security.Cryptography; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; namespace SlugGeneratorLibrary { @@ -29,11 +27,11 @@ public static string GenerateUnderscores(string text) public static string GenerateUnique(string text) { - // append text with current milliseconds and a random number to ensure uniqueness - string millisecondsText = text + '-' + DateTimeOffset.UtcNow. - ToUnixTimeMilliseconds().ToString() + '-' + - RandomNumberGenerator.GetInt32(0, 100).ToString(); - return GenerateHyphens(millisecondsText); + if (text is null) + throw new ArgumentNullException(nameof(text)); + // append text with a GUID-based suffix to greatly reduce collision risk + string uniqueText = text + '-' + Guid.NewGuid().ToString("N"); + return GenerateHyphens(uniqueText); } } diff --git a/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs b/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs index 14312c2..9d169a4 100644 --- a/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs +++ b/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs @@ -1,6 +1,4 @@ using SlugGeneratorLibrary; -using System.Net.Security; -using Xunit; namespace SlugGeneratorUnitTesting { @@ -70,7 +68,7 @@ public void InputWithArabicCharacters() } [Fact] - public void SlugStringExtention() + public void SlugStringExtension() { Assert.Equal("hello-world", "hello world".ToSlug()); } @@ -91,8 +89,9 @@ public void CustomGenerate() [Fact] public void UniqueSlug() { - Assert.NotEqual(SlugGenerator.GenerateUnique("شقة في الجميلية"), - SlugGenerator.GenerateUnique("شقة في الجميلية")); + string input = "شقة في الجميلية"; + Assert.NotEqual(SlugGenerator.GenerateUnique(input), + SlugGenerator.GenerateUnique(input)); } } } From b177ca4ad498d863c251cf232ba9e1e88f2dd821 Mon Sep 17 00:00:00 2001 From: Mostafa Shraief Date: Wed, 29 Apr 2026 14:26:56 +0300 Subject: [PATCH 6/9] fix: improve Regex to handle more cases and check null with mordern way --- SlugGeneratorLibrary/SlugGenerator.cs | 20 +++++++++---------- .../SlugAlgorithmUnitTest.cs | 1 + 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/SlugGeneratorLibrary/SlugGenerator.cs b/SlugGeneratorLibrary/SlugGenerator.cs index 0f1346e..8740f03 100644 --- a/SlugGeneratorLibrary/SlugGenerator.cs +++ b/SlugGeneratorLibrary/SlugGenerator.cs @@ -6,12 +6,11 @@ public static class SlugGenerator { public static string CustomGenerate(string text, char separator) { - if (text is null) - throw new ArgumentNullException(nameof(text)); - text = text.Trim() - .ToLowerInvariant(); - text = Regex.Replace(text, @"[+()^*%#@!/\\.,|`~]+", string.Empty); - text = Regex.Replace(text, @"[\s_-]+", separator.ToString()); + ArgumentNullException.ThrowIfNull(text); + text = Regex.Replace(text.Trim() + .ToLowerInvariant(), + @"[+()^*%#@!/\\.,|`~]+", string.Empty); + text = Regex.Replace(text.Trim(), @"[\s_-]+", separator.ToString()); return text; } @@ -27,11 +26,12 @@ public static string GenerateUnderscores(string text) public static string GenerateUnique(string text) { - if (text is null) - throw new ArgumentNullException(nameof(text)); + ArgumentNullException.ThrowIfNull(text); // append text with a GUID-based suffix to greatly reduce collision risk - string uniqueText = text + '-' + Guid.NewGuid().ToString("N"); - return GenerateHyphens(uniqueText); + string uniqueText = text; + string slugifiedText = GenerateHyphens(uniqueText); + slugifiedText += '-' + Guid.NewGuid().ToString("N"); + return slugifiedText; } } diff --git a/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs b/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs index 9d169a4..251ccd9 100644 --- a/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs +++ b/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs @@ -59,6 +59,7 @@ public void InputWithSpecialCharactersToRemove() Assert.Equal("hello", SlugGenerator.GenerateHyphens("Hello()")); Assert.Equal("helloworld", SlugGenerator.GenerateHyphens("Hello()!@#%^*+/\\.|`~,world")); Assert.Equal("hello-world", SlugGenerator.GenerateHyphens("Hello_- ()!@#%^*+/\\.|`~,- world")); + Assert.Equal("hello-world", SlugGenerator.GenerateHyphens("!@# Hello World")); } [Fact] From 2922a751e39c8cc4d2276b5b4113ea057dcd062b Mon Sep 17 00:00:00 2001 From: Mostafa Shraief Date: Wed, 29 Apr 2026 15:27:45 +0300 Subject: [PATCH 7/9] fix: enhance dotnet workflow - build in `release` mode. - output test results into structured format. --- .github/workflows/dotnet.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 7fcd6dc..bc40a25 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -23,6 +23,12 @@ jobs: - name: Restore dependencies run: dotnet restore - name: Build - run: dotnet build --no-restore + run: dotnet build --no-restore -c Release -p:TreatWarningsAsErrors=true - name: Test - run: dotnet test --no-build --verbosity normal + run: dotnet test --no-build --verbosity normal --logger "trx;LogFileName=test_results.trx" + - name: Upload Test Results + uses: actions/upload-artifact@v4 + if: always() # This ensures it runs even if tests fail + with: + name: test-results + path: "**/test_results.trx" From 22fac151a3c9d806072e3c38326a2b53bfcad66d Mon Sep 17 00:00:00 2001 From: Mostafa Shraief Date: Wed, 29 Apr 2026 15:38:04 +0300 Subject: [PATCH 8/9] test: resolve build warning in NullInput test by applying null-forgiving operator --- SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs b/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs index 251ccd9..d9dab6d 100644 --- a/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs +++ b/SlugGeneratorUnitTesting/SlugAlgorithmUnitTest.cs @@ -20,7 +20,7 @@ public void ConvertToLowerCase() [Fact] public void NullInput() { - Assert.Throws(() => SlugGenerator.GenerateHyphens(null)); + Assert.Throws(() => SlugGenerator.GenerateHyphens(null!)); } [Fact] From 7cff968c8567318030a1b365ce3ecd7414219e0e Mon Sep 17 00:00:00 2001 From: Mostafa Shraief Date: Wed, 29 Apr 2026 15:58:00 +0300 Subject: [PATCH 9/9] fix: enable testing in Realease mode --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index bc40a25..097ee00 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -25,7 +25,7 @@ jobs: - name: Build run: dotnet build --no-restore -c Release -p:TreatWarningsAsErrors=true - name: Test - run: dotnet test --no-build --verbosity normal --logger "trx;LogFileName=test_results.trx" + run: dotnet test --no-build -c Release --verbosity normal --logger "trx;LogFileName=test_results.trx" - name: Upload Test Results uses: actions/upload-artifact@v4 if: always() # This ensures it runs even if tests fail