Skip to content

Commit

Permalink
fix: #487 multiple one-to-one relationships throwing exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
ascott18 committed Jan 29, 2025
1 parent 6734909 commit ecf28fa
Show file tree
Hide file tree
Showing 14 changed files with 385 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- Fix error in .NET 9 thrown by vite development middleware if the HTTPS cert directory doesn't exist.
- Fix `ViewModel.$load` and `ListViewModel.$load` not properly working with `.useResponseCaching()`.
- Entities that own multiple one-to-one relationships should no longer throw errors when generating.

# 5.3.1

Expand Down
2 changes: 1 addition & 1 deletion docs/modeling/model-components/data-sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ Given a property and a client-provided string value, perform some filtering on t

<Prop def="IQueryable<T> ApplyListSearchTerm(IQueryable<T> query, IFilterParameters parameters)" />

Applies filters to the query based on the specified search term. See [[Search]](/modeling/model-components/attributes/search.md) for a detailed look at how searching works in Coalesce.
Applies predicates to the query based on the search term in `parameters.Search`. See [[Search]](/modeling/model-components/attributes/search.md) for a detailed look at how searching works in Coalesce.


<Prop def="IQueryable<T> ApplyListSorting(IQueryable<T> query, IListParameters parameters)" />
Expand Down
21 changes: 21 additions & 0 deletions docs/modeling/model-types/entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,27 @@ In cases where the foreign key is not named after the navigation property with `
Collection navigation properties can be used in a straightforward manner. In the event where the inverse property on the other side of the relationship cannot be determined, `[InversePropertyAttribute]` will need to be used. [EF Core provides documentation](https://learn.microsoft.com/en-us/ef/core/modeling/relationships/mapping-attributes#inversepropertyattribute) on how to use this attribute. Errors will be displayed at generation time if an inverse property cannot be determined without the attribute.


#### One-to-one Relationships
One-to-one relationships can be represented in Coalesce, but require fairly specific configuration to satisfy both EF and Coalesce's needs. Specifically, the dependent/child side of the one-to-one (the entity whose PK is also a FK), must explicitly annotate its PK with `[ForeignKey]` pointing at the parent navigation property. For example:

``` c#
public class OneToOneParent
{
public int Id { get; set; }

public OneToOneChild? Child { get; set; }
}

public class OneToOneChild
{
[Key, ForeignKey("Parent")]
public int ParentId { get; set; }

public OneToOneParent? Parent { get; set; }
}
```


### Attributes

Coalesce provides a number of C# attributes that can be used to decorate your model classes and their properties in order to customize behavior, appearance, security, and more. Coalesce also supports a number of annotations from `System.ComponentModel.DataAnnotations`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,8 @@ private void WriteClassPropertyMetadata(TypeScriptCodeBuilder b, ClassViewModel
case PropertyRole.ReferenceNavigation:
// TS Type: "ModelReferenceNavigationProperty"
b.StringProp("role", "referenceNavigation");
b.Line($"get foreignKey() {{ return {GetClassMetadataRef(model)}.props.{prop.ForeignKeyProperty.JsVariable} as ForeignKeyProperty }},");
// Note: `prop.ForeignKeyProperty.Role` might be PrimaryKey, not ForeignKey, in the case of 1-to-1 relationships.
b.Line($"get foreignKey() {{ return {GetClassMetadataRef(model)}.props.{prop.ForeignKeyProperty.JsVariable} as {prop.ForeignKeyProperty.Role}Property }},");
b.Line($"get principalKey() {{ return {GetClassMetadataRef(prop.Object)}.props.{prop.Object.PrimaryKey.JsVariable} as PrimaryKeyProperty }},");

if (prop.InverseProperty != null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace IntelliTect.Coalesce.Tests.TargetClasses.TestDbContext
{
public class OneToOneParent
{
public int Id { get; set; }

// [InverseProperty(nameof(Child1.Parent))]
public OneToOneChild1 Child1 { get; set; }

[InverseProperty(nameof(Child2.Parent))] // Can be specified, but not required.
public OneToOneChild2 Child2 { get; set; }
}

public class OneToOneChild1
{
[Key, ForeignKey("Parent")]
public int ParentId { get; set; }
public OneToOneParent Parent { get; set; }
}

public class OneToOneChild2
{
[Key, ForeignKey("Parent")]
public int ParentId { get; set; }
public OneToOneParent Parent { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ public class AppDbContext : DbContext
[InternalUse]
public DbSet<DbSetIsInternalUse> Internals { get; set; }

public DbSet<OneToOneParent> OneToOneParents { get; set; }
public DbSet<OneToOneChild1> OneToOneChild1s { get; set; }
public DbSet<OneToOneChild2> OneToOneChild2s { get; set; }

public AppDbContext() : this(Guid.NewGuid().ToString()) { }

public AppDbContext(string memoryDatabaseName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Text;
using Xunit;
using static IntelliTect.Coalesce.DataAnnotations.DateTypeAttribute;
using static System.Runtime.InteropServices.JavaScript.JSType;

namespace IntelliTect.Coalesce.Tests.TypeDefinition
{
Expand Down Expand Up @@ -283,5 +284,33 @@ public void Comment_IsCorrect(PropertyViewModelData data, string expected)
PropertyViewModel vm = data;
Assert.Equal(expected, vm.Comment, ignoreLineEndingDifferences: true);
}


[Theory]
[PropertyViewModelData<OneToOneParent>(nameof(OneToOneParent.Child1))]
[PropertyViewModelData<OneToOneParent>(nameof(OneToOneParent.Child2))]
public void OneToOne_ParentNavigations_HasCorrectMetadata(PropertyViewModelData data)
{
PropertyViewModel vm = data;
Assert.Equal(PropertyRole.ReferenceNavigation, vm.Role);
Assert.Equal(vm.Parent.PropertyByName(nameof(OneToOneParent.Id)), vm.ForeignKeyProperty);
Assert.Equal(nameof(OneToOneChild1.Parent), vm.InverseProperty.Name);
}

[Theory]
[PropertyViewModelData<OneToOneChild1>(nameof(OneToOneChild1.Parent))]
[PropertyViewModelData<OneToOneChild2>(nameof(OneToOneChild2.Parent))]
public void OneToOne_ChildNavigations_HasCorrectMetadata(PropertyViewModelData data)
{
PropertyViewModel vm = data;
Assert.Equal(PropertyRole.ReferenceNavigation, vm.Role);
Assert.Equal(vm.Parent.PropertyByName("ParentId"), vm.ForeignKeyProperty);

// We currently assume that reverence inverses are only ever collection navigations.
// This could change in the future, but at least for now the typescript types don't allow
// for representing this here, which is fine and doesn't currently hurt anything.
Assert.Null(vm.InverseProperty);
}

}
}
11 changes: 10 additions & 1 deletion src/IntelliTect.Coalesce/TypeDefinition/PropertyViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,15 @@ public PropertyViewModel? ForeignKeyProperty
?? Name + ConventionalIdSuffix;

prop = EffectiveParent.PropertyByName(name);

if (prop == null && InverseProperty?.Role == PropertyRole.ReferenceNavigation)
{
// If the other side of the relationship is also a reference navigation,
// this is a 1-to-1. If there's otherwise no FK specified, assume that the PK
// of this type is the FK we're looking for here.
// See test models "OneToOneParent"/"OneToOneChild1" for example.
return EffectiveParent.PrimaryKey;
}
}

if (prop == null || !prop.Type.IsValidKeyType || !prop.IsDbMapped)
Expand Down Expand Up @@ -541,7 +550,7 @@ public PropertyViewModel? ReferenceNavigationProperty
this.GetAttributeValue<ForeignKeyAttribute>(a => a.Name)

// Use the ForeignKey Attribute on the object property if it is there.
?? EffectiveParent.Properties.SingleOrDefault(p => Name == p.GetAttributeValue<ForeignKeyAttribute>(a => a.Name))?.Name
?? EffectiveParent.Properties.FirstOrDefault(p => Name == p.GetAttributeValue<ForeignKeyAttribute>(a => a.Name))?.Name

// Else, by convention remove the Id at the end.
?? (Name.EndsWith(ConventionalIdSuffix) ? Name.Substring(0, Name.Length - ConventionalIdSuffix.Length) : null);
Expand Down
2 changes: 1 addition & 1 deletion src/IntelliTect.Coalesce/Validation/ValidateContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ public static ValidationHelper Validate(ReflectionRepository repository)
}
catch (Exception ex)
{
assert.IsTrue(false, $"Exception: {ex.Message}");
assert.IsTrue(false, $"Exception: {ex.ToString()}");
}
}

Expand Down
4 changes: 3 additions & 1 deletion src/coalesce-vue/src/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,9 @@ export interface ModelReferenceNavigationProperty
extends PropertyBase,
ModelValue {
readonly role: "referenceNavigation";
readonly foreignKey: ForeignKeyProperty;
// Note: This can be a PrimaryKey in a 1-to-1 situation where the child record's
// PK is the foreign key into the parent's type.
readonly foreignKey: ForeignKeyProperty | PrimaryKeyProperty;
readonly principalKey: PrimaryKeyProperty;
readonly inverseNavigation?: ModelCollectionNavigationProperty;
}
Expand Down
15 changes: 15 additions & 0 deletions src/test-targets/api-clients.g.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

117 changes: 117 additions & 0 deletions src/test-targets/metadata.g.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit ecf28fa

Please sign in to comment.