public void ValidateRelationships()

in src/Core/Configurations/RuntimeConfigValidator.cs [876:1044]


    public void ValidateRelationships(RuntimeConfig runtimeConfig, IMetadataProviderFactory sqlMetadataProviderFactory)
    {
        // To avoid creating many lists of invalid columns we instantiate before looping through entities.
        // List.Clear() is O(1) so clearing the list, for re-use, inside of the loops is fine.
        List<string> invalidColumns = new();

        // Loop through each entity in the config and verify its relationship.
        foreach ((string entityName, Entity entity) in runtimeConfig.Entities)
        {
            // Skipping relationship validation if entity has no relationship
            // or if graphQL is disabled.
            if (entity.Relationships is null || !entity.GraphQL.Enabled)
            {
                continue;
            }

            string databaseName = runtimeConfig.GetDataSourceNameFromEntityName(entityName);
            ISqlMetadataProvider sqlMetadataProvider = sqlMetadataProviderFactory.GetMetadataProvider(databaseName);

            foreach ((string relationshipName, EntityRelationship relationship) in entity.Relationships!)
            {
                ValidateSourceAndTargetFieldsAsBackingColumns(
                    entityName: entityName,
                    relationshipName: relationshipName,
                    relationship: relationship,
                    sqlMetadataProvider: sqlMetadataProvider,
                    invalidColumns: invalidColumns);

                // Validation to ensure DatabaseObject is correctly inferred from the entity name.
                DatabaseObject? sourceObject, targetObject;
                if (!sqlMetadataProvider.EntityToDatabaseObject.TryGetValue(entityName, out sourceObject))
                {
                    sourceObject = null;
                    HandleOrRecordException(new DataApiBuilderException(
                        message: $"Could not infer database object for source entity: {entityName} in relationship: {relationshipName}." +
                            $" Check if the entity: {entityName} is correctly defined in the config.",
                        statusCode: HttpStatusCode.ServiceUnavailable,
                        subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
                }

                if (!sqlMetadataProvider.EntityToDatabaseObject.TryGetValue(relationship.TargetEntity, out targetObject))
                {
                    targetObject = null;
                    HandleOrRecordException(new DataApiBuilderException(
                        message: $"Could not infer database object for target entity: {relationship.TargetEntity} in relationship: {relationshipName}." +
                            $" Check if the entity: {relationship.TargetEntity} is correctly defined in the config.",
                        statusCode: HttpStatusCode.ServiceUnavailable,
                        subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
                }

                if (sourceObject is null || targetObject is null)
                {
                    continue;
                }

                DatabaseTable sourceDatabaseObject = (DatabaseTable)sourceObject;
                DatabaseTable targetDatabaseObject = (DatabaseTable)targetObject;
                if (relationship.LinkingObject is not null)
                {
                    (string linkingTableSchema, string linkingTableName) = sqlMetadataProvider.ParseSchemaAndDbTableName(relationship.LinkingObject)!;
                    DatabaseTable linkingDatabaseObject = new(linkingTableSchema, linkingTableName);

                    if (relationship.LinkingSourceFields is null || relationship.SourceFields is null)
                    {
                        if (!sqlMetadataProvider.VerifyForeignKeyExistsInDB(linkingDatabaseObject, sourceDatabaseObject))
                        {
                            HandleOrRecordException(new DataApiBuilderException(
                            message: $"Could not find relationship between Linking Object: {relationship.LinkingObject}" +
                                $" and entity: {entityName}.",
                            statusCode: HttpStatusCode.ServiceUnavailable,
                            subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
                        }
                    }

                    if (relationship.LinkingTargetFields is null || relationship.TargetFields is null)
                    {
                        if (!sqlMetadataProvider.VerifyForeignKeyExistsInDB(linkingDatabaseObject, targetDatabaseObject))
                        {
                            HandleOrRecordException(new DataApiBuilderException(
                            message: $"Could not find relationship between Linking Object: {relationship.LinkingObject}" +
                                $" and entity: {relationship.TargetEntity}.",
                            statusCode: HttpStatusCode.ServiceUnavailable,
                            subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
                        }
                    }

                    if (!_runtimeConfigProvider.IsLateConfigured)
                    {
                        string sourceDBOName = sqlMetadataProvider.EntityToDatabaseObject[entityName].FullName;
                        string targetDBOName = sqlMetadataProvider.EntityToDatabaseObject[relationship.TargetEntity].FullName;
                        string cardinality = relationship.Cardinality.ToString().ToLower();
                        RelationShipPair linkedSourceRelationshipPair = new(linkingDatabaseObject, sourceDatabaseObject);
                        RelationShipPair linkedTargetRelationshipPair = new(linkingDatabaseObject, targetDatabaseObject);
                        ForeignKeyDefinition? fKDef;
                        string referencedSourceColumns = relationship.SourceFields is not null ? string.Join(",", relationship.SourceFields) :
                            sqlMetadataProvider.PairToFkDefinition!.TryGetValue(linkedSourceRelationshipPair, out fKDef) ?
                            string.Join(",", fKDef.ReferencedColumns) : string.Empty;
                        string referencingSourceColumns = relationship.LinkingSourceFields is not null ? string.Join(",", relationship.LinkingSourceFields) :
                            sqlMetadataProvider.PairToFkDefinition!.TryGetValue(linkedSourceRelationshipPair, out fKDef) ?
                            string.Join(",", fKDef.ReferencingColumns) : string.Empty;
                        string referencedTargetColumns = relationship.TargetFields is not null ? string.Join(",", relationship.TargetFields) :
                            sqlMetadataProvider.PairToFkDefinition!.TryGetValue(linkedTargetRelationshipPair, out fKDef) ?
                            string.Join(",", fKDef.ReferencedColumns) : string.Empty;
                        string referencingTargetColumns = relationship.LinkingTargetFields is not null ? string.Join(",", relationship.LinkingTargetFields) :
                            sqlMetadataProvider.PairToFkDefinition!.TryGetValue(linkedTargetRelationshipPair, out fKDef) ?
                            string.Join(",", fKDef.ReferencingColumns) : string.Empty;

                        _logger.LogDebug(
                            message: "{entityName}: {sourceDBOName}({referencedSourceColumns}) is related to {cardinality} " +
                            "{relationship.TargetEntity}: {targetDBOName}({referencedTargetColumns}) by " +
                            "{relationship.LinkingObject}(linking.source.fields: {referencingSourceColumns}), (linking.target.fields: {referencingTargetColumns})",
                            entityName,
                            sourceDBOName,
                            referencedSourceColumns,
                            cardinality,
                            relationship.TargetEntity,
                            targetDBOName,
                            referencedTargetColumns,
                            relationship.LinkingObject,
                            referencingSourceColumns,
                            referencingTargetColumns);
                    }
                }

                if (relationship.LinkingObject is null
                    && (relationship.SourceFields is null || relationship.TargetFields is null))
                {
                    if (!sqlMetadataProvider.VerifyForeignKeyExistsInDB(sourceDatabaseObject, targetDatabaseObject))
                    {
                        HandleOrRecordException(new DataApiBuilderException(
                            message: $"Could not find relationship between entities: {entityName} and {relationship.TargetEntity}.",
                            statusCode: HttpStatusCode.ServiceUnavailable,
                            subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
                    }
                }

                if (relationship.LinkingObject is null && !_runtimeConfigProvider.IsLateConfigured)
                {
                    RelationShipPair sourceTargetRelationshipPair = new(sourceDatabaseObject, targetDatabaseObject);
                    RelationShipPair targetSourceRelationshipPair = new(targetDatabaseObject, sourceDatabaseObject);
                    string sourceDBOName = sqlMetadataProvider.EntityToDatabaseObject[entityName].FullName;
                    string targetDBOName = sqlMetadataProvider.EntityToDatabaseObject[relationship.TargetEntity].FullName;
                    string cardinality = relationship.Cardinality.ToString().ToLower();
                    ForeignKeyDefinition? fKDef;
                    string sourceColumns = relationship.SourceFields is not null ? string.Join(",", relationship.SourceFields) :
                        sqlMetadataProvider.PairToFkDefinition!.TryGetValue(sourceTargetRelationshipPair, out fKDef) ?
                        string.Join(",", fKDef.ReferencingColumns) :
                        sqlMetadataProvider.PairToFkDefinition!.TryGetValue(targetSourceRelationshipPair, out fKDef) ?
                        string.Join(",", fKDef.ReferencedColumns) : string.Empty;
                    string targetColumns = relationship.TargetFields is not null ? string.Join(",", relationship.TargetFields) :
                        sqlMetadataProvider.PairToFkDefinition!.TryGetValue(sourceTargetRelationshipPair, out fKDef) ?
                        string.Join(",", fKDef.ReferencedColumns) :
                        sqlMetadataProvider.PairToFkDefinition!.TryGetValue(targetSourceRelationshipPair, out fKDef) ?
                        string.Join(",", fKDef.ReferencingColumns) : string.Empty;

                    _logger.LogDebug(
                        message: "{entityName}: {sourceDBOName}({sourceColumns}) is related to {cardinality} {relationshipTargetEntity}: {targetDBOName}({targetColumns}).",
                        entityName,
                        sourceDBOName,
                        sourceColumns,
                        cardinality,
                        relationship.TargetEntity,
                        targetDBOName,
                        targetColumns
                        );
                }
            }
        }
    }