diff --git a/EFCore.NamingConventions.Test/NamingConventionsOptionsExtensionTest.cs b/EFCore.NamingConventions.Test/NamingConventionsOptionsExtensionTest.cs index 1ede229..abe7826 100644 --- a/EFCore.NamingConventions.Test/NamingConventionsOptionsExtensionTest.cs +++ b/EFCore.NamingConventions.Test/NamingConventionsOptionsExtensionTest.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using EFCore.NamingConventions.Internal; using Xunit; @@ -33,6 +32,7 @@ public static IEnumerable Convention() NamingConvention.SnakeCase => new NamingConventionsOptionsExtension().WithSnakeCaseNamingConvention(), NamingConvention.LowerCase => new NamingConventionsOptionsExtension().WithLowerCaseNamingConvention(), NamingConvention.CamelCase => new NamingConventionsOptionsExtension().WithCamelCaseNamingConvention(), + NamingConvention.KebabCase => new NamingConventionsOptionsExtension().WithKebabCaseNamingConvention(), NamingConvention.UpperCase => new NamingConventionsOptionsExtension().WithUpperCaseNamingConvention(), NamingConvention.UpperSnakeCase => new NamingConventionsOptionsExtension().WithUpperSnakeCaseNamingConvention(), _ => throw new ArgumentOutOfRangeException($"Unhandled enum value: {convention}, NamingConventionsOptionsExtension not defined for the test") diff --git a/EFCore.NamingConventions.Test/RewriterTest.cs b/EFCore.NamingConventions.Test/RewriterTest.cs index 5af5e3a..1db4258 100644 --- a/EFCore.NamingConventions.Test/RewriterTest.cs +++ b/EFCore.NamingConventions.Test/RewriterTest.cs @@ -25,4 +25,8 @@ public void Camel_case() [Fact] public void Upper_case() => Assert.Equal("FULLNAME", new UpperCaseNameRewriter(CultureInfo.InvariantCulture).RewriteName("FullName")); + + [Fact] + public void Kebab_case() + => Assert.Equal("full-name", new KebabCaseNameRewriter(CultureInfo.InvariantCulture).RewriteName("FullName")); } diff --git a/EFCore.NamingConventions/Internal/KebabCaseNameRewriter.cs b/EFCore.NamingConventions/Internal/KebabCaseNameRewriter.cs new file mode 100644 index 0000000..3a80fb5 --- /dev/null +++ b/EFCore.NamingConventions/Internal/KebabCaseNameRewriter.cs @@ -0,0 +1,70 @@ +using System; +using System.Globalization; +using System.Text; + +namespace EFCore.NamingConventions.Internal +{ + public class KebabCaseNameRewriter : INameRewriter + { + private readonly CultureInfo _culture; + + public KebabCaseNameRewriter(CultureInfo cultureInfo) => _culture = cultureInfo; + + public string RewriteName(string name) + { + var builder = new StringBuilder(name.Length + Math.Min(2, name.Length / 5)); + var previousCategory = default(UnicodeCategory?); + + for (var currentIndex = 0; currentIndex < name.Length; currentIndex++) + { + var currentChar = name[currentIndex]; + + if (currentChar == '-') + { + builder.Append('-'); + previousCategory = null; + continue; + } + + var currentCategory = char.GetUnicodeCategory(currentChar); + + switch (currentCategory) + { + case UnicodeCategory.UppercaseLetter: + case UnicodeCategory.TitlecaseLetter: + if (previousCategory == UnicodeCategory.SpaceSeparator || + previousCategory == UnicodeCategory.LowercaseLetter || + previousCategory != UnicodeCategory.DecimalDigitNumber && + previousCategory != null && + currentIndex > 0 && + currentIndex + 1 < name.Length && + char.IsLower(name[currentIndex + 1])) + { + builder.Append('-'); + } + + currentChar = char.ToLower(currentChar, _culture); + break; + case UnicodeCategory.LowercaseLetter: + case UnicodeCategory.DecimalDigitNumber: + if (previousCategory == UnicodeCategory.SpaceSeparator) + { + builder.Append('-'); + } + break; + default: + if (previousCategory != null) + { + previousCategory = UnicodeCategory.SpaceSeparator; + } + continue; + } + + builder.Append(currentChar); + previousCategory = currentCategory; + } + + return builder.ToString(); + } + } +} diff --git a/EFCore.NamingConventions/Internal/NamingConvention.cs b/EFCore.NamingConventions/Internal/NamingConvention.cs index 801298a..e57739e 100644 --- a/EFCore.NamingConventions/Internal/NamingConvention.cs +++ b/EFCore.NamingConventions/Internal/NamingConvention.cs @@ -6,6 +6,7 @@ public enum NamingConvention SnakeCase, LowerCase, CamelCase, + KebabCase, UpperCase, UpperSnakeCase } diff --git a/EFCore.NamingConventions/Internal/NamingConventionSetPlugin.cs b/EFCore.NamingConventions/Internal/NamingConventionSetPlugin.cs index 0df43bb..87ee85b 100644 --- a/EFCore.NamingConventions/Internal/NamingConventionSetPlugin.cs +++ b/EFCore.NamingConventions/Internal/NamingConventionSetPlugin.cs @@ -3,7 +3,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Conventions; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; -using JetBrains.Annotations; namespace EFCore.NamingConventions.Internal; @@ -28,6 +27,7 @@ public ConventionSet ModifyConventions(ConventionSet conventionSet) NamingConvention.SnakeCase => new SnakeCaseNameRewriter(culture ?? CultureInfo.InvariantCulture), NamingConvention.LowerCase => new LowerCaseNameRewriter(culture ?? CultureInfo.InvariantCulture), NamingConvention.CamelCase => new CamelCaseNameRewriter(culture ?? CultureInfo.InvariantCulture), + NamingConvention.KebabCase => new KebabCaseNameRewriter(culture ?? CultureInfo.InvariantCulture), NamingConvention.UpperCase => new UpperCaseNameRewriter(culture ?? CultureInfo.InvariantCulture), NamingConvention.UpperSnakeCase => new UpperSnakeCaseNameRewriter(culture ?? CultureInfo.InvariantCulture), _ => throw new ArgumentOutOfRangeException("Unhandled enum value: " + namingStyle) diff --git a/EFCore.NamingConventions/Internal/NamingConventionsOptionsExtension.cs b/EFCore.NamingConventions/Internal/NamingConventionsOptionsExtension.cs index 8dffcf5..108ff00 100644 --- a/EFCore.NamingConventions/Internal/NamingConventionsOptionsExtension.cs +++ b/EFCore.NamingConventions/Internal/NamingConventionsOptionsExtension.cs @@ -13,7 +13,7 @@ public class NamingConventionsOptionsExtension : IDbContextOptionsExtension private NamingConvention _namingConvention; private CultureInfo? _culture; - public NamingConventionsOptionsExtension() {} + public NamingConventionsOptionsExtension() { } protected NamingConventionsOptionsExtension(NamingConventionsOptionsExtension copyFrom) { _namingConvention = copyFrom._namingConvention; @@ -74,7 +74,15 @@ public virtual NamingConventionsOptionsExtension WithCamelCaseNamingConvention(C return clone; } - public void Validate(IDbContextOptions options) {} + public virtual NamingConventionsOptionsExtension WithKebabCaseNamingConvention(CultureInfo? culture = null) + { + var clone = Clone(); + clone._namingConvention = NamingConvention.KebabCase; + clone._culture = culture; + return clone; + } + + public void Validate(IDbContextOptions options) { } public void ApplyServices(IServiceCollection services) => services.AddEntityFrameworkNamingConventions(); @@ -83,7 +91,7 @@ private sealed class ExtensionInfo : DbContextOptionsExtensionInfo { private string? _logFragment; - public ExtensionInfo(IDbContextOptionsExtension extension) : base(extension) {} + public ExtensionInfo(IDbContextOptionsExtension extension) : base(extension) { } private new NamingConventionsOptionsExtension Extension => (NamingConventionsOptionsExtension)base.Extension; @@ -106,6 +114,7 @@ public override string LogFragment NamingConvention.UpperCase => "using upper case naming", NamingConvention.UpperSnakeCase => "using upper snake-case naming", NamingConvention.CamelCase => "using camel-case naming", + NamingConvention.KebabCase => "using kebab-case naming", _ => throw new ArgumentOutOfRangeException("Unhandled enum value: " + Extension._namingConvention) }); diff --git a/EFCore.NamingConventions/NamingConventionsExtensions.cs b/EFCore.NamingConventions/NamingConventionsExtensions.cs index 6f173f9..5ad8788 100644 --- a/EFCore.NamingConventions/NamingConventionsExtensions.cs +++ b/EFCore.NamingConventions/NamingConventionsExtensions.cs @@ -1,7 +1,6 @@ using System.Globalization; -using Microsoft.EntityFrameworkCore.Infrastructure; -using JetBrains.Annotations; using EFCore.NamingConventions.Internal; +using Microsoft.EntityFrameworkCore.Infrastructure; // ReSharper disable once CheckNamespace namespace Microsoft.EntityFrameworkCore; @@ -24,7 +23,7 @@ public static DbContextOptionsBuilder UseSnakeCaseNamingConvention( } public static DbContextOptionsBuilder UseSnakeCaseNamingConvention( - this DbContextOptionsBuilder optionsBuilder , CultureInfo? culture = null) + this DbContextOptionsBuilder optionsBuilder, CultureInfo? culture = null) where TContext : DbContext => (DbContextOptionsBuilder)UseSnakeCaseNamingConvention((DbContextOptionsBuilder)optionsBuilder, culture); @@ -47,7 +46,7 @@ public static DbContextOptionsBuilder UseLowerCaseNamingConvention optionsBuilder, CultureInfo? culture = null) where TContext : DbContext - => (DbContextOptionsBuilder)UseLowerCaseNamingConvention((DbContextOptionsBuilder)optionsBuilder ,culture); + => (DbContextOptionsBuilder)UseLowerCaseNamingConvention((DbContextOptionsBuilder)optionsBuilder, culture); public static DbContextOptionsBuilder UseUpperCaseNamingConvention( this DbContextOptionsBuilder optionsBuilder, @@ -111,4 +110,25 @@ public static DbContextOptionsBuilder UseCamelCaseNamingConvention (DbContextOptionsBuilder)UseCamelCaseNamingConvention((DbContextOptionsBuilder)optionsBuilder, culture); + + public static DbContextOptionsBuilder UseKebabCaseNamingConvention( + this DbContextOptionsBuilder optionsBuilder, + CultureInfo? culture = null) + { + Check.NotNull(optionsBuilder, nameof(optionsBuilder)); + + var extension = (optionsBuilder.Options.FindExtension() + ?? new NamingConventionsOptionsExtension()) + .WithKebabCaseNamingConvention(culture); + + ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); + + return optionsBuilder; + } + + public static DbContextOptionsBuilder UseKebabCaseNamingConvention( + this DbContextOptionsBuilder optionsBuilder, + CultureInfo? culture = null) + where TContext : DbContext + => (DbContextOptionsBuilder)UseKebabCaseNamingConvention((DbContextOptionsBuilder)optionsBuilder, culture); } diff --git a/README.md b/README.md index afb887e..2bef2c6 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ SELECT c.id, c.full_name ## Supported naming conventions * UseSnakeCaseNamingConvention: `FullName` becomes `full_name` +* UseKebabCaseNamingConvention: `FullName` becomes `full-name` * UseLowerCaseNamingConvention: `FullName` becomes `fullname` * UseCamelCaseNamingConvention: `FullName` becomes `fullName` * UseUpperCaseNamingConvention: `FullName` becomes `FULLNAME`