Hotchocolate: GraphQL with DDD approach domain architecture problem

Created on 22 May 2020  ·  14Comments  ·  Source: ChilliCream/hotchocolate

My domain entity aggregate root looks like this:

    public class Book : Entity<long>
    {
        private BookInformation _bookInformation;
        private bool _inStock;

        public BookInformation BookInformation => _bookInformation;
        public bool InStock => _inStock;

        public Book() { }

        private Book(BookInformation bookInformation)
        {
            _bookInformation = bookInformation;
            _inStock = true;
        }

        public static Book Create(string title, string author, string subject, string isbn)
        {
            var bookInformation = new BookInformation(title, author, subject, isbn);
            var book = new Book(bookInformation);

            return book;
        }
    // and other methods (...)
    }

And my Entity class has a list of events (of type INotification):

 private List<INotification> _domainEvents;
 public IReadOnlyCollection<INotification> DomainEvents => _domainEvents?.AsReadOnly();

INotification is a markup interface from the MediatR library. My problem is the following - when I add GraphQL with the following setup:

services.AddGraphQL(
                    SchemaBuilder.New()
                        .AddQueryType<QueryType>()
                        .Create(),
                    new QueryExecutionOptions
                    {
                        ForceSerialExecution = true
                    });
(...)
app.UseGraphQL().UsePlayground();

and the Query:

    public class Query
    {
        [UseSelection]
        public IQueryable<Book> GetBooks([Service] LibraryDbContext context)
            => context.Books;
    }

I get the following error on project startup

An unhandled exception of type 'HotChocolate.SchemaException' occurred in
HotChocolate.Types.dll: 'Interface INotification has no fields declared. - Type: INotification'
Stack trace:
at HotChocolate.Configuration.TypeInitializer.EnsureNoErrors()
at HotChocolate.Configuration.TypeInitializer.CompleteTypes(DiscoveredTypes discoveredTypes)
at HotChocolate.Configuration.TypeInitializer.Initialize(Func`1 schemaResolver, IReadOnlySchemaOptions options)
at HotChocolate.SchemaBuilder.Create()
at HotChocolate.SchemaBuilder.HotChocolate.ISchemaBuilder.Create()

I tried to introduce QueryType like this:

    public class QueryType : ObjectType<Query>
    {
        protected override void Configure(IObjectTypeDescriptor<Query> descriptor)
        {
            // How to ignore these DomainEvents property here from the Query?
        }
    }

but I don't know how can I ignore the DomainEvents property from the model. How can I achieve this? Or am I doing this from the wrong side?

❓ question 🌶 hot chocolate

All 14 comments

You can add

[GraphQLIgnore]
 public IReadOnlyCollection<INotification> DomainEvents => _domainEvents?.AsReadOnly();

or create an object type for book:

    public class QueryType : ObjectType<Book>
    {
        protected override void Configure(IObjectTypeDescriptor<Book> descriptor)
        {
           descriptor.Ignore(x => x.DomainEvents);
        }
    }

if there are actually different type of events you can also add Interfaces Or Union Types based on the interface

Indeed, adding [GraphQLIgnore] attribute works well. But as far as I understand if I create this QueryType for Book I have to use it when register graphql in my IServiceCollection by .AddQueryType<QueryType>() am I right?

But then my graphql playground shows me this:
image

there is no Books array, why?

And this:
image
is not returning any correct value

ah i just copied the class from your example.
so:

public class BookType : ObjectType<Book>
    {
        protected override void Configure(IObjectTypeDescriptor<Book> descriptor)
        {
           descriptor.Ignore(x => x.DomainEvents);
        }
    }
`

But you still need the root type

public class QueryType : ObjectType<Query>
    {
        protected override void Configure(IObjectTypeDescriptor<Query> descriptor)
        { 
        }
    }
`

you can also make this with pure code first with this on the Books class

[GraphQLIgnore]
 public IReadOnlyCollection<INotification> DomainEvents => _domainEvents?.AsReadOnly();

adn this on the schema builder:

.AddQueryType<Query>()

@PascalSenn thanks for the answer :)
If I have only .AddQueryType<Query>() registered it still says that Interface INotification has no fields declared. But if I add .AddQueryType<BookType>() the error is following:

System.ArgumentException: 'An item with the same key has already been added. Key: Query'

Here is my registration:

    services.AddGraphQL(
        SchemaBuilder.New()
            .AddQueryType<Query>()
            .AddQueryType<BookType>()
            .Create(),
        new QueryExecutionOptions
        {
            ForceSerialExecution = true
        });

and here are my query types:

    public class BookType : ObjectType<Book>
    {
        protected override void Configure(IObjectTypeDescriptor<Book> descriptor)
        {
            descriptor.Ignore(x => x.DomainEvents);
        }
    }

    public class QueryType : ObjectType<Query>
    {
        protected override void Configure(IObjectTypeDescriptor<Query> descriptor)
        {
        }
    }

    public class Query
    {
        [UseSelection]
        public IQueryable<Book> GetBooks([Service] LibraryDbContext context)
            => context.Books;
    }

Yes you only need to add one query type

  SchemaBuilder.New()
            .AddQueryType<Query>()
            .AddType<BookType>()
            .Create(),

you also do not need QueryType in this case 👍

@PascalSenn Great, it works! :)

I have another question. Do I need a public ctor in my Book entity? I had private parameterless ctor because EFCore needs it, but I see that GraphQL needs a public one. Is there any way to keep my entity encapsulated like before (private ctor). The same question about missing 'set accesor on my properties:

The property 'System.String Author' has no 'set' accessor (Parameter 'member')"

Can I somehow workaround this?

I have a public property: public bool InStock => _inStock; and it reads a value from a backend field private bool _inStock;

Unless you use it as an input type you do not need any ctor

in case of an input type we map all public {get,set,} properties into the schema.
you can also have readonly {get;} properties that are set over a pulbic ctor

public class FooInput {
    public string Bar {get;set;}
}
public class FooInput {
     public FooInput(string bar) 
     {
        Bar = bar;
     }
     public string Bar {get;}
}

So it is not able to with with backend fields like in my case?

public class Book : Entity<long>
{
    private bool _inStock;
    public bool InStock => _inStock;  // This is a property I would like to query with GraphQL

    private BookInformation _bookInformation;
    public BookInformation BookInformation => _bookInformation;

    private Book(BookInformation bookInformation)
    {
        _bookInformation = bookInformation;
        _inStock = true;
    }

    public static Book Create(string title, string author, string subject, string isbn)
    {
        var bookInformation = new BookInformation(title, author, subject, isbn);
        var book = new Book(bookInformation);

        return book;
    }
}
(...)

This should work

This should work

But I have this error:
image

Ah, yes with UseSelection it does not work. sorry for the inconvenience. You can not write this as a type safe select statement:

bboks,Select(x => new Book(){InStock = x.InStock}); 

Indeed, without UseSelection attribute it works but of course the generated SQL query is very inefficient :(

Ok i am gonna close this issue as the original question is answered 👍

Was this page helpful?
0 / 5 - 0 ratings

Related issues

lTimeless picture lTimeless  ·  5Comments

marcin-janiak picture marcin-janiak  ·  4Comments

IKolosynskyi picture IKolosynskyi  ·  3Comments

hognevevle picture hognevevle  ·  3Comments

nigel-sampson picture nigel-sampson  ·  5Comments