Skip to content

A generic interface with a primary key generic type parameter used with two different primary key types causes a domain build failure #332

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
ondrejtucny opened this issue Aug 11, 2023 · 5 comments

Comments

@ondrejtucny
Copy link

When the following two entities are declared:

[HierarchyRoot(InheritanceSchema.SingleTable)]
[TableMapping("a_list")]
public sealed class AList : LegacyUserTrackedEntity, ICodebookEntity<short>
{
    [Key(0)]
    [Field]
    [FieldMapping("id_list")]
    public short Id { get; set; }
}

[HierarchyRoot(InheritanceSchema.SingleTable)]
[TableMapping("c_system")]
public sealed class CSystem : LegacyUserTrackedEntity, ICodebookEntity<int>
{
    [Key]
    [Field]
    [FieldMapping("id")]
    public int Id { get; set; }
}

DataObjects throws this error: Implementors of ICodebookEntity interface belong to hierarchies with different key structure: AList & CSystem.

However, the interfaces are ICodebookEntity<short> and ICodebookEntity<int> — two different things.

Using version 7.0.3.

@alex-kulakov
Copy link
Contributor

Hello @ondrejtucny,

I think you have misunderstood the error. The problem is not with generic interface itself. DO counts implementors of ICodeBookEntity and ICookBookEntity separately, and if an interface has more than one implementor then all implementors will be checked for key structure - for number of key fields and for types of those fields.

Following model I have created works just fine:

  public interface ISomeEntity<TEntityKey> : IEntity
  {
    [Field]
    TEntityKey Id { get; }

    [Field]
    string Name { get; set; }

    [Field]
    DateTime CreationDate { get; set; }
  }

  [HierarchyRoot(InheritanceSchema.SingleTable)]
  public sealed class IntImplementor : Entity, ISomeEntity<int>
  {
    [Field, Key(0)]
    public int Id { get; private set; }

    [Field]
    public string Name { get; set; }

    [Field]
    public DateTime CreationDate { get; set; }
  }

  [HierarchyRoot(InheritanceSchema.SingleTable)]
  public sealed class LongImplementor : Entity, ISomeEntity<long>
  {
    [Field, Key]
    public long Id { get; private set; }

    [Field]
    public string Name { get; set; }

    [Field]
    public DateTime CreationDate { get; set; }
  }

  [HierarchyRoot(InheritanceSchema.SingleTable)]
  public sealed class ShortImplementor : Entity, ISomeEntity<short>
  {
    [Field, Key(0)]
    public short Id { get; private set; }

    [Field]
    public string Name { get; set; }

    [Field]
    public DateTime CreationDate { get; set; }
  }

I believe that you have various key fields number within each interface.

You can take a look at

var count = hierarchies.Count;
if (count == 0) {
throw new DomainBuilderException(string.Format(
Strings.ExXImplementorsDontBelongToAnyHierarchy, interfaceDef.Name));
}
if (count == 1) {
context.ModelInspectionResult.SingleHierarchyInterfaces.Add(interfaceDef);
}
else {
HierarchyDef master = null;
foreach (var candidate in hierarchies) {
if (master == null) {
master = candidate;
continue;
}
context.Validator.ValidateHierarchyEquality(interfaceDef, master, candidate);
}
}
hierarchyDef = hierarchies.First();
}

you can see calling for validation if count is more that 1

and the validation itself

public void ValidateHierarchyEquality(TypeDef @interface, HierarchyDef first, HierarchyDef second)
{
// TypeId mode must match
if (first.IncludeTypeId != second.IncludeTypeId) {
throw new DomainBuilderException(string.Format(
Strings.ExImplementorsOfXInterfaceBelongToHierarchiesOneOfWhichIncludesTypeIdButAnotherDoesntYZ,
@interface.Name, first.Root.Name, second.Root.Name));
}
// Number of key fields must match
if (first.KeyFields.Count != second.KeyFields.Count) {
throw new DomainBuilderException(string.Format(
Strings.ExImplementorsOfXInterfaceBelongToHierarchiesWithDifferentKeyStructureYZ,
@interface.Name, first.Root.Name, second.Root.Name));
}
// Type of each key field must match
for (var i = 0; i < first.KeyFields.Count; i++) {
var masterField = first.Root.Fields[first.KeyFields[i].Name];
var candidateField = second.Root.Fields[second.KeyFields[i].Name];
if (masterField.ValueType != candidateField.ValueType) {
throw new DomainBuilderException(string.Format(
Strings.ExImplementorsOfXInterfaceBelongToHierarchiesWithDifferentKeyStructureYZ,
@interface.Name, first.Root.Name, second.Root.Name));
}
}
}

@alex-kulakov
Copy link
Contributor

Hello @ondrejtucny,

Any updates on this? Did my answer help to lid a problem with model? Should I dig dipper for this? In this case I need your assistance to reproduce the issue.

@ondrejtucny
Copy link
Author

Update on when exactly this problem occurs: If the generic interface inherits from a normal interface which in turn inherits from IEntity, then this error occurs. If the generic interface directly inherits from IEntity, then it works.

The following declaration causes the error:

    public interface ICodebookEntity : IEntity
    {
    }

    public interface ICodebookEntity<out TKey> : ICodebookEntity
        where TKey : struct
    {
        [Field]
        TKey Id { get; }

        [Field]
        string Code { get; }

        [Field]
        DateTime DateLastModified { get; }
    }

However, when declared as follows, the hierarchy works just fine:

    public interface ICodebookEntity
    {
    }

    public interface ICodebookEntity<out TKey> : IEntity, ICodebookEntity
        where TKey : struct
    {
        [Field]
        TKey Id { get; }

        [Field]
        string Code { get; }

        [Field]
        DateTime DateLastModified { get; }
    }

@alex-kulakov
Copy link
Contributor

@ondrejtucny,

Thank you for the update, I will re-check and give you my update a bit later.

@alex-kulakov
Copy link
Contributor

Hello @ondrejtucny ,

I've created two models according to your latest examples, they look like so

namespace Xtensive.Orm.Tests.Issues.IssueGithub0332_GenericIntefaceCausesErrorOnBuildModel
{
  namespace GoodExample
  {
    public interface ICodebookEntity
    {
      
    }

    public interface ICodebookEntity<out TKey> : IEntity, ICodebookEntity
        where TKey : struct
    {
      [Field]
      TKey Id { get; }

      [Field]
      string Code { get; }

      [Field]
      DateTime DateLastModified { get; }
    }

    public class IntCodeBookEntity : Entity, ICodebookEntity<int>
    {
      [Field, Key]
      public int Id { get; private set; }

      [Field]
      public string Code { get; set; }

      [Field]
      public DateTime DateLastModified { get; set; }
    }

    public class LongCodeBookEntity : Entity, ICodebookEntity<long>
    {
      [Field, Key]
      public long Id { get; private set; }

      [Field]
      public string Code { get; set; }

      [Field]
      public DateTime DateLastModified { get; set; }
    }
  }

  namespace BadExample
  {
    public interface ICodebookEntity : IEntity
    {
      
    }

    public interface ICodebookEntity<out TKey> : ICodebookEntity
        where TKey : struct
    {
      [Field]
      TKey Id { get; }

      [Field]
      string Code { get; }

      [Field]
      DateTime DateLastModified { get; }
    }

    public class IntCodeBookEntity : Entity, ICodebookEntity<int>
    {
      [Field, Key]
      public int Id { get; private set; }

      [Field]
      public string Code { get; set; }

      [Field]
      public DateTime DateLastModified { get; set; }
    }

    public class LongCodeBookEntity : Entity, ICodebookEntity<long>
    {
      [Field, Key]
      public long Id { get; private set; }

      [Field]
      public string Code { get; set; }

      [Field]
      public DateTime DateLastModified { get; set; }
    }
  }
}

And I've created two tests that build domain with these types in model like so

  public class IssueGithub0332_GenericIntefaceCausesErrorOnBuild
  {
    [Test]
    public void WorkingCase()
    {
      //the factory just creates configuration with connection to test storage environment
      var configuration = DomainConfigurationFactory.Create();
      configuration.UpgradeMode = DomainUpgradeMode.Recreate;
      configuration.Types.Register(typeof(Models.GoodExample.IntCodeBookEntity));
      configuration.Types.Register(typeof(Models.GoodExample.LongCodeBookEntity));
      configuration.Types.Register(typeof(Models.GoodExample.ICodebookEntity));
      configuration.Types.Register(typeof(Models.GoodExample.ICodebookEntity<>));

      using (var domain = Domain.Build(configuration)) { }
    }

    [Test]
    public void NotWorkingCase()
    {
      //the factory just creates configuration with connection to test storage environment
      var configuration = DomainConfigurationFactory.Create();
      configuration.UpgradeMode = DomainUpgradeMode.Recreate;
      configuration.Types.Register(typeof(Models.BadExample.IntCodeBookEntity));
      configuration.Types.Register(typeof(Models.BadExample.LongCodeBookEntity));
      configuration.Types.Register(typeof(Models.BadExample.ICodebookEntity));
      configuration.Types.Register(typeof(Models.BadExample.ICodebookEntity<>));

      using (var domain = Domain.Build(configuration)) { }
    }
  }

Both tests have completed successfully.

I also tried not to declare [Field] in classes and tried to move one of non-key fields to non-generic ICodebookEntity interface. Works just fine.
The tests were run on the latest for 7.0 branch version - 7.0.6 version.

Can you guide me to what I should change to have such error as you're getting? Also, if you can, please, verify that your case still doesn't work on v7.0.6.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants