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;