Ignore read-only properties in input object types when using Hot Chocolate

Viewed 1057

I'm using Hot Chocolate as a GraphQL server. All my models inherit one base class that contains a property that does not have a public setter:

public class BaseModel
{
    ...

    public DateTime LastModified { get; private set; }
}

These models are used as output and as input objects. For my input objects I get the following exception:

No compatible constructor found for input type type SomeModel. Either you have to provide a public constructor with settable properties or a public constructor that allows to pass in values for read-only properties. There was no way to set the following properties: LastModified.

I could add [GraphQLIgnore], but this will also ignore the property for my output object types.

I tried registering a type for my base class to ignore the property only for all of my input object types:

public class BaseModelInputType : InputObjectType<BaseModel>
{
    protected override void Configure(IInputObjectTypeDescriptor<BaseModel> descriptor)
    {
        descriptor.Ignore(x => x.LastModified);
    }
}

And in the startup:

services.AddGraphQLServer()
    .AddType<BaseModelInputType>()
    .AddQueryType<Query>()
    .AddMutationType<Mutation>()

But this only seem to work on the concrete classes.

1 Answers

I ended up individually registering all input types using a common base class:

public class BaseInputType<TModel> : InputObjectType<TModel>
{
    private Expression<Func<TModel, object>> CreatePropertySelector(PropertyInfo prop)
    {
        var parameter = Expression.Parameter(typeof(TModel), "x");
        var property = Expression.Property(parameter, prop);

        return Expression.Lambda<Func<TModel, object>>(Expression.Convert(property, typeof(object)), parameter);
    }

    protected override void Configure(IInputObjectTypeDescriptor<TModel> descriptor)
    {
        var properties = typeof(TModel).GetProperties();
        var idProp = properties.FirstOrDefault(x => x.Name == nameof(BaseEntityModel.Id));
        var readOnlyProps = properties.Where(x => !x.CanWrite || x.SetMethod?.IsPublic != true).ToArray();
        var booleanProps = properties.Where(x => !readOnlyProps.Contains(x) && x.PropertyType == typeof(bool))
            .ToArray();

        // Make all boolean props optional
        foreach (var booleanProp in booleanProps)
        {
            descriptor.Field(this.CreatePropertySelector(booleanProp)).Type<BooleanType>();
        }

        // Ignore all properties without a public setter
        foreach (var propSelector in readOnlyProps.Select(this.CreatePropertySelector))
        {
            descriptor.Ignore(propSelector);
        }

        // Make id property in input types optional
        if (idProp != null)
        {
            descriptor.Field(this.CreatePropertySelector(idProp)).Type<StringType>();
        }
    }
}

This probably isn't the best solution, but it works for us. CreatePropertySelector is needed because the overload that takes a string doesn't correctly override the field description.

Related