File: System\Runtime\Serialization\EnumDataContract.cs
Web Access
Project: src\src\libraries\System.Private.DataContractSerialization\src\System.Private.DataContractSerialization.csproj (System.Private.DataContractSerialization)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Xml;
 
namespace System.Runtime.Serialization.DataContracts
{
    internal sealed class EnumDataContract : DataContract
    {
        internal const string ContractTypeString = nameof(EnumDataContract);
        public override string? ContractType => ContractTypeString;
 
        private readonly EnumDataContractCriticalHelper _helper;
 
        [RequiresDynamicCode(DataContract.SerializerAOTWarning)]
        [RequiresUnreferencedCode(DataContract.SerializerTrimmerWarning)]
        internal EnumDataContract(Type type) : base(new EnumDataContractCriticalHelper(type))
        {
            _helper = (base.Helper as EnumDataContractCriticalHelper)!;
        }
 
        internal static Type? GetBaseType(XmlQualifiedName baseContractName)
        {
            return EnumDataContractCriticalHelper.GetBaseType(baseContractName);
        }
 
        public override DataContract BaseContract
        {
            [RequiresDynamicCode(DataContract.SerializerAOTWarning)]
            [RequiresUnreferencedCode(DataContract.SerializerTrimmerWarning)]
            get => _helper.BaseContract;
        }
 
        internal XmlQualifiedName BaseContractName
        {
            get => _helper.BaseContractName;
            [RequiresDynamicCode(DataContract.SerializerAOTWarning)]
            [RequiresUnreferencedCode(DataContract.SerializerTrimmerWarning)]
            set => _helper.BaseContractName = value;
        }
 
        internal List<DataMember> Members
        {
            get => _helper.Members;
            set => _helper.Members = value;
        }
 
        public override ReadOnlyCollection<DataMember> DataMembers => (Members == null) ? ReadOnlyCollection<DataMember>.Empty : Members.AsReadOnly();
 
        internal List<long>? Values
        {
            get => _helper.Values;
            set => _helper.Values = value;
        }
 
        internal bool IsFlags
        {
            get => _helper.IsFlags;
            set => _helper.IsFlags = value;
        }
 
        internal bool IsULong => _helper.IsULong;
 
        internal XmlDictionaryString[]? ChildElementNames => _helper.ChildElementNames;
 
        internal override bool CanContainReferences => false;
 
        private sealed class EnumDataContractCriticalHelper : DataContract.DataContractCriticalHelper
        {
            private static readonly Dictionary<Type, XmlQualifiedName> s_typeToName = new Dictionary<Type, XmlQualifiedName>();
            private static readonly Dictionary<XmlQualifiedName, Type> s_nameToType = new Dictionary<XmlQualifiedName, Type>();
 
            private DataContract _baseContract;
            private List<DataMember> _members;
            private List<long>? _values;
            private bool _isULong;
            private bool _isFlags;
            private readonly bool _hasDataContract;
            private XmlDictionaryString[]? _childElementNames;
 
            static EnumDataContractCriticalHelper()
            {
                Add(typeof(sbyte), DictionaryGlobals.SignedByteLocalName.Value);        // "byte"
                Add(typeof(byte), DictionaryGlobals.UnsignedByteLocalName.Value);       // "unsignedByte"
                Add(typeof(short), DictionaryGlobals.ShortLocalName.Value);             // "short"
                Add(typeof(ushort), DictionaryGlobals.UnsignedShortLocalName.Value);    // "unsignedShort"
                Add(typeof(int), DictionaryGlobals.IntLocalName.Value);                 // "int"
                Add(typeof(uint), DictionaryGlobals.UnsignedIntLocalName.Value);        // "unsignedInt"
                Add(typeof(long), DictionaryGlobals.LongLocalName.Value);               // "long"
                Add(typeof(ulong), DictionaryGlobals.UnsignedLongLocalName.Value);      // "unsignedLong"
            }
 
            internal static void Add(Type type, string localName)
            {
                XmlQualifiedName xmlName = CreateQualifiedName(localName, Globals.SchemaNamespace);
                s_typeToName.Add(type, xmlName);
                s_nameToType.Add(xmlName, type);
            }
 
            internal static XmlQualifiedName GetBaseContractName(Type type)
            {
                s_typeToName.TryGetValue(type, out XmlQualifiedName? retVal);
 
                Debug.Assert(retVal != null);   // Enums can only have certain base types. We shouldn't come up empty here.
                return retVal;
            }
 
            internal static Type? GetBaseType(XmlQualifiedName baseContractName)
            {
                s_nameToType.TryGetValue(baseContractName, out Type? retVal);
                return retVal;
            }
 
            [RequiresDynamicCode(DataContract.SerializerAOTWarning)]
            [RequiresUnreferencedCode(DataContract.SerializerTrimmerWarning)]
            internal EnumDataContractCriticalHelper(
                [DynamicallyAccessedMembers(ClassDataContract.DataContractPreserveMemberTypes)]
                Type type) : base(type)
            {
                XmlName = DataContract.GetXmlName(type, out _hasDataContract);
                Type baseType = Enum.GetUnderlyingType(type);
                XmlQualifiedName baseTypeName = GetBaseContractName(baseType);
                _baseContract = DataContract.GetBuiltInDataContract(baseTypeName.Name, baseTypeName.Namespace)!;
                // Setting XmlName might be redundant. But I don't want to miss an edge case.
                _baseContract.XmlName = baseTypeName;
                ImportBaseType(baseType);
                IsFlags = type.IsDefined(Globals.TypeOfFlagsAttribute, false);
                ImportDataMembers();
 
                XmlDictionary dictionary = new XmlDictionary(2 + Members.Count);
                Name = dictionary.Add(XmlName.Name);
                Namespace = dictionary.Add(XmlName.Namespace);
                _childElementNames = new XmlDictionaryString[Members.Count];
                for (int i = 0; i < Members.Count; i++)
                    _childElementNames[i] = dictionary.Add(Members[i].Name);
                if (TryGetDCAttribute(type, out DataContractAttribute? dataContractAttribute))
                {
                    if (dataContractAttribute.IsReference)
                    {
                        DataContract.ThrowInvalidDataContractException(
                                SR.Format(SR.EnumTypeCannotHaveIsReference,
                                    DataContract.GetClrTypeFullName(type),
                                    dataContractAttribute.IsReference,
                                    false),
                                type);
                    }
                }
            }
 
            internal DataContract BaseContract => _baseContract;
 
            internal XmlQualifiedName BaseContractName
            {
                get => _baseContract.XmlName;
 
                [RequiresDynamicCode(DataContract.SerializerAOTWarning)]
                [RequiresUnreferencedCode(DataContract.SerializerTrimmerWarning)]
                set
                {
                    Type? baseType = GetBaseType(value);
                    if (baseType == null)
                        ThrowInvalidDataContractException(
                                SR.Format(SR.InvalidEnumBaseType, value.Name, value.Namespace, XmlName.Name, XmlName.Namespace));
                    ImportBaseType(baseType);
                    _baseContract = DataContract.GetBuiltInDataContract(value.Name, value.Namespace)!;
                    // Setting XmlName might be redundant. But I don't want to miss an edge case.
                    _baseContract.XmlName = value;
                }
            }
 
            internal List<DataMember> Members
            {
                get => _members;
                set => _members = value;
            }
 
            internal List<long>? Values
            {
                get => _values;
                set => _values = value;
            }
 
            internal bool IsFlags
            {
                get => _isFlags;
                set => _isFlags = value;
            }
 
            internal bool IsULong
            {
                get => _isULong;
                set => _isULong = value;
            }
 
            internal XmlDictionaryString[]? ChildElementNames
            {
                get => _childElementNames;
                set => _childElementNames = value;
            }
 
            private void ImportBaseType(Type baseType)
            {
                _isULong = (baseType == Globals.TypeOfULong);
            }
 
            [MemberNotNull(nameof(_members))]
            private void ImportDataMembers()
            {
                Type type = UnderlyingType;
                FieldInfo[] fields = type.GetFields(BindingFlags.Static | BindingFlags.Public);
                Dictionary<string, DataMember> memberValuesTable = new Dictionary<string, DataMember>();
                List<DataMember> tempMembers = new List<DataMember>(fields.Length);
                List<long> tempValues = new List<long>(fields.Length);
 
                for (int i = 0; i < fields.Length; i++)
                {
                    FieldInfo field = fields[i];
                    bool enumMemberValid = false;
                    if (_hasDataContract)
                    {
                        object[] memberAttributes = field.GetCustomAttributes(Globals.TypeOfEnumMemberAttribute, false).ToArray();
                        if (memberAttributes != null && memberAttributes.Length > 0)
                        {
                            if (memberAttributes.Length > 1)
                                ThrowInvalidDataContractException(SR.Format(SR.TooManyEnumMembers, DataContract.GetClrTypeFullName(field.DeclaringType!), field.Name));
                            EnumMemberAttribute memberAttribute = (EnumMemberAttribute)memberAttributes[0];
 
                            DataMember memberContract = new DataMember(field);
                            if (memberAttribute.IsValueSetExplicitly)
                            {
                                if (string.IsNullOrEmpty(memberAttribute.Value))
                                    ThrowInvalidDataContractException(SR.Format(SR.InvalidEnumMemberValue, field.Name, DataContract.GetClrTypeFullName(type)));
                                memberContract.Name = memberAttribute.Value;
                            }
                            else
                                memberContract.Name = field.Name;
                            memberContract.Order = _isULong ? (long)Convert.ToUInt64(field.GetValue(null)) : Convert.ToInt64(field.GetValue(null));
                            ClassDataContract.CheckAndAddMember(tempMembers, memberContract, memberValuesTable);
                            enumMemberValid = true;
                        }
 
                        object[] dataMemberAttributes = field.GetCustomAttributes(Globals.TypeOfDataMemberAttribute, false).ToArray();
                        if (dataMemberAttributes != null && dataMemberAttributes.Length > 0)
                            ThrowInvalidDataContractException(SR.Format(SR.DataMemberOnEnumField, DataContract.GetClrTypeFullName(field.DeclaringType!), field.Name));
                    }
                    else
                    {
#pragma warning disable SYSLIB0050 // FieldInfo.IsNotSerialized is obsolete
                        if (!field.IsNotSerialized)
                        {
                            DataMember memberContract = new DataMember(field) { Name = field.Name };
                            memberContract.Order = _isULong ? (long)Convert.ToUInt64(field.GetValue(null)) : Convert.ToInt64(field.GetValue(null));
                            ClassDataContract.CheckAndAddMember(tempMembers, memberContract, memberValuesTable);
                            enumMemberValid = true;
                        }
#pragma warning restore SYSLIB0050
                    }
 
                    if (enumMemberValid)
                    {
                        object? enumValue = field.GetValue(null);
                        if (_isULong)
                            tempValues.Add((long)Convert.ToUInt64(enumValue, null));
                        else
                            tempValues.Add(Convert.ToInt64(enumValue, null));
                    }
                }
 
                Interlocked.MemoryBarrier();
                _members = tempMembers;
                _values = tempValues;
            }
        }
 
        internal void WriteEnumValue(XmlWriterDelegator writer, object value)
        {
            long longValue = IsULong ? (long)Convert.ToUInt64(value, null) : Convert.ToInt64(value, null);
            for (int i = 0; i < Values!.Count; i++)
            {
                if (longValue == Values[i])
                {
                    writer.WriteString(ChildElementNames![i].Value);
                    return;
                }
            }
            if (IsFlags)
            {
                int zeroIndex = -1;
                bool noneWritten = true;
                for (int i = 0; i < Values.Count; i++)
                {
                    long current = Values[i];
                    if (current == 0)
                    {
                        zeroIndex = i;
                        continue;
                    }
                    if (longValue == 0)
                        break;
                    if ((current & longValue) == current)
                    {
                        if (noneWritten)
                            noneWritten = false;
                        else
                            writer.WriteString(DictionaryGlobals.Space.Value);
 
                        writer.WriteString(ChildElementNames![i].Value);
                        longValue &= ~current;
                    }
                }
                // enforce that enum value was completely parsed
                if (longValue != 0)
                    throw XmlObjectSerializer.CreateSerializationException(SR.Format(SR.InvalidEnumValueOnWrite, value, DataContract.GetClrTypeFullName(UnderlyingType)));
 
                if (noneWritten && zeroIndex >= 0)
                    writer.WriteString(ChildElementNames![zeroIndex].Value);
            }
            else
                throw XmlObjectSerializer.CreateSerializationException(SR.Format(SR.InvalidEnumValueOnWrite, value, DataContract.GetClrTypeFullName(UnderlyingType)));
        }
 
        internal object ReadEnumValue(XmlReaderDelegator reader)
        {
            string stringValue = reader.ReadElementContentAsString();
            long longValue = 0;
            int i = 0;
            if (IsFlags)
            {
                // Skip initial spaces
                for (; i < stringValue.Length; i++)
                    if (stringValue[i] != ' ')
                        break;
 
                // Read space-delimited values
                int startIndex = i;
                int count;
                for (; i < stringValue.Length; i++)
                {
                    if (stringValue[i] == ' ')
                    {
                        count = i - startIndex;
                        if (count > 0)
                            longValue |= ReadEnumValue(stringValue, startIndex, count);
                        for (++i; i < stringValue.Length; i++)
                            if (stringValue[i] != ' ')
                                break;
                        startIndex = i;
                        if (i == stringValue.Length)
                            break;
                    }
                }
                count = i - startIndex;
                if (count > 0)
                    longValue |= ReadEnumValue(stringValue, startIndex, count);
            }
            else
            {
                if (stringValue.Length == 0)
                    throw XmlObjectSerializer.CreateSerializationException(SR.Format(SR.InvalidEnumValueOnRead, stringValue, DataContract.GetClrTypeFullName(UnderlyingType)));
                longValue = ReadEnumValue(stringValue, 0, stringValue.Length);
            }
 
            if (IsULong)
                return Enum.ToObject(UnderlyingType, (object)(ulong)longValue);
            return Enum.ToObject(UnderlyingType, (object)longValue);
        }
 
        private long ReadEnumValue(string value, int index, int count)
        {
            for (int i = 0; i < Members.Count; i++)
            {
                string memberName = Members[i].Name;
                if (memberName.Length == count && string.CompareOrdinal(value, index, memberName, 0, count) == 0)
                {
                    return Values![i];
                }
            }
            throw XmlObjectSerializer.CreateSerializationException(SR.Format(SR.InvalidEnumValueOnRead, value.Substring(index, count), DataContract.GetClrTypeFullName(UnderlyingType)));
        }
 
        internal string GetStringFromEnumValue(long value)
        {
            if (IsULong)
            {
                return XmlConvert.ToString((ulong)value);
            }
            else
            {
                return XmlConvert.ToString(value);
            }
        }
 
        internal long GetEnumValueFromString(string value)
        {
            if (IsULong)
            {
                return (long)XmlConverter.ToUInt64(value);
            }
            else
            {
                return XmlConverter.ToInt64(value);
            }
        }
 
        internal override bool Equals(object? other, HashSet<DataContractPairKey>? checkedContracts)
        {
            if (IsEqualOrChecked(other, checkedContracts))
                return true;
 
            if (base.Equals(other, null))
            {
                if (other is EnumDataContract enumContract)
                {
                    if (Members.Count != enumContract.Members.Count || Values?.Count != enumContract.Values?.Count)
                        return false;
                    string[] memberNames1 = new string[Members.Count], memberNames2 = new string[Members.Count];
                    for (int i = 0; i < Members.Count; i++)
                    {
                        memberNames1[i] = Members[i].Name;
                        memberNames2[i] = enumContract.Members[i].Name;
                    }
                    Array.Sort(memberNames1);
                    Array.Sort(memberNames2);
                    for (int i = 0; i < Members.Count; i++)
                    {
                        if (memberNames1[i] != memberNames2[i])
                            return false;
                    }
 
                    return (IsFlags == enumContract.IsFlags);
                }
            }
            return false;
        }
 
        [RequiresDynamicCode(DataContract.SerializerAOTWarning)]
        [RequiresUnreferencedCode(DataContract.SerializerTrimmerWarning)]
        internal override void WriteXmlValue(XmlWriterDelegator xmlWriter, object obj, XmlObjectSerializerWriteContext? context)
        {
            WriteEnumValue(xmlWriter, obj);
        }
 
        [RequiresDynamicCode(DataContract.SerializerAOTWarning)]
        [RequiresUnreferencedCode(DataContract.SerializerTrimmerWarning)]
        internal override object ReadXmlValue(XmlReaderDelegator xmlReader, XmlObjectSerializerReadContext? context)
        {
            object obj = ReadEnumValue(xmlReader);
            context?.AddNewObject(obj);
            return obj;
        }
    }
}