< Summary

Class:Azure.Search.Documents.Indexes.FieldBuilder
Assembly:Azure.Search.Documents
File(s):C:\Git\azure-sdk-for-net\sdk\search\Azure.Search.Documents\src\Indexes\FieldBuilder.cs
Covered lines:151
Uncovered lines:13
Coverable lines:164
Total lines:452
Line coverage:92% (151 of 164)
Covered branches:61
Total branches:68
Branch coverage:89.7% (61 of 68)

Metrics

MethodCyclomatic complexity Line coverage Branch coverage
.cctor()-100%100%
.ctor()-100%100%
get_Serializer()-100%100%
Build(...)-91.67%75%
Build(...)-100%100%
GetDataTypeInfo(...)-100%100%
TryGetEnumerableElementType(...)-100%100%
get_Unknown()-100%100%
Simple(...)-100%100%
Complex(...)-100%100%
AsCollection(...)-100%100%
.ctor()-100%100%
Match(...)-100%100%
.ctor(...)-100%100%
Match(...)-100%100%
.ctor(...)-100%100%
Match(...)-100%100%
.ctor(...)-100%100%
TryGet(...)-93.33%95%
get_Properties()-100%100%
.ctor(...)-100%100%
.ctor(...)-0%100%
get_Name()-100%100%
get_SerializedName()-100%100%
get_PropertyType()-100%100%
op_Implicit(...)-0%100%
GetCustomAttributes(...)-100%100%
get_Shared()-0%100%
.ctor()-0%100%
ConvertMemberName(...)-0%0%

File(s)

C:\Git\azure-sdk-for-net\sdk\search\Azure.Search.Documents\src\Indexes\FieldBuilder.cs

#LineLine coverage
 1// Copyright (c) Microsoft Corporation. All rights reserved.
 2// Licensed under the MIT License.
 3
 4using System;
 5using System.Collections;
 6using System.Collections.Generic;
 7using System.Collections.ObjectModel;
 8using System.Diagnostics;
 9using System.Linq;
 10using System.Reflection;
 11using Azure.Core;
 12#if EXPERIMENTAL_SERIALIZER
 13using Azure.Core.Serialization;
 14#endif
 15using Azure.Search.Documents.Indexes.Models;
 16#if EXPERIMENTAL_SPATIAL
 17using Azure.Core.Spatial;
 18#endif
 19
 20namespace Azure.Search.Documents.Indexes
 21{
 22    /// <summary>
 23    /// Builds field definitions for a search index by reflecting over a user-defined model type.
 24    /// </summary>
 25    public class FieldBuilder
 26    {
 27        private const string HelpLink = "https://aka.ms/azsdk/net/search/fieldbuilder";
 28
 129        private static readonly IReadOnlyDictionary<Type, SearchFieldDataType> s_primitiveTypeMap =
 130            new ReadOnlyDictionary<Type, SearchFieldDataType>(
 131                new Dictionary<Type, SearchFieldDataType>()
 132                {
 133                    [typeof(string)] = SearchFieldDataType.String,
 134                    [typeof(int)] = SearchFieldDataType.Int32,
 135                    [typeof(long)] = SearchFieldDataType.Int64,
 136                    [typeof(double)] = SearchFieldDataType.Double,
 137                    [typeof(bool)] = SearchFieldDataType.Boolean,
 138                    [typeof(DateTime)] = SearchFieldDataType.DateTimeOffset,
 139                    [typeof(DateTimeOffset)] = SearchFieldDataType.DateTimeOffset,
 140#if EXPERIMENTAL_SPATIAL
 141                    [typeof(PointGeometry)] = SearchFieldDataType.GeographyPoint,
 142#endif
 143                });
 44
 145        private static readonly ISet<Type> s_unsupportedTypes =
 146            new HashSet<Type>
 147            {
 148                typeof(decimal),
 149            };
 50
 51        /// <summary>
 52        /// Initializes a new instance of the <see cref="FieldBuilder"/> class.
 53        /// </summary>
 15354        public FieldBuilder()
 55        {
 15356        }
 57
 58        /// <summary>
 59        /// Gets or sets the <see cref="ObjectSerializer"/> to use to generate field names that match JSON property name
 60        /// You should use hte same value as <see cref="SearchClientOptions.Serializer"/>.
 61        /// <see cref="JsonObjectSerializer"/> will be used if no value is provided.
 62        /// </summary>
 45963        public ObjectSerializer Serializer { get; set; }
 64
 65        /// <summary>
 66        /// Creates a list of <see cref="SearchField"/> objects corresponding to
 67        /// the properties of the type supplied.
 68        /// </summary>
 69        /// <param name="modelType">
 70        /// The type for which fields will be created, based on its properties.
 71        /// </param>
 72        /// <returns>A collection of fields.</returns>
 73        /// <exception cref="ArgumentNullException"><paramref name="modelType"/>.</exception>
 74        public IList<SearchField> Build(Type modelType)
 75        {
 15376            Argument.AssertNotNull(modelType, nameof(modelType));
 77
 78            ArgumentException FailOnNonObjectDataType()
 79            {
 1280                string errorMessage =
 1281                    $"Type '{modelType}' does not have properties which map to fields of an Azure Search index. Please u
 1282                    "class or struct with public properties.";
 83
 1284                throw new ArgumentException(errorMessage, nameof(modelType));
 85            }
 86
 15387            Serializer ??= new JsonObjectSerializer();
 15388            IMemberNameConverter nameProvider = Serializer as IMemberNameConverter ?? DefaultSerializedNameProvider.Shar
 89
 15390            if (ObjectInfo.TryGet(modelType, nameProvider, out ObjectInfo info))
 91            {
 14192                if (info.Properties.Length == 0)
 93                {
 094                    throw FailOnNonObjectDataType();
 95                }
 96
 97                // Use Stack to avoid a dependency on ImmutableStack for now.
 14198                return Build(modelType, info, nameProvider, new Stack<Type>(new[] { modelType }));
 99            }
 100
 12101            throw FailOnNonObjectDataType();
 102        }
 103
 104        private static IList<SearchField> Build(
 105            Type modelType,
 106            ObjectInfo info,
 107            IMemberNameConverter nameProvider,
 108            Stack<Type> processedTypes)
 109        {
 110            SearchField BuildField(ObjectPropertyInfo prop)
 111            {
 112                // The IMemberNameConverter will return null for implementation-specific ways of ignoring members.
 113                static bool ShouldIgnore(Attribute attribute) =>
 7709114                    attribute is FieldBuilderIgnoreAttribute;
 115
 19409116                IList<Attribute> attributes = prop.GetCustomAttributes(true).Cast<Attribute>().ToArray();
 19409117                if (attributes.Any(ShouldIgnore))
 118                {
 2119                    return null;
 120                }
 121
 122                SearchField CreateComplexField(SearchFieldDataType dataType, Type underlyingClrType, ObjectInfo info)
 123                {
 3128124                    if (processedTypes.Contains(underlyingClrType))
 125                    {
 126                        // Skip recursive types.
 1127                        return null;
 128                    }
 129
 3127130                    processedTypes.Push(underlyingClrType);
 131                    try
 132                    {
 3127133                        IList<SearchField> subFields =
 3127134                            Build(underlyingClrType, info, nameProvider, processedTypes);
 135
 3127136                        if (prop.SerializedName is null)
 137                        {
 138                            // Member is unsupported or ignored.
 0139                            return null;
 140                        }
 141
 142                        // Start with a ComplexField to make sure all properties default to language defaults.
 3127143                        SearchField field = new ComplexField(prop.SerializedName, dataType);
 28110144                        foreach (SearchField subField in subFields)
 145                        {
 10928146                            field.Fields.Add(subField);
 147                        }
 148
 3127149                        return field;
 150                    }
 151                    finally
 152                    {
 3127153                        processedTypes.Pop();
 3127154                    }
 3127155                }
 156
 157                SearchField CreateSimpleField(SearchFieldDataType SearchFieldDataType)
 158                {
 16275159                    if (prop.SerializedName is null)
 160                    {
 161                        // Member is unsupported or ignored.
 0162                        return null;
 163                    }
 164
 165                    // Start with a SimpleField to make sure all properties default to language defaults.
 16275166                    SearchField field = new SimpleField(prop.SerializedName, SearchFieldDataType);
 46388167                    foreach (Attribute attribute in attributes)
 168                    {
 169                        switch (attribute)
 170                        {
 171                            case SearchableFieldAttribute searchableFieldAttribute:
 2212172                                ((ISearchFieldAttribute)searchableFieldAttribute).SetField(field);
 2212173                                break;
 174
 175                            case SimpleFieldAttribute simpleFieldAttribute:
 2218176                                ((ISearchFieldAttribute)simpleFieldAttribute).SetField(field);
 2218177                                break;
 178
 179                            default:
 2489180                                Type attributeType = attribute.GetType();
 181
 182                                // Match on name to avoid dependency - don't want to force people not using
 183                                // this feature to bring in the annotations component.
 184                                //
 185                                // Also, ignore key attributes on sub-fields.
 2489186                                if (attributeType.FullName == "System.ComponentModel.DataAnnotations.KeyAttribute" &&
 2489187                                    processedTypes.Count <= 1)
 188                                {
 138189                                    field.IsKey = true;
 190                                }
 191                                break;
 192                        }
 193                    }
 194
 16275195                    return field;
 196                }
 197
 198                ArgumentException FailOnUnknownDataType()
 199                {
 4200                    string errorMessage =
 4201                        $"Property '{prop.Name}' is of type '{prop.PropertyType}', which does not map to an " +
 4202                        "Azure Search data type. Please use a supported data type or mark the property with [FieldBuilde
 4203                        $"and define the field by creating a SearchField object. See {HelpLink} for more information.";
 204
 4205                    return new ArgumentException(errorMessage, nameof(modelType))
 4206                    {
 4207                        HelpLink = HelpLink,
 4208                    };
 209                }
 210
 19407211                IDataTypeInfo dataTypeInfo = GetDataTypeInfo(prop.PropertyType, nameProvider);
 212
 19407213                return dataTypeInfo.Match(
 19411214                    onUnknownDataType: () => throw FailOnUnknownDataType(),
 19407215                    onSimpleDataType: CreateSimpleField,
 19407216                    onComplexDataType: CreateComplexField);
 217            }
 218
 22673219            return info.Properties.Select(BuildField).Where(field => field != null).ToList();
 220        }
 221
 222        private static IDataTypeInfo GetDataTypeInfo(Type propertyType, IMemberNameConverter nameProvider)
 223        {
 224            static bool IsNullableType(Type type) =>
 11522225                type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
 226
 27797227            if (s_primitiveTypeMap.TryGetValue(propertyType, out SearchFieldDataType SearchFieldDataType))
 228            {
 16275229                return DataTypeInfo.Simple(SearchFieldDataType);
 230            }
 11522231            else if (IsNullableType(propertyType))
 232            {
 2535233                return GetDataTypeInfo(propertyType.GenericTypeArguments[0], nameProvider);
 234            }
 8987235            else if (TryGetEnumerableElementType(propertyType, out Type elementType))
 236            {
 5855237                IDataTypeInfo elementTypeInfo = GetDataTypeInfo(elementType, nameProvider);
 5855238                return DataTypeInfo.AsCollection(elementTypeInfo);
 239            }
 3132240            else if (ObjectInfo.TryGet(propertyType, nameProvider, out ObjectInfo info))
 241            {
 3128242                return DataTypeInfo.Complex(SearchFieldDataType.Complex, propertyType, info);
 243            }
 244            else
 245            {
 4246                return DataTypeInfo.Unknown;
 247            }
 248        }
 249
 250        private static bool TryGetEnumerableElementType(Type candidateType, out Type elementType)
 251        {
 252            static Type GetElementTypeIfIEnumerable(Type t) =>
 37902253                t.IsConstructedGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)
 37902254                    ? t.GenericTypeArguments[0]
 37902255                    : null;
 256
 8987257            elementType = GetElementTypeIfIEnumerable(candidateType);
 8987258            if (elementType != null)
 259            {
 1171260                return true;
 261            }
 262            else
 263            {
 7816264                TypeInfo ti = candidateType.GetTypeInfo();
 7816265                var listElementTypes = ti
 7816266                    .ImplementedInterfaces
 7816267                    .Select(GetElementTypeIfIEnumerable)
 36731268                    .Where(p => p != null)
 7816269                    .ToList();
 270
 7816271                if (listElementTypes.Count == 1)
 272                {
 4684273                    elementType = listElementTypes[0];
 4684274                    return true;
 275                }
 276                else
 277                {
 3132278                    return false;
 279                }
 280            }
 281        }
 282
 283        private interface IDataTypeInfo
 284        {
 285            T Match<T>(
 286                Func<T> onUnknownDataType,
 287                Func<SearchFieldDataType, T> onSimpleDataType,
 288                Func<SearchFieldDataType, Type, ObjectInfo, T> onComplexDataType);
 289        }
 290
 291        private static class DataTypeInfo
 292        {
 7293            public static IDataTypeInfo Unknown { get; } = new UnknownDataTypeInfo();
 294
 20825295            public static IDataTypeInfo Simple(SearchFieldDataType SearchFieldDataType) => new SimpleDataTypeInfo(Search
 296
 297            public static IDataTypeInfo Complex(SearchFieldDataType SearchFieldDataType, Type underlyingClrType, ObjectI
 4431298                new ComplexDataTypeInfo(SearchFieldDataType, underlyingClrType, info);
 299
 300            public static IDataTypeInfo AsCollection(IDataTypeInfo dataTypeInfo) =>
 5855301                dataTypeInfo.Match(
 5857302                    onUnknownDataType: () => Unknown,
 10405303                    onSimpleDataType: SearchFieldDataType => Simple(SearchFieldDataType.Collection(SearchFieldDataType))
 5855304                    onComplexDataType: (SearchFieldDataType, underlyingClrType, info) =>
 7158305                        Complex(SearchFieldDataType.Collection(SearchFieldDataType), underlyingClrType, info));
 306
 307            private sealed class UnknownDataTypeInfo : IDataTypeInfo
 308            {
 1309                public UnknownDataTypeInfo()
 310                {
 1311                }
 312
 313                public T Match<T>(
 314                    Func<T> onUnknownDataType,
 315                    Func<SearchFieldDataType, T> onSimpleDataType,
 316                    Func<SearchFieldDataType, Type, ObjectInfo, T> onComplexDataType)
 6317                    => onUnknownDataType();
 318            }
 319
 320            private sealed class SimpleDataTypeInfo : IDataTypeInfo
 321            {
 322                private readonly SearchFieldDataType _dataType;
 323
 20825324                public SimpleDataTypeInfo(SearchFieldDataType SearchFieldDataType)
 325                {
 20825326                    _dataType = SearchFieldDataType;
 20825327                }
 328
 329                public T Match<T>(
 330                    Func<T> onUnknownDataType,
 331                    Func<SearchFieldDataType, T> onSimpleDataType,
 332                    Func<SearchFieldDataType, Type, ObjectInfo, T> onComplexDataType)
 20825333                    => onSimpleDataType(_dataType);
 334            }
 335
 336            private sealed class ComplexDataTypeInfo : IDataTypeInfo
 337            {
 338                private readonly SearchFieldDataType _dataType;
 339                private readonly Type _underlyingClrType;
 340                private readonly ObjectInfo _info;
 341
 4431342                public ComplexDataTypeInfo(SearchFieldDataType SearchFieldDataType, Type underlyingClrType, ObjectInfo i
 343                {
 4431344                    _dataType = SearchFieldDataType;
 4431345                    _underlyingClrType = underlyingClrType;
 4431346                    _info = info;
 4431347                }
 348
 349                public T Match<T>(
 350                    Func<T> onUnknownDataType,
 351                    Func<SearchFieldDataType, T> onSimpleDataType,
 352                    Func<SearchFieldDataType, Type, ObjectInfo, T> onComplexDataType)
 4431353                    => onComplexDataType(_dataType, _underlyingClrType, _info);
 354            }
 355        }
 356
 357        private class ObjectInfo
 358        {
 3269359            private ObjectInfo(ObjectPropertyInfo[] properties)
 360            {
 3269361                Properties = properties;
 3269362            }
 363
 364            public static bool TryGet(Type type, IMemberNameConverter nameProvider, out ObjectInfo info)
 365            {
 366                // Close approximation to Newtonsoft.Json.Serialization.DefaultContractResolver that was used in Microso
 3285367                if (!type.IsPrimitive && !type.IsEnum && !s_unsupportedTypes.Contains(type) && !s_primitiveTypeMap.Conta
 368                {
 369                    const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPubl
 370
 3270371                    List<ObjectPropertyInfo> properties = new List<ObjectPropertyInfo>();
 47188372                    foreach (PropertyInfo property in type.GetProperties(bindingFlags))
 373                    {
 20324374                        string serializedName = nameProvider.ConvertMemberName(property);
 20324375                        if (serializedName != null)
 376                        {
 19411377                            properties.Add(new ObjectPropertyInfo(property, serializedName));
 378                        }
 379                    }
 380
 36268381                    foreach (FieldInfo field in type.GetFields(bindingFlags))
 382                    {
 14864383                        string serializedName = nameProvider.ConvertMemberName(field);
 14864384                        if (serializedName != null)
 385                        {
 0386                            properties.Add(new ObjectPropertyInfo(field, serializedName));
 387                        }
 388                    }
 389
 3270390                    if (properties.Count != 0)
 391                    {
 3269392                        info = new ObjectInfo(properties.ToArray());
 3269393                        return true;
 394                    }
 395                }
 396
 16397                info = null;
 16398                return false;
 399            }
 400
 3409401            public ObjectPropertyInfo[] Properties { get; }
 402        }
 403
 404        private struct ObjectPropertyInfo
 405        {
 406            private readonly MemberInfo _memberInfo;
 407
 408            public ObjectPropertyInfo(PropertyInfo property, string serializedName)
 409            {
 410                Debug.Assert(serializedName != null, $"{nameof(serializedName)} cannot be null");
 411
 19411412                _memberInfo = property;
 413
 19411414                SerializedName = serializedName;
 19411415                PropertyType = property.PropertyType;
 19411416            }
 417
 418            public ObjectPropertyInfo(FieldInfo field, string serializedName)
 419            {
 420                Debug.Assert(serializedName != null, $"{nameof(serializedName)} cannot be null");
 421
 0422                _memberInfo = field;
 423
 0424                SerializedName = serializedName;
 0425                PropertyType = field.FieldType;
 0426            }
 427
 4428            public string Name => _memberInfo.Name;
 429
 38804430            public string SerializedName { get; }
 431
 19411432            public Type PropertyType { get; }
 433
 434            public static implicit operator MemberInfo(ObjectPropertyInfo property) =>
 0435                property._memberInfo;
 436
 437            public object[] GetCustomAttributes(bool inherit) =>
 19409438                _memberInfo.GetCustomAttributes(inherit);
 439        }
 440
 441        private class DefaultSerializedNameProvider : IMemberNameConverter
 442        {
 0443            public static IMemberNameConverter Shared { get; } = new DefaultSerializedNameProvider();
 444
 0445            private DefaultSerializedNameProvider()
 446            {
 0447            }
 448
 0449            public string ConvertMemberName(MemberInfo member) => member?.Name;
 450        }
 451    }
 452}