1 //---------------------------------------------------------------------
2 // <copyright file="TypeResolver.cs" company="Microsoft">
3 //      Copyright (c) Microsoft Corporation.  All rights reserved.
4 // </copyright>
5 //
6 // @owner  Microsoft
7 // @backupOwner Microsoft
8 //---------------------------------------------------------------------
9 
10 namespace System.Data.Common.EntitySql
11 {
12     using System;
13     using System.Collections.Generic;
14     using System.Data.Entity;
15     using System.Data.Metadata.Edm;
16     using System.Diagnostics;
17     using System.Linq;
18 
19     /// <summary>
20     /// Represents eSQL metadata member expression class.
21     /// </summary>
22     internal enum MetadataMemberClass
23     {
24         Type,
25         FunctionGroup,
26         InlineFunctionGroup,
27         Namespace,
28         EnumMember
29     }
30 
31     /// <summary>
32     /// Abstract class representing an eSQL expression classified as <see cref="ExpressionResolutionClass.MetadataMember"/>.
33     /// </summary>
34     internal abstract class MetadataMember : ExpressionResolution
35     {
36         protected MetadataMember(MetadataMemberClass @class, string name)
base(ExpressionResolutionClass.MetadataMember)37             : base(ExpressionResolutionClass.MetadataMember)
38         {
39             Debug.Assert(!String.IsNullOrEmpty(name), "name must not be empty");
40 
41             MetadataMemberClass = @class;
42             Name = name;
43         }
44 
45         internal override string ExpressionClassName { get { return MetadataMemberExpressionClassName; } }
46         internal static string MetadataMemberExpressionClassName { get { return Strings.LocalizedMetadataMemberExpression; } }
47 
48         internal readonly MetadataMemberClass MetadataMemberClass;
49         internal readonly string Name;
50         /// <summary>
51         /// Return the name of the <see cref="MetadataMemberClass"/> for error messages.
52         /// </summary>
53         internal abstract string MetadataMemberClassName { get; }
54 
CreateMetadataMemberNameEqualityComparer(StringComparer stringComparer)55         internal static IEqualityComparer<MetadataMember> CreateMetadataMemberNameEqualityComparer(StringComparer stringComparer)
56         {
57             return new MetadataMemberNameEqualityComparer(stringComparer);
58         }
59 
60         private sealed class MetadataMemberNameEqualityComparer : IEqualityComparer<MetadataMember>
61         {
62             private readonly StringComparer _stringComparer;
63 
MetadataMemberNameEqualityComparer(StringComparer stringComparer)64             internal MetadataMemberNameEqualityComparer(StringComparer stringComparer)
65             {
66                 _stringComparer = stringComparer;
67             }
68 
Equals(MetadataMember x, MetadataMember y)69             bool IEqualityComparer<MetadataMember>.Equals(MetadataMember x, MetadataMember y)
70             {
71                 Debug.Assert(x != null && y != null, "metadata members must not be null");
72                 return _stringComparer.Equals(x.Name, y.Name);
73             }
74 
GetHashCode(MetadataMember obj)75             int IEqualityComparer<MetadataMember>.GetHashCode(MetadataMember obj)
76             {
77                 Debug.Assert(obj != null, "metadata member must not be null");
78                 return _stringComparer.GetHashCode(obj.Name);
79             }
80         }
81     }
82 
83     /// <summary>
84     /// Represents an eSQL metadata member expression classified as <see cref="MetadataMemberClass.Namespace"/>.
85     /// </summary>
86     internal sealed class MetadataNamespace : MetadataMember
87     {
MetadataNamespace(string name)88         internal MetadataNamespace(string name) : base(MetadataMemberClass.Namespace, name) { }
89 
90         internal override string MetadataMemberClassName { get { return NamespaceClassName; } }
91         internal static string NamespaceClassName { get { return Strings.LocalizedNamespace; } }
92     }
93 
94     /// <summary>
95     /// Represents an eSQL metadata member expression classified as <see cref="MetadataMemberClass.Type"/>.
96     /// </summary>
97     internal sealed class MetadataType : MetadataMember
98     {
MetadataType(string name, TypeUsage typeUsage)99         internal MetadataType(string name, TypeUsage typeUsage)
100             : base(MetadataMemberClass.Type, name)
101         {
102             Debug.Assert(typeUsage != null, "typeUsage must not be null");
103             TypeUsage = typeUsage;
104         }
105 
106         internal override string MetadataMemberClassName { get { return TypeClassName; } }
107         internal static string TypeClassName { get { return Strings.LocalizedType; } }
108 
109         internal readonly TypeUsage TypeUsage;
110     }
111 
112     /// <summary>
113     /// Represents an eSQL metadata member expression classified as <see cref="MetadataMemberClass.EnumMember"/>.
114     /// </summary>
115     internal sealed class MetadataEnumMember : MetadataMember
116     {
MetadataEnumMember(string name, TypeUsage enumType, EnumMember enumMember)117         internal MetadataEnumMember(string name, TypeUsage enumType, EnumMember enumMember)
118             : base(MetadataMemberClass.EnumMember, name)
119         {
120             Debug.Assert(enumType != null, "enumType must not be null");
121             Debug.Assert(enumMember != null, "enumMember must not be null");
122             EnumType = enumType;
123             EnumMember = enumMember;
124         }
125 
126         internal override string MetadataMemberClassName { get { return EnumMemberClassName; } }
127         internal static string EnumMemberClassName { get { return Strings.LocalizedEnumMember; } }
128 
129         internal readonly TypeUsage EnumType;
130         internal readonly EnumMember EnumMember;
131     }
132 
133     /// <summary>
134     /// Represents an eSQL metadata member expression classified as <see cref="MetadataMemberClass.FunctionGroup"/>.
135     /// </summary>
136     internal sealed class MetadataFunctionGroup : MetadataMember
137     {
MetadataFunctionGroup(string name, IList<EdmFunction> functionMetadata)138         internal MetadataFunctionGroup(string name, IList<EdmFunction> functionMetadata)
139             : base(MetadataMemberClass.FunctionGroup, name)
140         {
141             Debug.Assert(functionMetadata != null && functionMetadata.Count > 0, "FunctionMetadata must not be null or empty");
142             FunctionMetadata = functionMetadata;
143         }
144 
145         internal override string MetadataMemberClassName { get { return FunctionGroupClassName; } }
146         internal static string FunctionGroupClassName { get { return Strings.LocalizedFunction; } }
147 
148         internal readonly IList<EdmFunction> FunctionMetadata;
149     }
150 
151     /// <summary>
152     /// Represents an eSQL metadata member expression classified as <see cref="MetadataMemberClass.InlineFunctionGroup"/>.
153     /// </summary>
154     internal sealed class InlineFunctionGroup : MetadataMember
155     {
InlineFunctionGroup(string name, IList<InlineFunctionInfo> functionMetadata)156         internal InlineFunctionGroup(string name, IList<InlineFunctionInfo> functionMetadata)
157             : base(MetadataMemberClass.InlineFunctionGroup, name)
158         {
159             Debug.Assert(functionMetadata != null && functionMetadata.Count > 0, "FunctionMetadata must not be null or empty");
160             FunctionMetadata = functionMetadata;
161         }
162 
163         internal override string MetadataMemberClassName { get { return InlineFunctionGroupClassName; } }
164         internal static string InlineFunctionGroupClassName { get { return Strings.LocalizedInlineFunction; } }
165 
166         internal readonly IList<InlineFunctionInfo> FunctionMetadata;
167     }
168 
169     /// <summary>
170     /// Represents eSQL type and namespace name resolver.
171     /// </summary>
172     internal sealed class TypeResolver
173     {
174         private readonly Perspective _perspective;
175         private readonly ParserOptions _parserOptions;
176         private readonly Dictionary<string, MetadataNamespace> _aliasedNamespaces;
177         private readonly HashSet<MetadataNamespace> _namespaces;
178         /// <summary>
179         /// name -> list(overload)
180         /// </summary>
181         private readonly Dictionary<string, List<InlineFunctionInfo>> _functionDefinitions;
182         private bool _includeInlineFunctions;
183         private bool _resolveLeftMostUnqualifiedNameAsNamespaceOnly;
184 
185         /// <summary>
186         /// Initializes TypeResolver instance
187         /// </summary>
TypeResolver(Perspective perspective, ParserOptions parserOptions)188         internal TypeResolver(Perspective perspective, ParserOptions parserOptions)
189         {
190             EntityUtil.CheckArgumentNull(perspective, "perspective");
191 
192             _perspective = perspective;
193             _parserOptions = parserOptions;
194             _aliasedNamespaces = new Dictionary<string, MetadataNamespace>(parserOptions.NameComparer);
195             _namespaces = new HashSet<MetadataNamespace>(MetadataMember.CreateMetadataMemberNameEqualityComparer(parserOptions.NameComparer));
196             _functionDefinitions = new Dictionary<string, List<InlineFunctionInfo>>(parserOptions.NameComparer);
197             _includeInlineFunctions = true;
198             _resolveLeftMostUnqualifiedNameAsNamespaceOnly = false;
199         }
200 
201         /// <summary>
202         /// Returns perspective.
203         /// </summary>
204         internal Perspective Perspective
205         {
206             get { return _perspective; }
207         }
208 
209         /// <summary>
210         /// Returns namespace imports.
211         /// </summary>
212         internal ICollection<MetadataNamespace> NamespaceImports
213         {
214             get { return _namespaces; }
215         }
216 
217         /// <summary>
218         /// Returns <see cref="TypeUsage"/> for <see cref="PrimitiveTypeKind.String"/>.
219         /// </summary>
220         internal TypeUsage StringType
221         {
222             get { return _perspective.MetadataWorkspace.GetCanonicalModelTypeUsage(PrimitiveTypeKind.String); }
223         }
224 
225         /// <summary>
226         /// Returns <see cref="TypeUsage"/> for <see cref="PrimitiveTypeKind.Boolean"/>.
227         /// </summary>
228         internal TypeUsage BooleanType
229         {
230             get { return _perspective.MetadataWorkspace.GetCanonicalModelTypeUsage(PrimitiveTypeKind.Boolean); }
231         }
232 
233         /// <summary>
234         /// Returns <see cref="TypeUsage"/> for <see cref="PrimitiveTypeKind.Int64"/>.
235         /// </summary>
236         internal TypeUsage Int64Type
237         {
238             get { return _perspective.MetadataWorkspace.GetCanonicalModelTypeUsage(PrimitiveTypeKind.Int64); }
239         }
240 
241         /// <summary>
242         /// Adds an aliased namespace import.
243         /// </summary>
AddAliasedNamespaceImport(string alias, MetadataNamespace @namespace, ErrorContext errCtx)244         internal void AddAliasedNamespaceImport(string alias, MetadataNamespace @namespace, ErrorContext errCtx)
245         {
246             if (_aliasedNamespaces.ContainsKey(alias))
247             {
248                 throw EntityUtil.EntitySqlError(errCtx, Strings.NamespaceAliasAlreadyUsed(alias));
249             }
250 
251             _aliasedNamespaces.Add(alias, @namespace);
252         }
253 
254         /// <summary>
255         /// Adds a non-aliased namespace import.
256         /// </summary>
257         internal void AddNamespaceImport(MetadataNamespace @namespace, ErrorContext errCtx)
258         {
259             if (_namespaces.Contains(@namespace))
260             {
261                 throw EntityUtil.EntitySqlError(errCtx, Strings.NamespaceAlreadyImported(@namespace.Name));
262             }
263 
264             _namespaces.Add(@namespace);
265         }
266 
267         #region Inline function declarations
268         /// <summary>
269         /// Declares inline function in the query local metadata.
270         /// </summary>
DeclareInlineFunction(string name, InlineFunctionInfo functionInfo)271         internal void DeclareInlineFunction(string name, InlineFunctionInfo functionInfo)
272         {
273             Debug.Assert(!String.IsNullOrEmpty(name), "name must not be null or empty");
274             Debug.Assert(functionInfo != null, "functionInfo != null");
275 
276             List<InlineFunctionInfo> overloads;
277             if (!_functionDefinitions.TryGetValue(name, out overloads))
278             {
279                 overloads = new List<InlineFunctionInfo>();
280                 _functionDefinitions.Add(name, overloads);
281             }
282 
283             //
284             // Check overload uniqueness.
285             //
286             if (overloads.Exists(overload =>
287                 overload.Parameters.Select(p => p.ResultType).SequenceEqual(functionInfo.Parameters.Select(p => p.ResultType), TypeUsageStructuralComparer.Instance)))
288             {
289                 throw EntityUtil.EntitySqlError(functionInfo.FunctionDefAst.ErrCtx, Strings.DuplicatedInlineFunctionOverload(name));
290             }
291 
292             overloads.Add(functionInfo);
293         }
294 
295         private sealed class TypeUsageStructuralComparer : IEqualityComparer<TypeUsage>
296         {
297             internal static readonly TypeUsageStructuralComparer Instance = new TypeUsageStructuralComparer();
298 
TypeUsageStructuralComparer()299             private TypeUsageStructuralComparer() { }
300 
Equals(TypeUsage x, TypeUsage y)301             public bool Equals(TypeUsage x, TypeUsage y)
302             {
303                 return TypeSemantics.IsStructurallyEqual(x, y);
304             }
305 
GetHashCode(TypeUsage obj)306             public int GetHashCode(TypeUsage obj)
307             {
308                 Debug.Fail("Not implemented");
309                 return 0;
310             }
311         }
312         #endregion
313 
EnterFunctionNameResolution(bool includeInlineFunctions)314         internal IDisposable EnterFunctionNameResolution(bool includeInlineFunctions)
315         {
316             bool savedIncludeInlineFunctions = _includeInlineFunctions;
317             _includeInlineFunctions = includeInlineFunctions;
318             return new Disposer(delegate { this._includeInlineFunctions = savedIncludeInlineFunctions; });
319         }
320 
EnterBackwardCompatibilityResolution()321         internal IDisposable EnterBackwardCompatibilityResolution()
322         {
323             Debug.Assert(!_resolveLeftMostUnqualifiedNameAsNamespaceOnly, "EnterBackwardCompatibilityResolution() is not reentrant.");
324             _resolveLeftMostUnqualifiedNameAsNamespaceOnly = true;
325             return new Disposer(delegate
326             {
327                 Debug.Assert(this._resolveLeftMostUnqualifiedNameAsNamespaceOnly, "_resolveLeftMostUnqualifiedNameAsNamespaceOnly must be true.");
328                 this._resolveLeftMostUnqualifiedNameAsNamespaceOnly = false;
329             });
330         }
331 
ResolveMetadataMemberName(string[] name, ErrorContext errCtx)332         internal MetadataMember ResolveMetadataMemberName(string[] name, ErrorContext errCtx)
333         {
334             Debug.Assert(name != null && name.Length > 0, "name must not be empty");
335 
336             MetadataMember metadataMember;
337             if (name.Length == 1)
338             {
339                 metadataMember = ResolveUnqualifiedName(name[0], false /* partOfQualifiedName */, errCtx);
340             }
341             else
342             {
343                 metadataMember = ResolveFullyQualifiedName(name, name.Length, errCtx);
344             }
345             Debug.Assert(metadataMember != null, "metadata member name resolution must not return null");
346 
347             return metadataMember;
348         }
349 
ResolveMetadataMemberAccess(MetadataMember qualifier, string name, ErrorContext errCtx)350         internal MetadataMember ResolveMetadataMemberAccess(MetadataMember qualifier, string name, ErrorContext errCtx)
351         {
352             string fullName = GetFullName(qualifier.Name, name);
353             if (qualifier.MetadataMemberClass == MetadataMemberClass.Namespace)
354             {
355                 //
356                 // Try resolving as a type.
357                 //
358                 MetadataType type;
359                 if (TryGetTypeFromMetadata(fullName, out type))
360                 {
361                     return type;
362                 }
363 
364                 //
365                 // Try resolving as a function.
366                 //
367                 MetadataFunctionGroup function;
368                 if (TryGetFunctionFromMetadata(qualifier.Name, name, out function))
369                 {
370                     return function;
371                 }
372 
373                 //
374                 // Otherwise, resolve as a namespace.
375                 //
376                 return new MetadataNamespace(fullName);
377             }
378             else if (qualifier.MetadataMemberClass == MetadataMemberClass.Type)
379             {
380                 var type = (MetadataType)qualifier;
381                 if (TypeSemantics.IsEnumerationType(type.TypeUsage))
382                 {
383                     EnumMember member;
384                     if (_perspective.TryGetEnumMember((EnumType)type.TypeUsage.EdmType, name, _parserOptions.NameComparisonCaseInsensitive /*ignoreCase*/, out member))
385                     {
386                         Debug.Assert(member != null, "member != null");
387                         Debug.Assert(_parserOptions.NameComparer.Equals(name, member.Name), "_parserOptions.NameComparer.Equals(name, member.Name)");
388                         return new MetadataEnumMember(fullName, type.TypeUsage, member);
389                     }
390                     else
391                     {
392                         throw EntityUtil.EntitySqlError(errCtx, Strings.NotAMemberOfType(name, qualifier.Name));
393                     }
394                 }
395             }
396 
397             throw EntityUtil.EntitySqlError(errCtx, Strings.InvalidMetadataMemberClassResolution(
398                 qualifier.Name, qualifier.MetadataMemberClassName, MetadataNamespace.NamespaceClassName));
399         }
400 
ResolveUnqualifiedName(string name, bool partOfQualifiedName, ErrorContext errCtx)401         internal MetadataMember ResolveUnqualifiedName(string name, bool partOfQualifiedName, ErrorContext errCtx)
402         {
403             Debug.Assert(!String.IsNullOrEmpty(name), "name must not be empty");
404 
405             //
406             // In the case of Name1.Name2...NameN and if backward compatibility mode is on, then resolve Name1 as namespace only, ignore any other possible resolutions.
407             //
408             bool resolveAsNamespaceOnly = partOfQualifiedName && _resolveLeftMostUnqualifiedNameAsNamespaceOnly;
409 
410             //
411             // In the case of Name1.Name2...NameN, ignore functions while resolving Name1: functions don't have members.
412             //
413             bool includeFunctions = !partOfQualifiedName;
414 
415             //
416             // Try resolving as an inline function.
417             //
418             InlineFunctionGroup inlineFunctionGroup;
419             if (!resolveAsNamespaceOnly &&
420                 includeFunctions && TryGetInlineFunction(name, out inlineFunctionGroup))
421             {
422                 return inlineFunctionGroup;
423             }
424 
425             //
426             // Try resolving as a namespace alias.
427             //
428             MetadataNamespace aliasedNamespaceImport;
429             if (_aliasedNamespaces.TryGetValue(name, out aliasedNamespaceImport))
430             {
431                 return aliasedNamespaceImport;
432             }
433 
434             if (!resolveAsNamespaceOnly)
435             {
436                 //
437                 // Try resolving as a type or functionGroup in the global namespace or as an imported member.
438                 // Throw if ambiguous.
439                 //
440                 MetadataType type = null;
441                 MetadataFunctionGroup functionGroup = null;
442 
443                 if (!TryGetTypeFromMetadata(name, out type))
444                 {
445                     if (includeFunctions)
446                     {
447                         //
448                         // If name looks like a multipart identifier, try resolving it in the global namespace.
449                         // Escaped multipart identifiers usually appear in views: select [NS1.NS2.Product](...) from ...
450                         //
451                         var multipart = name.Split('.');
452                         if (multipart.Length > 1 && multipart.All(p => p.Length > 0))
453                         {
454                             var functionName = multipart[multipart.Length - 1];
455                             var namespaceName = name.Substring(0, name.Length - functionName.Length - 1);
456                             TryGetFunctionFromMetadata(namespaceName, functionName, out functionGroup);
457                         }
458                     }
459                 }
460 
461                 //
462                 // Try resolving as an imported member.
463                 //
464                 MetadataNamespace importedMemberNamespace = null;
465                 foreach (MetadataNamespace namespaceImport in _namespaces)
466                 {
467                     string fullName = GetFullName(namespaceImport.Name, name);
468 
469                     MetadataType importedType;
470                     if (TryGetTypeFromMetadata(fullName, out importedType))
471                     {
472                         if (type == null && functionGroup == null)
473                         {
474                             type = importedType;
475                             importedMemberNamespace = namespaceImport;
476                         }
477                         else
478                         {
479                             throw AmbiguousMetadataMemberName(errCtx, name, namespaceImport, importedMemberNamespace);
480                         }
481                     }
482 
483                     MetadataFunctionGroup importedFunctionGroup;
484                     if (includeFunctions && TryGetFunctionFromMetadata(namespaceImport.Name, name, out importedFunctionGroup))
485                     {
486                         if (type == null && functionGroup == null)
487                         {
488                             functionGroup = importedFunctionGroup;
489                             importedMemberNamespace = namespaceImport;
490                         }
491                         else
492                         {
493                             throw AmbiguousMetadataMemberName(errCtx, name, namespaceImport, importedMemberNamespace);
494                         }
495                     }
496                 }
497                 if (type != null)
498                 {
499                     return type;
500                 }
501                 if (functionGroup != null)
502                 {
503                     return functionGroup;
504                 }
505             }
506 
507             //
508             // Otherwise, resolve as a namespace.
509             //
510             return new MetadataNamespace(name);
511         }
512 
ResolveFullyQualifiedName(string[] name, int length, ErrorContext errCtx)513         private MetadataMember ResolveFullyQualifiedName(string[] name, int length, ErrorContext errCtx)
514         {
515             Debug.Assert(name != null && length > 1 && length <= name.Length, "name must not be empty");
516 
517             //
518             // Resolve N in N.R
519             //
520             MetadataMember left;
521             if (length == 2)
522             {
523                 //
524                 // If N is a single name, ignore functions: functions don't have members.
525                 //
526                 left = ResolveUnqualifiedName(name[0], true /* partOfQualifiedName */, errCtx);
527             }
528             else
529             {
530                 left = ResolveFullyQualifiedName(name, length - 1, errCtx);
531             }
532 
533             //
534             // Get R in N.R
535             //
536             string rightName = name[length - 1];
537             Debug.Assert(!String.IsNullOrEmpty(rightName), "rightName must not be empty");
538 
539             //
540             // Resolve R in the context of N
541             //
542             return ResolveMetadataMemberAccess(left, rightName, errCtx);
543         }
544 
AmbiguousMetadataMemberName(ErrorContext errCtx, string name, MetadataNamespace ns1, MetadataNamespace ns2)545         private static Exception AmbiguousMetadataMemberName(ErrorContext errCtx, string name, MetadataNamespace ns1, MetadataNamespace ns2)
546         {
547             throw EntityUtil.EntitySqlError(errCtx, Strings.AmbiguousMetadataMemberName(name, ns1.Name, ns2 != null ? ns2.Name : null));
548         }
549 
550         /// <summary>
551         /// Try get type from the model using the fully qualified name.
552         /// </summary>
TryGetTypeFromMetadata(string typeFullName, out MetadataType type)553         private bool TryGetTypeFromMetadata(string typeFullName, out MetadataType type)
554         {
555             TypeUsage typeUsage;
556             if (_perspective.TryGetTypeByName(typeFullName, _parserOptions.NameComparisonCaseInsensitive /* ignore case */, out typeUsage))
557             {
558                 type = new MetadataType(typeFullName, typeUsage);
559                 return true;
560             }
561             else
562             {
563                 type = null;
564                 return false;
565             }
566         }
567 
568         /// <summary>
569         /// Try get function from the model using the fully qualified name.
570         /// </summary>
TryGetFunctionFromMetadata(string namespaceName, string functionName, out MetadataFunctionGroup functionGroup)571         internal bool TryGetFunctionFromMetadata(string namespaceName, string functionName, out MetadataFunctionGroup functionGroup)
572         {
573             IList<EdmFunction> functionMetadata;
574             if (_perspective.TryGetFunctionByName(namespaceName, functionName, _parserOptions.NameComparisonCaseInsensitive /* ignore case */, out functionMetadata))
575             {
576                 functionGroup = new MetadataFunctionGroup(GetFullName(namespaceName, functionName), functionMetadata);
577                 return true;
578             }
579             else
580             {
581                 functionGroup = null;
582                 return false;
583             }
584         }
585 
586         /// <summary>
587         /// Try get function from the local metadata using the fully qualified name.
588         /// </summary>
TryGetInlineFunction(string functionName, out InlineFunctionGroup inlineFunctionGroup)589         private bool TryGetInlineFunction(string functionName, out InlineFunctionGroup inlineFunctionGroup)
590         {
591             List<InlineFunctionInfo> inlineFunctionMetadata;
592             if (_includeInlineFunctions && _functionDefinitions.TryGetValue(functionName, out inlineFunctionMetadata))
593             {
594                 inlineFunctionGroup = new InlineFunctionGroup(functionName, inlineFunctionMetadata);
595                 return true;
596             }
597             else
598             {
599                 inlineFunctionGroup = null;
600                 return false;
601             }
602         }
603 
604         /// <summary>
605         /// Builds a dot-separated multipart identifier off the provided <paramref name="names"/>.
606         /// </summary>
GetFullName(params string[] names)607         internal static string GetFullName(params string[] names)
608         {
609             Debug.Assert(names != null && names.Length > 0, "names must not be null or empty");
610             return String.Join(".", names);
611         }
612     }
613 }
614