diff --git a/src/Microsoft.OData.CodeGen/Templates/ODataT4CodeGenerator.cs b/src/Microsoft.OData.CodeGen/Templates/ODataT4CodeGenerator.cs index 040e7d68..06e887bf 100644 --- a/src/Microsoft.OData.CodeGen/Templates/ODataT4CodeGenerator.cs +++ b/src/Microsoft.OData.CodeGen/Templates/ODataT4CodeGenerator.cs @@ -10,6 +10,7 @@ namespace Microsoft.OData.CodeGen.Templates { using System; + using System.CodeDom.Compiler; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; @@ -18,6 +19,7 @@ namespace Microsoft.OData.CodeGen.Templates using System.Net; using System.Security; using System.Text; + using System.Text.RegularExpressions; using System.Xml; using System.Xml.Linq; using Microsoft.OData.Edm.Csdl; @@ -2466,7 +2468,16 @@ internal void WritePropertiesForSingleType(IEnumerable properties) foreach (IEdmProperty property in properties.Where(i => i.PropertyKind == EdmPropertyKind.Navigation)) { string propertyType; - string propertyName = this.context.EnableNamingAlias ? Customization.CustomizeNaming(property.Name) : property.Name; + string propertyName; + if (IdentifierMappings.TryGetValue(property.Name, out var mappedPropertyName)) + { + propertyName = mappedPropertyName; + } + else + { + propertyName = this.context.EnableNamingAlias ? Customization.CustomizeNaming(property.Name) : property.Name; + } + if (property.Type is Microsoft.OData.Edm.EdmCollectionTypeReference) { propertyType = GetSourceOrReturnTypeName(property.Type); @@ -2495,6 +2506,7 @@ internal void WriteEntityType(IEdmEntityType entityType, Dictionary + /// Set all property mappings for + /// + /// Name of type used to verify conflicts against + /// Type object to map properties + internal void SetPropertyIdentifierMappings(string typeName, IEdmStructuredType structuredType) + { + this.SetPropertyIdentifierMappingsIfNameConflicts(typeName, structuredType); + this.SetPropertyIdentifierMappingsIfBaseConflicts(structuredType); + this.SetPropertyIdentifierMappingsIfInvalidIdentifier(structuredType); + } + + /// + /// List of base class properties that should not be used in generated classes + /// + private static readonly IEnumerable BaseClassProperties = typeof(Client.DataServiceQuerySingle).GetProperties().Select(prop => prop.Name); + + /// + /// Map identifiers that conflict with base class identifiers to TypeNameIdentiferName + /// and if still non-unique, add incrementing integer suffix + /// + /// Object containing DeclaredProperties to map + internal void SetPropertyIdentifierMappingsIfBaseConflicts(IEdmStructuredType structuredType) + { + Func customizePropertyName = (name) => { return this.context.EnableNamingAlias ? Customization.CustomizeNaming(name) : name; }; + + // PropertyName in VB is case-insensitive. + bool isLanguageCaseSensitive = this.context.TargetLanguage == LanguageOption.CSharp; + + var baseProperties = BaseClassProperties; + if (!isLanguageCaseSensitive) + { + baseProperties = BaseClassProperties.Select(prop => prop.ToUpperInvariant()); + } + + var propertiesToRename = structuredType.DeclaredProperties.Where(prop => baseProperties.Contains((isLanguageCaseSensitive ? customizePropertyName(prop.Name) : prop.Name.ToUpperInvariant()))); + if (propertiesToRename.Any()) + { + UniqueIdentifierService uniqueIdentifierService = + new UniqueIdentifierService(structuredType.DeclaredProperties.Select(prop => prop.Name), isLanguageCaseSensitive); + + var typeName = customizePropertyName(((IEdmNamedElement)structuredType).Name); + foreach (var property in propertiesToRename) + { + var renamedPropertyName = uniqueIdentifierService.GetUniqueIdentifier(typeName + Customization.CustomizeNaming(property.Name)); + if (IdentifierMappings.ContainsKey(property.Name)) + { + IdentifierMappings[property.Name] = renamedPropertyName; + } + else + { + IdentifierMappings.Add(property.Name, renamedPropertyName); + } + } + } + } + + /// + /// CSharp code provider instance + /// + private static readonly CodeDomProvider CSharpProvider = new Microsoft.CSharp.CSharpCodeProvider(); + + /// + /// VB code provider instance + /// + private static readonly CodeDomProvider VBProvider = new Microsoft.VisualBasic.VBCodeProvider(); + + /// + /// Map invalid identifiers to valid names to avoid generating code that does not compile + /// + /// Object containing properties to map + internal void SetPropertyIdentifierMappingsIfInvalidIdentifier(IEdmStructuredType structuredType) + { + bool isLanguageCaseSensitive = this.context.TargetLanguage == LanguageOption.CSharp; + UniqueIdentifierService uniqueIdentifierService = + new UniqueIdentifierService(IdentifierMappings.Select(mapping => mapping.Value), isLanguageCaseSensitive); + + Func customizePropertyName = (name) => { return this.context.EnableNamingAlias ? Customization.CustomizeNaming(name) : name; }; + var codeDomProvider = (this.context.TargetLanguage == LanguageOption.CSharp ? CSharpProvider : VBProvider); + var propertiesToRename = structuredType.DeclaredProperties.Where(prop => !codeDomProvider.IsValidIdentifier(prop.Name) + && !LanguageKeywords.Contains(prop.Name)); + + foreach (var property in propertiesToRename) + { + var customizedPropertyName = customizePropertyName(property.Name); + var validName = uniqueIdentifierService.GetUniqueIdentifier(GetValidIdentifier(customizedPropertyName)); + if (IdentifierMappings.ContainsKey(property.Name)) + { + if (!codeDomProvider.IsValidIdentifier(IdentifierMappings[property.Name])) + { + IdentifierMappings[property.Name] = validName; + } + } + else + { + IdentifierMappings.Add(property.Name, validName); + } + } + } + + /// + /// Regex match any non-identifier characters, used to generate valid identifiers
+ /// See C# definitions: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/lexical-structure#643-identifiers
+ /// See VB definitions: https://learn.microsoft.com/en-us/dotnet/visual-basic/reference/language-specification/lexical-grammar#identifiers
+ /// See Unicode character class definitions: https://www.unicode.org/reports/tr18/#General_Category_Property + ///
+ private static readonly Regex RegexMatchNonIdentifier = new Regex(@"[^\p{L}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\p{Cf}]", RegexOptions.Compiled); + + /// + /// Default prefix character used to prefix non-alpha characters in identifiers + /// + private const char DefaultPrefixCharacter = '_'; + + /// + /// Get a valid C#/VB identifier for based on C#/VB identifier specification
+ /// - Removes characters not permitted in C#/VB identifiers
+ /// - Prefixes with underscore (_) if it doesn't start with a letter + ///
+ /// + /// C# and VB use very similar rules for identifier names
+ /// See C# definitions: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/lexical-structure#643-identifiers
+ /// See VB definitions: https://learn.microsoft.com/en-us/dotnet/visual-basic/reference/language-specification/lexical-grammar#identifiers + ///
+ /// Identifier to validate and modify + /// Modified or unchanged if no changes are needed + private string GetValidIdentifier(string name) + { + var codeDomProvider = (this.context.TargetLanguage == LanguageOption.CSharp ? CSharpProvider : VBProvider); + if (codeDomProvider.IsValidIdentifier(name)) + { + return name; + } + + var segments = RegexMatchNonIdentifier.Split(name); + var validName = new StringBuilder(); + var isFirst = true; + foreach (var segment in segments.Where(token => !string.IsNullOrWhiteSpace(token))) + { + if (isFirst && !this.context.EnableNamingAlias) + { + validName.Append(segment); + } + else + { + var titleCaseSegment = Customization.CustomizeNaming(segment); + validName.Append(titleCaseSegment); + } + + isFirst = false; + } + + if (validName.Length < 1) + { + throw new Exception($"Failed to make valid identifier for '{name}'"); + } + + if (!char.IsLetter(validName[0]) && validName[0] != DefaultPrefixCharacter) + { + validName.Insert(0, DefaultPrefixCharacter); + } + + return validName.ToString(); + } + internal void WriteTypeStaticCreateMethod(string typeName, IEdmStructuredType structuredType) { Debug.Assert(structuredType != null, "structuredType != null"); @@ -3063,7 +3238,7 @@ internal void WritePropertiesForStructuredType(IEdmStructuredType structuredType }); } - // Private name should not confict with field name + // Private name should not conflict with field name UniqueIdentifierService uniqueIdentifierService = new UniqueIdentifierService(propertyInfos.Select(_ => _.FixedPropertyName), this.context.TargetLanguage == LanguageOption.CSharp); @@ -4901,13 +5076,13 @@ internal override void WriteGeneratedEdmModel(string escapedEdmxString) this.Write(@", out edmModel, out errors)) { - global::System.Text.StringBuilder errorMessages = new global::System.Text.StringBuilder(); - foreach (var error in errors) - { - errorMessages.Append(error.ErrorMessage); - errorMessages.Append(""; ""); - } - throw new global::System.InvalidOperationException(errorMessages.ToString()); + global::System.Text.StringBuilder errorMessages = new global::System.Text.StringBuilder(); + foreach (var error in errors) + { + errorMessages.Append(error.ErrorMessage); + errorMessages.Append(""; ""); + } + throw new global::System.InvalidOperationException(errorMessages.ToString()); } return edmModel; @@ -5206,7 +5381,7 @@ internal override void WritePropertyForStructuredType(PropertyOptions propertyOp this.Write(this.ToStringHelper.ToStringWithCulture(T4Version)); -this.Write("\")]\r\n\r\n"); +this.Write("\")]\r\n"); if (this.context.EnableNamingAlias || IdentifierMappings.ContainsKey(propertyOptions.OriginalPropertyName)) @@ -5617,9 +5792,7 @@ internal override void WriteBoundFunctionInEntityTypeReturnSingleResult(bool hid this.Write(" public virtual "); -this.Write(this.ToStringHelper.ToStringWithCulture(hideBaseMethod ? this.OverloadsModifier : string.Empty)); - -this.Write(" "); +this.Write(this.ToStringHelper.ToStringWithCulture(hideBaseMethod ? $"{this.OverloadsModifier} " : string.Empty)); this.Write(this.ToStringHelper.ToStringWithCulture(isReturnEntity ? returnTypeNameWithSingleSuffix : string.Format(CultureInfo.InvariantCulture, this.DataServiceQuerySingleStructureTemplate, returnTypeName))); diff --git a/src/Microsoft.OData.CodeGen/Templates/ODataT4CodeGenerator.ttinclude b/src/Microsoft.OData.CodeGen/Templates/ODataT4CodeGenerator.ttinclude index 08cb2241..bda70d82 100644 --- a/src/Microsoft.OData.CodeGen/Templates/ODataT4CodeGenerator.ttinclude +++ b/src/Microsoft.OData.CodeGen/Templates/ODataT4CodeGenerator.ttinclude @@ -18,6 +18,7 @@ <#@ Assembly Name="System.Windows.Forms.dll" #> <#@ Assembly Name="Microsoft.OData.Edm.dll" #> <#@ Import Namespace="System" #> +<#@ Import Namespace= "System.CodeDom.Compiler"#> <#@ Import Namespace="System.Collections.Generic" #> <#@ Import Namespace="System.Diagnostics" #> <#@ Import Namespace="System.Globalization" #> @@ -26,6 +27,7 @@ <#@ Import Namespace="System.Net"#> <#@ Import Namespace="System.Security"#> <#@ Import Namespace="System.Text"#> +<#@ Import Namespace= "System.Text.RegularExpressions"#> <#@ Import Namespace="System.Xml"#> <#@ Import Namespace="System.Xml.Linq" #> <#@ Import Namespace="Microsoft.OData.Edm.Csdl" #> @@ -2321,7 +2323,16 @@ public abstract class ODataClientTemplate : TemplateBase foreach (IEdmProperty property in properties.Where(i => i.PropertyKind == EdmPropertyKind.Navigation)) { string propertyType; - string propertyName = this.context.EnableNamingAlias ? Customization.CustomizeNaming(property.Name) : property.Name; + string propertyName; + if (IdentifierMappings.TryGetValue(property.Name, out var mappedPropertyName)) + { + propertyName = mappedPropertyName; + } + else + { + propertyName = this.context.EnableNamingAlias ? Customization.CustomizeNaming(property.Name) : property.Name; + } + if (property.Type is Microsoft.OData.Edm.EdmCollectionTypeReference) { propertyType = GetSourceOrReturnTypeName(property.Type); @@ -2350,6 +2361,7 @@ public abstract class ODataClientTemplate : TemplateBase IEdmEntityType current = entityType; while (current != null) { + this.SetPropertyIdentifierMappings(entityType.Name, entityType); this.WritePropertiesForSingleType(current.DeclaredProperties); current = (IEdmEntityType)current.BaseType; } @@ -2389,7 +2401,6 @@ public abstract class ODataClientTemplate : TemplateBase this.WriteObsoleteAttribute(GetRevisionAnnotations(entityType), /* isClass */ true); this.WriteStructurdTypeDeclaration(entityType, this.BaseEntityType); - this.SetPropertyIdentifierMappingsIfNameConflicts(entityType.Name, entityType); this.WriteTypeStaticCreateMethod(entityType.Name, entityType); this.WritePropertiesForStructuredType(entityType); @@ -2407,7 +2418,7 @@ public abstract class ODataClientTemplate : TemplateBase { this.WriteSummaryCommentForStructuredType(this.context.EnableNamingAlias ? Customization.CustomizeNaming(complexType.Name) : complexType.Name, GetDescriptionAnnotation(complexType)?.Value); this.WriteStructurdTypeDeclaration(complexType, string.Empty); - this.SetPropertyIdentifierMappingsIfNameConflicts(complexType.Name, complexType); + this.SetPropertyIdentifierMappings(complexType.Name, complexType); this.WriteTypeStaticCreateMethod(complexType.Name, complexType); this.WritePropertiesForStructuredType(complexType); @@ -2763,6 +2774,170 @@ public abstract class ODataClientTemplate : TemplateBase } } + /// + /// Set all property mappings for + /// + /// Name of type used to verify conflicts against + /// Type object to map properties + internal void SetPropertyIdentifierMappings(string typeName, IEdmStructuredType structuredType) + { + this.SetPropertyIdentifierMappingsIfNameConflicts(typeName, structuredType); + this.SetPropertyIdentifierMappingsIfBaseConflicts(structuredType); + this.SetPropertyIdentifierMappingsIfInvalidIdentifier(structuredType); + } + + /// + /// List of base class properties that should not be used in generated classes + /// + private static readonly IEnumerable BaseClassProperties = typeof(Client.DataServiceQuerySingle).GetProperties().Select(prop => prop.Name); + + /// + /// Map identifiers that conflict with base class identifiers to TypeNameIdentiferName + /// and if still non-unique, add incrementing integer suffix + /// + /// Object containing DeclaredProperties to map + internal void SetPropertyIdentifierMappingsIfBaseConflicts(IEdmStructuredType structuredType) + { + Func customizePropertyName = (name) => { return this.context.EnableNamingAlias ? Customization.CustomizeNaming(name) : name; }; + + // PropertyName in VB is case-insensitive. + bool isLanguageCaseSensitive = this.context.TargetLanguage == LanguageOption.CSharp; + + var baseProperties = BaseClassProperties; + if (!isLanguageCaseSensitive) + { + baseProperties = BaseClassProperties.Select(prop => prop.ToUpperInvariant()); + } + + var propertiesToRename = structuredType.DeclaredProperties.Where(prop => baseProperties.Contains((isLanguageCaseSensitive ? customizePropertyName(prop.Name) : prop.Name.ToUpperInvariant()))); + if (propertiesToRename.Any()) + { + UniqueIdentifierService uniqueIdentifierService = + new UniqueIdentifierService(structuredType.DeclaredProperties.Select(prop => prop.Name), isLanguageCaseSensitive); + + var typeName = customizePropertyName(((IEdmNamedElement)structuredType).Name); + foreach (var property in propertiesToRename) + { + var renamedPropertyName = uniqueIdentifierService.GetUniqueIdentifier(typeName + Customization.CustomizeNaming(property.Name)); + if (IdentifierMappings.ContainsKey(property.Name)) + { + IdentifierMappings[property.Name] = renamedPropertyName; + } + else + { + IdentifierMappings.Add(property.Name, renamedPropertyName); + } + } + } + } + + /// + /// CSharp code provider instance + /// + private static readonly CodeDomProvider CSharpProvider = new Microsoft.CSharp.CSharpCodeProvider(); + + /// + /// VB code provider instance + /// + private static readonly CodeDomProvider VBProvider = new Microsoft.VisualBasic.VBCodeProvider(); + + /// + /// Map invalid identifiers to valid names to avoid generating code that does not compile + /// + /// Object containing properties to map + internal void SetPropertyIdentifierMappingsIfInvalidIdentifier(IEdmStructuredType structuredType) + { + bool isLanguageCaseSensitive = this.context.TargetLanguage == LanguageOption.CSharp; + UniqueIdentifierService uniqueIdentifierService = + new UniqueIdentifierService(IdentifierMappings.Select(mapping => mapping.Value), isLanguageCaseSensitive); + + Func customizePropertyName = (name) => { return this.context.EnableNamingAlias ? Customization.CustomizeNaming(name) : name; }; + var codeDomProvider = (this.context.TargetLanguage == LanguageOption.CSharp ? CSharpProvider : VBProvider); + var propertiesToRename = structuredType.DeclaredProperties.Where(prop => !codeDomProvider.IsValidIdentifier(prop.Name) + && !LanguageKeywords.Contains(prop.Name)); + + foreach (var property in propertiesToRename) + { + var customizedPropertyName = customizePropertyName(property.Name); + var validName = uniqueIdentifierService.GetUniqueIdentifier(GetValidIdentifier(customizedPropertyName)); + if (IdentifierMappings.ContainsKey(property.Name)) + { + if (!codeDomProvider.IsValidIdentifier(IdentifierMappings[property.Name])) + { + IdentifierMappings[property.Name] = validName; + } + } + else + { + IdentifierMappings.Add(property.Name, validName); + } + } + } + + /// + /// Regex match any non-identifier characters, used to generate valid identifiers
+ /// See C# definitions: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/lexical-structure#643-identifiers
+ /// See VB definitions: https://learn.microsoft.com/en-us/dotnet/visual-basic/reference/language-specification/lexical-grammar#identifiers
+ /// See Unicode character class definitions: https://www.unicode.org/reports/tr18/#General_Category_Property + ///
+ private static readonly Regex RegexMatchNonIdentifier = new Regex(@"[^\p{L}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\p{Cf}]", RegexOptions.Compiled); + + /// + /// Default prefix character used to prefix non-alpha characters in identifiers + /// + private const char DefaultPrefixCharacter = '_'; + + /// + /// Get a valid C#/VB identifier for based on C#/VB identifier specification
+ /// - Removes characters not permitted in C#/VB identifiers
+ /// - Prefixes with underscore (_) if it doesn't start with a letter + ///
+ /// + /// C# and VB use very similar rules for identifier names
+ /// See C# definitions: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/lexical-structure#643-identifiers
+ /// See VB definitions: https://learn.microsoft.com/en-us/dotnet/visual-basic/reference/language-specification/lexical-grammar#identifiers + ///
+ /// Identifier to validate and modify + /// Modified or unchanged if no changes are needed + private string GetValidIdentifier(string name) + { + var codeDomProvider = (this.context.TargetLanguage == LanguageOption.CSharp ? CSharpProvider : VBProvider); + if (codeDomProvider.IsValidIdentifier(name)) + { + return name; + } + + var segments = RegexMatchNonIdentifier.Split(name); + var validName = new StringBuilder(); + var isFirst = true; + foreach (var segment in segments.Where(token => !string.IsNullOrWhiteSpace(token))) + { + if (isFirst && !this.context.EnableNamingAlias) + { + validName.Append(segment); + } + else + { + var titleCaseSegment = Customization.CustomizeNaming(segment); + validName.Append(titleCaseSegment); + } + + isFirst = false; + } + + if (validName.Length < 1) + { + throw new Exception($"Failed to make valid identifier for '{name}'"); + } + + if (!char.IsLetter(validName[0]) && validName[0] != DefaultPrefixCharacter) + { + validName.Insert(0, DefaultPrefixCharacter); + } + + return validName.ToString(); + } + internal void WriteTypeStaticCreateMethod(string typeName, IEdmStructuredType structuredType) { Debug.Assert(structuredType != null, "structuredType != null"); @@ -2918,7 +3093,7 @@ public abstract class ODataClientTemplate : TemplateBase }); } - // Private name should not confict with field name + // Private name should not conflict with field name UniqueIdentifierService uniqueIdentifierService = new UniqueIdentifierService(propertyInfos.Select(_ => _.FixedPropertyName), this.context.TargetLanguage == LanguageOption.CSharp); @@ -4473,13 +4648,13 @@ namespace <#= fullNamespace #> if (!global::Microsoft.OData.Edm.Csdl.CsdlReader.TryParse(reader, <#= this.context.IgnoreUnexpectedElementsAndAttributes ? "true" : "false" #>, out edmModel, out errors)) { - global::System.Text.StringBuilder errorMessages = new global::System.Text.StringBuilder(); - foreach (var error in errors) - { - errorMessages.Append(error.ErrorMessage); - errorMessages.Append("; "); - } - throw new global::System.InvalidOperationException(errorMessages.ToString()); + global::System.Text.StringBuilder errorMessages = new global::System.Text.StringBuilder(); + foreach (var error in errors) + { + errorMessages.Append(error.ErrorMessage); + errorMessages.Append("; "); + } + throw new global::System.InvalidOperationException(errorMessages.ToString()); } return edmModel; @@ -4655,7 +4830,6 @@ namespace <#= fullNamespace #> WriteObsoleteAttribute(propertyOptions.RevisionAnnotations); #> [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "<#=T4Version#>")] - <#+ if (this.context.EnableNamingAlias || IdentifierMappings.ContainsKey(propertyOptions.OriginalPropertyName)) { @@ -4674,7 +4848,6 @@ namespace <#= fullNamespace #> WriteRequiredAttribute($"{propertyOptions.PropertyName} is required."); } #> - <#+ if (!string.IsNullOrEmpty(propertyOptions.PropertyAttribute)) { @@ -4854,7 +5027,7 @@ namespace <#= fullNamespace #> <#+ } #> - public virtual <#= hideBaseMethod ? this.OverloadsModifier : string.Empty #> <#= isReturnEntity ? returnTypeNameWithSingleSuffix : string.Format(CultureInfo.InvariantCulture, this.DataServiceQuerySingleStructureTemplate, returnTypeName) #> <#= functionName #>(<#= parameters #><#= useEntityReference ? ", bool useEntityReference = false" : string.Empty #>) + public virtual <#= hideBaseMethod ? $"{this.OverloadsModifier} " : string.Empty #><#= isReturnEntity ? returnTypeNameWithSingleSuffix : string.Format(CultureInfo.InvariantCulture, this.DataServiceQuerySingleStructureTemplate, returnTypeName) #> <#= functionName #>(<#= parameters #><#= useEntityReference ? ", bool useEntityReference = false" : string.Empty #>) { global::System.Uri requestUri; Context.TryGetUri(this, out requestUri); diff --git a/src/ODataConnectedService_VS2022Plus/ODataConnectedService_VS2022Plus.csproj b/src/ODataConnectedService_VS2022Plus/ODataConnectedService_VS2022Plus.csproj index 6ecfc8ec..3ad368c3 100644 --- a/src/ODataConnectedService_VS2022Plus/ODataConnectedService_VS2022Plus.csproj +++ b/src/ODataConnectedService_VS2022Plus/ODataConnectedService_VS2022Plus.csproj @@ -104,7 +104,7 @@ all - 17.9.1 + 17.6.0 diff --git a/test/ODataConnectedService.Tests/CodeGenReferences/DupNamesWithCamelCase.vb b/test/ODataConnectedService.Tests/CodeGenReferences/DupNamesWithCamelCase.vb index d93fa28a..97f371f5 100644 --- a/test/ODataConnectedService.Tests/CodeGenReferences/DupNamesWithCamelCase.vb +++ b/test/ODataConnectedService.Tests/CodeGenReferences/DupNamesWithCamelCase.vb @@ -27,6 +27,7 @@ Namespace DupNames Public Sub New(ByVal serviceRoot As Global.System.Uri) Me.New(serviceRoot, Global.Microsoft.OData.Client.ODataProtocolVersion.V4) End Sub + ''' ''' Initialize a new EntityContainer object. ''' @@ -221,23 +222,23 @@ Namespace DupNames MyBase.New(query) End Sub ''' - ''' There are no comments for DupPropertyName in the schema. + ''' There are no comments for DupPropertyName1 in the schema. ''' _ _ - Public Overridable ReadOnly Property DupPropertyName() As Global.Microsoft.OData.Client.DataServiceQuery(Of DupNames.DupWithTypeName1) + Public Overridable ReadOnly Property DupPropertyName1() As Global.Microsoft.OData.Client.DataServiceQuery(Of DupNames.DupWithTypeName1) Get If Not Me.IsComposable Then Throw New Global.System.NotSupportedException("The previous function is not composable.") End If - If (Me._DupPropertyName Is Nothing) Then - Me._DupPropertyName = Context.CreateQuery(Of DupNames.DupWithTypeName1)(GetPath("dupPropertyName")) + If (Me._DupPropertyName1 Is Nothing) Then + Me._DupPropertyName1 = Context.CreateQuery(Of DupNames.DupWithTypeName1)(GetPath("dupPropertyName")) End If - Return Me._DupPropertyName + Return Me._DupPropertyName1 End Get End Property _ - Private _DupPropertyName As Global.Microsoft.OData.Client.DataServiceQuery(Of DupNames.DupWithTypeName1) + Private _DupPropertyName1 As Global.Microsoft.OData.Client.DataServiceQuery(Of DupNames.DupWithTypeName1) End Class ''' ''' There are no comments for DupWithTypeName in the schema. diff --git a/test/ODataConnectedService.Tests/CodeGenReferences/DupNamesWithCamelCaseDSC.cs b/test/ODataConnectedService.Tests/CodeGenReferences/DupNamesWithCamelCaseDSC.cs index c1ac410f..234cacc1 100644 --- a/test/ODataConnectedService.Tests/CodeGenReferences/DupNamesWithCamelCaseDSC.cs +++ b/test/ODataConnectedService.Tests/CodeGenReferences/DupNamesWithCamelCaseDSC.cs @@ -25,6 +25,7 @@ public EntityContainer(global::System.Uri serviceRoot) : this(serviceRoot, global::Microsoft.OData.Client.ODataProtocolVersion.V4) { } + /// /// Initialize a new EntityContainer object. /// @@ -225,26 +226,26 @@ public partial class DupWithTypeNameSingle : global::Microsoft.OData.Client.Data /// Initialize a new DupWithTypeNameSingle object. /// public DupWithTypeNameSingle(global::Microsoft.OData.Client.DataServiceContext context, string path) - : base(context, path) { } + : base(context, path) {} /// /// Initialize a new DupWithTypeNameSingle object. /// public DupWithTypeNameSingle(global::Microsoft.OData.Client.DataServiceContext context, string path, bool isComposable) - : base(context, path, isComposable) { } + : base(context, path, isComposable) {} /// /// Initialize a new DupWithTypeNameSingle object. /// public DupWithTypeNameSingle(global::Microsoft.OData.Client.DataServiceQuerySingle query) - : base(query) { } + : base(query) {} /// - /// There are no comments for DupPropertyName in the schema. + /// There are no comments for DupPropertyName1 in the schema. /// [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "2.4.0")] [global::Microsoft.OData.Client.OriginalNameAttribute("dupPropertyName")] - public virtual global::Microsoft.OData.Client.DataServiceQuery DupPropertyName + public virtual global::Microsoft.OData.Client.DataServiceQuery DupPropertyName1 { get { @@ -252,15 +253,15 @@ public DupWithTypeNameSingle(global::Microsoft.OData.Client.DataServiceQuerySing { throw new global::System.NotSupportedException("The previous function is not composable."); } - if ((this._DupPropertyName == null)) + if ((this._DupPropertyName1 == null)) { - this._DupPropertyName = Context.CreateQuery(GetPath("dupPropertyName")); + this._DupPropertyName1 = Context.CreateQuery(GetPath("dupPropertyName")); } - return this._DupPropertyName; + return this._DupPropertyName1; } } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "2.4.0")] - private global::Microsoft.OData.Client.DataServiceQuery _DupPropertyName; + private global::Microsoft.OData.Client.DataServiceQuery _DupPropertyName1; } /// /// There are no comments for DupWithTypeName in the schema. @@ -652,19 +653,19 @@ public partial class DupWithTypeName1Single : global::Microsoft.OData.Client.Dat /// Initialize a new DupWithTypeName1Single object. /// public DupWithTypeName1Single(global::Microsoft.OData.Client.DataServiceContext context, string path) - : base(context, path) { } + : base(context, path) {} /// /// Initialize a new DupWithTypeName1Single object. /// public DupWithTypeName1Single(global::Microsoft.OData.Client.DataServiceContext context, string path, bool isComposable) - : base(context, path, isComposable) { } + : base(context, path, isComposable) {} /// /// Initialize a new DupWithTypeName1Single object. /// public DupWithTypeName1Single(global::Microsoft.OData.Client.DataServiceQuerySingle query) - : base(query) { } + : base(query) {} } /// diff --git a/test/ODataConnectedService.Tests/CodeGenReferences/KeywordsAsNames.cs b/test/ODataConnectedService.Tests/CodeGenReferences/KeywordsAsNames.cs index c1745077..4a38d982 100644 --- a/test/ODataConnectedService.Tests/CodeGenReferences/KeywordsAsNames.cs +++ b/test/ODataConnectedService.Tests/CodeGenReferences/KeywordsAsNames.cs @@ -24,6 +24,7 @@ public New(global::System.Uri serviceRoot) : this(serviceRoot, global::Microsoft.OData.Client.ODataProtocolVersion.V4) { } + /// /// Initialize a new New object. /// @@ -168,25 +169,25 @@ public partial class eventSingle : global::Microsoft.OData.Client.DataServiceQue /// Initialize a new eventSingle object. /// public eventSingle(global::Microsoft.OData.Client.DataServiceContext context, string path) - : base(context, path) { } + : base(context, path) {} /// /// Initialize a new eventSingle object. /// public eventSingle(global::Microsoft.OData.Client.DataServiceContext context, string path, bool isComposable) - : base(context, path, isComposable) { } + : base(context, path, isComposable) {} /// /// Initialize a new eventSingle object. /// public eventSingle(global::Microsoft.OData.Client.DataServiceQuerySingle<@event> query) - : base(query) { } + : base(query) {} /// - /// There are no comments for event in the schema. + /// There are no comments for event1 in the schema. /// [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "2.4.0")] - public virtual global::Namespace1.eventSingle @event + public virtual global::Namespace1.eventSingle event1 { get { @@ -194,15 +195,15 @@ public eventSingle(global::Microsoft.OData.Client.DataServiceQuerySingle<@event> { throw new global::System.NotSupportedException("The previous function is not composable."); } - if ((this._event == null)) + if ((this._event1 == null)) { - this._event = new global::Namespace1.eventSingle(this.Context, GetPath("event")); + this._event1 = new global::Namespace1.eventSingle(this.Context, GetPath("event")); } - return this._event; + return this._event1; } } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "2.4.0")] - private global::Namespace1.eventSingle _event; + private global::Namespace1.eventSingle _event1; } /// /// There are no comments for event in the schema. diff --git a/test/ODataConnectedService.Tests/ODataConnectedService.Tests.csproj b/test/ODataConnectedService.Tests/ODataConnectedService.Tests.csproj index 96f33429..9a534e01 100644 --- a/test/ODataConnectedService.Tests/ODataConnectedService.Tests.csproj +++ b/test/ODataConnectedService.Tests/ODataConnectedService.Tests.csproj @@ -128,7 +128,7 @@ 1.3.2 - 17.9.1 + 17.6.0 4.5.0 diff --git a/test/ODataConnectedService.Tests/Templates/ODataClientTemplateUnitTests.cs b/test/ODataConnectedService.Tests/Templates/ODataClientTemplateUnitTests.cs index 971e291e..a299d2b7 100644 --- a/test/ODataConnectedService.Tests/Templates/ODataClientTemplateUnitTests.cs +++ b/test/ODataConnectedService.Tests/Templates/ODataClientTemplateUnitTests.cs @@ -10,9 +10,9 @@ using System.Collections.Generic; using System.Linq; using FluentAssertions; +using Microsoft.OData.CodeGen.Templates; using Microsoft.OData.Edm; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Microsoft.OData.CodeGen.Templates; namespace ODataConnectedService.Tests { @@ -335,12 +335,12 @@ internal override HashSet LanguageKeywords return new HashSet(StringComparer.Ordinal) { "abstract", "as", "base", "byte", "bool", "break", "case", "catch", "char", "checked", "class", "const", "continue", - "decimal", "default", "delegate", "do", "double", "else", "enum", "event", "explicit", "extern", "false", "for", - "foreach", "finally", "fixed", "float", "goto", "if", "implicit", "in", "int", "interface", "internal", "is", "lock", - "long", "namespace", "new", "null", "object", "operator", "out", "override", "params", "private", "protected", "public", - "readonly", "ref", "return", "sbyte", "sealed", "string", "short", "sizeof", "stackalloc", "static", "struct", "switch", - "this", "throw", "true", "try", "typeof", "uint", "ulong", "unchecked", "unsafe", "ushort", "using", "virtual", "volatile", - "void", "while" + "decimal", "default", "delegate", "do", "double", "else", "enum", "event", "explicit", "extern", "false", "for", + "foreach", "finally", "fixed", "float", "goto", "if", "implicit", "in", "int", "interface", "internal", "is", "lock", + "long", "namespace", "new", "null", "object", "operator", "out", "override", "params", "private", "protected", "public", + "readonly", "ref", "return", "sbyte", "sealed", "string", "short", "sizeof", "stackalloc", "static", "struct", "switch", + "this", "throw", "true", "try", "typeof", "uint", "ulong", "unchecked", "unsafe", "ushort", "using", "virtual", "volatile", + "void", "while" }; } } @@ -372,7 +372,9 @@ internal override string UriOperationParameterConstructor internal override string UriEntityOperationParameterConstructor { - get { return "new global::Microsoft.OData.Client.UriEntityOperationParameter(\"{0}\", {1}, {2})"; + get + { + return "new global::Microsoft.OData.Client.UriEntityOperationParameter(\"{0}\", {1}, {2})"; } } @@ -2645,7 +2647,7 @@ public void WriteDupNames() Context = new ODataT4CodeGenerator.CodeGenerationContext(DupNamesEdmx, namespacePrefix); var template = new ODataClientTemplateImp(Context); var complexType = Context.GetSchemaElements("Namespace1").OfType().First(); - template.SetPropertyIdentifierMappingsIfNameConflicts(complexType.Name, complexType); + template.SetPropertyIdentifierMappings(complexType.Name, complexType); template.WritePropertiesForStructuredType(complexType); Type propertyOptionsType = typeof(ODataT4CodeGenerator.ODataClientTemplate.PropertyOptions); @@ -2727,7 +2729,7 @@ public void WriteDupNamesWithCamelCase() Context.EnableNamingAlias = true; var template = new ODataClientTemplateImp(Context); var complexType = Context.GetSchemaElements("Namespace1").OfType().First(); - template.SetPropertyIdentifierMappingsIfNameConflicts(complexType.Name, complexType); + template.SetPropertyIdentifierMappings(complexType.Name, complexType); template.WritePropertiesForStructuredType(complexType); Type propertyOptionsType = typeof(ODataT4CodeGenerator.ODataClientTemplate.PropertyOptions); @@ -2803,6 +2805,216 @@ public void WriteDupNamesWithCamelCase() #endregion + #region Tests for invalid identifiers + + private const string InvalidNamesEdmx = @" + + + + + + + + + + + + +"; + + [TestMethod] + public void WriteInvalidNames() + { + var namespacePrefix = string.Empty; + Context = new ODataT4CodeGenerator.CodeGenerationContext(InvalidNamesEdmx, namespacePrefix); + var template = new ODataClientTemplateImp(Context); + var complexType = Context.GetSchemaElements("Namespace1").OfType().First(); + template.SetPropertyIdentifierMappings(complexType.Name, complexType); + template.WritePropertiesForStructuredType(complexType); + + Type propertyOptionsType = typeof(ODataT4CodeGenerator.ODataClientTemplate.PropertyOptions); + var expectedActions = new List + { + $"WritePropertyForStructuredType({propertyOptionsType})", + $"WritePropertyForStructuredType({propertyOptionsType})", + $"WritePropertyForStructuredType({propertyOptionsType})", + $"WritePropertyForStructuredType({propertyOptionsType})" + }; + template.CalledActions.Should().Contain(expectedActions); + + var expectedUsedPropertyOptions = new List { + new ODataT4CodeGenerator.ODataClientTemplate.PropertyOptions { + PropertyType = "String", + OriginalPropertyName = "name with Spaces", + PropertyName = "nameWithSpaces", + FixedPropertyName = "nameWithSpaces", + PrivatePropertyName = "_nameWithSpaces", + PropertyInitializationValue = null, + PropertyAttribute = "", + PropertyDescription = null, + PropertyMaxLength = null, + WriteOnPropertyChanged = false, + IsNullable = false, + RevisionAnnotations = new ConcurrentDictionary() + }, + new ODataT4CodeGenerator.ODataClientTemplate.PropertyOptions { + PropertyType = "String", + OriginalPropertyName = "Invalid$characters", + PropertyName = "InvalidCharacters", + FixedPropertyName = "InvalidCharacters", + PrivatePropertyName = "_InvalidCharacters", + PropertyInitializationValue = null, + PropertyAttribute = "", + PropertyDescription = null, + PropertyMaxLength = null, + WriteOnPropertyChanged = false, + IsNullable = false, + RevisionAnnotations = new ConcurrentDictionary() + }, + new ODataT4CodeGenerator.ODataClientTemplate.PropertyOptions { + PropertyType = "String", + OriginalPropertyName = "invalid-characters", + PropertyName = "invalidCharacters", + FixedPropertyName = "invalidCharacters", + PrivatePropertyName = "_invalidCharacters", + PropertyInitializationValue = null, + PropertyAttribute = "", + PropertyDescription = null, + PropertyMaxLength = null, + WriteOnPropertyChanged = false, + IsNullable = false, + RevisionAnnotations = new ConcurrentDictionary() + }, + new ODataT4CodeGenerator.ODataClientTemplate.PropertyOptions { + PropertyType = "String", + OriginalPropertyName = "Context", + PropertyName = "ComplexTypeContext", + FixedPropertyName = "ComplexTypeContext", + PrivatePropertyName = "_ComplexTypeContext", + PropertyInitializationValue = null, + PropertyAttribute = "", + PropertyDescription = null, + PropertyMaxLength = null, + WriteOnPropertyChanged = false, + IsNullable = false, + RevisionAnnotations = new ConcurrentDictionary() + }, + new ODataT4CodeGenerator.ODataClientTemplate.PropertyOptions { + PropertyType = "String", + OriginalPropertyName = "1numericName", + PropertyName = "_1numericName", + FixedPropertyName = "_1numericName", + PrivatePropertyName = "__1numericName", + PropertyInitializationValue = null, + PropertyAttribute = "", + PropertyDescription = null, + PropertyMaxLength = null, + WriteOnPropertyChanged = false, + IsNullable = false, + RevisionAnnotations = new ConcurrentDictionary() + }, + }; + template.UsedPropertyOptions.Should().Equal(expectedUsedPropertyOptions); + } + + [TestMethod] + public void WriteInvalidNamesWithCamelCase() + { + var namespacePrefix = string.Empty; + Context = new ODataT4CodeGenerator.CodeGenerationContext(InvalidNamesEdmx, namespacePrefix); + Context.EnableNamingAlias = true; + var template = new ODataClientTemplateImp(Context); + var complexType = Context.GetSchemaElements("Namespace1").OfType().First(); + template.SetPropertyIdentifierMappings(complexType.Name, complexType); + template.WritePropertiesForStructuredType(complexType); + + Type propertyOptionsType = typeof(ODataT4CodeGenerator.ODataClientTemplate.PropertyOptions); + var expectedActions = new List + { + $"WritePropertyForStructuredType({propertyOptionsType})", + $"WritePropertyForStructuredType({propertyOptionsType})", + $"WritePropertyForStructuredType({propertyOptionsType})", + $"WritePropertyForStructuredType({propertyOptionsType})" + }; + template.CalledActions.Should().Contain(expectedActions); + + var expectedUsedPropertyOptions = new List { + new ODataT4CodeGenerator.ODataClientTemplate.PropertyOptions { + PropertyType = "String", + OriginalPropertyName = "name with Spaces", + PropertyName = "NameWithSpaces", + FixedPropertyName = "NameWithSpaces", + PrivatePropertyName = "_NameWithSpaces", + PropertyInitializationValue = null, + PropertyAttribute = "", + PropertyDescription = null, + PropertyMaxLength = null, + WriteOnPropertyChanged = false, + IsNullable = false, + RevisionAnnotations = new ConcurrentDictionary() + }, + new ODataT4CodeGenerator.ODataClientTemplate.PropertyOptions { + PropertyType = "String", + OriginalPropertyName = "Invalid$characters", + PropertyName = "InvalidCharacters", + FixedPropertyName = "InvalidCharacters", + PrivatePropertyName = "_InvalidCharacters", + PropertyInitializationValue = null, + PropertyAttribute = "", + PropertyDescription = null, + PropertyMaxLength = null, + WriteOnPropertyChanged = false, + IsNullable = false, + RevisionAnnotations = new ConcurrentDictionary() + }, + new ODataT4CodeGenerator.ODataClientTemplate.PropertyOptions { + PropertyType = "String", + OriginalPropertyName = "invalid-characters", + PropertyName = "InvalidCharacters1", + FixedPropertyName = "InvalidCharacters1", + PrivatePropertyName = "_InvalidCharacters1", + PropertyInitializationValue = null, + PropertyAttribute = "", + PropertyDescription = null, + PropertyMaxLength = null, + WriteOnPropertyChanged = false, + IsNullable = false, + RevisionAnnotations = new ConcurrentDictionary() + }, + new ODataT4CodeGenerator.ODataClientTemplate.PropertyOptions { + PropertyType = "String", + OriginalPropertyName = "Context", + PropertyName = "ComplexTypeContext", + FixedPropertyName = "ComplexTypeContext", + PrivatePropertyName = "_ComplexTypeContext", + PropertyInitializationValue = null, + PropertyAttribute = "", + PropertyDescription = null, + PropertyMaxLength = null, + WriteOnPropertyChanged = false, + IsNullable = false, + RevisionAnnotations = new ConcurrentDictionary() + }, + new ODataT4CodeGenerator.ODataClientTemplate.PropertyOptions { + PropertyType = "String", + OriginalPropertyName = "1numericName", + PropertyName = "_1numericName", + FixedPropertyName = "_1numericName", + PrivatePropertyName = "__1numericName", + PropertyInitializationValue = null, + PropertyAttribute = "", + PropertyDescription = null, + PropertyMaxLength = null, + WriteOnPropertyChanged = false, + IsNullable = false, + RevisionAnnotations = new ConcurrentDictionary() + }, + }; + template.UsedPropertyOptions.Should().Equal(expectedUsedPropertyOptions); + } + + #endregion Tests for invalid identifiers + [TestMethod] public void GetFixedNameShouldReadNonKeywords() {