Giter Club home page Giter Club logo

ngraphql's Introduction

NGraphQL - GraphQL for .NET

NGraphQL is a framework for implementing GraphQL APIs in .NET. It provides server- and client-side components.
Here is an overview of the project, what is different and why I created it in the first place.

Features

  • Conforms to GraphQL Specification, Oct 2021 Edition.
  • GraphQL model is defined using plain c# (POCO) classes decorated with some attributes. Unlike other .NET GraphQL solutions, NGraphQL-based API definitions look and feel like real .NET artifacts - strongly typed, compact and readable.
  • Server and client components. ASP.NET Core -based HTTP server implementation following the standard "serving over HTTP" rules
  • Subscriptions are fully supported
  • Light-weight but capable GraphQL Client - supports strongly-type objects for the returned data. Easy client-side API for subscriptions
  • Modular construction - separately coded modules define parts of the overall GraphQL API Schema; modules are registered with the GraphQL host server which implements the GraphQL API.
  • Supports parallel execution of Query requests
  • Sync and Async resolver methods
  • Full Introspection support
  • Schema descriptions are automatically imported from XML comments in c# code
  • Full support for fragments and standard directives (@include, @skip, @deprecated)
  • Custom Scalars out of the box (Double, ID, Uuid, DateTime etc)
  • Fast, efficient query parser; query cache - parsed queries are saved in cache for future reuse with different input variables
  • Facilities for input validation and returning failures as multiple GraphQL errors
  • Robust implementation of batching (N+1 problem)
  • Integration with relational databases and ORMs - the BookStore Sample shows a GraphQL server on top of a data-connected application, with batching support.

Packages and Components

NGraphQL binaries are distributed as a set of NuGet packages:

Package Description
NGraphQL Basic classes shared by client and server components.
NGraphQL.Client GraphQL client.
NGraphQL.Server GraphQL server implementation not tied to a specific transport protocol.
NGraphQL.Server.AspNetCore GraphQL HTTP server based on ASP.NET Core stack.

Examples

The repo contains a Test project with HTTP server: Things.GraphQL.HttpServer. You can launch it directly as a startup project in Visual Studio.

Install the GraphQL Playground for Chrome extension from Chrome store, and launch the project. It will start the web server, and will open the GraphQL Playground page. Enter the following URL as the target: http://localhost:55571/graphql, and run a sample query: "query { things {name kind theFlags aBCGuids} }". The test server implements a GraphQL API about abstract Things, and it is void of any real semantic meaning - it is for testing purpose only. The purpose of this app is to provide a number of types and methods covering the many aspects of the GraphQL protocol.

Run the unit tests and see the many request/response examples used there. The unit tests write a detailed log as they go. Run the tests, locate the log file in the bin folder, and look inside for many examples of GraphQL requests and responses along with the metrics. See this file here: UnitTestsLog.

See also Star Wars Example in a separate github repository.

VITA ORM contains a sample project implementing a GraphQL Server for a BookStore sample application. Among other things, it shows how (N+1) problem can be efficiently handled automagically by a smart-enough ORM. Most of the related entities like Book.Publisher or Book.Authors are batch-loaded automatically by the ORM.

Documentation

See the Wiki pages for this project.

System requirements

.NET Standard 2.0, .NET 6/8.

Other GraphQL on .NET solutions

ngraphql's People

Contributors

rivantsov avatar romanivantsov avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

ngraphql's Issues

No longer run after upgraded to ver 1.7

Hi @rivantsov ,

After upgraded to ver 1.7, the graphql seems not working anymore. I see you had made some changes on the startup strategy, not sure if I need to make any changes on my side here.

Btw, I see there is also builder.AddGraphQLServer extensions, could you please also add for services (IServiceCollection)?

Thanks.

Error with Altair

Hi @rivantsov ,

I have tried to switch to Altair from the GraphiQL, but hit the following errors.
Is this framework issue or Altair issue?
Notes: it is working fine with GraphiQL.

image

Mutation with IFormFile field

Hi @rivantsov ,

My I know how do we handle for mutation case with IFormFile argument ?
In REST, I use multipart form data to solve this problem, wondering how this is handled in ngraphql.

Thanks.

Error format in NGraphQL

Hi @rivantsov ,

I have a Entity validation logic as below.

        [Entity]
        [Validate(typeof(AuthorizationModule), nameof(AuthorizationModule.ValidateLogin))]
        [EntitySavingEvent(typeof(AuthorizationModule), nameof(AuthorizationModule.SavingLogin))]
        [Display("{LoginId}/{Username}")]
        public interface ILogin
        {
           ...
        }


        public static void ValidateLogin(ILogin login)
        {
            var record = EntityHelper.GetRecord(login);
            if (record.Status == EntityStatus.New)
            {
                var session = record.Session;
                var existing = session.FindLogin(login.Username, login.Tenant);
                session.ValidateEntity(login, existing == null, "", "", null, $"Login with username {login.Username} already exists.");
            }
        }

When calling from REST, I have the error formatted as below.

[
  {
    "code": "",
    "message": "Login with username [email protected] already exists.",
    "tag": "",
    "path": "ILogin/ILogin/01F6PX8W98JTDD05XNDCQC0YN8",
    "parameters": {}
  }
]

However, when calling from NGraphQL, I have the following error format:

{
  "errors": [
    {
      "message": "Client faults detected.",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "register"
      ],
      "extensions": {
        "code": "RESOLVER_ERROR",
        "Details": "Vita.Entities.ClientFaultException: Client faults detected.\r\n   at Vita.Entities.OperationContext.ThrowValidation()\r\n   at Vita.Entities.Runtime.EntitySession.SaveChanges()\r\n   at Aether.Booking.BookingModule.RegisterCustomer(OperationContext context, RegisterCustomerInput input) in E:\\JSL\\Aether\\aetherall\\Aether.Booking\\BookingModule_Register.cs:line 73\r\n   at Aether.Booking.Api.GraphQL.BookingResolvers.Register(IFieldContext context, RegisterCustomerInput input) in E:\\JSL\\Aether\\aetherall\\Aether.Booking.Api\\GraphQL\\BookingResolvers_Mutation_Register.cs:line 12\r\nFaults:\r\nLogin with username [email protected] already exists.\r\n"
      }
    },
    {
      "message": "Client faults detected.",
      "locations": [],
      "path": [],
      "extensions": {
        "code": "SERVER_ERROR",
        "Details": "Vita.Entities.ClientFaultException: Client faults detected.\r\n   at Vita.Entities.OperationContext.ThrowValidation()\r\n   at Vita.Entities.Runtime.EntitySession.SaveChanges()\r\n   at Aether.Booking.Api.GraphQL.BookingResolvers.EndRequest(IRequestContext request) in E:\\JSL\\Aether\\aetherall\\Aether.Booking.Api\\GraphQL\\BookingResolvers.cs:line 25\r\n   at NGraphQL.Server.Execution.OperationFieldExecuter.ExecuteOperationFieldAsync()\r\n   at NGraphQL.Server.Execution.RequestHandler.ExecuteAllNonParallel(IList`1 executers)\r\n   at NGraphQL.Server.Execution.RequestHandler.ExecuteOperationAsync(GraphQLOperation op, OutputObjectScope topScope)\r\n   at NGraphQL.Server.Execution.RequestHandler.ExecuteAsync()\r\n   at NGraphQL.Server.GraphQLServer.ExecuteRequestAsync(RequestContext context)\r\nFaults:\r\nLogin with username [email protected] already exists.\r\n  Login with username [email protected] already exists.\r\n"
      }
    }
  ],
  "data": {}
}

Do you think it is possible to make both error format in a more consistent way, at least the message in GraphQL should be "Login with username [email protected] already exists.".

Resolver method name

When I name the method with GetUsers, it will hit the error.
Since there is type specified, I expect it will not get confused, bug?

       // Error when name the method GetUsers:
      // Resolver method CommunityQueryResolvers.GetUsers: parameter count mismatch with field arguments, expected 3, with added IFieldContext and possibly Parent object parameter. 
       [ResolvesField("users", typeof(ICommunityQuery))]
        public IList<IUser> GetUsersByQuery(IFieldContext context)
        {
            return _session.EntitySet<IUser>().ToList();
        }

        // Error when name the method GetUsers:
       // Resolver method CommunityQueryResolvers.GetUsers: parameter count mismatch with field arguments, expected 1, with added IFieldContext and possibly Parent object parameter. 
        [ResolvesField("users", typeof(Community_))] 
        public IList<IUser> GetUsersByCommunity(IFieldContext context, ICommunity community, string type)
        {
            var members = type == null || type == "Member" ? _session.EntitySet<IMemberInCommunity>()
                                                                .Where(x => x.Community == community)
                                                                .Select(x => x.User).ToList() : new List<IUser>();

            var officers = type == null || type == "Officer" ? _session.EntitySet<IUser>()
                                                                .Where(x => x.OfficerCommunity == community)
                                                                .ToList() : new List<IUser>();

            return (IList<IUser>)members.Union(officers).Distinct();
        }
 public class Community_
    {       

        [GraphQLName("users")]
        public List<User_> GetUsers([Null] string type) { return default; }
    }
  }

   public interface ICommunityQuery
    {
     
       [GraphQLName("users")]
        IList<User_> GetUsers();
    }

Using subscriptions with non .NET clients or playgrounds

The latest release contains full implementation of Subscriptions, server and full support by GraphQLClient (Client in NGraphQL). Unit tests run successfully using this client.
The question comes about other clients (js/ts in browser) or playground apps like Graphiql. I did not try it yet in playgrounds, but I will in the coming weeks and will report here, if it's doable and how. The situation is that GraphQL spec 'defines' subscriptions, but does not really specify any details of implementation of the actual protocol, so implementations may vary.
Looks like the today the common agreed protocol is this:

https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md

and NGraphQL implementation is based on it, as close as possible.
One important note - the Subscribe message, with subscription query inside, is sent over WebSocket connection, not through GraphQL usual Post request. So do not try to do POST with subscription query, like you do with Query or Mutation - that would not work.
For accessing NGraphQL subscription from JS in the page or other client. Two steps:

  1. Establish WebSocket connection
  2. Use the protocol above to exchange json messages over this connection

For item 1, NGraphQL is using SignalR for websockets, so here are the instructions of how to connect to SignalR server from JS:

https://learn.microsoft.com/en-us/aspnet/core/signalr/javascript-client?view=aspnetcore-8.0&tabs=visual-studio

Edit: NGraphQL subscription endpoint for websockets: /graphql/subscriptions

After connecting, start sending/receiving messages according to the protocol. NGraphQL server supports all message including ConnectionInit, Ping, Subscribe, Complete etc. I am sure there is some ready to use JS/React component doing this, some GraphQL client. Let me know if something does not work.

In the meantime, I will try to play with playgrounds and see if they can make to work with NGraphQL server.

GraphQL vs REST

Hi @rivantsov ,

Could you please share with me what is your biggest motivation (or anyone) to move from REST to GraphQL?
I have tried GraphQL for the POC and I don't feel like I am fully utilize its power, since all stuff can be done by the conventional REST with a more simpler way.

With GraphQL, we need to specific each single fields for retrieval, also for the input variables we even need to specific the datatype in the query. For eg, I need to specifically define LoginRequest as my input variable type, while in REST all I need to do is just a JSON with correct fields.

I think the GraphQL is powerful but I am not sure it is a good fit for me. I would like to seek for your opinion before going any further.

Thank you very much.

Question about mapping

Hi @rivantsov ,

I have some questions that require your clarification.

Base on the following mapping:

  MapEntity<LoginResult>().To(x => new LoginResponse
        {
            Flags = x.Login.FlagsInt(),
            LastLoggedInOn = x.Login == null ? null : x.Login.LastSuccessOnClientTime,
            RefreshToken = x.Status == LoginStatus.Success
                                    ? AuthorizationModule.CreateRefreshToken(EntityHelper.GetSession(x).Context, true) : null
        });

Questions:

  1. Will these 3 mapping being evaluated even the user doesn't select any of the attributes?
  2. Will all of these mapping being evaluated if only one of the attribute is selected?
  3. In my LoginResult entity, there is already an attribute LastLoggedInOn, but I would like this being overridden as shown in the code above, this is possible right?
  4. I would like these mapping to be evaluated only if selected by user, possible?

Enum return capital case

Hi @rivantsov ,

The enum type is returned as full capital case, is this a bug?

// My enum
public enum LoginStatus
{
    Success,
    Failed,
    OneTimePasswordUsed,
    Disabled,
}

// Enum register in graphql module
EnumTypes.Add(typeof(LoginStatus));
// Result from REST API
// The status has value exactly as per defined
{
  "status": "Success",
  "loginId": "01F61K04NHSFKB20WVK90W392P",
  "username": "[email protected]",
  "name": "Admin",
  "flags": 0,
  "lastLoggedInOn": "2021-05-23T04:45:21.773Z",
  "refreshToken": "Osh08jdsxzbmB2LM5pGOferjo7oU2klYX+xjUbMWtMUYQ+7dugJyUizx4JFK3YR9s8PKdUoBdFix0bzDfCbvIlv3si8fymInyc31Mb4MXQn4oqRdHDPulBVP23Fb/zKQX//15cowhfMA5Q5WC5JaQ9JK+sljdrNy2qDRkhKnAuMos1/KN9Fgb5yhWpsVCCRSw029Croj/tDkfAwXqhXd2BL10+D7EYaM93JWzer6Id7AV8FJe76Ei5vAznKPJL5fjvozjyBV0gdCkNbPUMNF32pNdPztVSMA4eKEqhxHDR61VM1SjrThtJnd0PR9s22xA/VP9J4rZV8b9HvaKNRIzJNvqGX4A7gtqxOmM3ph5k4/hVo3fcgb3uuzr9/DvGsx2IOM+kKWkZeO+IV3AG44JzSb+ijIAYm1ALloNTKZRrEFo5+42xRsH9It+pznTF+BlbDGm1aGjAUyCvjx3/Sdn29kqrDaMX+zkLoLlbhh/j8="
}

//Result from GraphQL
// The status has value of full capital case SUCCESS
{
  "data": {
    "login": {
      "loginId": "01F61K04NHSFKB20WVK90W392P",
      "status": "SUCCESS",
      "name": "Admin"
    }
  }
}

Altair schema doesn't support for generic type

Hi,

With the following, GraphiQL is working fine, but not Altair, is this due to Altair is too strict or something the framework doesn't handle properly?

 public class SearchOutput<T>
  {
      [Null] public PageInfo PageInfo { get; set; }
      public IList<T> Results { get; set; }
  }
 
ObjectTypes.Add( typeof(SearchOutput<Category>));

MapEntity<SearchOutput<ICategory>>().To<SearchOutput<Category>>();

image

Subscription

See there is subscription method in the example, is it ready already? It would be interesting to try that out. Thanks.

[Question] Is multiple resolver possible?

Hi @rivantsov ,

Can we have multiple resolvers, eg, separated by query and mutation?
If yes, what happen if both resolvers are triggered during the run? Will both also going through the BeginRequest and EndRequest?
Will that be any implication of that?

Thanks in advance.

[Question] Null annotation

Hi @rivantsov ,

May I know is the Null annotation only useful for input type?
For output fields, I don't see any different with or without it, I could be wrong.

Thanks!

DateTime is not returned in UTC

Hi @rivantsov ,

I have an issue that related to UTC datetime again. :(

My service is running in hosting server with UTC timezone.

My entity has all datetime annotated with UTC.

public interface IMembership
{
    [PrimaryKey, CascadeDelete] ICompanyIdentity CompanyIdentity { get; set; }
    [NoUpdate, CascadeDelete] ICompany Company { get; set; }
    [NoUpdate] IUser User { get; set; }
    int Points { get; }
    IList<IVoucher> Vouchers { get; }
    IList<IPointTrans> PointTrans { get; }
    int VisitCounter { get; set; }
    [Utc, Nullable] DateTime? FirstVisit { get; set; } // Stored in UTC DateTime
    [Utc, Nullable] DateTime? LastVisit { get; set; } // Stored in UTC DateTime
    [Utc, Nullable] DateTime? LastRedeemedVoucher { get; set; } // Stored in UTC DateTime
    bool LastVisitRating { get; set; }
    [Utc, Auto(AutoType.CreatedOn)] DateTime CreatedOn { get; }
    [Utc, Auto(AutoType.UpdatedOn)] DateTime UpdatedOn { get; }
}

The values stored in database are correct as expected.
image

The response from NGraphQL is without the UTC:
image

Thanks in advance!

[Question] How to hide fields from server side?

Hi @rivantsov ,

Suppose we want to hide some fields base on certain condition from the server, how can we do that?

Eg,
user query the following:
SomeEntity { field1 field2 field3 }

while we have our direct mapping as following:
Mapping().To();

Due to some reason, we want to always return null to field3 even those there is value.

The condition logic could be complex and not feasible in mapping expression.

Thanks in advance.

DateTime with Timezone, inconsistent between Query and Mutation

Hi @rivantsov ,

I found the DateTime return from query and mutation seems inconsistent, however I can't be sure.

Here is what I found, a Company object type with the following mapping:

  .MapEntity<ICompany>()
            .To(x => new Company
            {
                Id = x.Tenant.Id,
                CurrentPlanId = x.CurrentPlan == null ? null : x.CurrentPlan.Id,
                ExpiryDate = x.CurrentPlan == null ? null : x.CurrentPlan.ExpiryDate,
            });

Result return from mutation:

{
  "data": {
    "buyPlan": {
      "expiryDate": "2024-05-26T00:00:00+08:00"
    }
  }
}

Result return from query:

{
  "data": {
    "companies": [
      {
        "expiryDate": "2024-05-26T00:00:00"
      }
    ]
  }
}

The result from mutation in UTC format but not in query.

Here is how I define the ExpiryDate in entity:

[DateOnly, NoUpdate] DateTime ExpiryDate { get; set; }

The value stored in DB
image

Is this a bug?

Mutation return void

Hi @rivantsov ,

I have one quick question, can we return void from mutation?
For eg, I have a logout api, and it has nothing to return, accept the http status 200 or exception.
In this case, what is the best practice for graphql?

Thanks in advance.

Query type field

Hi @rivantsov ,

When reading this, it suggests that for each mutation should have a dedicated payload, and the payload should return a field of Query type.

I am wondering how do we achieve this with NGraphQL?

Thanks in advance.

Empty json input causing error

Hi @rivantsov ,

My input object's attributes are nullable, when I pass in empty json as input, it hit query error, is this normal?

My input object:

public class SearchUserParamsType
{
    [Null] public string Id { get; set; }
    [Null] public string Name { get; set; }
    [Null] public string Contact { get; set; }
    [Null] public string Email { get; set; }
}

Reproduction video:

empty_json

[Bug] Mapping failed when subclass from the abstract class

Here is my model (simplify):

public interface IAccount
{
  string Name {get; set;}
  ILogin Login {get; set;}
}

public abstract class Account_
{
   public string Name;
   public string Username; // It works if move this attribute down to Account class
}

public class Account : Account_ 
{
}

My Mapping in GraphQLModule:

MapEntity<IAccount>().To(x => new Account { Username = x.Login.Username });

Error:

================= GraphQL Model Errors Detected =========================
Field 'Account.username' (module BookingGraphQLModule) has no associated resolver or mapped entity field.
================= End GraphQL Model Errors ==============================

[Bug] Failed to register with generic class

image

================= GraphQL Model Errors Detected =========================
GraphQL type SearchOutput`1 already registered; module: BookingGraphQLModule.
================= End GraphQL Model Errors ==============================

Object type as Input type

Hi @rivantsov ,

Can I have object type as input type?
I mean, suppose my input object type is exactly the same as my output object type, do I still need to declare 2 separate type for it?
If not, then which should I register into, as an object type or input type?

Thanks.

Should the model mapping based on GraphQLName?

Hi @rivantsov ,

I hit the error when have GraphQLName defined in the model.

================= GraphQL Model Errors Detected =========================
Field 'unloading' has no associated resolver or mapped entity field. Field: 'Berth.unloading', mapping from (entity) type 'Quintiq.Scheduler.Entity.IBerth', (module 'SchedulerGraphQLModule').
Field 'unloading' has no associated resolver or mapped entity field. Field: 'Berth.unloading', mapping from (entity) type 'Quintiq.Scheduler.GraphQL.Model.Berth', (module 'SchedulerGraphQLModule').
================= End GraphQL Model Errors ==============================

I suppose the GraphQLName is for external reference only, and the mapping should be based on the actual field name, is this not the case?

Entity schema:

 [Entity]
  public interface IBerth
  {
      [PersistOrderIn("SeqOfBerth")]
      IList<IUnloading> Unloadings { get; set; }
  }

GraphQL Model:

    public class Berth
    {
        // Error if uncomment the GraphQLName
        //[GraphQLName("unloading")]
        public IList<Unloading> Unloadings;
    }

Thank you.

Enum type in string

The input type will complaint if a string has been passed to a Enum type.
Is it possible to make it read from string as well?

mutation MyMutation { login ( request: {type :"Form",loginId:"jasonlaw", password:"12345"} ) { status authenticationToken failedMessage loginId flags user { id name email picture { key url } phoneNumber hasMemberRole hasVendorRole hasMemberRole officer { name password community { id name description address city state postcode country useLogoAsBackgroundImage logo { key url } } } member { address adminLevel community { id name description address city state postcode country useLogoAsBackgroundImage logo { key url } } } } } }

{ "errors": [ { "message": "Invalid value '\"Form\"', expected Enum value.", "locations": [ { "line": 2, "column": 27 } ], "path": [ "login", "request" ], "extensions": { "code": "INPUT_ERROR" } } ], "data": null }

Empty schema

@rivantsov ,

May I know what should I check if the schema cannot be shown in the GraphiQL or Altair?
There is no error and I can't figure out what is going wrong.

Thanks.

Subscription example

May I know is there any example for subscription, even is integrated with other packages? Thanks.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.