NakedObjectsGroup / NakedObjectsFramework

Implementation of the 'naked objects pattern' on .NET platform. Turns a POCO domain model (that follows a few simple conventions) into a complete application. See the ReadMe (at the bottom of this page) for more details.
Apache License 2.0
252 stars 50 forks source link

Issue navigating to child property where the foreign key is not the primary key of the parent #411

Closed dannief closed 2 years ago

dannief commented 2 years ago

If this is not the right forum, please let me know. Any insight would be appreciated

The message on the UI is

Object does not exist
Message: The requested object might have been deleted by you or another user. If not, please contact your system administrator.

And partial output from the server is:

System.ArgumentException: Entity type 'BestPracticeCriteriaScore' is defined with a single key property, but 3 values were passed to the 'Find' method.
   at Microsoft.EntityFrameworkCore.Internal.EntityFinder`1.FindTracked(Object[] keyValues, IReadOnlyList`1& keyProperties)
   at Microsoft.EntityFrameworkCore.Internal.EntityFinder`1.Find(Object[] keyValues)
   at Microsoft.EntityFrameworkCore.Internal.EntityFinder`1.Microsoft.EntityFrameworkCore.Internal.IEntityFinder.Find(Object[] keyValues)
   at Microsoft.EntityFrameworkCore.DbContext.Find(Type entityType, Object[] keyValues)
   at NakedFramework.Persistor.EFCore.Component.EFCoreObjectStore.FindByKeys(Type type, Object[] keys)
   at NakedFramework.Core.Component.ObjectPersistor.FindByKeys(Type type, Object[] keys)
   at NakedFramework.Facade.Impl.Utility.EntityOidStrategy.GetDomainObject(String[] keys, Type type)
NakedFramework.Facade.Impl.Utility.EntityOidStrategy: 2022-03-22 09:22:06,745 [.NET ThreadPool Worker] WARN  NakedFramework.Facade.Impl.Utility.EntityOidStrategy - Domain Object not found keys:  14 4 1 type: SolutionArchitecture.Model.BestPracticeCriteriaScore
NakedObjects.Rest.App.Demo.RestfulObjectsController: 2022-03-22 09:22:06,752 [.NET ThreadPool Worker] ERROR NakedObjects.Rest.App.Demo.RestfulObjectsController - Context: NakedObjects.Rest.App.Demo.RestfulObjectsController.GetObject (Template.Server) State SolutionArchitecture.Model.BestPracticeCriteriaScore;14--4--1;false;
NakedFramework.Facade.Error.ObjectResourceNotFoundNOSException: No such domain object SolutionArchitecture.Model.BestPracticeCriteriaScore-14--4--1: null adapter

The system seems to be passing the primary key (14) and the values used as the foreign key in the child object (4 and 1)

See information about the model below. Issue happens when navigating to the SystemBestPracticeScore.BestPracticeCriteriaScore property

public partial class BestPracticeCriteriaScore
{
    public virtual int Id { get; set; }

    [Hidden]
    public virtual int BestPracticeId { get; set; }

    public virtual int Score { get; set; }

    public virtual string Criteria { get; set; } = null!;

    public virtual BestPractice BestPractice { get; set; } = null!;

    public virtual ICollection<SystemBestPracticeScore> SystemBestPracticeScores { get; set; } = new HashSet<SystemBestPracticeScore>();
}

public partial class SystemBestPracticeScore
{
    #region Injected Services
    //An implementation of this interface is injected automatically by the framework
    public IDomainObjectContainer Container { set; protected get; }
    #endregion

    [Hidden]
    public virtual int Id { get; set; }

    [Hidden]
    public virtual int SystemId { get; set; }

    public virtual int BestPracticeId { get; set; }

    public virtual int Score { get; set; }

    [MultiLine(3)]
    [Optionally]
    public virtual string? Notes { get; set; }
    /// <summary>
    /// detail of progress made since the last assessment
    /// </summary>
    [MultiLine(3)]
    [Optionally]
    public virtual string? Progress { get; set; }
    /// <summary>
    /// next action items to be completed by the squad
    /// </summary>
    [MultiLine(3)]
    [Optionally]
    public virtual string? NextMilestone { get; set; }

    [Named("Citeria & Score")]
    public virtual BestPracticeCriteriaScore BestPracticeCriteriaScore { get; set; } = null!;

    [Disabled]
    public virtual System System { get; set; } = null!;
}

The models are configured as below:

modelBuilder.Entity<BestPracticeCriteriaScore>(entity =>
{
    entity.ToTable("best_practice_criteria_score", "assessments");

    entity.HasComment("list of criteria and associated score for a best practice");

    entity.HasIndex(e => e.Id, "best_practice_criteria_score_idx")
        .IsUnique();

    entity.HasIndex(e => new { e.BestPracticeId, e.Score }, "best_practice_criteria_score_uk_best_practice_id__score")
        .IsUnique();

    entity.HasAlternateKey(e => new { e.BestPracticeId, e.Score });

    entity.Property(e => e.Id).HasColumnName("id");

    entity.Property(e => e.BestPracticeId).HasColumnName("best_practice_id");

    entity.Property(e => e.Criteria)
        .HasColumnType("character varying")
        .HasColumnName("criteria");

    entity.Property(e => e.Score).HasColumnName("score");

    entity.HasOne(d => d.BestPractice)
        .WithMany(p => p.BestPracticeCriteriaScores)
        .HasForeignKey(d => d.BestPracticeId)
        .OnDelete(DeleteBehavior.Cascade)
        .HasConstraintName("best_practice_criteria_score_fk_best_practice_id");
});

modelBuilder.Entity<SystemBestPracticeScore>(entity =>
{
    entity.HasKey(e => e.Id)
        .HasName("system_best_practice_score_pk");

    entity.ToTable("system_best_practice_score", "assessments");

    entity.HasComment("list of best practices scores attained by a system");

    entity.HasIndex(e => new { e.SystemId, e.BestPracticeId, e.Score })
       .IsUnique();

    entity.Property(e => e.Id).HasColumnName("id");

    entity.Property(e => e.SystemId).HasColumnName("system_id");

    entity.Property(e => e.BestPracticeId)
        .HasColumnName("best_practice_id");

    entity.Property(e => e.Score)
        .HasColumnName("score");

    entity.Property(e => e.NextMilestone)
        .HasColumnType("character varying")
        .HasColumnName("next_milestone")
        .HasComment("next action items to be completed by the squad");

    entity.Property(e => e.Notes)
        .HasColumnType("character varying")
        .HasColumnName("notes");

    entity.Property(e => e.Progress)
        .HasColumnType("character varying")
        .HasColumnName("progress")
        .HasComment("detail of progress made since the last assessment");

    entity.HasOne(d => d.System)
        .WithMany(p => p.SystemBestPracticeScores)
        .HasForeignKey(d => d.SystemId)
        .OnDelete(DeleteBehavior.Restrict)
        .HasConstraintName("system_best_practice_score_fk_system_id");

    entity.HasOne(d => d.BestPracticeCriteriaScore)
        .WithMany(p => p.SystemBestPracticeScores)    
        .HasPrincipalKey(p => new {p.BestPracticeId, p.Score})   
        .HasForeignKey(d => new { d.BestPracticeId, d.Score })
        .OnDelete(DeleteBehavior.Cascade)
        .HasConstraintName("system_best_practice_score_fk_best_practice_id__score");
});
richardpawson commented 2 years ago

This is the right place to post. Based on a quick look, this does look like a bug in our code. We think the problem stems from this line in the mapping:

entity.HasAlternateKey(e => new { e.BestPracticeId, e.Score });

We've never used HasAlternateKeyin our own applications which is why we haven't ever seen this problem. Within the framework we use an API function called GetKeys() to find the key to build the url. However this method returns both the primary and alternate key, so NakedObjects is then building a faulty key for the object.

I will flag this as a Bug for now and we will fix it for the next release. Meantime, to help you to get going again, I suggest you try temporarily commenting-out the alternate key from the mapping. Let us know if that does or doesn't fix the problem.

dannief commented 2 years ago

Removing the HasAlternateKey produced the same error. I think this is due to EF introducing an alternate key by convention. See https://docs.microsoft.com/en-us/ef/core/modeling/keys?tabs=data-annotations#alternate-keys

Alternate keys are typically introduced for you when needed and you do not need to manually configure them. By convention, an alternate key is introduced for you when you identify a property which isn't the primary key as the target of a relationship.

richardpawson commented 2 years ago

Well if EF is introducing an alternate key by convention then the problem (from Naked Objects perspective) is essentially the same. I think we can safely say that at present NakedObjects does not work with alternate keys. It should be possible for us to fix this - we need to change the way we access the API such that we are just getting the primary key instead of all the keys.

In the meanitme ...

I am assuming that the reason you have not used the PK as the FK in an association is because you want to display to the user different_ 'identifiers' for associated objects in different contexts - is that correct? If so, I suggest you just use the object property, with no associated FK property (or with an FK property to the primary key, Hiddenfrom the user). Then add a derived (readonly) property that displays the desired identifier from the associated object.

Or if you want the user to be able to change the associated object by editing the 'alternate' identifier then you could either:

(However, in either case, if the alternate identifier is relevant to the user I would have expected to see it used as, or within, the object's title).

scascarini commented 2 years ago

Need to cherry-pick fix back to NO12/NF1

richardpawson commented 2 years ago

Stef has fixed the issue, and we have uploaded a new version of the NakedObjects.Server package (v12.0.1) to the Nuget public gallery. Please try this out and report back if it fixes your particular model.

dannief commented 2 years ago

Thank you. Will this issue be fixed in v13 beta as well soon? I am using the Template Server project downloaded from this repository, but that project is referencing v13 beta packages

richardpawson commented 2 years ago

v13.0.0-beta02 released with the fix. Please confirm back that it is now working for you.

dannief commented 2 years ago

I can confirm that navigation now works. However, I am getting an error if I change the the value of the child property and then save.

Error message received from server
Message: Object reference not set to an instance of an object.
Code: InternalServerError(500)
Description: A software error has occurred on the server.
Stack Trace :
at NakedFramework.Persistor.EFCore.Component.EFCoreLocalContext.<>c.<PreSave>b__43_5(INakedObjectAdapter no)
at System.Collections.Generic.List`1.ForEach(Action`1 action)
at NakedFramework.Persistor.EFCore.Component.EFCoreLocalContext.PreSave()
at NakedFramework.Persistor.EFCore.Component.EFCoreObjectStore.<>c.<PreSave>b__66_0(EFCoreLocalContext c)
at System.Array.ForEach[T](T[] array, Action`1 action)
at NakedFramework.Core.Util.CollectionUtils.ForEach[T](T[] toIterate, Action`1 action)
at NakedFramework.Persistor.EFCore.Component.EFCoreObjectStore.PreSave()
at NakedFramework.Persistor.EFCore.Component.EFCoreObjectStore.RecurseUntilAllChangesApplied(Int32 depth)
at NakedFramework.Persistor.EFCore.Component.EFCoreObjectStore.EndTransaction()
at NakedFramework.Core.Transaction.NestedTransaction.Commit()
at NakedFramework.Core.Component.TransactionManager.EndTransaction()
at NakedFramework.Facade.Impl.Impl.FrameworkFacade.End(Boolean success)
at NakedFramework.Rest.API.RestfulObjectsControllerBase.InitAndHandleErrors(Func`1 f)
richardpawson commented 2 years ago

Thanks. So we can look into this, can you clarify with a specific example of which property on which object you were changing and how you were saving it (I assume you were directly editing the object on screen and hitting Save rather than doing it through a method. Again, this is not an issue we have encountered so it may again be to do with the alternate keys, but we need to get to the bottom of it.

dannief commented 2 years ago

Yes. I was directly editing the object on screen and clicked Save. I edited the BestPracticeCriteriaScore property of the SystemBestPracticeScore. (This same child object is related to the parent using a foreign key that is not a primary key.) The domain object is Bounded, so a drop down list was generated on the UI. I selected another value and clicked Save

richardpawson commented 2 years ago

Thanks for clarification, Debbie-Ann. On the previous issue I stated that using Alternate Keys (with EF Core) was something we had little experience with, and it is clear that we haven't got to the bottom of it yet. I've added another ticket (#412) to warn anyone else about this area in general. I will try to contact you offline to discuss how best we can help you get going.