diff --git a/Application/EdFi.Ods.Common/Models/Domain/Association.cs b/Application/EdFi.Ods.Common/Models/Domain/Association.cs index 26a68d61c5..e5d1cbfb1e 100644 --- a/Application/EdFi.Ods.Common/Models/Domain/Association.cs +++ b/Application/EdFi.Ods.Common/Models/Domain/Association.cs @@ -80,7 +80,6 @@ internal Association(DomainModel domainModel, AssociationDefinition associationD IsIdentifying = associationDefinition.IsIdentifying; IsRequired = associationDefinition.IsRequired; - IsRequiredCollection = associationDefinition.Cardinality == Cardinality.OneToOneOrMore; ConstraintByDatabaseEngine = associationDefinition.ConstraintNames; PotentiallyLogical = associationDefinition.PotentiallyLogical; } @@ -100,8 +99,6 @@ internal Association(DomainModel domainModel, AssociationDefinition associationD public bool IsIdentifying { get; } - public bool IsRequiredCollection { get; } - /// /// Indicates whether or not the association should be enforced (this allows a reference to be un-resolvable). /// @@ -146,6 +143,9 @@ public bool IsSelfReferencing => PrimaryEntityFullName.Name.EqualsIgnoreCase(SecondaryEntityFullName.Name) && PrimaryEntityFullName.Schema.Equals(SecondaryEntityFullName.Schema); + /// + /// Indicates that the values of the association are required on the receiving entity. + /// public bool IsRequired { get; } /// diff --git a/Application/EdFi.Ods.Common/Models/Domain/AssociationView.cs b/Application/EdFi.Ods.Common/Models/Domain/AssociationView.cs index c860d746ea..ac12b0e017 100644 --- a/Application/EdFi.Ods.Common/Models/Domain/AssociationView.cs +++ b/Application/EdFi.Ods.Common/Models/Domain/AssociationView.cs @@ -107,10 +107,10 @@ internal AssociationView(DomainModel domainModel, Association association, bool .Where(av => av != null) .Any(av => // An optional collection - (av.AssociationType == AssociationViewType.ManyToOne && !av.Association.IsRequiredCollection) + (av.AssociationType == AssociationViewType.ManyToOne && !av.Inverse.IsRequiredCollection) // An optional incoming one-to-one reference - || (av.AssociationType == AssociationViewType.OneToOneIncoming && !av.IsRequired)); + || (av.AssociationType == AssociationViewType.OneToOneIncoming && !av.IsRequiredIncomingAssociation)); if (isContainingEntityPresenceOptional) { @@ -121,7 +121,7 @@ internal AssociationView(DomainModel domainModel, Association association, bool if (AssociationType == AssociationViewType.ManyToOne || AssociationType == AssociationViewType.OneToOneIncoming) { - return !IsRequired; + return !Association.IsRequired; } // All other associations do not represent dependencies @@ -436,14 +436,33 @@ public string RoleName /// public AssociationView Inverse { get; internal set; } - public bool IsRequired + /// + /// Indicates that the properties of an incoming association are required on the receiving entity (for outgoing associations, + /// see and ). + /// + public bool IsRequiredIncomingAssociation { - get { return Association.IsRequired; } + get => Association.IsRequired && AssociationType.IsIncoming(); } + /// + /// Indicates that the association view represents an outgoing one-to-one relationship (single object) with an entity that must exist. + /// + public bool IsRequiredEmbeddedObject + { + get => + AssociationType == AssociationViewType.OneToOneOutgoing + && Association.Cardinality == Cardinality.OneToOne; + } + + /// + /// Indicates that the association view represents an outgoing one-to-many relationship (collection) that must contain at least one item. + /// public bool IsRequiredCollection { - get { return Association.IsRequiredCollection; } + get => + AssociationType == AssociationViewType.OneToMany + && Association.Cardinality == Cardinality.OneToOneOrMore; } FullName IHasNameContext.ParentFullName diff --git a/Application/EdFi.Ods.Common/Models/Domain/AssociationViewType.cs b/Application/EdFi.Ods.Common/Models/Domain/AssociationViewType.cs index baaa8822b7..8c6dfe890a 100644 --- a/Application/EdFi.Ods.Common/Models/Domain/AssociationViewType.cs +++ b/Application/EdFi.Ods.Common/Models/Domain/AssociationViewType.cs @@ -16,4 +16,23 @@ public enum AssociationViewType ToExtension, FromCore } + + public static class AssociationViewTypeExtensions + { + public static bool IsIncoming(this AssociationViewType associationViewType) + { + return (associationViewType == AssociationViewType.FromBase + || associationViewType == AssociationViewType.FromCore + || associationViewType == AssociationViewType.OneToOneIncoming + || associationViewType == AssociationViewType.ManyToOne); + } + + public static bool IsOutgoing(this AssociationViewType associationViewType) + { + return (associationViewType == AssociationViewType.ToDerived + || associationViewType == AssociationViewType.ToExtension + || associationViewType == AssociationViewType.OneToMany + || associationViewType == AssociationViewType.OneToOneOutgoing); + } + } } diff --git a/Application/EdFi.Ods.Common/Models/Resource/Reference.cs b/Application/EdFi.Ods.Common/Models/Resource/Reference.cs index 402d44a698..217c682fe5 100644 --- a/Application/EdFi.Ods.Common/Models/Resource/Reference.cs +++ b/Application/EdFi.Ods.Common/Models/Resource/Reference.cs @@ -82,7 +82,7 @@ public Resource ReferencedResource public bool IsRequired { - get { return Association.IsRequired; } + get { return Association.IsRequiredIncomingAssociation; } } /// diff --git a/Application/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataDefinitionsFactory.cs b/Application/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataDefinitionsFactory.cs index 7882a9ee4b..b4408da046 100644 --- a/Application/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataDefinitionsFactory.cs +++ b/Application/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataDefinitionsFactory.cs @@ -156,7 +156,7 @@ private Schema CreateResourceChildSchema(ResourceChildItem resourceChildItem, Op resourceChildItem.EmbeddedObjects.Select( e => new { - IsRequired = e.Association.IsRequired, + IsRequired = e.Association?.IsRequiredEmbeddedObject ?? false, Key = e.JsonPropertyName, Schema = CreateEmbeddedObjectSchema(e, openApiMetadataResource) })).ToList(); @@ -324,6 +324,7 @@ private Schema CreateResourceSchema(OpenApiMetadataResource openApiMetadataResou x => new PropertySchemaInfo { PropertyName = x.JsonPropertyName, + IsRequired = x.Association?.IsRequiredEmbeddedObject ?? false, Sort = SortOrder(x.PropertyName, false), Schema = CreateEmbeddedObjectSchema(x, openApiMetadataResource) })).Concat( diff --git a/Application/EdFi.Ods.Tests/EdFi.Ods.Common/Models/Domain/AssociationViewTests.cs b/Application/EdFi.Ods.Tests/EdFi.Ods.Common/Models/Domain/AssociationViewTests.cs index 124438ac04..6bc238d8f5 100644 --- a/Application/EdFi.Ods.Tests/EdFi.Ods.Common/Models/Domain/AssociationViewTests.cs +++ b/Application/EdFi.Ods.Tests/EdFi.Ods.Common/Models/Domain/AssociationViewTests.cs @@ -2522,25 +2522,25 @@ protected override void Act() .ToList(); _nonNavigableOptionalManyToOneAssociations = allAssociations.Where(a => - !a.IsNavigable && !a.IsSelfReferencing && !a.IsRequired + !a.IsNavigable && !a.IsSelfReferencing && !a.Association.IsRequired && a.AssociationType == AssociationViewType.ManyToOne) .ToList(); _nonNavigableOptionalOneToOneIncomingAssociations = allAssociations.Where(a => - a.AssociationType == AssociationViewType.OneToOneIncoming && !a.IsNavigable && !a.IsRequired) + a.AssociationType == AssociationViewType.OneToOneIncoming && !a.IsNavigable && !a.Association.IsRequired) .ToList(); _topLevelRequiredManyToOneAssociations = allAssociations.Where( a => a.AssociationType == AssociationViewType.ManyToOne && !a.IsNavigable - && a.IsRequired + && a.Association.IsRequired && a.ThisEntity.IsAggregateRoot) .ToList(); var childRequiredManyToOneAssociations = allAssociations.Where( a => a.AssociationType == AssociationViewType.ManyToOne && !a.IsNavigable - && a.IsRequired + && a.Association.IsRequired && !a.ThisEntity.IsAggregateRoot) .ToList(); @@ -2564,37 +2564,37 @@ protected override void Act() _requiredEmbeddedObjectsWithRequiredAssociations = childRequiredManyToOneAssociations .Where(a => a.ThisEntity.ParentAssociation.AssociationType == AssociationViewType.OneToOneIncoming) .Where(a => - a.ThisEntity.ParentAssociation.Inverse.IsRequired + a.ThisEntity.ParentAssociation.Inverse.Association.IsRequired // Check one level up for required child items (if it's in the there) && (a.ThisEntity.Parent.Parent == null || (a.ThisEntity.Parent.ParentAssociation.AssociationType == AssociationViewType.ManyToOne && a.ThisEntity.Parent.ParentAssociation.Inverse.IsRequiredCollection) || (a.ThisEntity.Parent.ParentAssociation.AssociationType == AssociationViewType.OneToOneIncoming - && a.ThisEntity.Parent.ParentAssociation.Inverse.IsRequired)) + && a.ThisEntity.Parent.ParentAssociation.Inverse.Association.IsRequired)) // Check two levels up for required child items (if it's in the there) && (a.ThisEntity.Parent.Parent?.Parent == null || (a.ThisEntity.Parent.Parent.ParentAssociation.AssociationType == AssociationViewType.ManyToOne && a.ThisEntity.Parent.Parent.ParentAssociation.Inverse.IsRequiredCollection) || (a.ThisEntity.Parent.Parent.ParentAssociation.AssociationType == AssociationViewType.OneToOneIncoming - && a.ThisEntity.Parent.Parent.ParentAssociation.Inverse.IsRequired))) + && a.ThisEntity.Parent.Parent.ParentAssociation.Inverse.Association.IsRequired))) .ToList(); _optionalEmbeddedObjectsWithRequiredAssociations = childRequiredManyToOneAssociations .Where(a => a.ThisEntity.ParentAssociation.AssociationType == AssociationViewType.OneToOneIncoming) .Where(a => - !a.ThisEntity.ParentAssociation.Inverse.IsRequired + !a.ThisEntity.ParentAssociation.Inverse.Association.IsRequired // Check one level up for optional child items (if it's in the there) || (a.ThisEntity.Parent.Parent != null && ((a.ThisEntity.Parent.ParentAssociation.AssociationType == AssociationViewType.ManyToOne && !a.ThisEntity.Parent.ParentAssociation.Inverse.IsRequiredCollection) - || (a.ThisEntity.Parent.ParentAssociation.AssociationType == AssociationViewType.OneToOneIncoming && !a.ThisEntity.Parent.ParentAssociation.Inverse.IsRequired))) + || (a.ThisEntity.Parent.ParentAssociation.AssociationType == AssociationViewType.OneToOneIncoming && !a.ThisEntity.Parent.ParentAssociation.Inverse.Association.IsRequired))) // Check two levels up for optional child items (if it's in the there) || (a.ThisEntity.Parent.Parent?.Parent != null && ((a.ThisEntity.Parent.Parent.ParentAssociation.AssociationType == AssociationViewType.ManyToOne && !a.ThisEntity.Parent.Parent.ParentAssociation.Inverse.IsRequiredCollection) - || (a.ThisEntity.Parent.Parent.ParentAssociation.AssociationType == AssociationViewType.OneToOneIncoming && !a.ThisEntity.Parent.Parent.ParentAssociation.Inverse.IsRequired)))) + || (a.ThisEntity.Parent.Parent.ParentAssociation.AssociationType == AssociationViewType.OneToOneIncoming && !a.ThisEntity.Parent.Parent.ParentAssociation.Inverse.Association.IsRequired)))) .ToList(); _untestedAssociations = allAssociations @@ -2884,7 +2884,7 @@ public void Should_indicate_that_a_required_reference_on_a_required_embedded_obj { var association = _resourceModel.GetAllResources() .SelectMany(res => res.EmbeddedObjects) - .Where(eo => eo.Association.IsRequired) + .Where(eo => eo.Association.Association.IsRequired) .SelectMany(eo => eo.ObjectType.References.Where(r => r.IsRequired)) .Select(r => r.Association) .FirstOrDefault(); diff --git a/Utilities/CodeGeneration/EdFi.Ods.CodeGen/Generators/DatabaseMetadataProvider.cs b/Utilities/CodeGeneration/EdFi.Ods.CodeGen/Generators/DatabaseMetadataProvider.cs index 3bc2f927ba..ab0fb6c80f 100644 --- a/Utilities/CodeGeneration/EdFi.Ods.CodeGen/Generators/DatabaseMetadataProvider.cs +++ b/Utilities/CodeGeneration/EdFi.Ods.CodeGen/Generators/DatabaseMetadataProvider.cs @@ -29,6 +29,7 @@ protected override object Build() bool AssociationIsNotOneToOne(Association a) => a.Cardinality != Cardinality.OneToOne + && a.Cardinality != Cardinality.OneToZeroOrOne && a.Cardinality != Cardinality.OneToOneInheritance && a.Cardinality != Cardinality.OneToOneExtension; @@ -80,7 +81,7 @@ DatabaseMetadata DatabaseMetadataForAssociation(Association a) return new { NamespaceName = EdFiConventions.BuildNamespace(BaseNamespaceName, TemplateContext.SchemaProperCaseName), - IndexMetaData = databaseMetadata.OrderBy(x => x.TableName).ThenBy(x => x.Name) + IndexMetadata = databaseMetadata.OrderBy(x => x.TableName).ThenBy(x => x.Name) }; } } diff --git a/Utilities/CodeGeneration/EdFi.Ods.CodeGen/Generators/Entities.cs b/Utilities/CodeGeneration/EdFi.Ods.CodeGen/Generators/Entities.cs index 72eabfb40a..112c748abe 100644 --- a/Utilities/CodeGeneration/EdFi.Ods.CodeGen/Generators/Entities.cs +++ b/Utilities/CodeGeneration/EdFi.Ods.CodeGen/Generators/Entities.cs @@ -357,6 +357,7 @@ private IEnumerable GetClasses(Aggregate aggregate, Entity entity) ClassName = entity.Name, NamespacePrefix = GetCommonRelativeNamespacePrefix(entity), OtherClassName = a.OtherEntity.Name, OtherNamespacePrefix = GetCommonRelativeNamespacePrefix(a.OtherEntity), + IsRequired = a.IsRequiredEmbeddedObject, }), RelocatedExtensionOneToOnes = entity.EdFiStandardEntityAssociation?.OtherEntity.NavigableOneToOnes .Where(a => a.OtherEntity.Schema == entity.Schema) diff --git a/Utilities/CodeGeneration/EdFi.Ods.CodeGen/Generators/Resources/PropertyData.cs b/Utilities/CodeGeneration/EdFi.Ods.CodeGen/Generators/Resources/PropertyData.cs index 5310455788..d182a18a8d 100644 --- a/Utilities/CodeGeneration/EdFi.Ods.CodeGen/Generators/Resources/PropertyData.cs +++ b/Utilities/CodeGeneration/EdFi.Ods.CodeGen/Generators/Resources/PropertyData.cs @@ -157,7 +157,7 @@ public object Render() ? AssembleOtherUnifiedChild(ExtensionAssociations) : null, ImplicitPropertyName = Associations.Any() - ? Associations.OrderByDescending(x => x.IsRequired) + ? Associations.OrderByDescending(x => x.Association.IsRequired) .First() .Name : null, diff --git a/Utilities/CodeGeneration/EdFi.Ods.CodeGen/Mustache/Entities.mustache b/Utilities/CodeGeneration/EdFi.Ods.CodeGen/Mustache/Entities.mustache index 522a98b95c..b2bab3b90e 100644 --- a/Utilities/CodeGeneration/EdFi.Ods.CodeGen/Mustache/Entities.mustache +++ b/Utilities/CodeGeneration/EdFi.Ods.CodeGen/Mustache/Entities.mustache @@ -233,7 +233,7 @@ namespace {{AggregateNamespace}} // One-to-one relationships // ------------------------------------------------------------- {{#OneToOnes}} - [ValidateObject] + {{#IsRequired}}[Required]{{/IsRequired}}[ValidateObject] public virtual {{AggregateNamespacePrefix}}{{OtherClassName}} {{OtherClassName}} { get