Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

EF Core with GraphQL

I'm currently exploring the GraphQL development and I'm currently exploring what kind of SQL queries are Generated via EF Core and I observed that no matter that my GraphQL query includes only a few fields the EF Core sends SQL Select for all fields of the Entity.

This is the code I'm using now:

public class DoctorType : ObjectGraphType<Doctors>
{
    public DoctorType()
    {
            Field(d => d.PrefixTitle);
            Field(d => d.FName);
            Field(d => d.MName);
            Field(d => d.LName);
            Field(d => d.SufixTitle);
            Field(d => d.Image);
            Field(d => d.EGN);
            Field(d => d.Description);
            Field(d => d.UID_Code); 
    }
}

public class Doctors : ApplicationUser
{
    public string Image { get; set; }
    [StringLength(50)]
    public string UID_Code { get; set; }
}

the query I'm using is

{
  doctors{
    fName
    lName
  }
}

The SQL generated selects all fields of the Doctor entity.

Is there any way to further optimize that the generated SQL query from EF Core?

I'm guessing this happens because the DoctorType inherits from ObjectGraphType<Doctors> and not from some Projection of the Doctor, but I can't think of a clever workaround of this?

Any suggestions?

EDIT:

I'm using GraphQL.NET (graphql-dotnet) by Joe McBride version 2.4.0

EDIT 2:

Either I'm doing it wrong or I don't know.

As one of the comments suggested i downloaded GraphQL.EntityFramework Nuget package by SimonCropp

I did all the configuration needed for it:

        services.AddDbContext<ScheduleDbContext>(options =>
        {
            options.UseMySql(Configuration.GetConnectionString("DefaultConnection"));
        });

        using (var myDataContext = new ScheduleDbContext())
        {
            EfGraphQLConventions.RegisterInContainer(services, myDataContext);
        }

My Object graph Type is looking as follows

public class SpecializationType : EfObjectGraphType<Specializations>
{
    public SpecializationType(IEfGraphQLService graphQlService)
        :base(graphQlService)
    {
        Field(p => p.SpecializationId);
        Field(p => p.Code);
        Field(p => p.SpecializationName);
    }
}

My query looks is:

public class RootQuery : EfObjectGraphType
{
    public RootQuery(IEfGraphQLService efGraphQlService,
        ScheduleDbContext dbContext) : base(efGraphQlService)
    {
        Name = "Query";

        AddQueryField<SpecializationType, Specializations>("specializationsQueryable", resolve: ctx => dbContext.Specializations);

    }
}

and I'm using this graphQL query

{
  specializationsQueryable
  {
    specializationName
  }
}

The debug log show that the generated SQL query is

SELECT `s`.`SpecializationId`, `s`.`Code`, `s`.`SpecializationName`
FROM `Specializations` AS `s`

even though I want only specializationName field and I'm expecting it to be:

SELECT `s`.`SpecializationName`
FROM `Specializations` AS `s`

UPDATE

I guess so far I didn't understand how graphQL really worked. I thought that there is some behind the scene fetch of data but there isn't.

The primary fetch is done in the query's field resolver :

FieldAsync<ListGraphType<DoctorType>>("doctors", resolve: async ctx => await doctorServices.ListAsync());

and as long the result to the resolver is the full object in my case the resolver return List of Doctors entity, it will query the Database for the whole entity (all fields). No optimisations are done out of the box from GraphQL doesn't matter if you return IQueryable or else of the entity you are querying.

Every conclusion here is thought of mine it is not 100% guaranteed right

So what I've did is create a group of Helper methods which are creating an selection Expression to use in the LINQ query. The helpers are using resolver's context.SubFields property to get the fields needed.

The problem is that you need for every level of the query only the leaves, say some query "specializations" with "SpecializationName" and "Code" and the "Doctors" with their "Name" and else. In this case in the RootQuery specializations field's resolver you need only the Specializations entity projection so: SpecializationName and Code , then when it goes to fetch all Doctors from the "doctors" Field in SpecializationType the resolver's context has different SubFields which should be used for the projection of the Doctor.

The problem with the above is, when you use query batches i guess even if you dont the thing is that the Doctors Field in SpecializationType needs the SpecializationId fetched in the RootQuery specializations Field.

I guess i didn't explain good what i went through.

Base line is as far as I understand we have to dynamically create selectors which the linq should use to project the entity.

I'm posting my approach here:

    public class RootQuery : EfObjectGraphType
{
    public RootQuery(IEfGraphQLService efGraphQlService, ISpecializationGraphQlServices specializationServices,
        IDoctorGraphQlServices doctorServices, ScheduleDbContext dbContext) : base(efGraphQlService)
    {
        Name = "Query";

        FieldAsync<ListGraphType<SpecializationType>>("specializations"
            , resolve: async ctx => {

                var selectedFields = GraphQLResolverContextHelpers.GetFirstLevelLeavesNamesPascalCase(ctx.SubFields);
                var expression = BuildLinqSelectorObject.DynamicSelectGenerator<Specializations>(selectedFields.ToArray());

                return await specializationServices.ListAsync(selector: expression);
            });
    }
}

SpecializationType

 public class SpecializationType : EfObjectGraphType<Specializations>
{
    public SpecializationType(IEfGraphQLService graphQlService
        , IDataLoaderContextAccessor accessor, IDoctorGraphQlServices doctorServices)
        : base(graphQlService)
    {
        Field(p => p.SpecializationId);
        Field(p => p.Code);
        Field(p => p.SpecializationName);
        Field<ListGraphType<DoctorType>, IEnumerable<Doctors>>()
            .Name("doctors")
            .ResolveAsync(ctx =>
            {

                var selectedFields = GraphQLResolverContextHelpers.GetFirstLevelLeavesNamesPascalCase(ctx.SubFields);
                selectedFields = GraphQLResolverContextHelpers.AppendParrentNodeToEachItem(selectedFields, parentNode: "Doctor");
                selectedFields = selectedFields.Union(new[] { "Specializations_SpecializationId" });

                var expression = BuildLinqSelectorObject.BuildSelector<SpecializationsDoctors, SpecializationsDoctors>(selectedFields);

                var doctorsLoader = accessor.Context
                    .GetOrAddCollectionBatchLoader<int, Doctors>(
                        "GetDoctorsBySpecializationId"
                        , (collection, token) =>
                        {
                            return doctorServices.GetDoctorsBySpecializationIdAsync(collection, token, expression);
                        });
                return doctorsLoader.LoadAsync(ctx.Source.SpecializationId);
            });
    }
}

DoctorsServices:

public class DoctorGraphQlServices : IDoctorGraphQlServices
{
    public ScheduleDbContext _dbContext { get; set; }

    public DoctorGraphQlServices(ScheduleDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<List<Doctors>> ListAsync(int? specializationId = null)
    {
        var doctors = _dbContext.Doctors.AsQueryable();

        if(specializationId != null)
        {
            doctors = doctors.Where(d => d.Specializations.Any(s => s.Specializations_SpecializationId == specializationId));
        }

        return await doctors.ToListAsync();
    }

    public async Task<ILookup<int, Doctors>> GetDoctorsBySpecializationIdAsync(IEnumerable<int> specializationIds, CancellationToken token, Expression<Func<SpecializationsDoctors, SpecializationsDoctors>> selector = null)
    {
        var doctors = await _dbContext.SpecializationsDoctors
            .Include(s => s.Doctor)
            .Where(spDocs => specializationIds.Any(sp => sp == spDocs.Specializations_SpecializationId))
            .Select(selector: selector)
            .ToListAsync();

        return doctors.ToLookup(i => i.Specializations_SpecializationId, i => i.Doctor);
    }

}

SpecializationServices

public class SpeciaizationGraphQlServices : ISpecializationGraphQlServices
{

    public ScheduleDbContext _dbContext { get; set; }

    public SpeciaizationGraphQlServices(ScheduleDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<dynamic> ListAsync(string doctorId = null, Expression<Func<Specializations, Specializations>> selector = null)
    {
        var specializations = _dbContext.Specializations.AsQueryable();

        if (!string.IsNullOrEmpty(doctorId))
        {
            specializations = specializations.Where(s => s.Doctors.Any(d => d.Doctors_Id == doctorId));
        }

        return await specializations.Select(selector).ToListAsync();

    }

    public async Task<ILookup<string, Specializations>> GetSpecializationsByDoctorIdAsync(IEnumerable<string> doctorIds, CancellationToken token)
    {
        var specializations = await _dbContext.SpecializationsDoctors
            .Include(s => s.Specialization)
            .Where(spDocs => doctorIds.Any(sp => sp == spDocs.Doctors_Id))
            .ToListAsync();

        return specializations.ToLookup(i => i.Doctors_Id, i => i.Specialization);
    }

    public IQueryable<Specializations> List(string doctorId = null)
    {
        var specializations = _dbContext.Specializations.AsQueryable();

        if (!string.IsNullOrEmpty(doctorId))
        {
            specializations = specializations.Where(s => s.Doctors.Any(d => d.Doctors_Id == doctorId));
        }

        return specializations;
    }
}

This post has become pretty large, sorry for the span..

like image 213
Stefan PEev Avatar asked Jan 28 '19 13:01

Stefan PEev


3 Answers

For DoctorType, check the defined ObjectGraphType which is used to return Doctors.

For example, I have PlayerType like below:

public class PlayerType : ObjectGraphType<Player>
{
    public PlayerType(ISkaterStatisticRepository skaterStatisticRepository)
    {
        Field(x => x.Id);
        Field(x => x.Name, true);
        Field(x => x.BirthPlace);
        Field(x => x.Height);
        Field(x => x.WeightLbs);
        Field<StringGraphType>("birthDate", resolve: context => context.Source.BirthDate.ToShortDateString());
        Field<ListGraphType<SkaterStatisticType>>("skaterSeasonStats",
            arguments: new QueryArguments(new QueryArgument<IntGraphType> { Name = "id" }),
            resolve: context => skaterStatisticRepository.Get(context.Source.Id), description: "Player's skater stats");
    }
}

And I return Field<ListGraphType<PlayerType>> by

public class NHLStatsQuery : ObjectGraphType
{
    public NHLStatsQuery(IPlayerRepository playerRepository, NHLStatsContext dbContext)
    {
        Field<ListGraphType<PlayerType>>(
            "players",
            resolve: context => {
                return dbContext.Players.Select(p =>new Player { Id = p.Id, Name = p.Name });
                //return playerRepository.All();
            });
    }
}

For the query and its columns, it is controlled by resolve in Field.

No matter what fields you want to return, make sure the columns defined in PlayerType are returned in resolve.

like image 170
Edward Avatar answered Oct 05 '22 20:10

Edward


There was a talk about GraphQL with EF Core 6 by @jeremylikness on .NET Conf 2021. I would recommend using .NET 6 and check his talk out:

https://devblogs.microsoft.com/dotnet/get-to-know-ef-core-6/#graphql

https://aka.ms/graphql-efcore

https://www.youtube.com/watch?v=GBvTRcV4PVA

https://www.youtube.com/watch?v=4nqjB_z5CU0

Here is an example implementation using Hot Chocolate GraphQL server:

https://chillicream.com/docs/hotchocolate/integrations/entity-framework

This is what Microsoft wrote about GraphQL for EF Core 6.0 in their High-level plan:

GraphQL has been gaining traction over the last few years across a variety of platforms. We plan to investigate the space and find ways to improve the experience with .NET. This will involve working with the community on understanding and supporting the existing ecosystem. It may also involve specific investment from Microsoft, either in the form of contributions to existing work or in developing complimentary pieces in the Microsoft stack.

https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-6.0/plan#graphql

like image 39
Ogglas Avatar answered Oct 05 '22 22:10

Ogglas


I suggest you:

1-use dto models and map them with database models

This means that you need to convert input dto model in database model to save in db; and also convert database models got from entity framework database select into dto model.

This is the classic approach used when you made a generic api, that for example get dto model data in input request, convert dto to save data in database, and viceversa.

2-map dto model to graphqltypes (objectgraphtype and inputobjectgraphtype)

This means that for each dto model could be necessary write 1 objectgraphtype and 1 inputobjectgraphtype.

TO DO THIS I'VE CREATE AN AUTOMATIC DTO TO GRAPHTYPE CONVERTER, so you don't need to write K and K of codes!! (see link at the end)

3-DON'T USE ADDDBCONTEXT! Graphql middleware use a singleton pattern; everything used via Dependecy injection in graphql is singleton externally, even if it is register as scoped (AddDbContext means "scoped").

This means that you have 1 connection opened to startup. You can't do 2 db operation in the same time!

In the real life you can't use AddDbContext with Graphql!

You can use factory pattern to do this. So, don't pass dbcontext in Dependency injection, but a Func and instantiate dbcontext explicitally.

Here a complete implementation example: https://github.com/graphql-dotnet/graphql-dotnet/issues/576#issuecomment-626661695

like image 38
Alessandro Lazzara Avatar answered Oct 05 '22 20:10

Alessandro Lazzara