ChilliCream / graphql-platform

Welcome to the home of the Hot Chocolate GraphQL server for .NET, the Strawberry Shake GraphQL client for .NET and Banana Cake Pop the awesome Monaco based GraphQL IDE.
https://chillicream.com
MIT License
5.03k stars 723 forks source link

Add support for the default interface method implementation #7030

Open livwvil opened 2 months ago

livwvil commented 2 months ago

Product

Hot Chocolate

Is your feature request related to a problem?

Lets imagine we want the Hot Chocolate to produce a schema like:

interface IChannel {
  id: ID!
  description: String!
  status: ChannelStatus!
}

union IAnotherChannel = ChannelTypeB | ChannelTypeC

type ChannelTypeA implements IChannel {
  id: ID!
  description: String!
  status: ChannelStatus!
  ip: String!
}

type ChannelTypeB implements IChannel {
  id: ID!
  description: String!
  status: ChannelStatus!
  port: String!
}

type ChannelTypeC implements IChannel {
  id: ID!
  description: String!
  status: ChannelStatus!
  adapter: String!
}

I have to use records in my project. So I did somethink like:

[GraphQLName("IChannel")]
public interface IChannelDto
{
    [ID]
    Guid Id { get; init; }

    string Description { get; init; }

    #region workaround
    // TODO: delete after Hot Chocolate Team fixes one of two issue:
    // - ExtendObjectType should extend interface and all derived classes
    // - interface default methods implementations must be used in derived classes
    Task<ChannelStatus> GetStatusAsync() { throw new NotImplementedException(); }
    #endregion
}

[ExtendObjectType(typeof(IChannelDto))]
public class ChannelResolvers
{
    public async Task<ChannelStatus> GetStatusAsync()
    {
        // Do some stuff
    }
}

[GraphQLName("ChannelTypeA")]
public record ChannelTypeADto
(
    [property: ID] Guid Id,
    string Description,
    string IP,
) : IChannelDto;

[UnionType("IAnotherChannel")]
public interface IAnotherChannelDto : IChannelDto { };

[GraphQLName("ChannelTypeB")]
public record ChannelTypeBDto
(
    [property: ID] Guid Id,
    string Description,
    string Port,
) : IAnotherChannelDto;

[GraphQLName("ChannelTypeC")]
public record ChannelTypeCDto
(
    [property: ID] Guid Id,
    string Description,
    string Adapter,
) : IAnotherChannelDto;

Here is a problem:

  1. The ExtendObjectType annotation does not extend interfaces by design but I need it, so I made a workaround. That stub function is never called and I only need it to create the schema.
  2. I can't just remove the class with the ExtendObjectType annotation and put the default implementation inside the interface. Hot chocolate want me to add a Status field inside all derived records.

I must use both the ExtendObjectType annotation and that stub default implementation in the interface. If I could replace records with classes, I would do something like:

[InterfaceType("IChannel")]
public abstract class ChannelDto
{
    [ID]
    public Guid Id { get; init; }

    public string Description { get; init; }

    public async Task<ChannelStatus> GetStatusAsync()
    {
        // Do some stuff
    }
}

[GraphQLName("ChannelTypeA")]
public class ChannelTypeADto : ChannelDto
{
    public string IP { get; init; }
}

[UnionType("IAnotherChannel")]
public abstract class AnotherChannelDto : ChannelDto { };

[GraphQLName("ChannelTypeB")]
public class ChannelTypeBDto : AnotherChannelDto
{
    public string Port { get; init; }
}

[GraphQLName("ChannelTypeC")]
public class ChannelTypeCDto : AnotherChannelDto
{ 
    public string Adapter { get; init; }
}

This works nice!

The solution you'd like

One of:

  1. Let the ExtendObjectType annotation extend interfaces.
  2. Add support for the default method implementation in the schema inference algorithm.