File: System\DirectoryServices\ActiveDirectory\ActiveDirectorySchemaProperty.cs
Web Access
Project: src\src\runtime\src\libraries\System.DirectoryServices\src\System.DirectoryServices.csproj (System.DirectoryServices)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;

namespace System.DirectoryServices.ActiveDirectory
{
    internal enum SearchFlags : int
    {
        None = 0,
        IsIndexed = 1,
        IsIndexedOverContainer = 2,
        IsInAnr = 4,
        IsOnTombstonedObject = 8,
        IsTupleIndexed = 32
    }

    public class ActiveDirectorySchemaProperty : IDisposable
    {
        // private variables
        private DirectoryEntry? _schemaEntry;
        private DirectoryEntry? _propertyEntry;
        private DirectoryEntry? _abstractPropertyEntry;
        private readonly NativeComInterfaces.IAdsProperty? _iadsProperty;
        private readonly DirectoryContext _context;
        internal bool isBound;
        private bool _disposed;
        private ActiveDirectorySchema? _schema;
        private bool _propertiesFromSchemaContainerInitialized;
        private bool _isDefunctOnServer;
        private SearchResult? _propertyValuesFromServer;

        // private variables for caching properties
        private readonly string _ldapDisplayName;
        private string? _commonName;
        private string? _oid;
        private ActiveDirectorySyntax _syntax = (ActiveDirectorySyntax)(-1);
        private bool _syntaxInitialized;
        private string? _description;
        private bool _descriptionInitialized;
        private bool _isSingleValued;
        private bool _isSingleValuedInitialized;
        private bool _isInGlobalCatalog;
        private bool _isInGlobalCatalogInitialized;
        private Nullable<int> _rangeLower;
        private bool _rangeLowerInitialized;
        private Nullable<int> _rangeUpper;
        private bool _rangeUpperInitialized;
        private bool _isDefunct;
        private SearchFlags _searchFlags = SearchFlags.None;
        private bool _searchFlagsInitialized;
        private ActiveDirectorySchemaProperty? _linkedProperty;
        private bool _linkedPropertyInitialized;
        private Nullable<int> _linkId;
        private bool _linkIdInitialized;
        private byte[]? _schemaGuidBinaryForm;

        // OMObjectClass values for the syntax
        //0x2B0C0287731C00854A
        private static readonly OMObjectClass s_dnOMObjectClass = new OMObjectClass(new byte[] { 0x2B, 0x0C, 0x02, 0x87, 0x73, 0x1C, 0x00, 0x85, 0x4A });
        //0x2A864886F7140101010C
        private static readonly OMObjectClass s_dNWithStringOMObjectClass = new OMObjectClass(new byte[] { 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x14, 0x01, 0x01, 0x01, 0x0C });
        //0x2A864886F7140101010B
        private static readonly OMObjectClass s_dNWithBinaryOMObjectClass = new OMObjectClass(new byte[] { 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x14, 0x01, 0x01, 0x01, 0x0B });
        //0x2A864886F71401010106
        private static readonly OMObjectClass s_replicaLinkOMObjectClass = new OMObjectClass(new byte[] { 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x14, 0x01, 0x01, 0x01, 0x06 });
        //0x2B0C0287731C00855C
        private static readonly OMObjectClass s_presentationAddressOMObjectClass = new OMObjectClass(new byte[] { 0x2B, 0x0C, 0x02, 0x87, 0x73, 0x1C, 0x00, 0x85, 0x5C });
        //0x2B0C0287731C00853E
        private static readonly OMObjectClass s_accessPointDnOMObjectClass = new OMObjectClass(new byte[] { 0x2B, 0x0C, 0x02, 0x87, 0x73, 0x1C, 0x00, 0x85, 0x3E });
        //0x56060102050B1D
        private static readonly OMObjectClass s_oRNameOMObjectClass = new OMObjectClass(new byte[] { 0x56, 0x06, 0x01, 0x02, 0x05, 0x0B, 0x1D });

        // syntaxes
        private static readonly Syntax[] s_syntaxes = {/* CaseExactString */ new Syntax("2.5.5.3", 27, null),
                                              /* CaseIgnoreString */ new Syntax("2.5.5.4", 20, null),
                                              /* NumericString */ new Syntax("2.5.5.6", 18, null),
                                              /* DirectoryString */ new Syntax("2.5.5.12", 64, null),
                                              /* OctetString */ new Syntax("2.5.5.10", 4, null),
                                              /* SecurityDescriptor */ new Syntax("2.5.5.15", 66, null),
                                              /* Int */ new Syntax("2.5.5.9", 2, null),
                                              /* Int64 */ new Syntax("2.5.5.16", 65, null),
                                              /* Bool */ new Syntax("2.5.5.8", 1, null),
                                              /* Oid */ new Syntax("2.5.5.2", 6, null),
                                              /* GeneralizedTime */ new Syntax("2.5.5.11", 24, null),
                                              /* UtcTime */ new Syntax("2.5.5.11", 23, null),
                                              /* DN */ new Syntax("2.5.5.1", 127, s_dnOMObjectClass),
                                              /* DNWithBinary */ new Syntax("2.5.5.7", 127, s_dNWithBinaryOMObjectClass),
                                              /* DNWithString */ new Syntax("2.5.5.14", 127, s_dNWithStringOMObjectClass),
                                              /* Enumeration */ new Syntax("2.5.5.9", 10, null),
                                              /* IA5String */ new Syntax("2.5.5.5", 22, null),
                                              /* PrintableString */ new Syntax("2.5.5.5", 19, null),
                                              /* Sid */ new Syntax("2.5.5.17", 4, null),
                                              /* AccessPointDN */ new Syntax("2.5.5.14", 127, s_accessPointDnOMObjectClass),
                                              /* ORName */ new Syntax("2.5.5.7", 127, s_oRNameOMObjectClass),
                                              /* PresentationAddress */ new Syntax("2.5.5.13", 127, s_presentationAddressOMObjectClass),
                                              /* ReplicaLink */ new Syntax("2.5.5.10", 127, s_replicaLinkOMObjectClass)};

        #region constructors
        public ActiveDirectorySchemaProperty(DirectoryContext context, string ldapDisplayName)
        {
            ArgumentNullException.ThrowIfNull(context);

            if ((context.Name == null) && (!context.isRootDomain()))
            {
                throw new ArgumentException(SR.ContextNotAssociatedWithDomain, nameof(context));
            }

            if (context.Name != null)
            {
                // the target should be a valid forest name or a server
                if (!((context.isRootDomain()) || (context.isADAMConfigSet()) || (context.isServer())))
                {
                    throw new ArgumentException(SR.NotADOrADAM, nameof(context));
                }
            }

            ArgumentNullException.ThrowIfNull(ldapDisplayName);

            if (ldapDisplayName.Length == 0)
            {
                throw new ArgumentException(SR.EmptyStringParameter, nameof(ldapDisplayName));
            }

            _context = new DirectoryContext(context);

            // validate the context
            _schemaEntry = DirectoryEntryManager.GetDirectoryEntry(context, WellKnownDN.SchemaNamingContext);
            _schemaEntry.Bind(true);

            _ldapDisplayName = ldapDisplayName;
            // common name of the property defaults to the ldap display name
            _commonName = ldapDisplayName;

            // set the bind flag
            this.isBound = false;
        }

        // internal constructor
        internal ActiveDirectorySchemaProperty(DirectoryContext context, string ldapDisplayName, DirectoryEntry? propertyEntry, DirectoryEntry? schemaEntry)
        {
            _context = context;
            _ldapDisplayName = ldapDisplayName;

            // common name of the property defaults to the ldap display name
            _propertyEntry = propertyEntry;
            _isDefunctOnServer = false;
            _isDefunct = _isDefunctOnServer;

            try
            {
                // initialize the directory entry for the abstract schema class
                _abstractPropertyEntry = DirectoryEntryManager.GetDirectoryEntryInternal(context, "LDAP://" + context.GetServerName() + "/schema/" + ldapDisplayName);
                _iadsProperty = (NativeComInterfaces.IAdsProperty)_abstractPropertyEntry.NativeObject;
            }
            catch (COMException e)
            {
                if (e.ErrorCode == unchecked((int)0x80005000))
                {
                    throw new ActiveDirectoryObjectNotFoundException(SR.DSNotFound, typeof(ActiveDirectorySchemaProperty), ldapDisplayName);
                }
                else
                {
                    throw ExceptionHelper.GetExceptionFromCOMException(context, e);
                }
            }
            catch (InvalidCastException)
            {
                // this means that we found an object but it is not a schema class
                throw new ActiveDirectoryObjectNotFoundException(SR.DSNotFound, typeof(ActiveDirectorySchemaProperty), ldapDisplayName);
            }
            catch (ActiveDirectoryObjectNotFoundException)
            {
                // this is the case where the context is a config set and we could not find an ADAM instance in that config set
                throw new ActiveDirectoryOperationException(SR.Format(SR.ADAMInstanceNotFoundInConfigSet, context.Name));
            }

            // set the bind flag
            this.isBound = true;
        }

        internal ActiveDirectorySchemaProperty(DirectoryContext context, string commonName, SearchResult propertyValuesFromServer, DirectoryEntry schemaEntry)
        {
            _context = context;
            _schemaEntry = schemaEntry;

            // all relevant properties have already been retrieved from the server
            _propertyValuesFromServer = propertyValuesFromServer;
            Debug.Assert(_propertyValuesFromServer != null);
            _propertiesFromSchemaContainerInitialized = true;
            _propertyEntry = GetSchemaPropertyDirectoryEntry();

            // names
            _commonName = commonName;
            _ldapDisplayName = (string)GetValueFromCache(PropertyManager.LdapDisplayName, true)!;

            // this constructor is only called for defunct classes
            _isDefunctOnServer = true;
            _isDefunct = _isDefunctOnServer;

            // set the bind flag
            this.isBound = true;
        }

        internal ActiveDirectorySchemaProperty(DirectoryContext context, string commonName, string ldapDisplayName, DirectoryEntry propertyEntry, DirectoryEntry schemaEntry)
        {
            _context = context;
            _schemaEntry = schemaEntry;
            _propertyEntry = propertyEntry;

            // names
            _commonName = commonName;
            _ldapDisplayName = ldapDisplayName;

            // this constructor is only called for defunct properties
            _isDefunctOnServer = true;
            _isDefunct = _isDefunctOnServer;

            // set the bind flag
            this.isBound = true;
        }

        #endregion constructors

        #region IDisposable

        public void Dispose()
        {
            Dispose(true);
        }

        // private Dispose method
        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                // check if this is an explicit Dispose
                // only then clean up the directory entries
                if (disposing)
                {
                    // dispose schema entry
                    if (_schemaEntry != null)
                    {
                        _schemaEntry.Dispose();
                        _schemaEntry = null;
                    }
                    // dispose property entry
                    if (_propertyEntry != null)
                    {
                        _propertyEntry.Dispose();
                        _propertyEntry = null;
                    }
                    // dispose abstract class entry
                    if (_abstractPropertyEntry != null)
                    {
                        _abstractPropertyEntry.Dispose();
                        _abstractPropertyEntry = null;
                    }
                    // dispose the schema object
                    _schema?.Dispose();
                }

                _disposed = true;
            }
        }
        #endregion IDisposable

        #region public methods
        public static ActiveDirectorySchemaProperty FindByName(DirectoryContext context, string ldapDisplayName)
        {
            ArgumentNullException.ThrowIfNull(context);

            ActiveDirectorySchemaProperty? schemaProperty = null;
            if ((context.Name == null) && (!context.isRootDomain()))
            {
                throw new ArgumentException(SR.ContextNotAssociatedWithDomain, nameof(context));
            }

            if (context.Name != null)
            {
                // the target should be a valid forest name or a server
                if (!((context.isRootDomain()) || (context.isADAMConfigSet()) || (context.isServer())))
                {
                    throw new ArgumentException(SR.NotADOrADAM, nameof(context));
                }
            }

            ArgumentNullException.ThrowIfNull(ldapDisplayName);

            if (ldapDisplayName.Length == 0)
            {
                throw new ArgumentException(SR.EmptyStringParameter, nameof(ldapDisplayName));
            }

            //  work with copy of the context
            context = new DirectoryContext(context);

            // create a schema property
            schemaProperty = new ActiveDirectorySchemaProperty(context, ldapDisplayName, (DirectoryEntry?)null, null);

            return schemaProperty;
        }

        public void Save()
        {
            CheckIfDisposed();

            if (!isBound)
            {
                try
                {
                    // create a new directory entry for this class
                    _schemaEntry ??= DirectoryEntryManager.GetDirectoryEntry(_context, WellKnownDN.SchemaNamingContext);

                    // this will create the class and set the CN value
                    string rdn = "CN=" + _commonName;
                    rdn = Utils.GetEscapedPath(rdn);
                    _propertyEntry = _schemaEntry.Children.Add(rdn, "attributeSchema");
                }
                catch (COMException e)
                {
                    throw ExceptionHelper.GetExceptionFromCOMException(_context, e);
                }
                catch (ActiveDirectoryObjectNotFoundException)
                {
                    // this is the case where the context is a config set and we could not find an ADAM instance in that config set
                    throw new ActiveDirectoryOperationException(SR.Format(SR.ADAMInstanceNotFoundInConfigSet, _context.Name));
                }

                // set the ldap display name property
                SetProperty(PropertyManager.LdapDisplayName, _ldapDisplayName);

                // set the oid value
                SetProperty(PropertyManager.AttributeID, _oid);

                // set the syntax
                if (_syntax != (ActiveDirectorySyntax)(-1))
                {
                    SetSyntax(_syntax);
                }

                // set the description
                SetProperty(PropertyManager.Description, _description);

                // set the isSingleValued attribute
                _propertyEntry.Properties[PropertyManager.IsSingleValued].Value = _isSingleValued;

                // set the isGlobalCatalogReplicated attribute
                _propertyEntry.Properties[PropertyManager.IsMemberOfPartialAttributeSet].Value = _isInGlobalCatalog;

                // set the isDefunct attribute
                _propertyEntry.Properties[PropertyManager.IsDefunct].Value = _isDefunct;

                // set the range lower attribute
                if (_rangeLower != null)
                {
                    _propertyEntry.Properties[PropertyManager.RangeLower].Value = (int)_rangeLower.Value;
                }

                // set the range upper attribute
                if (_rangeUpper != null)
                {
                    _propertyEntry.Properties[PropertyManager.RangeUpper].Value = (int)_rangeUpper.Value;
                }

                // set the searchFlags attribute
                if (_searchFlags != SearchFlags.None)
                {
                    _propertyEntry.Properties[PropertyManager.SearchFlags].Value = (int)_searchFlags;
                }

                // set the link id
                if (_linkId != null)
                {
                    _propertyEntry.Properties[PropertyManager.LinkID].Value = (int)_linkId.Value;
                }

                // set the schemaIDGuid property
                if (_schemaGuidBinaryForm != null)
                {
                    SetProperty(PropertyManager.SchemaIDGuid, _schemaGuidBinaryForm);
                }
            }

            try
            {
                // commit the classEntry to server
                _propertyEntry!.CommitChanges();

                // Refresh the schema cache on the schema role owner
                if (_schema == null)
                {
                    ActiveDirectorySchema schemaObject = ActiveDirectorySchema.GetSchema(_context);
                    bool alreadyUsingSchemaRoleOwnerContext = false;
                    DirectoryServer? schemaRoleOwner = null;

                    try
                    {
                        //
                        // if we are not already talking to the schema role owner, change the context
                        //
                        schemaRoleOwner = schemaObject.SchemaRoleOwner;
                        if (Utils.Compare(schemaRoleOwner.Name, _context.GetServerName()) != 0)
                        {
                            DirectoryContext schemaRoleOwnerContext = Utils.GetNewDirectoryContext(schemaRoleOwner.Name, DirectoryContextType.DirectoryServer, _context);
                            _schema = ActiveDirectorySchema.GetSchema(schemaRoleOwnerContext);
                        }
                        else
                        {
                            alreadyUsingSchemaRoleOwnerContext = true;
                            _schema = schemaObject;
                        }
                    }
                    finally
                    {
                        schemaRoleOwner?.Dispose();
                        if (!alreadyUsingSchemaRoleOwnerContext)
                        {
                            schemaObject.Dispose();
                        }
                    }
                }
                _schema.RefreshSchema();
            }
            catch (COMException e)
            {
                throw ExceptionHelper.GetExceptionFromCOMException(_context, e);
            }

            // now that the changes are committed to the server
            // update the defunct/non-defunct status of the class on the server
            _isDefunctOnServer = _isDefunct;

            // invalidate all properties
            _commonName = null;
            _oid = null;
            _syntaxInitialized = false;
            _descriptionInitialized = false;
            _isSingleValuedInitialized = false;
            _isInGlobalCatalogInitialized = false;
            _rangeLowerInitialized = false;
            _rangeUpperInitialized = false;
            _searchFlagsInitialized = false;
            _linkedPropertyInitialized = false;
            _linkIdInitialized = false;
            _schemaGuidBinaryForm = null;
            _propertiesFromSchemaContainerInitialized = false;

            // set bind flag
            isBound = true;
        }

        public override string ToString() => Name;

        public DirectoryEntry GetDirectoryEntry()
        {
            CheckIfDisposed();

            if (!isBound)
            {
                throw new InvalidOperationException(SR.CannotGetObject);
            }

            GetSchemaPropertyDirectoryEntry();
            Debug.Assert(_propertyEntry != null);

            return DirectoryEntryManager.GetDirectoryEntryInternal(_context, _propertyEntry.Path);
        }
        #endregion public methods

        #region public properties

        public string Name
        {
            get
            {
                CheckIfDisposed();
                return _ldapDisplayName;
            }
        }

        public string? CommonName
        {
            get
            {
                CheckIfDisposed();

                if (isBound)
                {
                    // get the property from the server
                    _commonName ??= (string)GetValueFromCache(PropertyManager.Cn, true)!;
                }
                return _commonName;
            }
            set
            {
                CheckIfDisposed();

                if (value != null && value.Length == 0)
                    throw new ArgumentException(SR.EmptyStringParameter, nameof(value));

                if (isBound)
                {
                    // set the value on the directory entry
                    SetProperty(PropertyManager.Cn, value);
                }
                _commonName = value;
            }
        }

        public string? Oid
        {
            get
            {
                CheckIfDisposed();

                if (isBound)
                {
                    if (_oid == null)
                    {
                        // get the property from the abstract schema/ schema container
                        // (for non-defunt classes this property is available in the abstract schema)
                        if (!_isDefunctOnServer)
                        {
                            try
                            {
                                _oid = _iadsProperty!.OID;
                            }
                            catch (COMException e)
                            {
                                throw ExceptionHelper.GetExceptionFromCOMException(_context, e);
                            }
                        }
                        else
                        {
                            _oid = (string)GetValueFromCache(PropertyManager.AttributeID, true)!;
                        }
                    }
                }
                return _oid;
            }
            set
            {
                CheckIfDisposed();

                if (value != null && value.Length == 0)
                    throw new ArgumentException(SR.EmptyStringParameter, nameof(value));

                if (isBound)
                {
                    // set the value on the directory entry
                    SetProperty(PropertyManager.AttributeID, value);
                }
                _oid = value;
            }
        }

        public ActiveDirectorySyntax Syntax
        {
            get
            {
                CheckIfDisposed();

                if (isBound)
                {
                    if (!_syntaxInitialized)
                    {
                        byte[]? omObjectClassBinaryForm = (byte[]?)GetValueFromCache(PropertyManager.OMObjectClass, false);
                        OMObjectClass? omObjectClass = (omObjectClassBinaryForm != null) ? new OMObjectClass(omObjectClassBinaryForm) : null;

                        _syntax = MapSyntax((string)GetValueFromCache(PropertyManager.AttributeSyntax, true)!,
                            (int)GetValueFromCache(PropertyManager.OMSyntax, true)!,
                            omObjectClass);
                        _syntaxInitialized = true;
                    }
                }
                return _syntax;
            }
            set
            {
                CheckIfDisposed();

                if (value < ActiveDirectorySyntax.CaseExactString || value > ActiveDirectorySyntax.ReplicaLink)
                {
                    throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(ActiveDirectorySyntax));
                }

                if (isBound)
                {
                    // set the value on the directory entry
                    SetSyntax(value);
                }
                _syntax = value;
            }
        }

        public string? Description
        {
            get
            {
                CheckIfDisposed();

                if (isBound)
                {
                    if (!_descriptionInitialized)
                    {
                        // get the property from the server
                        _description = (string?)GetValueFromCache(PropertyManager.Description, false);
                        _descriptionInitialized = true;
                    }
                }
                return _description;
            }
            set
            {
                CheckIfDisposed();

                if (value != null && value.Length == 0)
                    throw new ArgumentException(SR.EmptyStringParameter, nameof(value));

                if (isBound)
                {
                    // set the value on the directory entry
                    SetProperty(PropertyManager.Description, value);
                }
                _description = value;
            }
        }

        public bool IsSingleValued
        {
            get
            {
                CheckIfDisposed();

                if (isBound)
                {
                    if (!_isSingleValuedInitialized)
                    {
                        // get the property from the abstract schema/ schema container
                        // (for non-defunt classes this property is available in the abstract schema)
                        if (!_isDefunctOnServer)
                        {
                            try
                            {
                                _isSingleValued = !_iadsProperty!.MultiValued;
                            }
                            catch (COMException e)
                            {
                                throw ExceptionHelper.GetExceptionFromCOMException(_context, e);
                            }
                        }
                        else
                        {
                            _isSingleValued = (bool)GetValueFromCache(PropertyManager.IsSingleValued, true)!;
                        }
                        _isSingleValuedInitialized = true;
                    }
                }
                return _isSingleValued;
            }
            set
            {
                CheckIfDisposed();

                if (isBound)
                {
                    // get the distinguished name to construct the directory entry
                    GetSchemaPropertyDirectoryEntry();
                    Debug.Assert(_propertyEntry != null);

                    // set the value on the directory entry
                    _propertyEntry.Properties[PropertyManager.IsSingleValued].Value = value;
                }
                _isSingleValued = value;
            }
        }

        public bool IsIndexed
        {
            get
            {
                CheckIfDisposed();

                return IsSetInSearchFlags(SearchFlags.IsIndexed);
            }
            set
            {
                CheckIfDisposed();

                if (value)
                {
                    SetBitInSearchFlags(SearchFlags.IsIndexed);
                }
                else
                {
                    ResetBitInSearchFlags(SearchFlags.IsIndexed);
                }
            }
        }

        public bool IsIndexedOverContainer
        {
            get
            {
                CheckIfDisposed();

                return IsSetInSearchFlags(SearchFlags.IsIndexedOverContainer);
            }
            set
            {
                CheckIfDisposed();

                if (value)
                {
                    SetBitInSearchFlags(SearchFlags.IsIndexedOverContainer);
                }
                else
                {
                    ResetBitInSearchFlags(SearchFlags.IsIndexedOverContainer);
                }
            }
        }

        public bool IsInAnr
        {
            get
            {
                CheckIfDisposed();

                return IsSetInSearchFlags(SearchFlags.IsInAnr);
            }
            set
            {
                CheckIfDisposed();

                if (value)
                {
                    SetBitInSearchFlags(SearchFlags.IsInAnr);
                }
                else
                {
                    ResetBitInSearchFlags(SearchFlags.IsInAnr);
                }
            }
        }

        public bool IsOnTombstonedObject
        {
            get
            {
                CheckIfDisposed();

                return IsSetInSearchFlags(SearchFlags.IsOnTombstonedObject);
            }
            set
            {
                CheckIfDisposed();

                if (value)
                {
                    SetBitInSearchFlags(SearchFlags.IsOnTombstonedObject);
                }
                else
                {
                    ResetBitInSearchFlags(SearchFlags.IsOnTombstonedObject);
                }
            }
        }

        public bool IsTupleIndexed
        {
            get
            {
                CheckIfDisposed();

                return IsSetInSearchFlags(SearchFlags.IsTupleIndexed);
            }
            set
            {
                CheckIfDisposed();

                if (value)
                {
                    SetBitInSearchFlags(SearchFlags.IsTupleIndexed);
                }
                else
                {
                    ResetBitInSearchFlags(SearchFlags.IsTupleIndexed);
                }
            }
        }

        public bool IsInGlobalCatalog
        {
            get
            {
                CheckIfDisposed();

                if (isBound)
                {
                    if (!_isInGlobalCatalogInitialized)
                    {
                        // get the property from the server
                        object? value = GetValueFromCache(PropertyManager.IsMemberOfPartialAttributeSet, false);
                        _isInGlobalCatalog = (value != null) ? (bool)value : false;
                        _isInGlobalCatalogInitialized = true;
                    }
                }
                return _isInGlobalCatalog;
            }
            set
            {
                CheckIfDisposed();

                if (isBound)
                {
                    // get the distinguished name to construct the directory entry
                    GetSchemaPropertyDirectoryEntry();
                    Debug.Assert(_propertyEntry != null);

                    // set the value on the directory entry
                    _propertyEntry.Properties[PropertyManager.IsMemberOfPartialAttributeSet].Value = value;
                }
                _isInGlobalCatalog = value;
            }
        }

        public Nullable<int> RangeLower
        {
            get
            {
                CheckIfDisposed();

                if (isBound)
                {
                    if (!_rangeLowerInitialized)
                    {
                        // get the property from the server
                        // if the property is not set then we will return null
                        object? value = GetValueFromCache(PropertyManager.RangeLower, false);
                        if (value == null)
                        {
                            _rangeLower = null;
                        }
                        else
                        {
                            _rangeLower = (int)value;
                        }
                        _rangeLowerInitialized = true;
                    }
                }
                return _rangeLower;
            }
            set
            {
                CheckIfDisposed();

                if (isBound)
                {
                    // get the distinguished name to construct the directory entry
                    GetSchemaPropertyDirectoryEntry();
                    Debug.Assert(_propertyEntry != null);

                    // set the value on the directory entry
                    if (value == null)
                    {
                        if (_propertyEntry.Properties.Contains(PropertyManager.RangeLower))
                        {
                            _propertyEntry.Properties[PropertyManager.RangeLower].Clear();
                        }
                    }
                    else
                    {
                        _propertyEntry.Properties[PropertyManager.RangeLower].Value = (int)value.Value;
                    }
                }
                _rangeLower = value;
            }
        }

        public Nullable<int> RangeUpper
        {
            get
            {
                CheckIfDisposed();

                if (isBound)
                {
                    if (!_rangeUpperInitialized)
                    {
                        // get the property from the server
                        // if the property is not set then we will return null
                        object? value = GetValueFromCache(PropertyManager.RangeUpper, false);
                        if (value == null)
                        {
                            _rangeUpper = null;
                        }
                        else
                        {
                            _rangeUpper = (int)value;
                        }
                        _rangeUpperInitialized = true;
                    }
                }
                return _rangeUpper;
            }
            set
            {
                CheckIfDisposed();

                if (isBound)
                {
                    // get the distinguished name to construct the directory entry
                    GetSchemaPropertyDirectoryEntry();
                    Debug.Assert(_propertyEntry != null);

                    // set the value on the directory entry
                    if (value == null)
                    {
                        if (_propertyEntry.Properties.Contains(PropertyManager.RangeUpper))
                        {
                            _propertyEntry.Properties[PropertyManager.RangeUpper].Clear();
                        }
                    }
                    else
                    {
                        _propertyEntry.Properties[PropertyManager.RangeUpper].Value = (int)value.Value;
                    }
                }
                _rangeUpper = value;
            }
        }

        public bool IsDefunct
        {
            get
            {
                CheckIfDisposed();
                // this is initialized for bound properties in the constructor
                return _isDefunct;
            }
            set
            {
                CheckIfDisposed();

                if (isBound)
                {
                    // set the value on the directory entry
                    SetProperty(PropertyManager.IsDefunct, value);
                }
                _isDefunct = value;
            }
        }

        public ActiveDirectorySchemaProperty? Link
        {
            get
            {
                CheckIfDisposed();

                if (isBound)
                {
                    if (!_linkedPropertyInitialized)
                    {
                        object? value = GetValueFromCache(PropertyManager.LinkID, false);
                        int tempLinkId = (value != null) ? (int)value : -1;

                        if (tempLinkId != -1)
                        {
                            int linkIdToSearch = tempLinkId - 2 * (tempLinkId % 2) + 1;

                            try
                            {
                                _schemaEntry ??= DirectoryEntryManager.GetDirectoryEntry(_context, WellKnownDN.SchemaNamingContext);

                                string filter = "(&(" + PropertyManager.ObjectCategory + "=attributeSchema)" + "(" + PropertyManager.LinkID + "=" + linkIdToSearch + "))";
                                ReadOnlyActiveDirectorySchemaPropertyCollection linkedProperties = ActiveDirectorySchema.GetAllProperties(_context, _schemaEntry, filter);

                                if (linkedProperties.Count != 1)
                                {
                                    throw new ActiveDirectoryObjectNotFoundException(SR.Format(SR.LinkedPropertyNotFound, linkIdToSearch), typeof(ActiveDirectorySchemaProperty), null);
                                }

                                _linkedProperty = linkedProperties[0];
                            }
                            catch (COMException e)
                            {
                                throw ExceptionHelper.GetExceptionFromCOMException(_context, e);
                            }
                        }
                        _linkedPropertyInitialized = true;
                    }
                }
                return _linkedProperty;
            }
        }

        public Nullable<int> LinkId
        {
            get
            {
                CheckIfDisposed();

                if (isBound)
                {
                    if (!_linkIdInitialized)
                    {
                        object? value = GetValueFromCache(PropertyManager.LinkID, false);
                        // if the property was not set we will return null
                        if (value == null)
                        {
                            _linkId = null;
                        }
                        else
                        {
                            _linkId = (int)value;
                        }
                        _linkIdInitialized = true;
                    }
                }
                return _linkId;
            }
            set
            {
                CheckIfDisposed();

                if (isBound)
                {
                    // get the distinguished name to construct the directory entry
                    GetSchemaPropertyDirectoryEntry();
                    Debug.Assert(_propertyEntry != null);

                    // set the value on the directory entry
                    if (value == null)
                    {
                        if (_propertyEntry.Properties.Contains(PropertyManager.LinkID))
                        {
                            _propertyEntry.Properties[PropertyManager.LinkID].Clear();
                        }
                    }
                    else
                    {
                        _propertyEntry.Properties[PropertyManager.LinkID].Value = (int)value.Value;
                    }
                }
                _linkId = value;
            }
        }

        public Guid SchemaGuid
        {
            get
            {
                CheckIfDisposed();

                if (isBound)
                {
                    // get the property from the server
                    _schemaGuidBinaryForm ??= (byte[])GetValueFromCache(PropertyManager.SchemaIDGuid, true)!;
                }

                // we cache the byte array and create a new guid each time
                return new Guid(_schemaGuidBinaryForm!);
            }
            set
            {
                CheckIfDisposed();

                if (isBound)
                {
                    // set the value on the directory entry
                    SetProperty(PropertyManager.SchemaIDGuid, (value.Equals(Guid.Empty)) ? null : value.ToByteArray());
                }
                _schemaGuidBinaryForm = (value.Equals(Guid.Empty)) ? null : value.ToByteArray();
            }
        }

        #endregion public properties

        #region private methods

        private void CheckIfDisposed()
        {
            if (_disposed)
            {
                throw new ObjectDisposedException(GetType().Name);
            }
        }

        //
        // This method retrieves the value of a property (single valued) from the values
        // that were retrieved from the server. The "mustExist" parameter controls whether or
        // not an exception should be thrown if a value does not exist. If mustExist is true, this
        // will throw an exception is value does not exist.
        //
        private object? GetValueFromCache(string propertyName, bool mustExist)
        {
            object? value = null;

            // retrieve the properties from the server if necessary
            InitializePropertiesFromSchemaContainer();

            Debug.Assert(_propertyValuesFromServer != null);

            ResultPropertyValueCollection? propertyValues = null;
            try
            {
                propertyValues = _propertyValuesFromServer.Properties[propertyName];
                if ((propertyValues == null) || (propertyValues.Count < 1))
                {
                    if (mustExist)
                    {
                        throw new ActiveDirectoryOperationException(SR.Format(SR.PropertyNotFound, propertyName));
                    }
                }
                else
                {
                    value = propertyValues[0];
                }
            }
            catch (COMException e)
            {
                throw ExceptionHelper.GetExceptionFromCOMException(_context, e);
            }

            return value;
        }

        //
        // Just calls the static method GetPropertiesFromSchemaContainer with the correct context
        //
        private void InitializePropertiesFromSchemaContainer()
        {
            if (!_propertiesFromSchemaContainerInitialized)
            {
                _schemaEntry ??= DirectoryEntryManager.GetDirectoryEntry(_context, WellKnownDN.SchemaNamingContext);

                _propertyValuesFromServer = GetPropertiesFromSchemaContainer(_context, _schemaEntry, (_isDefunctOnServer) ? _commonName! : _ldapDisplayName, _isDefunctOnServer);
                _propertiesFromSchemaContainerInitialized = true;
            }
        }

        //
        // This method retrieves properties for this schema class from the schema container
        // on the server. For non-defunct classes only properties that are not available in the abstract
        // schema are retrieved.  For defunct classes, all the properties are retrieved.
        // The retrieved values are stored in a class variable "propertyValuesFromServer" which is a
        // hashtable indexed on the property name.
        //
        internal static SearchResult GetPropertiesFromSchemaContainer(DirectoryContext context, DirectoryEntry schemaEntry, string name, bool isDefunctOnServer)
        {
            SearchResult? propertyValuesFromServer = null;

            //
            // The properties that are loaded from the schemaContainer for non-defunct classes:
            // DistinguishedName
            // CommonName
            // Syntax - AttributeSyntax, OMSyntax, OMObjectClass
            // Description
            // IsIndexed, IsIndexedOverContainer, IsInAnr, IsOnTombstonedObject, IsTupleIndexed - SearchFlags
            // IsInGlobalCatalog - IsMemberOfPartialAttributeSet
            // LinkId (Link)
            // SchemaGuid - SchemaIdGuid
            // RangeLower
            // RangeUpper

            //
            // For defunct class we also load teh remaining properties
            // LdapDisplayName
            // Oid
            // IsSingleValued
            //

            // build the filter
            StringBuilder str = new StringBuilder(15);
            str.Append("(&(");
            str.Append(PropertyManager.ObjectCategory);
            str.Append("=attributeSchema)");
            str.Append('(');
            if (!isDefunctOnServer)
            {
                str.Append(PropertyManager.LdapDisplayName);
            }
            else
            {
                str.Append(PropertyManager.Cn);
            }
            str.Append('=');
            str.Append(Utils.GetEscapedFilterValue(name));
            str.Append(')');
            if (!isDefunctOnServer)
            {
                str.Append("(!(");
            }
            else
            {
                str.Append('(');
            }
            str.Append(PropertyManager.IsDefunct);
            if (!isDefunctOnServer)
            {
                str.Append("=TRUE)))");
            }
            else
            {
                str.Append("=TRUE))");
            }

            string[]? propertiesToLoad = null;
            if (!isDefunctOnServer)
            {
                propertiesToLoad = new string[12];

                propertiesToLoad[0] = PropertyManager.DistinguishedName;
                propertiesToLoad[1] = PropertyManager.Cn;
                propertiesToLoad[2] = PropertyManager.AttributeSyntax;
                propertiesToLoad[3] = PropertyManager.OMSyntax;
                propertiesToLoad[4] = PropertyManager.OMObjectClass;
                propertiesToLoad[5] = PropertyManager.Description;
                propertiesToLoad[6] = PropertyManager.SearchFlags;
                propertiesToLoad[7] = PropertyManager.IsMemberOfPartialAttributeSet;
                propertiesToLoad[8] = PropertyManager.LinkID;
                propertiesToLoad[9] = PropertyManager.SchemaIDGuid;
                propertiesToLoad[10] = PropertyManager.RangeLower;
                propertiesToLoad[11] = PropertyManager.RangeUpper;
            }
            else
            {
                propertiesToLoad = new string[15];

                propertiesToLoad[0] = PropertyManager.DistinguishedName;
                propertiesToLoad[1] = PropertyManager.Cn;
                propertiesToLoad[2] = PropertyManager.AttributeSyntax;
                propertiesToLoad[3] = PropertyManager.OMSyntax;
                propertiesToLoad[4] = PropertyManager.OMObjectClass;
                propertiesToLoad[5] = PropertyManager.Description;
                propertiesToLoad[6] = PropertyManager.SearchFlags;
                propertiesToLoad[7] = PropertyManager.IsMemberOfPartialAttributeSet;
                propertiesToLoad[8] = PropertyManager.LinkID;
                propertiesToLoad[9] = PropertyManager.SchemaIDGuid;
                propertiesToLoad[10] = PropertyManager.AttributeID;
                propertiesToLoad[11] = PropertyManager.IsSingleValued;
                propertiesToLoad[12] = PropertyManager.RangeLower;
                propertiesToLoad[13] = PropertyManager.RangeUpper;
                propertiesToLoad[14] = PropertyManager.LdapDisplayName;
            }

            //
            // Get all the values (don't need to use range retrieval as there are no multivalued attributes)
            //
            ADSearcher searcher = new ADSearcher(schemaEntry, str.ToString(), propertiesToLoad, SearchScope.OneLevel, false /* paged search */, false /* cache results */);

            try
            {
                propertyValuesFromServer = searcher.FindOne();
            }
            catch (COMException e)
            {
                if (e.ErrorCode == unchecked((int)0x80072030))
                {
                    // object is not found since we cannot even find the container in which to search
                    throw new ActiveDirectoryObjectNotFoundException(SR.DSNotFound, typeof(ActiveDirectorySchemaProperty), name);
                }
                else
                {
                    throw ExceptionHelper.GetExceptionFromCOMException(context, e);
                }
            }

            if (propertyValuesFromServer == null)
            {
                throw new ActiveDirectoryObjectNotFoundException(SR.DSNotFound, typeof(ActiveDirectorySchemaProperty), name);
            }

            return propertyValuesFromServer;
        }

        internal DirectoryEntry GetSchemaPropertyDirectoryEntry()
        {
            if (_propertyEntry == null)
            {
                InitializePropertiesFromSchemaContainer();
                _propertyEntry = DirectoryEntryManager.GetDirectoryEntry(_context, (string)GetValueFromCache(PropertyManager.DistinguishedName, true)!);
            }

            return _propertyEntry;
        }

        ///
        /// <summary>
        /// Initializes the search flags attribute value i.e. fetches it from
        /// the directory, if this object is bound.
        /// </summary>
        ///
        private void InitializeSearchFlags()
        {
            if (isBound)
            {
                if (!_searchFlagsInitialized)
                {
                    object? value = GetValueFromCache(PropertyManager.SearchFlags, false);

                    if (value != null)
                    {
                        _searchFlags = (SearchFlags)((int)value);
                    }
                    _searchFlagsInitialized = true;
                }
            }
        }

        private bool IsSetInSearchFlags(SearchFlags searchFlagBit)
        {
            InitializeSearchFlags();
            return (((int)_searchFlags & (int)searchFlagBit) != 0);
        }

        private void SetBitInSearchFlags(SearchFlags searchFlagBit)
        {
            InitializeSearchFlags();
            _searchFlags = (SearchFlags)((int)_searchFlags | (int)searchFlagBit);

            if (isBound)
            {
                // get the distinguished name to construct the directory entry
                GetSchemaPropertyDirectoryEntry();
                Debug.Assert(_propertyEntry != null);

                // set the value on the directory entry
                _propertyEntry.Properties[PropertyManager.SearchFlags].Value = (int)_searchFlags;
            }
        }

        private void ResetBitInSearchFlags(SearchFlags searchFlagBit)
        {
            InitializeSearchFlags();
            _searchFlags = (SearchFlags)((int)_searchFlags & ~((int)searchFlagBit));

            if (isBound)
            {
                // get the distinguished name to construct the directory entry
                GetSchemaPropertyDirectoryEntry();
                Debug.Assert(_propertyEntry != null);

                // set the value on the directory entry
                _propertyEntry.Properties[PropertyManager.SearchFlags].Value = (int)_searchFlags;
            }
        }

        private void SetProperty(string propertyName, object? value)
        {
            // get the distinguished name to construct the directory entry
            GetSchemaPropertyDirectoryEntry();
            Debug.Assert(_propertyEntry != null);

            if (value == null)
            {
                if (_propertyEntry.Properties.Contains(propertyName))
                {
                    _propertyEntry.Properties[propertyName].Clear();
                }
            }
            else
            {
                _propertyEntry.Properties[propertyName].Value = value;
            }
        }

        private ActiveDirectorySyntax MapSyntax(string syntaxId, int oMID, OMObjectClass? oMObjectClass)
        {
            for (int i = 0; i < s_syntaxes.Length; i++)
            {
                if (s_syntaxes[i].Equals(new Syntax(syntaxId, oMID, oMObjectClass)))
                {
                    return (ActiveDirectorySyntax)i;
                }
            }
            throw new ActiveDirectoryOperationException(SR.Format(SR.UnknownSyntax, _ldapDisplayName));
        }

        private void SetSyntax(ActiveDirectorySyntax syntax)
        {
            if ((((int)syntax) < 0) || (((int)syntax) > (s_syntaxes.Length - 1)))
            {
                throw new InvalidEnumArgumentException(nameof(syntax), (int)syntax, typeof(ActiveDirectorySyntax));
            }

            // get the distinguished name to construct the directory entry
            GetSchemaPropertyDirectoryEntry();
            Debug.Assert(_propertyEntry != null);

            _propertyEntry.Properties[PropertyManager.AttributeSyntax].Value = s_syntaxes[(int)syntax].attributeSyntax;
            _propertyEntry.Properties[PropertyManager.OMSyntax].Value = s_syntaxes[(int)syntax].oMSyntax;
            OMObjectClass? oMObjectClass = s_syntaxes[(int)syntax].oMObjectClass;
            if (oMObjectClass != null)
            {
                _propertyEntry.Properties[PropertyManager.OMObjectClass].Value = oMObjectClass.Data;
            }
        }
        #endregion private methods
    }
}