diff --git a/src/EFCore.Abstractions/UnicodeAttribute.cs b/src/EFCore.Abstractions/UnicodeAttribute.cs
new file mode 100644
index 00000000000..b973fc57fb0
--- /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. Can only be set on System.String properties.
+ ///
+ [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
+ public sealed class UnicodeAttribute : Attribute
+ {
+ ///
+ /// A value indicating whether the property can contain unicode characters or not.
+ ///
+ public bool IsUnicode { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A value indicating whether the property can contain unicode characters or not.
+ public UnicodeAttribute(bool isUnicode = true)
+ {
+ IsUnicode = isUnicode;
+ }
+ }
+}
diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs
index 505e4bb3337..0b6977b0065 100644
--- a/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs
+++ b/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs
@@ -287,6 +287,7 @@ protected virtual void GeneratePropertyDataAnnotations([NotNull] IProperty prope
GenerateRequiredAttribute(property);
GenerateColumnAttribute(property);
GenerateMaxLengthAttribute(property);
+ GenerateUnicodeAttribute(property);
var annotations = _annotationCodeGenerator
.FilterIgnoredAnnotations(property.GetAnnotations())
@@ -365,6 +366,29 @@ private void GenerateMaxLengthAttribute(IProperty property)
}
}
+ private void GenerateUnicodeAttribute(IProperty property)
+ {
+ if (property.ClrType != typeof(string))
+ {
+ return;
+ }
+
+ var isUnicode = property.IsUnicode();
+
+ if (isUnicode.HasValue)
+ {
+ if (!isUnicode.Value)
+ {
+ var unicodeAttribute = new AttributeWriter(nameof(UnicodeAttribute));
+
+ unicodeAttribute.AddParameter(_code.Literal(false));
+
+ _sb.AppendLine(unicodeAttribute.ToString());
+ }
+ else _sb.AppendLine(new AttributeWriter(nameof(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..c62cc824ba2
--- /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(unicode: 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 72ad7fb1783..b09ea26fcd7 100644
--- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs
+++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs
@@ -846,6 +846,56 @@ 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);
+ }),
+ 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; }
+ }
+}
+",
+ 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());
+ });
+ }
+
[ConditionalFact]
public void Properties_are_sorted_in_order_of_definition_in_table()
{
diff --git a/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs b/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs
index 8e1334daf82..0d8bb0cb754 100644
--- a/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs
+++ b/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs
@@ -2375,6 +2375,30 @@ public virtual void TimestampAttribute_throws_if_value_in_database_changed()
});
}
+ [ConditionalFact]
+ public virtual void Unicode_annotation_is_enabled()
+ {
+ var modelBuilder = CreateModelBuilder();
+
+ modelBuilder.Entity();
+
+ Validate(modelBuilder);
+
+ Assert.True(GetProperty(modelBuilder, "PersonFirstName").IsUnicode());
+ Assert.False(GetProperty(modelBuilder, "PersonLastName").IsUnicode());
+ }
+
+ protected class UnicodeAnnotationClass
+ {
+ public int Id { get; set; }
+
+ [Unicode]
+ public string PersonFirstName { get; set; }
+
+ [Unicode(false)]
+ public string PersonLastName { get; set; }
+ }
+
[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 01120e2dfc0..85fa6309b6a 100644
--- a/test/EFCore.Tests/Metadata/Conventions/PropertyAttributeConventionTest.cs
+++ b/test/EFCore.Tests/Metadata/Conventions/PropertyAttributeConventionTest.cs
@@ -543,6 +543,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.UnicodeProperty)).Metadata.IsUnicode());
+ Assert.False(entityTypeBuilder.Property(nameof(F.NonUnicodeProperty)).Metadata.IsUnicode());
+ }
+ #endregion
+
[ConditionalFact]
public void Property_attribute_convention_runs_for_private_property()
{
@@ -595,6 +645,9 @@ private static void RunConvention(InternalPropertyBuilder propertyBuilder)
new KeyAttributeConvention(dependencies)
.ProcessPropertyAdded(propertyBuilder, context);
+
+ new UnicodeAttributeConvention(dependencies)
+ .ProcessPropertyAdded(propertyBuilder, context);
}
private void RunConvention(InternalEntityTypeBuilder entityTypeBuilder)
@@ -628,6 +681,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; }
@@ -683,6 +742,12 @@ public class F
[Required]
public string Name;
+ [Unicode]
+ public string UnicodeProperty { get; set; }
+
+ [Unicode(false)]
+ public string NonUnicodeProperty { get; set; }
+
[Key]
public int MyPrimaryKey;