commit 2ee1db0be7568b6c10fa0beab4d1131abca8a280 Author: Shay Rojansky Date: Fri Sep 20 00:45:24 2019 +0200 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1468ece --- /dev/null +++ b/.editorconfig @@ -0,0 +1,40 @@ +; Top-most EditorConfig file +root = true + +; Unix-style newlines +[*] +end_of_line = LF + +; 4-column space indentation +[*.cs] +indent_style = space +indent_size = 4 +; trim_trailing_whitespace = true +insert_final_newline = true + +[*.cs] +dotnet_style_qualification_for_field = false:error +dotnet_style_qualification_for_property = false:error +dotnet_style_qualification_for_method = false:error +dotnet_style_qualification_for_event = false:error +dotnet_style_predefined_type_for_locals_parameters_members = true:error +dotnet_style_predefined_type_for_member_access = true:error +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +csharp_style_var_for_built_in_types = true:error +csharp_style_var_when_type_is_apparent = true:error +csharp_style_var_elsewhere = true:suggestion +csharp_style_expression_bodied_methods = true:suggestion +csharp_style_expression_bodied_constructors = true:suggestion +csharp_style_expression_bodied_operators = true:suggestion +csharp_style_expression_bodied_properties = true:suggestion +csharp_style_expression_bodied_indexers = true:suggestion +csharp_style_expression_bodied_accessors = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion +csharp_indent_case_contents_when_block = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fa84b48 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +* text=auto + +*.cs text=auto diff=csharp +*.csproj text=auto +*.sln text=auto +*.resx text=auto +*.xml text=auto +*.txt text=auto + +packages/ binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6699224 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +.DS_Store +*.resources +*.suo +*.user +*.sln.docstates +*.userprefs +/*.nupkg +.nuget/ +.idea/ +[Bb]in/ +[Bb]uild/ +[Oo]bj/ +[Oo]bj/ +packages/*/ +Packages/*/ +packages.stable +artifacts/ +# Roslyn cache directories +*.ide/ +.vs/ +TestResult.xml diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..ed8560e --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,13 @@ + + + + 8.0 + true + + + + 3.0.0-preview9.19423.6 + 3.0.0-preview9.19423.4 + + + diff --git a/EFCore.NamingConventions.Test/EFCore.NamingConventions.Test.csproj b/EFCore.NamingConventions.Test/EFCore.NamingConventions.Test.csproj new file mode 100644 index 0000000..6325b21 --- /dev/null +++ b/EFCore.NamingConventions.Test/EFCore.NamingConventions.Test.csproj @@ -0,0 +1,18 @@ + + + + netcoreapp3.0 + + + + + + + + + + + + + + diff --git a/EFCore.NamingConventions.Test/NamingConventionsTest.cs b/EFCore.NamingConventions.Test/NamingConventionsTest.cs new file mode 100644 index 0000000..b2474ff --- /dev/null +++ b/EFCore.NamingConventions.Test/NamingConventionsTest.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace EFCore.Naming.Test +{ + public class NamingTest + { + [Fact] + public void Table_name_is_rewritten() + { + using var context = new TestContext(); + var entityType = context.Model.FindEntityType(typeof(Blog)); + Assert.Equal("blog", entityType.GetTableName()); + } + + [Fact] + public void Column_name_is_rewritten() + { + using var context = new TestContext(); + var entityType = context.Model.FindEntityType(typeof(Blog)); + Assert.Equal("id", entityType.FindProperty("Id").GetColumnName()); + Assert.Equal("full_name", entityType.FindProperty("FullName").GetColumnName()); + } + + public class TestContext : DbContext + { + public DbSet Blog { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseInMemoryDatabase("test") + .UseSnakeCaseNamingConventions(); + } + + public class Blog + { + public int Id { get; set; } + public string FullName { get; set; } + } + } +} diff --git a/EFCore.NamingConventions.sln b/EFCore.NamingConventions.sln new file mode 100644 index 0000000..aba9be9 --- /dev/null +++ b/EFCore.NamingConventions.sln @@ -0,0 +1,24 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCore.NamingConventions", "EFCore.NamingConventions\EFCore.NamingConventions.csproj", "{F7AE70A6-D237-4257-84F4-A32375E78E10}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCore.NamingConventions.Test", "EFCore.NamingConventions.Test\EFCore.NamingConventions.Test.csproj", "{AF58C6DC-FF41-4019-B4F3-B8D59A2DAD5F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F7AE70A6-D237-4257-84F4-A32375E78E10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7AE70A6-D237-4257-84F4-A32375E78E10}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7AE70A6-D237-4257-84F4-A32375E78E10}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7AE70A6-D237-4257-84F4-A32375E78E10}.Release|Any CPU.Build.0 = Release|Any CPU + {AF58C6DC-FF41-4019-B4F3-B8D59A2DAD5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF58C6DC-FF41-4019-B4F3-B8D59A2DAD5F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF58C6DC-FF41-4019-B4F3-B8D59A2DAD5F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF58C6DC-FF41-4019-B4F3-B8D59A2DAD5F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + EndGlobalSection +EndGlobal diff --git a/EFCore.NamingConventions.sln.DotSettings b/EFCore.NamingConventions.sln.DotSettings new file mode 100644 index 0000000..c587f90 --- /dev/null +++ b/EFCore.NamingConventions.sln.DotSettings @@ -0,0 +1,24 @@ + + + + No + True + InternalsOnly + + + True + + + Implicit + NEVER + False + False + True + True + True + True + True + True + True + + diff --git a/EFCore.NamingConventions.snk b/EFCore.NamingConventions.snk new file mode 100644 index 0000000..c54133d Binary files /dev/null and b/EFCore.NamingConventions.snk differ diff --git a/EFCore.NamingConventions/Check.cs b/EFCore.NamingConventions/Check.cs new file mode 100644 index 0000000..c7f2e88 --- /dev/null +++ b/EFCore.NamingConventions/Check.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore +{ + [DebuggerStepThrough] + internal static class Check + { + [ContractAnnotation("value:null => halt")] + public static T NotNull([NoEnumeration] T value, [InvokerParameterName] [NotNull] string parameterName) + { + if (ReferenceEquals(value, null)) + { + NotEmpty(parameterName, nameof(parameterName)); + + throw new ArgumentNullException(parameterName); + } + + return value; + } + + [ContractAnnotation("value:null => halt")] + public static T NotNull( + [NoEnumeration] T value, + [InvokerParameterName] [NotNull] string parameterName, + [NotNull] string propertyName) + { + if (ReferenceEquals(value, null)) + { + NotEmpty(parameterName, nameof(parameterName)); + NotEmpty(propertyName, nameof(propertyName)); + + throw new ArgumentException(CoreStrings.ArgumentPropertyNull(propertyName, parameterName)); + } + + return value; + } + + [ContractAnnotation("value:null => halt")] + public static IReadOnlyList NotEmpty(IReadOnlyList value, [InvokerParameterName] [NotNull] string parameterName) + { + NotNull(value, parameterName); + + if (value.Count == 0) + { + NotEmpty(parameterName, nameof(parameterName)); + + throw new ArgumentException(AbstractionsStrings.CollectionArgumentIsEmpty(parameterName)); + } + + return value; + } + + [ContractAnnotation("value:null => halt")] + public static string NotEmpty(string value, [InvokerParameterName] [NotNull] string parameterName) + { + Exception e = null; + if (ReferenceEquals(value, null)) + { + e = new ArgumentNullException(parameterName); + } + else if (value.Trim().Length == 0) + { + e = new ArgumentException(AbstractionsStrings.ArgumentIsEmpty(parameterName)); + } + + if (e != null) + { + NotEmpty(parameterName, nameof(parameterName)); + + throw e; + } + + return value; + } + + public static string NullButNotEmpty([CanBeNull] string value, [InvokerParameterName] [NotNull] string parameterName) + { + if (!ReferenceEquals(value, null) + && (value.Length == 0)) + { + NotEmpty(parameterName, nameof(parameterName)); + + throw new ArgumentException(AbstractionsStrings.ArgumentIsEmpty(parameterName)); + } + + return value; + } + + public static IReadOnlyList HasNoNulls(IReadOnlyList value, [InvokerParameterName] [NotNull] string parameterName) + where T : class + { + NotNull(value, parameterName); + + if (value.Any(e => e == null)) + { + NotEmpty(parameterName, nameof(parameterName)); + + throw new ArgumentException(parameterName); + } + + return value; + } + } +} diff --git a/EFCore.NamingConventions/CodeAnnotations.cs b/EFCore.NamingConventions/CodeAnnotations.cs new file mode 100644 index 0000000..8a11e30 --- /dev/null +++ b/EFCore.NamingConventions/CodeAnnotations.cs @@ -0,0 +1,108 @@ +using System; + +// ReSharper disable once CheckNamespace +namespace JetBrains.Annotations +{ + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | + AttributeTargets.Property | AttributeTargets.Delegate | + AttributeTargets.Field)] + internal sealed class NotNullAttribute : Attribute + { + } + + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | + AttributeTargets.Property | AttributeTargets.Delegate | + AttributeTargets.Field)] + internal sealed class CanBeNullAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Parameter)] + internal sealed class InvokerParameterNameAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Parameter)] + internal sealed class NoEnumerationAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + internal sealed class ContractAnnotationAttribute : Attribute + { + public string Contract { get; } + + public bool ForceFullStates { get; } + + public ContractAnnotationAttribute([NotNull] string contract) + : this(contract, false) + { + } + + public ContractAnnotationAttribute([NotNull] string contract, bool forceFullStates) + { + Contract = contract; + ForceFullStates = forceFullStates; + } + } + + [AttributeUsage(AttributeTargets.All)] + internal sealed class UsedImplicitlyAttribute : Attribute + { + public UsedImplicitlyAttribute() + : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) + { + } + + public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags) + : this(useKindFlags, ImplicitUseTargetFlags.Default) + { + } + + public UsedImplicitlyAttribute(ImplicitUseTargetFlags targetFlags) + : this(ImplicitUseKindFlags.Default, targetFlags) + { + } + + public UsedImplicitlyAttribute( + ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) + { + UseKindFlags = useKindFlags; + TargetFlags = targetFlags; + } + + public ImplicitUseKindFlags UseKindFlags { get; } + public ImplicitUseTargetFlags TargetFlags { get; } + } + + [AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Delegate)] + internal sealed class StringFormatMethodAttribute : Attribute + { + public StringFormatMethodAttribute([NotNull] string formatParameterName) + => FormatParameterName = formatParameterName; + + [NotNull] + public string FormatParameterName { get; } + } + + [Flags] + internal enum ImplicitUseKindFlags + { + Default = Access | Assign | InstantiatedWithFixedConstructorSignature, + Access = 1, + Assign = 2, + InstantiatedWithFixedConstructorSignature = 4, + InstantiatedNoFixedConstructorSignature = 8 + } + + [Flags] + internal enum ImplicitUseTargetFlags + { + Default = Itself, + Itself = 1, + Members = 2, + WithMembers = Itself | Members + } +} diff --git a/EFCore.NamingConventions/EFCore.NamingConventions.csproj b/EFCore.NamingConventions/EFCore.NamingConventions.csproj new file mode 100644 index 0000000..29a370f --- /dev/null +++ b/EFCore.NamingConventions/EFCore.NamingConventions.csproj @@ -0,0 +1,27 @@ + + + + EFCore.NamingConventions + EFCore + netstandard2.1 + 1.0.0 + true + true + true + ../EFCore.NamingConventions.snk + Naming Conventions for Entity Framework Core Tables and Columns. + Shay Rojansky + Copyright 2019 © Shay Rojansky + Entity Framework Core;entity-framework-core;ef;efcore;orm;sql;postgresql;npgsql + Apache-2.0 + git + git://github.com/efcore/EFCore.NamingConventions + + + + + + + + + diff --git a/EFCore.NamingConventions/NamingConventions/Internal/NamingConvention.cs b/EFCore.NamingConventions/NamingConventions/Internal/NamingConvention.cs new file mode 100644 index 0000000..a1620e1 --- /dev/null +++ b/EFCore.NamingConventions/NamingConventions/Internal/NamingConvention.cs @@ -0,0 +1,8 @@ +namespace EFCore.NamingConventions.Internal +{ + enum NamingConvention + { + None, + SnakeCase + } +} diff --git a/EFCore.NamingConventions/NamingConventions/Internal/NamingConventionSetPlugin.cs b/EFCore.NamingConventions/NamingConventions/Internal/NamingConventionSetPlugin.cs new file mode 100644 index 0000000..546385e --- /dev/null +++ b/EFCore.NamingConventions/NamingConventions/Internal/NamingConventionSetPlugin.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Conventions; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; +using JetBrains.Annotations; + +namespace EFCore.NamingConventions.Internal +{ + public class NamingConventionSetPlugin : IConventionSetPlugin + { + readonly IDbContextOptions _options; + + public NamingConventionSetPlugin([NotNull] IDbContextOptions options) => _options = options; + + public ConventionSet ModifyConventions(ConventionSet conventionSet) + { + var namingStyle = _options.FindExtension().NamingConvention; + + if (namingStyle == NamingConvention.None) + return conventionSet; + + var nameRewriter = namingStyle switch + { + NamingConvention.SnakeCase => new SnakeCaseNameRewriter(), + _ => throw new NotImplementedException("Unhandled enum value: " + namingStyle) + }; + + conventionSet.EntityTypeAddedConventions.Add(nameRewriter); + conventionSet.PropertyAddedConventions.Add(nameRewriter); + return conventionSet; + } + } +} diff --git a/EFCore.NamingConventions/NamingConventions/Internal/NamingConventionsOptionsExtension.cs b/EFCore.NamingConventions/NamingConventions/Internal/NamingConventionsOptionsExtension.cs new file mode 100644 index 0000000..c6f1afb --- /dev/null +++ b/EFCore.NamingConventions/NamingConventions/Internal/NamingConventionsOptionsExtension.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Globalization; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using JetBrains.Annotations; + +namespace EFCore.NamingConventions.Internal +{ + public class NamingConventionsOptionsExtension : IDbContextOptionsExtension + { + DbContextOptionsExtensionInfo _info; + NamingConvention _namingConvention; + + public NamingConventionsOptionsExtension() {} + + protected NamingConventionsOptionsExtension([NotNull] NamingConventionsOptionsExtension copyFrom) + => _namingConvention = copyFrom._namingConvention; + + public virtual DbContextOptionsExtensionInfo Info => _info ??= new ExtensionInfo(this); + + protected virtual NamingConventionsOptionsExtension Clone() => new NamingConventionsOptionsExtension(this); + + internal virtual NamingConvention NamingConvention => _namingConvention; + + public virtual NamingConventionsOptionsExtension WithoutNaming() + { + var clone = Clone(); + clone._namingConvention = NamingConvention.None; + return clone; + } + + public virtual NamingConventionsOptionsExtension WithSnakeCaseNaming() + { + var clone = Clone(); + clone._namingConvention = NamingConvention.SnakeCase; + return clone; + } + + public void Validate(IDbContextOptions options) {} + + public void ApplyServices(IServiceCollection services) + => services.AddEntityFrameworkNamingConventions(); + + sealed class ExtensionInfo : DbContextOptionsExtensionInfo + { + string _logFragment; + + public ExtensionInfo(IDbContextOptionsExtension extension) : base(extension) {} + + new NamingConventionsOptionsExtension Extension + => (NamingConventionsOptionsExtension)base.Extension; + + public override bool IsDatabaseProvider => false; + + public override string LogFragment + => _logFragment ??= Extension._namingConvention switch + { + NamingConvention.SnakeCase => "using snake-case naming ", + _ => "" + }; + + public override long GetServiceProviderHashCode() => Extension._namingConvention.GetHashCode(); + + public override void PopulateDebugInfo(IDictionary debugInfo) + => debugInfo["Naming:" + nameof(NamingConventionsExtensions.UseSnakeCaseNamingConventions)] + = Extension._namingConvention.GetHashCode().ToString(CultureInfo.InvariantCulture); + } + } +} diff --git a/EFCore.NamingConventions/NamingConventions/Internal/SnakeCaseNameRewriter.cs b/EFCore.NamingConventions/NamingConventions/Internal/SnakeCaseNameRewriter.cs new file mode 100644 index 0000000..c522b5d --- /dev/null +++ b/EFCore.NamingConventions/NamingConventions/Internal/SnakeCaseNameRewriter.cs @@ -0,0 +1,79 @@ +using System.Globalization; +using System.Text; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Conventions; + +namespace EFCore.NamingConventions.Internal +{ + class SnakeCaseNameRewriter : IEntityTypeAddedConvention, IPropertyAddedConvention + { + public void ProcessEntityTypeAdded( + IConventionEntityTypeBuilder entityTypeBuilder, + IConventionContext context) + => entityTypeBuilder.ToTable( + ConvertToSnakeCase(entityTypeBuilder.Metadata.GetTableName()), + entityTypeBuilder.Metadata.GetSchema()); + + public void ProcessPropertyAdded( + IConventionPropertyBuilder propertyBuilder, + IConventionContext context) + => propertyBuilder.HasColumnName( + ConvertToSnakeCase(propertyBuilder.Metadata.GetColumnName())); + + static string ConvertToSnakeCase(string value) + { + const char underscore = '_'; + const UnicodeCategory noneCategory = UnicodeCategory.Control; + + var builder = new StringBuilder(); + var previousCategory = noneCategory; + + for (var currentIndex = 0; currentIndex < value.Length; currentIndex++) + { + var currentChar = value[currentIndex]; + if (currentChar == underscore) + { + builder.Append(underscore); + previousCategory = noneCategory; + continue; + } + + var currentCategory = char.GetUnicodeCategory(currentChar); + switch (currentCategory) + { + case UnicodeCategory.UppercaseLetter: + case UnicodeCategory.TitlecaseLetter: + if (previousCategory == UnicodeCategory.SpaceSeparator || + previousCategory == UnicodeCategory.LowercaseLetter || + previousCategory != UnicodeCategory.DecimalDigitNumber && + currentIndex > 0 && + currentIndex + 1 < value.Length && + char.IsLower(value[currentIndex + 1])) + { + builder.Append(underscore); + } + + currentChar = char.ToLower(currentChar); + break; + + case UnicodeCategory.LowercaseLetter: + case UnicodeCategory.DecimalDigitNumber: + if (previousCategory == UnicodeCategory.SpaceSeparator) + builder.Append(underscore); + break; + + default: + if (previousCategory != noneCategory) + previousCategory = UnicodeCategory.SpaceSeparator; + continue; + } + + builder.Append(currentChar); + previousCategory = currentCategory; + } + + return builder.ToString(); + } + } +} diff --git a/EFCore.NamingConventions/NamingConventionsExtensions.cs b/EFCore.NamingConventions/NamingConventionsExtensions.cs new file mode 100644 index 0000000..3eda091 --- /dev/null +++ b/EFCore.NamingConventions/NamingConventionsExtensions.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using JetBrains.Annotations; +using EFCore.NamingConventions.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore +{ + public static class NamingConventionsExtensions + { + public static DbContextOptionsBuilder UseSnakeCaseNamingConventions([NotNull] this DbContextOptionsBuilder optionsBuilder) + { + Check.NotNull(optionsBuilder, nameof(optionsBuilder)); + + var extension = (optionsBuilder.Options.FindExtension() ?? new NamingConventionsOptionsExtension()) + .WithSnakeCaseNaming(); + + ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); + + return optionsBuilder; + } + + public static DbContextOptionsBuilder UseSnakeCaseNamingConventions([NotNull] this DbContextOptionsBuilder optionsBuilder) + where TContext : DbContext + => (DbContextOptionsBuilder)UseSnakeCaseNamingConventions((DbContextOptionsBuilder)optionsBuilder); + } +} diff --git a/EFCore.NamingConventions/NamingConventionsServiceCollectionExtensions.cs b/EFCore.NamingConventions/NamingConventionsServiceCollectionExtensions.cs new file mode 100644 index 0000000..ffbd924 --- /dev/null +++ b/EFCore.NamingConventions/NamingConventionsServiceCollectionExtensions.cs @@ -0,0 +1,42 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; +using EFCore.NamingConventions.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Extension methods for . + /// + public static class NamingConventionsServiceCollectionExtensions + { + /// + /// + /// Adds the services required for proxy support in Entity Framework. You use this method when + /// using dependency injection in your application, such as with ASP.NET. For more information + /// on setting up dependency injection, see http://go.microsoft.com/fwlink/?LinkId=526890. + /// + /// + /// You only need to use this functionality when you want Entity Framework to resolve the services it uses + /// from an external dependency injection container. If you are not using an external + /// dependency injection container, Entity Framework will take care of creating the services it requires. + /// + /// + /// The to add services to. + /// + /// The same service collection so that multiple calls can be chained. + /// + public static IServiceCollection AddEntityFrameworkNamingConventions( + [NotNull] this IServiceCollection serviceCollection) + { + Check.NotNull(serviceCollection, nameof(serviceCollection)); + + new EntityFrameworkServicesBuilder(serviceCollection) + .TryAdd(); + + return serviceCollection; + } + } +} diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dd3e516 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Naming Conventions for Entity Framework Core Tables and Columns + +By default, EF Core will map to tables and columns named exactly after your .NET classes and properties. For example, mapping a typical Customer class to PostgreSQL will result in SQL such as the following: + +```sql +CREATE TABLE "Customers" ( + "Id" integer NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "FullName" text NULL, + CONSTRAINT "PK_Customers" PRIMARY KEY ("Id") +); + +SELECT c."Id", c."FullName" + FROM "Customers" AS c + WHERE c."FullName" = 'John Doe'; +``` + +For PostgreSQL specifically, this forces double-quotes to be added since unquoted identifiers are automatically converted to lower-case - and all those quotes are an eye-sore. But even if we're using another database such as SQL Server, maybe we just hate seeing upper-case letters in our database, and would rather have another naming convention. + +Down with same-name identifier tyranny! Simply add a reference to [EFCore.NamingConventions](https://www.nuget.org/packages/EFCore.NamingConventions/1.0.0-rc1) and enable a naming convention in your model's `OnConfiguring` method: + +```c# +protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseNpgsql(...) + .UseSnakeCaseNamingConventions(); +``` + +This will automatically make all your table and column names have snake_case naming: + +```sql +CREATE TABLE customers ( + id integer NOT NULL GENERATED BY DEFAULT AS IDENTITY, + full_name text NULL, + CONSTRAINT "PK_customers" PRIMARY KEY (id); + +SELECT c.id, c.full_name + FROM customers AS c + WHERE c.full_name = 'John Doe'; +``` + +Currently, only snake_case is supported, but we can add more conventions as people request them. + +Some important notes: + +* If you have an existing database, adding this naming convention will cause a migration to produced, renaming everything. Be very cautious when doing this (the process currently involves dropping and recreating primary keys). +* This plugin will work with any database provider and isn't related to PostgreSQL or Npgsql in any way. +* This is a community-maintained plugin: it isn't an official part of Entity Framework Core and isn't supported by Microsoft in any way. diff --git a/global.json b/global.json new file mode 100644 index 0000000..79a76ed --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "3.0.100-preview9-014004" + } +}