Skip to content

Commit

Permalink
feat: support discovering foreign keys via [ForeignKeyAttribute] plac…
Browse files Browse the repository at this point in the history
…ed on a collection navigation props. Support foreign keys that do not have a reference navigation prop.
  • Loading branch information
ascott18 committed Oct 10, 2023
1 parent d5a4016 commit c12c0da
Show file tree
Hide file tree
Showing 13 changed files with 177 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -342,13 +342,15 @@ public void WriteMethod_SaveToDto(TypeScriptCodeBuilder b)
}
else if (prop.IsForeignKey)
{
var navigationProp = prop.ReferenceNavigationProperty;

b.Line($"dto.{prop.JsonName} = this.{prop.JsVariable}();");

// If the Id isn't set, use the object and see if that is set. Allows a child to get an Id after the fact.
using (b.Block($"if (!dto.{prop.JsonName} && this.{navigationProp.JsVariable}())"))
if (prop.ReferenceNavigationProperty is {} navProp)
{
b.Line($"dto.{prop.JsonName} = this.{navigationProp.JsVariable}()!.{navigationProp.Object.PrimaryKey.JsVariable}();");
using (b.Block($"if (!dto.{prop.JsonName} && this.{navProp.JsVariable}())"))
{
b.Line($"dto.{prop.JsonName} = this.{navProp.JsVariable}()!.{navProp.Object.PrimaryKey.JsVariable}();");
}
}
}
else if (prop.Type.IsCollection)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,11 +219,15 @@ private void WriteClassPropertyMetadata(TypeScriptCodeBuilder b, ClassViewModel

case PropertyRole.ForeignKey:
// TS Type: "ForeignKeyProperty"
var navProp = prop.ReferenceNavigationProperty;
var principal = prop.ForeignKeyPrincipalType;
b.StringProp("role", "foreignKey");
b.Line($"get principalKey() {{ return {GetClassMetadataRef(navProp.Object)}.props.{navProp.Object.PrimaryKey.JsVariable} as PrimaryKeyProperty }},");
b.Line($"get principalType() {{ return {GetClassMetadataRef(navProp.Object)} }},");
b.Line($"get navigationProp() {{ return {GetClassMetadataRef(model)}.props.{navProp.JsVariable} as ModelReferenceNavigationProperty }},");
b.Line($"get principalKey() {{ return {GetClassMetadataRef(principal)}.props.{principal.PrimaryKey.JsVariable} as PrimaryKeyProperty }},");
b.Line($"get principalType() {{ return {GetClassMetadataRef(principal)} }},");

if (prop.ReferenceNavigationProperty is { } navProp)
{
b.Line($"get navigationProp() {{ return {GetClassMetadataRef(model)}.props.{navProp.JsVariable} as ModelReferenceNavigationProperty }},");
}
break;

case PropertyRole.ReferenceNavigation:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ public class ComplexModel
[InverseProperty(nameof(Test.ComplexModel))]
public ICollection<Test> Tests { get; set; }

/// <summary>
/// Test case for foreign keys without a reference navigation prop.
/// This configuration *will* be picked up by EF conventions.
/// </summary>
[ForeignKey(nameof(ComplexModelDependent.ParentId))]
public ICollection<ComplexModelDependent> Children { get; set; }

public int SingleTestId { get; set; }
public Test SingleTest { get; set; }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace IntelliTect.Coalesce.Tests.TargetClasses.TestDbContext
{
public class ComplexModelDependent
{
public int Id { get; set; }

// Foreign key without nav prop (discovered by ForeignKeyAttribute on the inverse collection navigation).
public int ParentId { get; set; }

public string Name { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class AppDbContext : DbContext
public DbSet<CaseProduct> CaseProducts { get; set; }

public DbSet<ComplexModel> ComplexModels { get; set; }
public DbSet<ComplexModelDependent> ComplexModelDependents { get; set; }
public DbSet<ReadOnlyEntityUsedAsMethodInput> ReadOnlyEntityUsedAsMethodInputs { get; set; }
public DbSet<RequiredAndInitModel> RequiredAndInitModels { get; set; }
public DbSet<Test> Tests { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using IntelliTect.Coalesce.Tests.TargetClasses.TestDbContext;
using IntelliTect.Coalesce.Tests.Util;
using IntelliTect.Coalesce.TypeDefinition;
using IntelliTect.Coalesce.TypeDefinition.Enums;
using System.Text;
using Xunit;
using static IntelliTect.Coalesce.DataAnnotations.DateTypeAttribute;
Expand Down Expand Up @@ -148,5 +149,17 @@ public void DateType_IsCorrect(ClassViewModelData data)
Assert.Equal(DateTypes.DateTime, vm.PropertyByName(nameof(ComplexModel.DateTimeOffset)).DateType);
Assert.Equal(DateTypes.DateTime, vm.PropertyByName(nameof(ComplexModel.DateTimeOffsetNullable)).DateType);
}

[Theory]
[PropertyViewModelData(typeof(ComplexModel), nameof(ComplexModel.SingleTestId))]
[PropertyViewModelData(typeof(ComplexModelDependent), nameof(ComplexModelDependent.ParentId))]
public void IsForeignKey_IsCorrect(PropertyViewModelData data)
{
PropertyViewModel vm = data;

Assert.True(vm.IsForeignKey);
Assert.NotNull(vm.ForeignKeyPrincipalType);
Assert.Equal(PropertyRole.ForeignKey, vm.Role);
}
}
}
6 changes: 3 additions & 3 deletions src/IntelliTect.Coalesce.Tests/Util/ClassViewModelData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public ClassViewModelData(Type targetType, Type viewModelType) : this()
SetupProps();
}

private void SetupProps()
protected void SetupProps()
{
if (ViewModelType == typeof(ReflectionClassViewModel))
{
Expand All @@ -56,7 +56,7 @@ private void SetupProps()
}
}

public void Deserialize(IXunitSerializationInfo info)
public virtual void Deserialize(IXunitSerializationInfo info)
{
var targetType = info.GetValue<string>(nameof(TargetType));
var viewModelType = info.GetValue<string>(nameof(ViewModelType));
Expand All @@ -82,7 +82,7 @@ public void Deserialize(IXunitSerializationInfo info)
SetupProps();
}

public void Serialize(IXunitSerializationInfo info)
public virtual void Serialize(IXunitSerializationInfo info)
{
info.AddValue(nameof(TargetType), TargetType.FullName);
info.AddValue(nameof(ViewModelType), ViewModelType.Name);
Expand Down
28 changes: 28 additions & 0 deletions src/IntelliTect.Coalesce.Tests/Util/ClassViewModelDataAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,34 @@ public override IEnumerable<object[]> GetData(MethodInfo testMethod)
}
}

internal class PropertyViewModelDataAttribute : Xunit.Sdk.DataAttribute
{
private readonly Type targetClass;
private readonly string propName;
private readonly object[] inlineData;

protected bool reflection = true;
protected bool symbol = true;

public PropertyViewModelDataAttribute(Type targetClass, string propName, params object[] additionalInlineData)
{
this.targetClass = targetClass;
this.propName = propName;
this.inlineData = additionalInlineData;
}

public override IEnumerable<object[]> GetData(MethodInfo testMethod)
{
if (reflection) yield return new[] {
new PropertyViewModelData(targetClass, propName, typeof(ReflectionClassViewModel))
}.Concat(inlineData).ToArray();

if (symbol) yield return new[] {
new PropertyViewModelData(targetClass, propName, typeof(SymbolClassViewModel))
}.Concat(inlineData).ToArray();
}
}

internal class ReflectionClassViewModelDataAttribute : ClassViewModelDataAttribute
{
public ReflectionClassViewModelDataAttribute(Type targetClass, params object[] additionalInlineData)
Expand Down
40 changes: 40 additions & 0 deletions src/IntelliTect.Coalesce.Tests/Util/PropertyViewModelData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using IntelliTect.Coalesce.TypeDefinition;
using System;
using Xunit.Abstractions;

namespace IntelliTect.Coalesce.Tests.Util
{
public class PropertyViewModelData : ClassViewModelData
{
public string PropName { get; private set; }

public PropertyViewModel PropertyViewModel => ClassViewModel.PropertyByName(PropName);

public PropertyViewModelData()
{
}

public PropertyViewModelData(Type targetType, string propName, Type viewModelType) : base(targetType, viewModelType)
{
PropName = propName;
}

public override void Deserialize(IXunitSerializationInfo info)
{
base.Deserialize(info);
PropName = info.GetValue<string>(nameof(PropName));
}

public override void Serialize(IXunitSerializationInfo info)
{
base.Serialize(info);
info.AddValue(nameof(PropName), PropName);
}

public static implicit operator PropertyViewModel(PropertyViewModelData self)
=> self.PropertyViewModel;

public override string ToString() =>
$"({(ViewModelType.Name.StartsWith("Sym") ? "Symbol" : "Reflect")}) {new ReflectionTypeViewModel(TargetType).FullyQualifiedName}.{PropName}";
}
}
46 changes: 42 additions & 4 deletions src/IntelliTect.Coalesce/TypeDefinition/PropertyViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,14 @@ public HiddenAttribute.Areas HiddenAreas
return value;
}

if (IsForeignKey || (IsPrimaryKey && DatabaseGenerated == DatabaseGeneratedOption.Identity))
if (IsForeignKey && ReferenceNavigationProperty != null)
{
// If the prop is a FK that has a reference navigation,
// hide the FK so that the reference navigation will be all that's shown.
return HiddenAttribute.Areas.All;
}

if (IsPrimaryKey && DatabaseGenerated == DatabaseGeneratedOption.Identity)
{
return HiddenAttribute.Areas.All;
}
Expand Down Expand Up @@ -439,9 +446,10 @@ public bool IsPrimaryKey
public bool IsAutoGeneratedPrimaryKey => IsPrimaryKey && DatabaseGenerated == DatabaseGeneratedOption.Identity;

/// <summary>
/// Returns true if this property is a foreign key. Guarantees that <see cref="ReferenceNavigationProperty"/> is not null.
/// Returns true if this property is a foreign key.
/// Guarantees that <see cref="ForeignKeyPrincipalType"/> is not null.
/// </summary>
public bool IsForeignKey => ReferenceNavigationProperty != null;
public bool IsForeignKey => ForeignKeyPrincipalType != null;


public DatabaseGeneratedOption DatabaseGenerated
Expand Down Expand Up @@ -506,7 +514,7 @@ public PropertyViewModel? ForeignKeyProperty
}

/// <summary>
/// Gets the property that is the object reference for this ID property.
/// If this is a foreign key property, returns the property that holds the reference navigation.
/// </summary>
public PropertyViewModel? ReferenceNavigationProperty
{
Expand Down Expand Up @@ -541,6 +549,36 @@ public PropertyViewModel? ReferenceNavigationProperty
}
}

/// <summary>
/// If this is a foreign key property, returns the type of the principal entity.
/// </summary>
public ClassViewModel? ForeignKeyPrincipalType
{
get
{
// Eliminate out anything that can't be a key right away.
if (!Type.IsValidKeyType) return null;

// Types/props that aren't DB mapped don't have properties that have relational meaning.
// EffectiveParent used here to correctly handle properties on base classes -
// we need to know that the class that is ultimately used is DB mapped.
if (!IsDbMapped || !EffectiveParent.IsDbMappedType) return null;

if (ReferenceNavigationProperty?.Object is { } navPropType) return navPropType;

// Support foreign keys without a reference navigation property.
// These are configured by putting [ForeignKeyAttribute] on the
// collection navigation on the other side of the relationship.
return Parent.Usages
.OfType<PropertyViewModel>()
.FirstOrDefault(p =>
p.Type.IsCollection &&
p.GetAttributeValue<ForeignKeyAttribute>(a => a.Name) == this.Name
)
?.Parent;
}
}


public string EditorOrder
{
Expand Down
9 changes: 5 additions & 4 deletions src/IntelliTect.Coalesce/Validation/ValidateContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,13 @@ public static ValidationHelper Validate(ReflectionRepository repository)
assert.IsNotNull(prop.Object.PrimaryKey, "No Primary key for related object. Ensure the target object has a [Key] attributed property.");
}
}
if (prop.IsForeignKey)

if (prop.ReferenceNavigationProperty is not null)
{
assert.IsNotNull(prop.ReferenceNavigationProperty, "Object property not found.");
assert.IsNotNull(prop.ReferenceNavigationProperty?.Object, "Object property related object not found.");
assert.IsNotNull(prop.ReferenceNavigationProperty?.Object?.PrimaryKey, "No primary key on type of this ID's Navigation Property.");
assert.IsNotNull(prop.ReferenceNavigationProperty.Object, "Object property related object not found.");
assert.IsNotNull(prop.ReferenceNavigationProperty.Object?.PrimaryKey, "No primary key on type of this ID's Navigation Property.");
}

if (prop.Type.IsCollection)
{
assert.AreNotEqual(prop.Type.FullyQualifiedName, prop.PureType.FullyQualifiedName, "Collection is not defined correctly.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@
:for="
filter.propMeta.role == 'primaryKey'
? list.$metadata.name
: filter.propMeta.navigationProp
: filter.propMeta.navigationProp ??
filter.propMeta.principalType
"
clearable
hide-details
Expand Down Expand Up @@ -254,9 +255,11 @@ export default defineComponent({
// `null` as a value is a filter that checks that the value is `null`.
isActive: value !== "" && value !== undefined,
displayName:
propMeta?.role == "foreignKey"
(propMeta?.role == "foreignKey"
? propMeta.navigationProp?.displayName
: propMeta?.displayName ?? key,
: undefined) ??
propMeta?.displayName ??
key,
} as FilterInfo;
return filterInfo;
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@
:for="
filter.propMeta.role == 'primaryKey'
? list.$metadata.name
: filter.propMeta.navigationProp
: filter.propMeta.navigationProp ??
filter.propMeta.principalType
"
clearable
hide-details
Expand Down Expand Up @@ -244,9 +245,11 @@ export default defineComponent({
// `null` as a value is a filter that checks that the value is `null`.
isActive: value !== "" && value !== undefined,
displayName:
propMeta?.role == "foreignKey"
(propMeta?.role == "foreignKey"
? propMeta.navigationProp?.displayName
: propMeta?.displayName ?? key,
: undefined) ??
propMeta?.displayName ??
key,
} as FilterInfo;
})
.sort((a, b) =>
Expand Down

0 comments on commit c12c0da

Please sign in to comment.