diff --git a/src/EFCore.Abstractions/UnicodeAttribute.cs b/src/EFCore.Abstractions/UnicodeAttribute.cs new file mode 100644 index 00000000000..b676df97c21 --- /dev/null +++ b/src/EFCore.Abstractions/UnicodeAttribute.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.EntityFrameworkCore +{ + /// + /// Configures the property as capable of persisting unicode characters. + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] + public sealed class UnicodeAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// A value indicating whether the property can contain unicode characters or not. + public UnicodeAttribute(bool unicode = true) + { + IsUnicode = unicode; + } + + /// + /// A value indicating whether the property can contain unicode characters or not. + /// + public bool IsUnicode { get; } + } +} diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs index 5ff7db2e354..8f70fa8a790 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs @@ -12,6 +12,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal @@ -291,6 +292,7 @@ protected virtual void GeneratePropertyDataAnnotations([NotNull] IProperty prope GenerateRequiredAttribute(property); GenerateColumnAttribute(property); GenerateMaxLengthAttribute(property); + GenerateUnicodeAttribute(property); var annotations = _annotationCodeGenerator .FilterIgnoredAnnotations(property.GetAnnotations()) @@ -369,6 +371,25 @@ private void GenerateMaxLengthAttribute(IProperty property) } } + private void GenerateUnicodeAttribute(IProperty property) + { + if (property.ClrType != typeof(string)) + { + return; + } + + var unicode = property.IsUnicode(); + if (unicode.HasValue) + { + var unicodeAttribute = new AttributeWriter(nameof(UnicodeAttribute)); + if (!unicode.Value) + { + unicodeAttribute.AddParameter(_code.Literal(false)); + } + _sb.AppendLine(unicodeAttribute.ToString()); + } + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs index 411b54f8be0..d4203620973 100644 --- a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs +++ b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs @@ -113,6 +113,7 @@ public virtual ConventionSet CreateConventionSet() var stringLengthAttributeConvention = new StringLengthAttributeConvention(Dependencies); var timestampAttributeConvention = new TimestampAttributeConvention(Dependencies); var backingFieldAttributeConvention = new BackingFieldAttributeConvention(Dependencies); + var unicodeAttributeConvention = new UnicodeAttributeConvention(Dependencies); conventionSet.PropertyAddedConventions.Add(backingFieldAttributeConvention); conventionSet.PropertyAddedConventions.Add(backingFieldConvention); @@ -126,6 +127,7 @@ public virtual ConventionSet CreateConventionSet() conventionSet.PropertyAddedConventions.Add(keyAttributeConvention); conventionSet.PropertyAddedConventions.Add(keyDiscoveryConvention); conventionSet.PropertyAddedConventions.Add(foreignKeyPropertyDiscoveryConvention); + conventionSet.PropertyAddedConventions.Add(unicodeAttributeConvention); conventionSet.EntityTypePrimaryKeyChangedConventions.Add(foreignKeyPropertyDiscoveryConvention); conventionSet.EntityTypePrimaryKeyChangedConventions.Add(valueGeneratorConvention); diff --git a/src/EFCore/Metadata/Conventions/UnicodeAttributeConvention.cs b/src/EFCore/Metadata/Conventions/UnicodeAttributeConvention.cs new file mode 100644 index 00000000000..80c34b9d849 --- /dev/null +++ b/src/EFCore/Metadata/Conventions/UnicodeAttributeConvention.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions +{ + /// + /// A convention that configures the Unicode based on the applied on the property. + /// + public class UnicodeAttributeConvention : PropertyAttributeConventionBase + { + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + public UnicodeAttributeConvention([NotNull] ProviderConventionSetBuilderDependencies dependencies) + : base(dependencies) + { + } + + /// + /// Called after a property is added to the entity type with an attribute on the associated CLR property or field. + /// + /// The builder for the property. + /// The attribute. + /// The member that has the attribute. + /// Additional information associated with convention execution. + protected override void ProcessPropertyAdded( + IConventionPropertyBuilder propertyBuilder, + UnicodeAttribute attribute, + MemberInfo clrMember, + IConventionContext context) + { + propertyBuilder.IsUnicode(attribute.IsUnicode, fromDataAnnotation: true); + } + } +} diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs index 0f602d51985..99ea4f5f595 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Linq; using Microsoft.EntityFrameworkCore.Internal; using Xunit; @@ -846,6 +847,60 @@ public partial class Entity }); } + [ConditionalFact] + public void UnicodeAttribute_is_generated_for_property() + { + Test( + modelBuilder => modelBuilder + .Entity( + "Entity", + x => + { + x.Property("Id"); + x.Property("A").HasMaxLength(34).IsUnicode(); + x.Property("B").HasMaxLength(34).IsUnicode(false); + x.Property("C").HasMaxLength(34); + }), + new ModelCodeGenerationOptions { UseDataAnnotations = true }, + code => + { + AssertFileContents( + @"using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +#nullable disable + +namespace TestNamespace +{ + public partial class Entity + { + [Key] + public int Id { get; set; } + [StringLength(34)] + [Unicode] + public string A { get; set; } + [StringLength(34)] + [Unicode(false)] + public string B { get; set; } + [StringLength(34)] + public string C { get; set; } + } +} +", + code.AdditionalFiles.Single(f => f.Path == "Entity.cs")); + }, + model => + { + var entitType = model.FindEntityType("TestNamespace.Entity"); + Assert.True(entitType.GetProperty("A").IsUnicode()); + Assert.False(entitType.GetProperty("B").IsUnicode()); + Assert.Null(entitType.GetProperty("C").IsUnicode()); + }); + } + [ConditionalFact] public void Comments_are_generated() { diff --git a/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs b/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs index 39e42d9b418..2bba7f319ab 100644 --- a/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs +++ b/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs @@ -2378,6 +2378,43 @@ public virtual void TimestampAttribute_throws_if_value_in_database_changed() }); } + [ConditionalFact] + public virtual void UnicodeAttribute_sets_unicode_for_properties_and_fields() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity(b => + { + b.Property(e => e.PersonMiddleName); + b.Property(e => e.PersonAddress); + }); + + Validate(modelBuilder); + + Assert.True(GetProperty(modelBuilder, "PersonFirstName").IsUnicode()); + Assert.False(GetProperty(modelBuilder, "PersonLastName").IsUnicode()); + + Assert.True(GetProperty(modelBuilder, "PersonMiddleName").IsUnicode()); + Assert.False(GetProperty(modelBuilder, "PersonAddress").IsUnicode()); + } + + protected class UnicodeAnnotationClass + { + public int Id { get; set; } + + [Unicode] + public string PersonFirstName { get; set; } + + [Unicode(false)] + public string PersonLastName { get; set; } + + [Unicode] + public string PersonMiddleName; + + [Unicode(false)] + public string PersonAddress; + } + [ConditionalFact] public virtual void OwnedEntityTypeAttribute_configures_one_reference_as_owned() { diff --git a/test/EFCore.Tests/Metadata/Conventions/PropertyAttributeConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/PropertyAttributeConventionTest.cs index 65d5868a819..727b1e18354 100644 --- a/test/EFCore.Tests/Metadata/Conventions/PropertyAttributeConventionTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/PropertyAttributeConventionTest.cs @@ -544,6 +544,56 @@ public void BackingFieldAttribute_does_not_override_configuration_from_explicit_ #endregion + #region UnicodeAttribute + [ConditionalFact] + public void UnicodeAttribute_overrides_configuration_from_convention_source() + { + var entityTypeBuilder = CreateInternalEntityTypeBuilder(); + + var propertyBuilder = entityTypeBuilder.Property(typeof(string), "UnicodeProperty", ConfigurationSource.Explicit); + + propertyBuilder.IsUnicode(false, ConfigurationSource.Convention); + + RunConvention(propertyBuilder); + + Assert.True(propertyBuilder.Metadata.IsUnicode()); + } + + [ConditionalFact] + public void UnicodeAttribute_does_not_override_configuration_from_explicit_source() + { + var entityTypeBuilder = CreateInternalEntityTypeBuilder(); + + var propertyBuilder = entityTypeBuilder.Property(typeof(string), "UnicodeProperty", ConfigurationSource.Explicit); + + propertyBuilder.IsUnicode(false, ConfigurationSource.Explicit); + + RunConvention(propertyBuilder); + + Assert.False(propertyBuilder.Metadata.IsUnicode()); + } + + [ConditionalFact] + public void UnicodeAttribute_sets_unicode_with_conventional_builder() + { + var modelBuilder = CreateModelBuilder(); + var entityTypeBuilder = modelBuilder.Entity(); + + Assert.True(entityTypeBuilder.Property(e => e.UnicodeProperty).Metadata.IsUnicode()); + Assert.False(entityTypeBuilder.Property(e => e.NonUnicodeProperty).Metadata.IsUnicode()); + } + + [ConditionalFact] + public void UnicodeAttribute_on_field_sets_unicode_with_conventional_builder() + { + var modelBuilder = CreateModelBuilder(); + var entityTypeBuilder = modelBuilder.Entity(); + + Assert.True(entityTypeBuilder.Property(nameof(F.UnicodeField)).Metadata.IsUnicode()); + Assert.False(entityTypeBuilder.Property(nameof(F.NonUnicodeField)).Metadata.IsUnicode()); + } + #endregion + [ConditionalFact] public void Property_attribute_convention_runs_for_private_property() { @@ -596,6 +646,9 @@ private static void RunConvention(InternalPropertyBuilder propertyBuilder) new KeyAttributeConvention(dependencies) .ProcessPropertyAdded(propertyBuilder, context); + + new UnicodeAttributeConvention(dependencies) + .ProcessPropertyAdded(propertyBuilder, context); } private void RunConvention(InternalEntityTypeBuilder entityTypeBuilder) @@ -629,6 +682,12 @@ private class A [Required] public string Name { get; set; } + [Unicode] + public string UnicodeProperty { get; set; } + + [Unicode(false)] + public string NonUnicodeProperty { get; set; } + [Key] public int MyPrimaryKey { get; set; } @@ -684,6 +743,12 @@ public class F [Required] public string Name; + [Unicode] + public string UnicodeField; + + [Unicode(false)] + public string NonUnicodeField; + [Key] public int MyPrimaryKey;