diff --git a/blog/2020/2020-03-18-entity-framework/ContosoUni/ContosoUni.csproj b/blog/2020/2020-03-18-entity-framework/ContosoUni/ContosoUni.csproj index ad79223..1e90f45 100644 --- a/blog/2020/2020-03-18-entity-framework/ContosoUni/ContosoUni.csproj +++ b/blog/2020/2020-03-18-entity-framework/ContosoUni/ContosoUni.csproj @@ -1,17 +1,22 @@ - netcoreapp5.0 + net8.0 - - - - - - - + + + + + + + + + + + + diff --git a/blog/2020/2020-03-18-entity-framework/ContosoUni/Query.cs b/blog/2020/2020-03-18-entity-framework/ContosoUni/Query.cs index 0aee3da..abc97d3 100644 --- a/blog/2020/2020-03-18-entity-framework/ContosoUni/Query.cs +++ b/blog/2020/2020-03-18-entity-framework/ContosoUni/Query.cs @@ -1,5 +1,6 @@ using System.Linq; using HotChocolate; +using HotChocolate.Data; using HotChocolate.Types; using HotChocolate.Types.Relay; @@ -8,21 +9,17 @@ namespace ContosoUniversity public class Query { [UseFirstOrDefault] - [UseSelection] - public IQueryable GetStudentById([Service]SchoolContext context, int studentId) => + public IQueryable GetStudentById([Service] SchoolContext context, int studentId) => context.Students.Where(t => t.Id == studentId); - [UseSelection] + [UsePaging] [UseFiltering] [UseSorting] - public IQueryable GetStudents([Service]SchoolContext context) => - context.Students; + public IQueryable GetStudents([Service] SchoolContext context) => context.Students; [UsePaging] - [UseSelection] [UseFiltering] [UseSorting] - public IQueryable GetCourses([Service]SchoolContext context) => - context.Courses; + public IQueryable GetCourses([Service] SchoolContext context) => context.Courses; } -} \ No newline at end of file +} diff --git a/blog/2020/2020-03-18-entity-framework/ContosoUni/Startup.cs b/blog/2020/2020-03-18-entity-framework/ContosoUni/Startup.cs index 408a0b4..7f1ea0a 100644 --- a/blog/2020/2020-03-18-entity-framework/ContosoUni/Startup.cs +++ b/blog/2020/2020-03-18-entity-framework/ContosoUni/Startup.cs @@ -7,7 +7,12 @@ using HotChocolate; using HotChocolate.AspNetCore; using HotChocolate.Execution.Configuration; - +using HotChocolate.AspNetCore.Serialization; +using HotChocolate.ApolloFederation; +using HotChocolate.Types; +using HotChocolate.ApolloFederation.Types; +using HotChocolate.Execution; +using System.IO; namespace ContosoUniversity { @@ -15,15 +20,22 @@ public class Startup { // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) + public async void ConfigureServices(IServiceCollection services) { services.AddDbContext(); + services.AddHttpResponseFormatter(); + + var gqlService = services + .AddGraphQLServer() + .AddQueryType() + .AddFiltering() + .AddSorting() + .ModifyPagingOptions(opt => opt.IncludeTotalCount = true) + .AddApolloFederation(FederationVersion.Federation26) + .ExportDirective(); - services.AddGraphQL( - SchemaBuilder.New() - .AddQueryType() - .Create(), - new QueryExecutionOptions { ForceSerialExecution = true }); + var schema = await gqlService.BuildSchemaAsync(); + await File.WriteAllTextAsync("./schema.graphql", schema.Print()); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -38,42 +50,67 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseRouting(); - app.UseGraphQL(); - app.UsePlayground(); - app.UseEndpoints(endpoints => { - endpoints.MapGet("/", async context => - { - await context.Response.WriteAsync("Hello World!"); - }); + endpoints + .MapGraphQL() + .WithOptions(new GraphQLServerOptions { Tool = { Enable = true } }); }); } private static void InitializeDatabase(IApplicationBuilder app) { - using (var serviceScope = app.ApplicationServices.GetService().CreateScope()) + using ( + var serviceScope = app.ApplicationServices + .GetService() + .CreateScope() + ) { var context = serviceScope.ServiceProvider.GetRequiredService(); if (context.Database.EnsureCreated()) { - var course = new Course { Credits = 10, Title = "Object Oriented Programming 1" }; - - context.Enrollments.Add(new Enrollment - { - Course = course, - Student = new Student { FirstMidName = "Rafael", LastName = "Foo", EnrollmentDate = DateTime.UtcNow } - }); - context.Enrollments.Add(new Enrollment + var course = new Course { - Course = course, - Student = new Student { FirstMidName = "Pascal", LastName = "Bar", EnrollmentDate = DateTime.UtcNow } - }); - context.Enrollments.Add(new Enrollment - { - Course = course, - Student = new Student { FirstMidName = "Michael", LastName = "Baz", EnrollmentDate = DateTime.UtcNow } - }); + Credits = 10, + Title = "Object Oriented Programming 1" + }; + + context.Enrollments.Add( + new Enrollment + { + Course = course, + Student = new Student + { + FirstMidName = "Rafael", + LastName = "Foo", + EnrollmentDate = DateTime.UtcNow + } + } + ); + context.Enrollments.Add( + new Enrollment + { + Course = course, + Student = new Student + { + FirstMidName = "Pascal", + LastName = "Bar", + EnrollmentDate = DateTime.UtcNow + } + } + ); + context.Enrollments.Add( + new Enrollment + { + Course = course, + Student = new Student + { + FirstMidName = "Michael", + LastName = "Baz", + EnrollmentDate = DateTime.UtcNow + } + } + ); context.SaveChangesAsync(); } } diff --git a/blog/2020/2020-03-18-entity-framework/ContosoUni/Student.cs b/blog/2020/2020-03-18-entity-framework/ContosoUni/Student.cs index e9eb6cf..ea1ac04 100644 --- a/blog/2020/2020-03-18-entity-framework/ContosoUni/Student.cs +++ b/blog/2020/2020-03-18-entity-framework/ContosoUni/Student.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using HotChocolate.Data; using HotChocolate.Types; namespace ContosoUniversity @@ -15,8 +16,7 @@ public class Student public string FirstMidName { get; set; } public DateTime EnrollmentDate { get; set; } - [UseSelection] [UseFiltering] public virtual ICollection Enrollments { get; set; } } -} \ No newline at end of file +} diff --git a/blog/2020/2020-03-18-entity-framework/ContosoUni/schema.graphql b/blog/2020/2020-03-18-entity-framework/ContosoUni/schema.graphql new file mode 100644 index 0000000..2356cd4 --- /dev/null +++ b/blog/2020/2020-03-18-entity-framework/ContosoUni/schema.graphql @@ -0,0 +1,233 @@ +schema @composeDirective(name: "oneOf") @link(url: "https:\/\/specs.apollo.dev\/federation\/v2.6", import: [ "@shareable", "@tag", "FieldSet", "@composeDirective" ]) { + query: Query +} + +type Course { + courseId: Int! + title: String + credits: Int! + enrollments: [Enrollment] +} + +"A connection to a list of items." +type CoursesConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [CoursesEdge!] + "A flattened list of the nodes." + nodes: [Course] + "Identifies the total count of items in the connection." + totalCount: Int! @cost(weight: "10") +} + +"An edge in a connection." +type CoursesEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Course +} + +type Enrollment { + enrollmentId: Int! + courseId: Int! + studentId: Int! + grade: Grade + course: Course + student: Student +} + +"Information about pagination in a connection." +type PageInfo { + "Indicates whether more edges exist following the set defined by the clients arguments." + hasNextPage: Boolean! @shareable + "Indicates whether more edges exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! @shareable + "When paginating backwards, the cursor to continue." + startCursor: String @shareable + "When paginating forwards, the cursor to continue." + endCursor: String @shareable +} + +type Query { + studentById(studentId: Int!): Student @cost(weight: "10") + students("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: StudentFilterInput @cost(weight: "10") order: [StudentSortInput!] @cost(weight: "10")): StudentsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + courses("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: CourseFilterInput @cost(weight: "10") order: [CourseSortInput!] @cost(weight: "10")): CoursesConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + _service: _Service! +} + +type Student { + id: Int! + lastName: String + firstMidName: String + enrollmentDate: DateTime! + enrollments(where: EnrollmentFilterInput @cost(weight: "10")): [Enrollment] +} + +"A connection to a list of items." +type StudentsConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [StudentsEdge!] + "A flattened list of the nodes." + nodes: [Student] + "Identifies the total count of items in the connection." + totalCount: Int! @cost(weight: "10") +} + +"An edge in a connection." +type StudentsEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Student +} + +"This type provides a field named sdl: String! which exposes the SDL of the service's schema. This SDL (schema definition language) is a printed version of the service's schema including the annotations of federation directives. This SDL does not include the additions of the federation spec." +type _Service { + sdl: String! +} + +input CourseFilterInput { + and: [CourseFilterInput!] + or: [CourseFilterInput!] + courseId: IntOperationFilterInput + title: StringOperationFilterInput + credits: IntOperationFilterInput + enrollments: ListFilterInputTypeOfEnrollmentFilterInput +} + +input CourseSortInput { + courseId: SortEnumType @cost(weight: "10") + title: SortEnumType @cost(weight: "10") + credits: SortEnumType @cost(weight: "10") +} + +input DateTimeOperationFilterInput { + eq: DateTime @cost(weight: "10") + neq: DateTime @cost(weight: "10") + in: [DateTime] @cost(weight: "10") + nin: [DateTime] @cost(weight: "10") + gt: DateTime @cost(weight: "10") + ngt: DateTime @cost(weight: "10") + gte: DateTime @cost(weight: "10") + ngte: DateTime @cost(weight: "10") + lt: DateTime @cost(weight: "10") + nlt: DateTime @cost(weight: "10") + lte: DateTime @cost(weight: "10") + nlte: DateTime @cost(weight: "10") +} + +input EnrollmentFilterInput { + and: [EnrollmentFilterInput!] + or: [EnrollmentFilterInput!] + enrollmentId: IntOperationFilterInput + courseId: IntOperationFilterInput + studentId: IntOperationFilterInput + grade: NullableOfGradeOperationFilterInput + course: CourseFilterInput + student: StudentFilterInput +} + +input IntOperationFilterInput { + eq: Int @cost(weight: "10") + neq: Int @cost(weight: "10") + in: [Int] @cost(weight: "10") + nin: [Int] @cost(weight: "10") + gt: Int @cost(weight: "10") + ngt: Int @cost(weight: "10") + gte: Int @cost(weight: "10") + ngte: Int @cost(weight: "10") + lt: Int @cost(weight: "10") + nlt: Int @cost(weight: "10") + lte: Int @cost(weight: "10") + nlte: Int @cost(weight: "10") +} + +input ListFilterInputTypeOfEnrollmentFilterInput { + all: EnrollmentFilterInput @cost(weight: "10") + none: EnrollmentFilterInput @cost(weight: "10") + some: EnrollmentFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") +} + +input NullableOfGradeOperationFilterInput { + eq: Grade @cost(weight: "10") + neq: Grade @cost(weight: "10") + in: [Grade] @cost(weight: "10") + nin: [Grade] @cost(weight: "10") +} + +input StringOperationFilterInput { + and: [StringOperationFilterInput!] + or: [StringOperationFilterInput!] + eq: String @cost(weight: "10") + neq: String @cost(weight: "10") + contains: String @cost(weight: "20") + ncontains: String @cost(weight: "20") + in: [String] @cost(weight: "10") + nin: [String] @cost(weight: "10") + startsWith: String @cost(weight: "20") + nstartsWith: String @cost(weight: "20") + endsWith: String @cost(weight: "20") + nendsWith: String @cost(weight: "20") +} + +input StudentFilterInput { + and: [StudentFilterInput!] + or: [StudentFilterInput!] + id: IntOperationFilterInput + lastName: StringOperationFilterInput + firstMidName: StringOperationFilterInput + enrollmentDate: DateTimeOperationFilterInput + enrollments: ListFilterInputTypeOfEnrollmentFilterInput +} + +input StudentSortInput { + id: SortEnumType @cost(weight: "10") + lastName: SortEnumType @cost(weight: "10") + firstMidName: SortEnumType @cost(weight: "10") + enrollmentDate: SortEnumType @cost(weight: "10") +} + +enum Grade { + A + B + C + D + F +} + +enum SortEnumType { + ASC + DESC +} + +"Marks underlying custom directive to be included in the Supergraph schema." +directive @composeDirective(name: String!) repeatable on SCHEMA + +"The purpose of the `cost` directive is to define a `weight` for GraphQL types, fields, and arguments. Static analysis can use these weights when calculating the overall cost of a query or response." +directive @cost("The `weight` argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc." weight: String!) on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM | INPUT_FIELD_DEFINITION + +"Links definitions within the document to external schemas." +directive @link("Gets imported specification url." url: String! "Gets optional list of imported element names." import: [String!]) repeatable on SCHEMA + +"The purpose of the `@listSize` directive is to either inform the static analysis about the size of returned lists (if that information is statically available), or to point the analysis to where to find that information." +directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!] "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `requireOneSlicingArgument` argument can be used to inform the static analysis that it should expect that exactly one of the defined slicing arguments is present in a query. If that is not the case (i.e., if none or multiple slicing arguments are present), the static analysis may throw an error." requireOneSlicingArgument: Boolean! = true) on FIELD_DEFINITION + +"Indicates that given object and\/or field can be resolved by multiple subgraphs." +directive @shareable repeatable on OBJECT | FIELD_DEFINITION + +"The `@specifiedBy` directive is used within the type system definition language to provide a URL for specifying the behavior of custom scalar definitions." +directive @specifiedBy("The specifiedBy URL points to a human-readable specification. This field will only read a result for scalar types." url: String!) on SCALAR + +"The `DateTime` scalar represents an ISO-8601 compliant date time type." +scalar DateTime @specifiedBy(url: "https:\/\/www.graphql-scalars.com\/date-time") + +"Scalar representing a set of fields." +scalar FieldSet + +"The _Any scalar is used to pass representations of entities from external services into the root _entities field for execution. Validation of the _Any scalar is done by matching the __typename and @external fields defined in the schema." +scalar _Any \ No newline at end of file diff --git a/global.json b/global.json index 19c87f2..3fea262 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,6 @@ { "sdk": { - "version": "6.0.300" + "version": "8.0.0", + "rollForward": "latestFeature" } }