public static void FixupForeignKeys(Association element) { List <ModelAttribute> fkProperties = element.Source.Attributes.Where(x => x.IsForeignKeyFor == element.Id) .Union(element.Target.Attributes.Where(x => x.IsForeignKeyFor == element.Id)) .ToList(); // EF6 can't have declared foreign keys for 1..1 / 0-1..1 / 1..0-1 / 0-1..0-1 relationships if (!string.IsNullOrEmpty(element.FKPropertyName) && element.Source.ModelRoot.EntityFrameworkVersion == EFVersion.EF6 && element.SourceMultiplicity != Multiplicity.ZeroMany && element.TargetMultiplicity != Multiplicity.ZeroMany) { element.FKPropertyName = null; } // if no FKs, remove all the attributes for this element if (string.IsNullOrEmpty(element.FKPropertyName) || element.Dependent == null) { List <ModelAttribute> unnecessaryProperties = fkProperties.Where(x => !x.IsIdentity).ToList(); if (unnecessaryProperties.Any()) { WarningDisplay.Show($"{element.GetDisplayText()} doesn't specify defined foreign keys. Removing foreign key attribute(s) {string.Join(", ", unnecessaryProperties.Select(x => x.GetDisplayText()))}"); } foreach (ModelAttribute attribute in unnecessaryProperties) { attribute.ClearFKMods(string.Empty); attribute.Delete(); } return; } // synchronize what's there to what should be there string[] currentForeignKeyPropertyNames = element.GetForeignKeyPropertyNames(); (IEnumerable <string> add, IEnumerable <ModelAttribute> remove) = fkProperties.Synchronize(currentForeignKeyPropertyNames, (attribute, name) => attribute.Name == name); List <ModelAttribute> removeList = remove.ToList(); fkProperties = fkProperties.Except(removeList).ToList(); // remove extras if (removeList.Any()) { WarningDisplay.Show($"{element.GetDisplayText()} has extra foreign keys. Removing unnecessary foreign key attribute(s) {string.Join(", ", removeList.Select(x => x.GetDisplayText()))}"); } for (int index = 0; index < removeList.Count; index++) { ModelAttribute attribute = removeList[index]; attribute.ClearFKMods(string.Empty); attribute.Delete(); removeList.RemoveAt(index--); } // reparent existing properties if needed foreach (ModelAttribute existing in fkProperties.Where(x => x.ModelClass != element.Dependent)) { existing.ClearFKMods(); existing.ModelClass.MoveAttribute(existing, element.Dependent); existing.SetFKMods(element); } // create new properties if they don't already exist foreach (string propertyName in add.Where(n => element.Dependent.Attributes.All(a => a.Name != n))) { element.Dependent.Attributes.Add(new ModelAttribute(element.Store, new PropertyAssignment(ModelAttribute.NameDomainPropertyId, propertyName))); } // make a pass through and fixup the types, summaries, etc. based on the principal's identity attributes ModelAttribute[] principalIdentityAttributes = element.Principal.AllIdentityAttributes.ToArray(); string summaryBoilerplate = element.GetSummaryBoilerplate(); for (int index = 0; index < currentForeignKeyPropertyNames.Length; index++) { ModelAttribute fkProperty = element.Dependent.Attributes.First(x => x.Name == currentForeignKeyPropertyNames[index]); ModelAttribute idProperty = principalIdentityAttributes[index]; bool required = element.Dependent == element.Source ? element.TargetMultiplicity == Multiplicity.One : element.SourceMultiplicity == Multiplicity.One; fkProperty.SetFKMods(element , summaryBoilerplate , required , idProperty.Type); } }
/// <inheritdoc /> public override void ElementPropertyChanged(ElementPropertyChangedEventArgs e) { base.ElementPropertyChanged(e); Association element = (Association)e.ModelElement; if (element.IsDeleted) { return; } Store store = element.Store; Transaction current = store.TransactionManager.CurrentTransaction; if (current.IsSerializing || ModelRoot.BatchUpdating) { return; } if (Equals(e.NewValue, e.OldValue)) { return; } List <string> errorMessages = EFCoreValidator.GetErrors(element).ToList(); BidirectionalAssociation bidirectionalAssociation = element as BidirectionalAssociation; using (Transaction inner = store.TransactionManager.BeginTransaction("Redraw Association")) { switch (e.DomainProperty.Name) { case "FKPropertyName": { string fkPropertyName = e.NewValue?.ToString(); bool fkPropertyError = false; // these can be multiples, separated by a comma string[] priorForeignKeyPropertyNames = e.OldValue?.ToString().Split(',').Select(n => n.Trim()).ToArray() ?? new string[0]; IEnumerable <ModelAttribute> priorForeignKeyModelAttributes = string.IsNullOrEmpty(e.OldValue?.ToString()) ? Array.Empty <ModelAttribute>() : priorForeignKeyPropertyNames .Select(oldValue => element.Dependent.Attributes.FirstOrDefault(a => a.Name == oldValue)) .Where(x => x != null) .ToArray(); string summaryBoilerplate = element.GetSummaryBoilerplate(); if (!string.IsNullOrEmpty(fkPropertyName)) { string tag = $"({element.Source.Name}:{element.Target.Name})"; if (element.Dependent == null) { errorMessages.Add($"{tag} can't have foreign keys defined; no dependent role found"); break; } string[] foreignKeyPropertyNames = element.GetForeignKeyPropertyNames(); int propertyCount = foreignKeyPropertyNames.Length; int identityCount = element.Principal.AllIdentityAttributes.Count(); if (propertyCount != identityCount) { errorMessages.Add($"{tag} foreign key must have zero or {identityCount} {(identityCount == 1 ? "property" : "properties")} defined, since " + $"{element.Principal.Name} has {identityCount} identity properties; found {propertyCount} instead"); fkPropertyError = true; } // validate names foreach (string propertyName in foreignKeyPropertyNames) { if (!CodeGenerator.IsValidLanguageIndependentIdentifier(propertyName)) { errorMessages.Add($"{tag} FK property name '{propertyName}' isn't a valid .NET identifier"); fkPropertyError = true; } if (element.Dependent.AllAttributes.Except(element.Dependent.Attributes).Any(a => a.Name == propertyName)) { errorMessages.Add($"{tag} FK property name '{propertyName}' is used in a base class of {element.Dependent.Name}"); fkPropertyError = true; } } fkPropertyError &= CheckFkAutoIdentityErrors(element, errorMessages); if (!fkPropertyError) { // remove any flags and locks on the attributes that were foreign keys foreach (ModelAttribute modelAttribute in priorForeignKeyModelAttributes) { modelAttribute.ClearFKData(summaryBoilerplate); } element.EnsureForeignKeyAttributes(); IEnumerable <ModelAttribute> currentForeignKeyModelAttributes = foreignKeyPropertyNames.Select(newValue => element.Dependent.Attributes.FirstOrDefault(a => a.Name == newValue)); foreach (ModelAttribute modelAttribute in currentForeignKeyModelAttributes) { modelAttribute.SetFKData(summaryBoilerplate); } } } else { // foreign key was removed // remove locks foreach (ModelAttribute modelAttribute in priorForeignKeyModelAttributes) { modelAttribute.ClearFKData(summaryBoilerplate); } } } break; case "SourceCustomAttributes": if (bidirectionalAssociation != null && !string.IsNullOrWhiteSpace(bidirectionalAssociation.SourceCustomAttributes)) { bidirectionalAssociation.SourceCustomAttributes = $"[{bidirectionalAssociation.SourceCustomAttributes.Trim('[', ']')}]"; CheckSourceForDisplayText(bidirectionalAssociation); } break; case "SourceDisplayText": if (bidirectionalAssociation != null) { CheckSourceForDisplayText(bidirectionalAssociation); } break; case "SourceMultiplicity": Multiplicity sourceMultiplicity = (Multiplicity)e.NewValue; // change unidirectional source cardinality // if target is dependent // source cardinality is 0..1 or 1 if (element.Target.IsDependentType && sourceMultiplicity == Multiplicity.ZeroMany) { errorMessages.Add($"Can't have a 0..* association from {element.Target.Name} to dependent type {element.Source.Name}"); break; } if ((sourceMultiplicity == Multiplicity.One && element.TargetMultiplicity == Multiplicity.One) || (sourceMultiplicity == Multiplicity.ZeroOne && element.TargetMultiplicity == Multiplicity.ZeroOne)) { if (element.SourceRole != EndpointRole.NotSet) { element.SourceRole = EndpointRole.NotSet; } if (element.TargetRole != EndpointRole.NotSet) { element.TargetRole = EndpointRole.NotSet; } } else { SetEndpointRoles(element); } // cascade delete behavior could now be illegal. Reset to default element.SourceDeleteAction = DeleteAction.Default; element.TargetDeleteAction = DeleteAction.Default; break; case "SourcePropertyName": string sourcePropertyNameErrorMessage = ValidateAssociationIdentifier(element, element.Target, (string)e.NewValue); if (EFModelDiagram.IsDropping && sourcePropertyNameErrorMessage != null) { element.Delete(); } else { errorMessages.Add(sourcePropertyNameErrorMessage); } break; case "SourceRole": if (element.Source.IsDependentType) { element.SourceRole = EndpointRole.Dependent; element.TargetRole = EndpointRole.Principal; } else if (!SetEndpointRoles(element)) { if (element.SourceRole == EndpointRole.Dependent && element.TargetRole != EndpointRole.Principal) { element.TargetRole = EndpointRole.Principal; } else if (element.SourceRole == EndpointRole.Principal && element.TargetRole != EndpointRole.Dependent) { element.TargetRole = EndpointRole.Dependent; } } break; case "TargetCustomAttributes": if (!string.IsNullOrWhiteSpace(element.TargetCustomAttributes)) { element.TargetCustomAttributes = $"[{element.TargetCustomAttributes.Trim('[', ']')}]"; CheckTargetForDisplayText(element); } break; case "TargetDisplayText": CheckTargetForDisplayText(element); break; case "TargetMultiplicity": Multiplicity newTargetMultiplicity = (Multiplicity)e.NewValue; // change unidirectional target cardinality // if target is dependent // target cardinality must be 0..1 or 1 if (element.Target.IsDependentType && newTargetMultiplicity == Multiplicity.ZeroMany) { errorMessages.Add($"Can't have a 0..* association from {element.Source.Name} to dependent type {element.Target.Name}"); break; } if ((element.SourceMultiplicity == Multiplicity.One && newTargetMultiplicity == Multiplicity.One) || (element.SourceMultiplicity == Multiplicity.ZeroOne && newTargetMultiplicity == Multiplicity.ZeroOne)) { if (element.SourceRole != EndpointRole.NotSet) { element.SourceRole = EndpointRole.NotSet; } if (element.TargetRole != EndpointRole.NotSet) { element.TargetRole = EndpointRole.NotSet; } } else { SetEndpointRoles(element); } // cascade delete behavior could now be illegal. Reset to default element.SourceDeleteAction = DeleteAction.Default; element.TargetDeleteAction = DeleteAction.Default; break; case "TargetPropertyName": // if we're creating an association via drag/drop, it's possible the existing property name // is the same as the default property name. The default doesn't get created until the transaction is // committed, so the drop's action will cause a name clash. Remove the clashing property, but // only if drag/drop. string targetPropertyNameErrorMessage = ValidateAssociationIdentifier(element, element.Source, (string)e.NewValue); if (EFModelDiagram.IsDropping && targetPropertyNameErrorMessage != null) { element.Delete(); } else { errorMessages.Add(targetPropertyNameErrorMessage); } break; case "TargetRole": if (element.Target.IsDependentType) { element.SourceRole = EndpointRole.Principal; element.TargetRole = EndpointRole.Dependent; } else if (!SetEndpointRoles(element)) { if (element.TargetRole == EndpointRole.Dependent && element.SourceRole != EndpointRole.Principal) { element.SourceRole = EndpointRole.Principal; } else if (element.TargetRole == EndpointRole.Principal && element.SourceRole != EndpointRole.Dependent) { element.SourceRole = EndpointRole.Dependent; } } break; } element.RedrawItem(); inner.Commit(); } errorMessages = errorMessages.Where(m => m != null).ToList(); if (errorMessages.Any()) { current.Rollback(); ErrorDisplay.Show(string.Join("\n", errorMessages)); } }