ChilliCream / ChilliCream/graphql-platform

ObjectType<T> classes which implements interface or abstract class are not respecting mongo conventions

Open
#6,291 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

🌶️ hot chocolate Area: Data Area: MongoDB
Dominant language
C#
Stars
5.8k
Forks
810
Avg merge
15h 39m
Merged PRs (30d)
98

Description

Is there an existing issue for this?
  • I have searched the existing issues
Product

Hot Chocolate

Describe the bug

Hi,
I'm trying to use HotChocolate with MongoDB and I'm using paging, filtering, sorting features. My query Type contains a list of base types which can be resolved with multiple types. I have a set of mongo conventions registered and one of them is CamelCaseElementNameConvention. I managed to overcome problems with deserializing mongo documents by forcing HC to project mongo discriminator _t (without that it is trying to create an instance of abstract class). But unfortunately the query which is generated for mongo has uppercase fields in projection (only for derived classes) and because of that those fields are not returned from DB (please see logs) .

Steps to reproduce
  1. I have following code:
//Domain classes
public class Person
{
    public string Id { get; set; }
    public int PersonProp1 { get; set; }
    public IEnumerable<Animal> PetList { get; set; }
}
public abstract class Animal
{
    public int AnimalProp1 { get; set; }
    public string AnimalProp2 { get; set; }
}
public class Cat : Animal
{
    public int CatProp1 { get; set; }
    public string CatProp2 { get; set; }
}
public class Dog : Animal
{
    public int DogProp1 { get; set; }
    public string DogProp2 { get; set; }
}

//Program.cs
var builder = WebApplication.CreateBuilder(args);

var pack = new ConventionPack
    {
        new CamelCaseElementNameConvention(),
        new StringIdStoredAsObjectIdConvention(),
        new IgnoreIfNullConvention(true),
        new IgnoreExtraElementsConvention(true)
    };
ConventionRegistry.Register("custom", pack, t => true);

BsonClassMap.RegisterClassMap<Animal>(cm => {
    cm.AutoMap();
    cm.AddKnownType(typeof(Cat));
    cm.AddKnownType(typeof(Dog));
});
BsonClassMap.RegisterClassMap<Cat>(cm => {
    cm.AutoMap();
});
BsonClassMap.RegisterClassMap<Dog>(cm => {
    cm.AutoMap();
});

var connectionString = builder.Configuration.GetConnectionString("MongoDb");
var url = new MongoUrl(connectionString);
var settings = MongoClientSettings.FromUrl(url);

var client = new MongoClient(settings);
var database = client.GetDatabase(url.DatabaseName);

builder.Services.AddSingleton(client);
builder.Services.AddSingleton(database);
builder.Services.AddSingleton(database.GetCollection<Person>("person"));

builder.Services.AddControllers();

builder.Services.AddGraphQLServer()
    .AddQueryType<PersonLookup>()
    .AddType<CatType>()
    .AddType<DogType>()
    .ModifyOptions(opt => opt.RemoveUnreachableTypes = true)
    .AddMongoDbPagingProviders()
    .AddMongoDbProjections()
    .AddMongoDbFiltering()
    .AddMongoDbSorting()
    .InitializeOnStartup();

var app = builder.Build();

app.MapGraphQL("/graphql");

app.Run();

//Schema definition
public class PersonLookup
{
    private const int _maxPageSize = 1000;

    [UseOffsetPaging(IncludeTotalCount = true, MaxPageSize = _maxPageSize)]
    [UseProjection]
    [UseFiltering]
    [UseSorting]
    public async Task<IExecutable<Person>> UserProfiles([Service] IMongoCollection<Person> collection)
    {
        return collection.AsExecutable();
    }
}

public class AnimalType : InterfaceType<Animal>
{
    protected override void Configure(IInterfaceTypeDescriptor<Animal> descriptor)
    {
        descriptor.Name(nameof(Animal));
    }
}

public class CatType : ObjectType<Cat>
{
    protected override void Configure(IObjectTypeDescriptor<Cat> descriptor)
    {
        descriptor.Name(nameof(Cat));
        descriptor.Implements<AnimalType>();
        descriptor.Field("_t").Type<StringType>().IsProjected().Resolve((context, ct) =>
        {
            //code won't run without resolver even if it is not used
            return nameof(Cat);
        });
    }
}

public class DogType : ObjectType<Dog>
{
    protected override void Configure(IObjectTypeDescriptor<Dog> descriptor)
    {
        descriptor.Name(nameof(Dog));
        descriptor.Implements<AnimalType>();
        descriptor.Field("_t").Type<StringType>().IsProjected().Resolve((context, ct) =>
        {
            return nameof(Dog);
        });
    }
}
  1. My sample data in DB looks like this - please note all the fields are in camel case
{
	"_id" : ObjectId("6495c776a56246502b2515f2"),
	"personProp1" : 6,
	"petList" : [
		{
			"_t" : "Dog",
			"animalProp1" : 5,
			"animalProp2" : "Animal Prop 8",
			"dogProp1" : 1,
			"dogProp2" : "Dog Prop 4"
		},
		{
			"_t" : "Dog",
			"animalProp1" : 5,
			"animalProp2" : "Animal Prop 8",
			"dogProp1" : 1,
			"dogProp2" : "Dog Prop 5"
		},
		{
			"_t" : "Cat",
			"animalProp1" : 6,
			"animalProp2" : "Animal Prop 5",
			"catProp1" : 3,
			"catProp2" : "Cat Prop 1"
		}
	]
}
  1. Query which use to get data
query MyQuery {
  userProfiles(where: {id: {eq: "6495c776a56246502b2515f2"}}) {
    items {
      petList {
        ... on Cat {
          catProp2
          animalProp2
          catProp1
          animalProp1
        }
        ... on Dog {
          dogProp2
          animalProp2
          dogProp1
          animalProp1
        }
        animalProp2
        animalProp1
      }
      id
      personProp1
    }
  }
}
Relevant log output
Requested document:
{
  "find" : "person",
  "filter" : {
    "_id" : {
      "$eq" : ObjectId("6495c776a56246502b2515f2")
    }
  },
  "projection" : {
    "personProp1" : 1,
    "_id" : 1,
    "petList.DogProp1" : 1, <-- should be: petList.dogProp1
    "petList.DogProp2" : 1, <-- should be: petList.dogProp2
    "petList._t" : 1,
    "petList.animalProp1" : 1,
    "petList.CatProp1" : 1, <-- should be: petList.catProp1
    "petList.animalProp2" : 1,
    "petList.CatProp2" : 1 <-- should be: petList.catProp2
  },
  "limit" : 11,
  "$db" : "testHC",
  "lsid" : {
    "id" : CSUUID("6c719ba1-9ca9-412b-9b03-f8a80d50fc66")
  },
  "$clusterTime" : {
    "clusterTime" : Timestamp(1687614162, 1),
    "signature" : {
      "hash" : new BinData(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
      "keyId" : NumberLong(0)
    }
  }
}

Returned document:
{
  "cursor" : {
    "firstBatch" : [{
        "_id" : ObjectId("6495c776a56246502b2515f2"),
        "personProp1" : 6,
        "petList" : [{
            "_t" : "Dog",
            "animalProp1" : 5,
            "animalProp2" : "Animal Prop 8"
          }, {
            "_t" : "Dog",
            "animalProp1" : 5,
            "animalProp2" : "Animal Prop 8"
          }, {
            "_t" : "Cat",
            "animalProp1" : 6,
            "animalProp2" : "Animal Prop 5"
          }]
      }],
    "id" : NumberLong(0),
    "ns" : "testHC.person"
  },
  "ok" : 1.0,
  "$clusterTime" : {
    "clusterTime" : Timestamp(1687614162, 1),
    "signature" : {
      "hash" : new BinData(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
      "keyId" : NumberLong(0)
    }
  },
  "operationTime" : Timestamp(1687614162, 1)
}
Additional Context?

No response

Version

13.2.1

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

No source file or test is named. Start by reproducing the sample with AddMongoDbProjections and inspect the generated MongoDB projection for ObjectType implementations of the Animal interface. Done means derived Cat and Dog fields use the registered camelCase element names, while the existing discriminator and base fields still work.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, graphql, mongodb
Domain
api, backend, database
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.