├── .gitignore ├── GraphQLDemo ├── GraphQLDemo.API │ ├── .dockerignore │ ├── DTOs │ │ ├── CourseDTO.cs │ │ ├── InstructorDTO.cs │ │ └── StudentDTO.cs │ ├── DataLoaders │ │ ├── InstructorDataLoader.cs │ │ └── UserDataLoader.cs │ ├── Dockerfile │ ├── GraphQLDemo.API.csproj │ ├── GraphQLDemo.API.csproj.user │ ├── Middlewares │ │ └── UseUser │ │ │ ├── UseUserAttribute.cs │ │ │ ├── UserAttribute.cs │ │ │ └── UserMiddleware.cs │ ├── Migrations │ │ ├── 20220218212605_Initial.Designer.cs │ │ ├── 20220218212605_Initial.cs │ │ └── SchoolDbContextModelSnapshot.cs │ ├── Models │ │ ├── Subject.cs │ │ └── User.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Schema │ │ ├── Filters │ │ │ └── CourseFilterType.cs │ │ ├── Mutations │ │ │ ├── CourseMutation.cs │ │ │ ├── CourseResult.cs │ │ │ ├── CourseTypeInput.cs │ │ │ ├── InstructorMutation.cs │ │ │ ├── InstructorResult.cs │ │ │ ├── InstructorTypeInput.cs │ │ │ └── Mutation.cs │ │ ├── Queries │ │ │ ├── CourseQuery.cs │ │ │ ├── CourseType.cs │ │ │ ├── ISearchResultType.cs │ │ │ ├── InstructorQuery.cs │ │ │ ├── InstructorType.cs │ │ │ ├── Query.cs │ │ │ ├── StudentType.cs │ │ │ └── UserType.cs │ │ ├── Sorters │ │ │ └── CourseSortType.cs │ │ └── Subscriptions │ │ │ └── Subscription.cs │ ├── Services │ │ ├── Courses │ │ │ └── CoursesRepository.cs │ │ ├── Instructors │ │ │ └── InstructorsRepository.cs │ │ └── SchoolDbContext.cs │ ├── Startup.cs │ ├── Validators │ │ ├── CourseTypeInputValidator.cs │ │ └── InstructorTypeInputValidator.cs │ ├── appsettings.Development.json │ └── appsettings.json ├── GraphQLDemo.Client │ ├── .config │ │ └── dotnet-tools.json │ ├── .graphqlrc.json │ ├── Generated │ │ └── GraphQLDemoClient.StrawberryShake.cs │ ├── GraphQLDemo.Client.csproj │ ├── GraphQLDemo.Client.csproj.user │ ├── Mutations │ │ ├── CreateCourseMutation.graphql │ │ ├── DeleteCourseMutation.graphql │ │ └── UpdateCourseMutation.graphql │ ├── Program.cs │ ├── Queries │ │ ├── CourseByIdQuery.graphql │ │ ├── CoursesQuery.graphql │ │ ├── InstructionsQuery.graphql │ │ └── SearchQuery.graphql │ ├── Scripts │ │ ├── CreateCourseScript.cs │ │ ├── GetCoursesScript.cs │ │ ├── LoginScript.cs │ │ └── SearchScript.cs │ ├── Stores │ │ └── TokenStore.cs │ ├── Subscriptions │ │ ├── CourseCreatedSubscription.graphql │ │ └── CourseUpdatedSubscription.graphql │ ├── appsettings.json │ ├── schema.extensions.graphql │ └── schema.graphql └── GraphQLDemo.sln └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | .vs 4 | *.db 5 | firebase-config.json 6 | appsettings.Production.json -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md 26 | 27 | firebase-config.json 28 | appsettings.Production.json -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/DTOs/CourseDTO.cs: -------------------------------------------------------------------------------- 1 | using GraphQLDemo.API.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace GraphQLDemo.API.DTOs 8 | { 9 | public class CourseDTO 10 | { 11 | public Guid Id { get; set; } 12 | public string Name { get; set; } 13 | public Subject Subject { get; set; } 14 | public string CreatorId { get; set; } 15 | 16 | public Guid InstructorId { get; set; } 17 | public InstructorDTO Instructor { get; set; } 18 | 19 | public IEnumerable Students { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/DTOs/InstructorDTO.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace GraphQLDemo.API.DTOs 7 | { 8 | public class InstructorDTO 9 | { 10 | public Guid Id { get; set; } 11 | public string FirstName { get; set; } 12 | public string LastName { get; set; } 13 | public double Salary { get; set; } 14 | 15 | public IEnumerable Courses { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/DTOs/StudentDTO.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace GraphQLDemo.API.DTOs 7 | { 8 | public class StudentDTO 9 | { 10 | public Guid Id { get; set; } 11 | public string FirstName { get; set; } 12 | public string LastName { get; set; } 13 | public double GPA { get; set; } 14 | 15 | public IEnumerable Courses { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/DataLoaders/InstructorDataLoader.cs: -------------------------------------------------------------------------------- 1 | using GraphQLDemo.API.DTOs; 2 | using GraphQLDemo.API.Services.Instructors; 3 | using GreenDonut; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace GraphQLDemo.API.DataLoaders 11 | { 12 | public class InstructorDataLoader : BatchDataLoader 13 | { 14 | private readonly InstructorsRepository _instructorsRepository; 15 | 16 | public InstructorDataLoader( 17 | InstructorsRepository instructorsRepository, 18 | IBatchScheduler batchScheduler, 19 | DataLoaderOptions options = null) 20 | : base(batchScheduler, options) 21 | { 22 | _instructorsRepository = instructorsRepository; 23 | } 24 | 25 | protected override async Task> LoadBatchAsync(IReadOnlyList keys, CancellationToken cancellationToken) 26 | { 27 | IEnumerable instructors = await _instructorsRepository.GetManyByIds(keys); 28 | 29 | return instructors.ToDictionary(i => i.Id); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/DataLoaders/UserDataLoader.cs: -------------------------------------------------------------------------------- 1 | using FirebaseAdmin; 2 | using FirebaseAdmin.Auth; 3 | using GraphQLDemo.API.Schema.Queries; 4 | using GreenDonut; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace GraphQLDemo.API.DataLoaders 12 | { 13 | public class UserDataLoader : BatchDataLoader 14 | { 15 | private const int MAX_FIREBASE_USERS_BATCH_SIZE = 100; 16 | 17 | private readonly FirebaseAuth _firebaseAuth; 18 | 19 | public UserDataLoader( 20 | FirebaseApp firebaseApp, 21 | IBatchScheduler batchScheduler) 22 | : base(batchScheduler, new DataLoaderOptions() 23 | { 24 | MaxBatchSize = MAX_FIREBASE_USERS_BATCH_SIZE 25 | }) 26 | { 27 | _firebaseAuth = FirebaseAuth.GetAuth(firebaseApp); 28 | } 29 | 30 | protected override async Task> LoadBatchAsync( 31 | IReadOnlyList userIds, 32 | CancellationToken cancellationToken) 33 | { 34 | List userIdentifiers = userIds.Select(u => new UidIdentifier(u)).ToList(); 35 | 36 | GetUsersResult usersResult = await _firebaseAuth.GetUsersAsync(userIdentifiers); 37 | 38 | return usersResult.Users.Select(u => new UserType() 39 | { 40 | Id = u.Uid, 41 | Username = u.DisplayName, 42 | PhotoUrl = u.PhotoUrl 43 | }).ToDictionary(u => u.Id); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | EXPOSE 443 7 | 8 | FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build 9 | WORKDIR /src 10 | COPY ["GraphQLDemo.API/GraphQLDemo.API.csproj", "GraphQLDemo.API/"] 11 | RUN dotnet restore "GraphQLDemo.API/GraphQLDemo.API.csproj" 12 | COPY . . 13 | WORKDIR "/src/GraphQLDemo.API" 14 | RUN dotnet build "GraphQLDemo.API.csproj" -c Release -o /app/build 15 | 16 | FROM build AS publish 17 | RUN dotnet publish "GraphQLDemo.API.csproj" -c Release -o /app/publish 18 | 19 | FROM base AS final 20 | WORKDIR /app 21 | COPY --from=publish /app/publish . 22 | ENTRYPOINT ["dotnet", "GraphQLDemo.API.dll"] -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/GraphQLDemo.API.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | b10b6642-bc94-4aaf-88fb-3cc2cbb949c5 6 | Linux 7 | preview 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/GraphQLDemo.API.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | ProjectDebugger 5 | 6 | 7 | GraphQLDemo.API 8 | false 9 | 10 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Middlewares/UseUser/UseUserAttribute.cs: -------------------------------------------------------------------------------- 1 | using HotChocolate.Types; 2 | using HotChocolate.Types.Descriptors; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Runtime.CompilerServices; 8 | using System.Threading.Tasks; 9 | 10 | namespace GraphQLDemo.API.Middlewares.UseUser 11 | { 12 | public class UseUserAttribute : ObjectFieldDescriptorAttribute 13 | { 14 | public UseUserAttribute([CallerLineNumber] int order = 0) 15 | { 16 | Order = order; 17 | } 18 | 19 | public override void OnConfigure(IDescriptorContext context, IObjectFieldDescriptor descriptor, MemberInfo member) 20 | { 21 | descriptor.Use(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Middlewares/UseUser/UserAttribute.cs: -------------------------------------------------------------------------------- 1 | using HotChocolate; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace GraphQLDemo.API.Middlewares.UseUser 8 | { 9 | public class UserAttribute : GlobalStateAttribute 10 | { 11 | public UserAttribute() : base(UserMiddleware.USER_CONTEXT_DATA_KEY) 12 | { 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Middlewares/UseUser/UserMiddleware.cs: -------------------------------------------------------------------------------- 1 | using FirebaseAdminAuthentication.DependencyInjection.Models; 2 | using GraphQLDemo.API.Models; 3 | using HotChocolate.Resolvers; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Security.Claims; 8 | using System.Threading.Tasks; 9 | 10 | namespace GraphQLDemo.API.Middlewares.UseUser 11 | { 12 | public class UserMiddleware 13 | { 14 | public const string USER_CONTEXT_DATA_KEY = "User"; 15 | 16 | private readonly FieldDelegate _next; 17 | 18 | public UserMiddleware(FieldDelegate next) 19 | { 20 | _next = next; 21 | } 22 | 23 | public async Task Invoke(IMiddlewareContext context) 24 | { 25 | if(context.ContextData.TryGetValue("ClaimsPrincipal", out object rawClaimsPrincipal) && 26 | rawClaimsPrincipal is ClaimsPrincipal claimsPrincipal) 27 | { 28 | bool emailVerified = bool.TryParse(claimsPrincipal.FindFirstValue(FirebaseUserClaimType.EMAIL_VERIFIED), out bool result) && result; 29 | 30 | User user = new User() 31 | { 32 | Id = claimsPrincipal.FindFirstValue(FirebaseUserClaimType.ID), 33 | Email = claimsPrincipal.FindFirstValue(FirebaseUserClaimType.EMAIL), 34 | Username = claimsPrincipal.FindFirstValue(FirebaseUserClaimType.USERNAME), 35 | EmailVerified = emailVerified, 36 | }; 37 | 38 | context.ContextData.Add(USER_CONTEXT_DATA_KEY, user); 39 | } 40 | 41 | await _next(context); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Migrations/20220218212605_Initial.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using GraphQLDemo.API.Services; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 9 | 10 | namespace GraphQLDemo.API.Migrations 11 | { 12 | [DbContext(typeof(SchoolDbContext))] 13 | [Migration("20220218212605_Initial")] 14 | partial class Initial 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 21 | .HasAnnotation("ProductVersion", "5.0.9") 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("CourseDTOStudentDTO", b => 25 | { 26 | b.Property("CoursesId") 27 | .HasColumnType("uniqueidentifier"); 28 | 29 | b.Property("StudentsId") 30 | .HasColumnType("uniqueidentifier"); 31 | 32 | b.HasKey("CoursesId", "StudentsId"); 33 | 34 | b.HasIndex("StudentsId"); 35 | 36 | b.ToTable("CourseDTOStudentDTO"); 37 | }); 38 | 39 | modelBuilder.Entity("GraphQLDemo.API.DTOs.CourseDTO", b => 40 | { 41 | b.Property("Id") 42 | .ValueGeneratedOnAdd() 43 | .HasColumnType("uniqueidentifier"); 44 | 45 | b.Property("CreatorId") 46 | .HasColumnType("nvarchar(max)"); 47 | 48 | b.Property("InstructorId") 49 | .HasColumnType("uniqueidentifier"); 50 | 51 | b.Property("Name") 52 | .HasColumnType("nvarchar(max)"); 53 | 54 | b.Property("Subject") 55 | .HasColumnType("int"); 56 | 57 | b.HasKey("Id"); 58 | 59 | b.HasIndex("InstructorId"); 60 | 61 | b.ToTable("Courses"); 62 | }); 63 | 64 | modelBuilder.Entity("GraphQLDemo.API.DTOs.InstructorDTO", b => 65 | { 66 | b.Property("Id") 67 | .ValueGeneratedOnAdd() 68 | .HasColumnType("uniqueidentifier"); 69 | 70 | b.Property("FirstName") 71 | .HasColumnType("nvarchar(max)"); 72 | 73 | b.Property("LastName") 74 | .HasColumnType("nvarchar(max)"); 75 | 76 | b.Property("Salary") 77 | .HasColumnType("float"); 78 | 79 | b.HasKey("Id"); 80 | 81 | b.ToTable("Instructors"); 82 | }); 83 | 84 | modelBuilder.Entity("GraphQLDemo.API.DTOs.StudentDTO", b => 85 | { 86 | b.Property("Id") 87 | .ValueGeneratedOnAdd() 88 | .HasColumnType("uniqueidentifier"); 89 | 90 | b.Property("FirstName") 91 | .HasColumnType("nvarchar(max)"); 92 | 93 | b.Property("GPA") 94 | .HasColumnType("float"); 95 | 96 | b.Property("LastName") 97 | .HasColumnType("nvarchar(max)"); 98 | 99 | b.HasKey("Id"); 100 | 101 | b.ToTable("Students"); 102 | }); 103 | 104 | modelBuilder.Entity("CourseDTOStudentDTO", b => 105 | { 106 | b.HasOne("GraphQLDemo.API.DTOs.CourseDTO", null) 107 | .WithMany() 108 | .HasForeignKey("CoursesId") 109 | .OnDelete(DeleteBehavior.Cascade) 110 | .IsRequired(); 111 | 112 | b.HasOne("GraphQLDemo.API.DTOs.StudentDTO", null) 113 | .WithMany() 114 | .HasForeignKey("StudentsId") 115 | .OnDelete(DeleteBehavior.Cascade) 116 | .IsRequired(); 117 | }); 118 | 119 | modelBuilder.Entity("GraphQLDemo.API.DTOs.CourseDTO", b => 120 | { 121 | b.HasOne("GraphQLDemo.API.DTOs.InstructorDTO", "Instructor") 122 | .WithMany("Courses") 123 | .HasForeignKey("InstructorId") 124 | .OnDelete(DeleteBehavior.Cascade) 125 | .IsRequired(); 126 | 127 | b.Navigation("Instructor"); 128 | }); 129 | 130 | modelBuilder.Entity("GraphQLDemo.API.DTOs.InstructorDTO", b => 131 | { 132 | b.Navigation("Courses"); 133 | }); 134 | #pragma warning restore 612, 618 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Migrations/20220218212605_Initial.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace GraphQLDemo.API.Migrations 5 | { 6 | public partial class Initial : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "Instructors", 12 | columns: table => new 13 | { 14 | Id = table.Column(type: "uniqueidentifier", nullable: false), 15 | FirstName = table.Column(type: "nvarchar(max)", nullable: true), 16 | LastName = table.Column(type: "nvarchar(max)", nullable: true), 17 | Salary = table.Column(type: "float", nullable: false) 18 | }, 19 | constraints: table => 20 | { 21 | table.PrimaryKey("PK_Instructors", x => x.Id); 22 | }); 23 | 24 | migrationBuilder.CreateTable( 25 | name: "Students", 26 | columns: table => new 27 | { 28 | Id = table.Column(type: "uniqueidentifier", nullable: false), 29 | FirstName = table.Column(type: "nvarchar(max)", nullable: true), 30 | LastName = table.Column(type: "nvarchar(max)", nullable: true), 31 | GPA = table.Column(type: "float", nullable: false) 32 | }, 33 | constraints: table => 34 | { 35 | table.PrimaryKey("PK_Students", x => x.Id); 36 | }); 37 | 38 | migrationBuilder.CreateTable( 39 | name: "Courses", 40 | columns: table => new 41 | { 42 | Id = table.Column(type: "uniqueidentifier", nullable: false), 43 | Name = table.Column(type: "nvarchar(max)", nullable: true), 44 | Subject = table.Column(type: "int", nullable: false), 45 | CreatorId = table.Column(type: "nvarchar(max)", nullable: true), 46 | InstructorId = table.Column(type: "uniqueidentifier", nullable: false) 47 | }, 48 | constraints: table => 49 | { 50 | table.PrimaryKey("PK_Courses", x => x.Id); 51 | table.ForeignKey( 52 | name: "FK_Courses_Instructors_InstructorId", 53 | column: x => x.InstructorId, 54 | principalTable: "Instructors", 55 | principalColumn: "Id", 56 | onDelete: ReferentialAction.Cascade); 57 | }); 58 | 59 | migrationBuilder.CreateTable( 60 | name: "CourseDTOStudentDTO", 61 | columns: table => new 62 | { 63 | CoursesId = table.Column(type: "uniqueidentifier", nullable: false), 64 | StudentsId = table.Column(type: "uniqueidentifier", nullable: false) 65 | }, 66 | constraints: table => 67 | { 68 | table.PrimaryKey("PK_CourseDTOStudentDTO", x => new { x.CoursesId, x.StudentsId }); 69 | table.ForeignKey( 70 | name: "FK_CourseDTOStudentDTO_Courses_CoursesId", 71 | column: x => x.CoursesId, 72 | principalTable: "Courses", 73 | principalColumn: "Id", 74 | onDelete: ReferentialAction.Cascade); 75 | table.ForeignKey( 76 | name: "FK_CourseDTOStudentDTO_Students_StudentsId", 77 | column: x => x.StudentsId, 78 | principalTable: "Students", 79 | principalColumn: "Id", 80 | onDelete: ReferentialAction.Cascade); 81 | }); 82 | 83 | migrationBuilder.CreateIndex( 84 | name: "IX_CourseDTOStudentDTO_StudentsId", 85 | table: "CourseDTOStudentDTO", 86 | column: "StudentsId"); 87 | 88 | migrationBuilder.CreateIndex( 89 | name: "IX_Courses_InstructorId", 90 | table: "Courses", 91 | column: "InstructorId"); 92 | } 93 | 94 | protected override void Down(MigrationBuilder migrationBuilder) 95 | { 96 | migrationBuilder.DropTable( 97 | name: "CourseDTOStudentDTO"); 98 | 99 | migrationBuilder.DropTable( 100 | name: "Courses"); 101 | 102 | migrationBuilder.DropTable( 103 | name: "Students"); 104 | 105 | migrationBuilder.DropTable( 106 | name: "Instructors"); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Migrations/SchoolDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using GraphQLDemo.API.Services; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | namespace GraphQLDemo.API.Migrations 10 | { 11 | [DbContext(typeof(SchoolDbContext))] 12 | partial class SchoolDbContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 19 | .HasAnnotation("ProductVersion", "5.0.9") 20 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 21 | 22 | modelBuilder.Entity("CourseDTOStudentDTO", b => 23 | { 24 | b.Property("CoursesId") 25 | .HasColumnType("uniqueidentifier"); 26 | 27 | b.Property("StudentsId") 28 | .HasColumnType("uniqueidentifier"); 29 | 30 | b.HasKey("CoursesId", "StudentsId"); 31 | 32 | b.HasIndex("StudentsId"); 33 | 34 | b.ToTable("CourseDTOStudentDTO"); 35 | }); 36 | 37 | modelBuilder.Entity("GraphQLDemo.API.DTOs.CourseDTO", b => 38 | { 39 | b.Property("Id") 40 | .ValueGeneratedOnAdd() 41 | .HasColumnType("uniqueidentifier"); 42 | 43 | b.Property("CreatorId") 44 | .HasColumnType("nvarchar(max)"); 45 | 46 | b.Property("InstructorId") 47 | .HasColumnType("uniqueidentifier"); 48 | 49 | b.Property("Name") 50 | .HasColumnType("nvarchar(max)"); 51 | 52 | b.Property("Subject") 53 | .HasColumnType("int"); 54 | 55 | b.HasKey("Id"); 56 | 57 | b.HasIndex("InstructorId"); 58 | 59 | b.ToTable("Courses"); 60 | }); 61 | 62 | modelBuilder.Entity("GraphQLDemo.API.DTOs.InstructorDTO", b => 63 | { 64 | b.Property("Id") 65 | .ValueGeneratedOnAdd() 66 | .HasColumnType("uniqueidentifier"); 67 | 68 | b.Property("FirstName") 69 | .HasColumnType("nvarchar(max)"); 70 | 71 | b.Property("LastName") 72 | .HasColumnType("nvarchar(max)"); 73 | 74 | b.Property("Salary") 75 | .HasColumnType("float"); 76 | 77 | b.HasKey("Id"); 78 | 79 | b.ToTable("Instructors"); 80 | }); 81 | 82 | modelBuilder.Entity("GraphQLDemo.API.DTOs.StudentDTO", b => 83 | { 84 | b.Property("Id") 85 | .ValueGeneratedOnAdd() 86 | .HasColumnType("uniqueidentifier"); 87 | 88 | b.Property("FirstName") 89 | .HasColumnType("nvarchar(max)"); 90 | 91 | b.Property("GPA") 92 | .HasColumnType("float"); 93 | 94 | b.Property("LastName") 95 | .HasColumnType("nvarchar(max)"); 96 | 97 | b.HasKey("Id"); 98 | 99 | b.ToTable("Students"); 100 | }); 101 | 102 | modelBuilder.Entity("CourseDTOStudentDTO", b => 103 | { 104 | b.HasOne("GraphQLDemo.API.DTOs.CourseDTO", null) 105 | .WithMany() 106 | .HasForeignKey("CoursesId") 107 | .OnDelete(DeleteBehavior.Cascade) 108 | .IsRequired(); 109 | 110 | b.HasOne("GraphQLDemo.API.DTOs.StudentDTO", null) 111 | .WithMany() 112 | .HasForeignKey("StudentsId") 113 | .OnDelete(DeleteBehavior.Cascade) 114 | .IsRequired(); 115 | }); 116 | 117 | modelBuilder.Entity("GraphQLDemo.API.DTOs.CourseDTO", b => 118 | { 119 | b.HasOne("GraphQLDemo.API.DTOs.InstructorDTO", "Instructor") 120 | .WithMany("Courses") 121 | .HasForeignKey("InstructorId") 122 | .OnDelete(DeleteBehavior.Cascade) 123 | .IsRequired(); 124 | 125 | b.Navigation("Instructor"); 126 | }); 127 | 128 | modelBuilder.Entity("GraphQLDemo.API.DTOs.InstructorDTO", b => 129 | { 130 | b.Navigation("Courses"); 131 | }); 132 | #pragma warning restore 612, 618 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Models/Subject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace GraphQLDemo.API.Models 7 | { 8 | public enum Subject 9 | { 10 | Mathematics, 11 | Science, 12 | History 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Models/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace GraphQLDemo.API.Models 7 | { 8 | public class User 9 | { 10 | public string Id { get; set; } 11 | public string Email { get; set; } 12 | public string Username { get; set; } 13 | public bool EmailVerified { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Program.cs: -------------------------------------------------------------------------------- 1 | using GraphQLDemo.API.Services; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using Microsoft.Extensions.Logging; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Threading.Tasks; 12 | 13 | namespace GraphQLDemo.API 14 | { 15 | public class Program 16 | { 17 | public static void Main(string[] args) 18 | { 19 | IHost host = CreateHostBuilder(args).Build(); 20 | 21 | using (IServiceScope scope = host.Services.CreateScope()) 22 | { 23 | IDbContextFactory contextFactory = 24 | scope.ServiceProvider.GetRequiredService>(); 25 | 26 | using(SchoolDbContext context = contextFactory.CreateDbContext()) 27 | { 28 | context.Database.Migrate(); 29 | } 30 | } 31 | 32 | host.Run(); 33 | } 34 | 35 | public static IHostBuilder CreateHostBuilder(string[] args) => 36 | Host.CreateDefaultBuilder(args) 37 | .ConfigureWebHostDefaults(webBuilder => 38 | { 39 | webBuilder.UseStartup(); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:48031", 7 | "sslPort": 44372 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "GraphQLDemo.API": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "launchUrl": "graphql", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 26 | "dotnetRunMessages": "true" 27 | }, 28 | "Docker": { 29 | "commandName": "Docker", 30 | "launchBrowser": true, 31 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", 32 | "publishAllPorts": true, 33 | "useSSL": true 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Schema/Filters/CourseFilterType.cs: -------------------------------------------------------------------------------- 1 | using GraphQLDemo.API.Schema.Queries; 2 | using HotChocolate.Data.Filters; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace GraphQLDemo.API.Schema.Filters 9 | { 10 | public class CourseFilterType : FilterInputType 11 | { 12 | protected override void Configure(IFilterInputTypeDescriptor descriptor) 13 | { 14 | descriptor.Ignore(c => c.Students); 15 | 16 | base.Configure(descriptor); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Schema/Mutations/CourseMutation.cs: -------------------------------------------------------------------------------- 1 | using AppAny.HotChocolate.FluentValidation; 2 | using FirebaseAdminAuthentication.DependencyInjection.Models; 3 | using FluentValidation.Results; 4 | using GraphQLDemo.API.DTOs; 5 | using GraphQLDemo.API.Middlewares.UseUser; 6 | using GraphQLDemo.API.Models; 7 | using GraphQLDemo.API.Schema.Queries; 8 | using GraphQLDemo.API.Schema.Subscriptions; 9 | using GraphQLDemo.API.Services.Courses; 10 | using GraphQLDemo.API.Validators; 11 | using HotChocolate; 12 | using HotChocolate.AspNetCore.Authorization; 13 | using HotChocolate.Subscriptions; 14 | using HotChocolate.Types; 15 | using System; 16 | using System.Collections.Generic; 17 | using System.Linq; 18 | using System.Security.Claims; 19 | using System.Threading.Tasks; 20 | 21 | namespace GraphQLDemo.API.Schema.Mutations 22 | { 23 | [ExtendObjectType(typeof(Mutation))] 24 | public class CourseMutation 25 | { 26 | private readonly CoursesRepository _coursesRepository; 27 | 28 | public CourseMutation(CoursesRepository coursesRepository) 29 | { 30 | _coursesRepository = coursesRepository; 31 | } 32 | 33 | [Authorize] 34 | [UseUser] 35 | public async Task CreateCourse( 36 | [UseFluentValidation, UseValidator] CourseTypeInput courseInput, 37 | [Service] ITopicEventSender topicEventSender, 38 | [User] User user) 39 | { 40 | string userId = user.Id; 41 | 42 | CourseDTO courseDTO = new CourseDTO() 43 | { 44 | Name = courseInput.Name, 45 | Subject = courseInput.Subject, 46 | InstructorId = courseInput.InstructorId, 47 | CreatorId = userId 48 | }; 49 | 50 | courseDTO = await _coursesRepository.Create(courseDTO); 51 | 52 | CourseResult course = new CourseResult() 53 | { 54 | Id = courseDTO.Id, 55 | Name = courseDTO.Name, 56 | Subject = courseDTO.Subject, 57 | InstructorId = courseDTO.InstructorId 58 | }; 59 | 60 | await topicEventSender.SendAsync(nameof(Subscription.CourseCreated), course); 61 | 62 | return course; 63 | } 64 | 65 | [Authorize] 66 | [UseUser] 67 | public async Task UpdateCourse(Guid id, 68 | [UseFluentValidation, UseValidator] CourseTypeInput courseInput, 69 | [Service] ITopicEventSender topicEventSender, 70 | [User] User user) 71 | { 72 | string userId = user.Id; 73 | 74 | CourseDTO courseDTO = await _coursesRepository.GetById(id); 75 | 76 | if(courseDTO == null) 77 | { 78 | throw new GraphQLException(new Error("Course not found.", "COURSE_NOT_FOUND")); 79 | } 80 | 81 | if (courseDTO.CreatorId != userId) 82 | { 83 | throw new GraphQLException(new Error("You do not have permission to update this course.", "INVALID_PERMISSION")); 84 | } 85 | 86 | courseDTO.Name = courseInput.Name; 87 | courseDTO.Subject = courseInput.Subject; 88 | courseDTO.InstructorId = courseInput.InstructorId; 89 | 90 | courseDTO = await _coursesRepository.Update(courseDTO); 91 | 92 | CourseResult course = new CourseResult() 93 | { 94 | Id = courseDTO.Id, 95 | Name = courseDTO.Name, 96 | Subject = courseDTO.Subject, 97 | InstructorId = courseDTO.InstructorId 98 | }; 99 | 100 | string updateCourseTopic = $"{course.Id}_{nameof(Subscription.CourseUpdated)}"; 101 | await topicEventSender.SendAsync(updateCourseTopic, course); 102 | 103 | return course; 104 | } 105 | 106 | [Authorize(Policy = "IsAdmin")] 107 | public async Task DeleteCourse(Guid id) 108 | { 109 | try 110 | { 111 | return await _coursesRepository.Delete(id); 112 | } 113 | catch (Exception) 114 | { 115 | return false; 116 | } 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Schema/Mutations/CourseResult.cs: -------------------------------------------------------------------------------- 1 | using GraphQLDemo.API.Models; 2 | using GraphQLDemo.API.Schema.Queries; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace GraphQLDemo.API.Schema.Mutations 9 | { 10 | public class CourseResult 11 | { 12 | public Guid Id { get; set; } 13 | public string Name { get; set; } 14 | public Subject Subject { get; set; } 15 | public Guid InstructorId { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Schema/Mutations/CourseTypeInput.cs: -------------------------------------------------------------------------------- 1 | using GraphQLDemo.API.Models; 2 | using GraphQLDemo.API.Schema.Queries; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace GraphQLDemo.API.Schema.Mutations 9 | { 10 | public class CourseTypeInput 11 | { 12 | public string Name { get; set; } 13 | public Subject Subject { get; set; } 14 | public Guid InstructorId { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Schema/Mutations/InstructorMutation.cs: -------------------------------------------------------------------------------- 1 | using AppAny.HotChocolate.FluentValidation; 2 | using GraphQLDemo.API.DTOs; 3 | using GraphQLDemo.API.Schema.Subscriptions; 4 | using GraphQLDemo.API.Services; 5 | using GraphQLDemo.API.Validators; 6 | using HotChocolate; 7 | using HotChocolate.AspNetCore.Authorization; 8 | using HotChocolate.Data; 9 | using HotChocolate.Subscriptions; 10 | using HotChocolate.Types; 11 | using System; 12 | using System.Threading.Tasks; 13 | 14 | namespace GraphQLDemo.API.Schema.Mutations 15 | { 16 | [ExtendObjectType(typeof(Mutation))] 17 | public class InstructorMutation 18 | { 19 | [Authorize] 20 | [UseDbContext(typeof(SchoolDbContext))] 21 | public async Task CreateInstructor( 22 | [UseFluentValidation, UseValidator] InstructorTypeInput instructorInput, 23 | [ScopedService] SchoolDbContext context, 24 | [Service] ITopicEventSender topicEventSender) 25 | { 26 | InstructorDTO instructorDTO = new InstructorDTO() 27 | { 28 | FirstName = instructorInput.FirstName, 29 | LastName = instructorInput.LastName, 30 | Salary = instructorInput.Salary, 31 | }; 32 | 33 | context.Add(instructorDTO); 34 | await context.SaveChangesAsync(); 35 | 36 | InstructorResult instructorResult = new InstructorResult() 37 | { 38 | Id = instructorDTO.Id, 39 | FirstName = instructorDTO.FirstName, 40 | LastName = instructorDTO.LastName, 41 | Salary = instructorDTO.Salary, 42 | }; 43 | 44 | await topicEventSender.SendAsync(nameof(Subscription.InstructorCreated), instructorResult); 45 | 46 | return instructorResult; 47 | } 48 | 49 | [Authorize] 50 | [UseDbContext(typeof(SchoolDbContext))] 51 | public async Task UpdateInstructor( 52 | Guid id, 53 | [UseFluentValidation, UseValidator] InstructorTypeInput instructorInput, 54 | [ScopedService] SchoolDbContext context) 55 | { 56 | InstructorDTO instructorDTO = await context.Instructors.FindAsync(id); 57 | 58 | if(instructorDTO == null) 59 | { 60 | throw new GraphQLException(new Error("Instructor not found.", "INSTRUCTOR_NOT_FOUND")); 61 | } 62 | 63 | instructorDTO.FirstName = instructorInput.FirstName; 64 | instructorDTO.LastName = instructorInput.LastName; 65 | instructorDTO.Salary = instructorInput.Salary; 66 | 67 | context.Update(instructorDTO); 68 | await context.SaveChangesAsync(); 69 | 70 | InstructorResult instructorResult = new InstructorResult() 71 | { 72 | Id = instructorDTO.Id, 73 | FirstName = instructorDTO.FirstName, 74 | LastName = instructorDTO.LastName, 75 | Salary = instructorDTO.Salary, 76 | }; 77 | 78 | return instructorResult; 79 | } 80 | 81 | [Authorize(Policy = "IsAdmin")] 82 | [UseDbContext(typeof(SchoolDbContext))] 83 | public async Task DeleteInstructor(Guid id, [ScopedService] SchoolDbContext context) 84 | { 85 | InstructorDTO instructorDTO = new InstructorDTO() 86 | { 87 | Id = id 88 | }; 89 | 90 | context.Remove(instructorDTO); 91 | 92 | try 93 | { 94 | await context.SaveChangesAsync(); 95 | 96 | return true; 97 | } 98 | catch (Exception) 99 | { 100 | return false; 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Schema/Mutations/InstructorResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GraphQLDemo.API.Schema.Mutations 4 | { 5 | public class InstructorResult 6 | { 7 | public Guid Id { get; set; } 8 | public string FirstName { get; set; } 9 | public string LastName { get; set; } 10 | public double Salary { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Schema/Mutations/InstructorTypeInput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GraphQLDemo.API.Schema.Mutations 4 | { 5 | public class InstructorTypeInput 6 | { 7 | public string FirstName { get; set; } 8 | public string LastName { get; set; } 9 | public double Salary { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Schema/Mutations/Mutation.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQLDemo.API.Schema.Mutations 2 | { 3 | public class Mutation 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Schema/Queries/CourseQuery.cs: -------------------------------------------------------------------------------- 1 | using GraphQLDemo.API.DTOs; 2 | using GraphQLDemo.API.Schema.Filters; 3 | using GraphQLDemo.API.Schema.Sorters; 4 | using GraphQLDemo.API.Services; 5 | using GraphQLDemo.API.Services.Courses; 6 | using HotChocolate; 7 | using HotChocolate.Data; 8 | using HotChocolate.Types; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Linq; 12 | using System.Threading.Tasks; 13 | 14 | namespace GraphQLDemo.API.Schema.Queries 15 | { 16 | [ExtendObjectType(typeof(Query))] 17 | public class CourseQuery 18 | { 19 | [UseDbContext(typeof(SchoolDbContext))] 20 | [UsePaging(IncludeTotalCount = true, DefaultPageSize = 10)] 21 | [UseProjection] 22 | [UseFiltering(typeof(CourseFilterType))] 23 | [UseSorting(typeof(CourseSortType))] 24 | public IQueryable GetCourses([ScopedService] SchoolDbContext context) 25 | { 26 | return context.Courses.Select(c => new CourseType() 27 | { 28 | Id = c.Id, 29 | Name = c.Name, 30 | Subject = c.Subject, 31 | InstructorId = c.InstructorId, 32 | CreatorId = c.CreatorId 33 | }); 34 | } 35 | 36 | [UseDbContext(typeof(SchoolDbContext))] 37 | public async Task GetCourseByIdAsync(Guid id, [ScopedService] SchoolDbContext context) 38 | { 39 | CourseDTO courseDTO = await context.Courses.FindAsync(id); 40 | 41 | if(courseDTO == null) 42 | { 43 | return null; 44 | } 45 | 46 | return new CourseType() 47 | { 48 | Id = courseDTO.Id, 49 | Name = courseDTO.Name, 50 | Subject = courseDTO.Subject, 51 | InstructorId = courseDTO.InstructorId, 52 | CreatorId = courseDTO.CreatorId 53 | }; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Schema/Queries/CourseType.cs: -------------------------------------------------------------------------------- 1 | using FirebaseAdmin.Auth; 2 | using GraphQLDemo.API.DataLoaders; 3 | using GraphQLDemo.API.DTOs; 4 | using GraphQLDemo.API.Models; 5 | using GraphQLDemo.API.Services.Instructors; 6 | using HotChocolate; 7 | using HotChocolate.Data; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | 14 | namespace GraphQLDemo.API.Schema.Queries 15 | { 16 | public class CourseType : ISearchResultType 17 | { 18 | public Guid Id { get; set; } 19 | public string Name { get; set; } 20 | public Subject Subject { get; set; } 21 | 22 | [IsProjected(true)] 23 | public Guid InstructorId { get; set; } 24 | 25 | [GraphQLNonNullType] 26 | public async Task Instructor([Service] InstructorDataLoader instructorDataLoader) 27 | { 28 | InstructorDTO instructorDTO = await instructorDataLoader.LoadAsync(InstructorId, CancellationToken.None); 29 | 30 | return new InstructorType() 31 | { 32 | Id = instructorDTO.Id, 33 | FirstName = instructorDTO.FirstName, 34 | LastName = instructorDTO.LastName, 35 | Salary = instructorDTO.Salary, 36 | }; 37 | } 38 | 39 | public IEnumerable Students { get; set; } 40 | 41 | [IsProjected(true)] 42 | public string CreatorId { get; set; } 43 | 44 | public async Task Creator([Service] UserDataLoader userDataLoader) 45 | { 46 | if(CreatorId == null) 47 | { 48 | return null; 49 | } 50 | 51 | return await userDataLoader.LoadAsync(CreatorId, CancellationToken.None); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Schema/Queries/ISearchResultType.cs: -------------------------------------------------------------------------------- 1 | using HotChocolate.Types; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace GraphQLDemo.API.Schema.Queries 8 | { 9 | [UnionType("SearchResult")] 10 | public interface ISearchResultType 11 | { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Schema/Queries/InstructorQuery.cs: -------------------------------------------------------------------------------- 1 | using GraphQLDemo.API.DTOs; 2 | using GraphQLDemo.API.Services; 3 | using HotChocolate; 4 | using HotChocolate.Data; 5 | using HotChocolate.Types; 6 | using System; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace GraphQLDemo.API.Schema.Queries 11 | { 12 | [ExtendObjectType(typeof(Query))] 13 | public class InstructorQuery 14 | { 15 | [UseDbContext(typeof(SchoolDbContext))] 16 | [UsePaging] 17 | [UseProjection] 18 | [UseFiltering] 19 | [UseSorting] 20 | public IQueryable GetInstructors([ScopedService] SchoolDbContext context) 21 | { 22 | return context.Instructors.Select(i => new InstructorType 23 | { 24 | Id = i.Id, 25 | FirstName = i.FirstName, 26 | LastName = i.LastName, 27 | Salary = i.Salary, 28 | }); 29 | } 30 | 31 | [UseDbContext(typeof(SchoolDbContext))] 32 | public async Task GetInstructorById(Guid id, [ScopedService] SchoolDbContext context) 33 | { 34 | InstructorDTO instructorDTO = await context.Instructors.FindAsync(id); 35 | 36 | if(instructorDTO == null) 37 | { 38 | return null; 39 | } 40 | 41 | return new InstructorType 42 | { 43 | Id = instructorDTO.Id, 44 | FirstName = instructorDTO.FirstName, 45 | LastName = instructorDTO.LastName, 46 | Salary = instructorDTO.Salary, 47 | }; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Schema/Queries/InstructorType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace GraphQLDemo.API.Schema.Queries 7 | { 8 | public class InstructorType : ISearchResultType 9 | { 10 | public Guid Id { get; set; } 11 | public string FirstName { get; set; } 12 | public string LastName { get; set; } 13 | public double Salary { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Schema/Queries/Query.cs: -------------------------------------------------------------------------------- 1 | using GraphQLDemo.API.Services; 2 | using HotChocolate; 3 | using HotChocolate.Data; 4 | using Microsoft.EntityFrameworkCore; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace GraphQLDemo.API.Schema.Queries 10 | { 11 | public class Query 12 | { 13 | [UseDbContext(typeof(SchoolDbContext))] 14 | public async Task> Search(string term, [ScopedService] SchoolDbContext context) 15 | { 16 | IEnumerable courses = await context.Courses 17 | .Where(c => c.Name.Contains(term)) 18 | .Select(c => new CourseType() 19 | { 20 | Id = c.Id, 21 | Name = c.Name, 22 | Subject = c.Subject, 23 | InstructorId = c.InstructorId, 24 | CreatorId = c.CreatorId 25 | }) 26 | .ToListAsync(); 27 | 28 | IEnumerable instructors = await context.Instructors 29 | .Where(i => i.FirstName.Contains(term) || i.LastName.Contains(term)) 30 | .Select(i => new InstructorType() 31 | { 32 | Id = i.Id, 33 | FirstName = i.FirstName, 34 | LastName = i.LastName, 35 | Salary = i.Salary, 36 | }) 37 | .ToListAsync(); 38 | 39 | return new List() 40 | .Concat(courses) 41 | .Concat(instructors); 42 | } 43 | 44 | [GraphQLDeprecated("This query is deprecated.")] 45 | public string Instructions => "Smash that like button and subscribe to SingletonSean"; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Schema/Queries/StudentType.cs: -------------------------------------------------------------------------------- 1 | using HotChocolate; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace GraphQLDemo.API.Schema.Queries 8 | { 9 | public class StudentType 10 | { 11 | public Guid Id { get; set; } 12 | public string FirstName { get; set; } 13 | public string LastName { get; set; } 14 | 15 | [GraphQLName("gpa")] 16 | public double GPA { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Schema/Queries/UserType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace GraphQLDemo.API.Schema.Queries 7 | { 8 | public class UserType 9 | { 10 | public string Id { get; set; } 11 | public string Username { get; set; } 12 | public string PhotoUrl { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Schema/Sorters/CourseSortType.cs: -------------------------------------------------------------------------------- 1 | using GraphQLDemo.API.Schema.Queries; 2 | using HotChocolate.Data.Sorting; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace GraphQLDemo.API.Schema.Sorters 9 | { 10 | public class CourseSortType : SortInputType 11 | { 12 | protected override void Configure(ISortInputTypeDescriptor descriptor) 13 | { 14 | descriptor.Ignore(c => c.Id); 15 | descriptor.Ignore(c => c.InstructorId); 16 | 17 | base.Configure(descriptor); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Schema/Subscriptions/Subscription.cs: -------------------------------------------------------------------------------- 1 | using GraphQLDemo.API.Schema.Mutations; 2 | using GraphQLDemo.API.Schema.Queries; 3 | using HotChocolate; 4 | using HotChocolate.Execution; 5 | using HotChocolate.Subscriptions; 6 | using HotChocolate.Types; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | 12 | namespace GraphQLDemo.API.Schema.Subscriptions 13 | { 14 | public class Subscription 15 | { 16 | [Subscribe] 17 | public CourseResult CourseCreated([EventMessage] CourseResult course) => course; 18 | 19 | [Subscribe] 20 | public InstructorResult InstructorCreated([EventMessage] InstructorResult instructor) => instructor; 21 | 22 | [SubscribeAndResolve] 23 | public ValueTask> CourseUpdated(Guid courseId, [Service] ITopicEventReceiver topicEventReceiver) 24 | { 25 | string topicName = $"{courseId}_{nameof(Subscription.CourseUpdated)}"; 26 | 27 | return topicEventReceiver.SubscribeAsync(topicName); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Services/Courses/CoursesRepository.cs: -------------------------------------------------------------------------------- 1 | using GraphQLDemo.API.DTOs; 2 | using Microsoft.EntityFrameworkCore; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace GraphQLDemo.API.Services.Courses 9 | { 10 | public class CoursesRepository 11 | { 12 | private readonly IDbContextFactory _contextFactory; 13 | 14 | public CoursesRepository(IDbContextFactory contextFactory) 15 | { 16 | _contextFactory = contextFactory; 17 | } 18 | 19 | public async Task> GetAll() 20 | { 21 | using (SchoolDbContext context = _contextFactory.CreateDbContext()) 22 | { 23 | return await context.Courses.ToListAsync(); 24 | } 25 | } 26 | 27 | public async Task GetById(Guid courseId) 28 | { 29 | using (SchoolDbContext context = _contextFactory.CreateDbContext()) 30 | { 31 | return await context.Courses.FirstOrDefaultAsync(c => c.Id == courseId); 32 | } 33 | } 34 | 35 | public async Task Create(CourseDTO course) 36 | { 37 | using(SchoolDbContext context = _contextFactory.CreateDbContext()) 38 | { 39 | context.Courses.Add(course); 40 | await context.SaveChangesAsync(); 41 | 42 | return course; 43 | } 44 | } 45 | 46 | public async Task Update(CourseDTO course) 47 | { 48 | using (SchoolDbContext context = _contextFactory.CreateDbContext()) 49 | { 50 | context.Courses.Update(course); 51 | await context.SaveChangesAsync(); 52 | 53 | return course; 54 | } 55 | } 56 | 57 | public async Task Delete(Guid id) 58 | { 59 | using (SchoolDbContext context = _contextFactory.CreateDbContext()) 60 | { 61 | CourseDTO course = new CourseDTO() 62 | { 63 | Id = id 64 | }; 65 | context.Courses.Remove(course); 66 | 67 | return await context.SaveChangesAsync() > 0; 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Services/Instructors/InstructorsRepository.cs: -------------------------------------------------------------------------------- 1 | using GraphQLDemo.API.DTOs; 2 | using Microsoft.EntityFrameworkCore; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace GraphQLDemo.API.Services.Instructors 9 | { 10 | public class InstructorsRepository 11 | { 12 | private readonly IDbContextFactory _contextFactory; 13 | 14 | public InstructorsRepository(IDbContextFactory contextFactory) 15 | { 16 | _contextFactory = contextFactory; 17 | } 18 | 19 | public async Task GetById(Guid instructorId) 20 | { 21 | using (SchoolDbContext context = _contextFactory.CreateDbContext()) 22 | { 23 | return await context.Instructors.FirstOrDefaultAsync(c => c.Id == instructorId); 24 | } 25 | } 26 | 27 | public async Task> GetManyByIds(IReadOnlyList instructorIds) 28 | { 29 | using (SchoolDbContext context = _contextFactory.CreateDbContext()) 30 | { 31 | return await context.Instructors 32 | .Where(i => instructorIds.Contains(i.Id)) 33 | .ToListAsync(); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Services/SchoolDbContext.cs: -------------------------------------------------------------------------------- 1 | using GraphQLDemo.API.DTOs; 2 | using Microsoft.EntityFrameworkCore; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace GraphQLDemo.API.Services 9 | { 10 | public class SchoolDbContext : DbContext 11 | { 12 | public SchoolDbContext(DbContextOptions options) 13 | : base(options) { } 14 | 15 | public DbSet Courses { get; set; } 16 | public DbSet Instructors { get; set; } 17 | public DbSet Students { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Startup.cs: -------------------------------------------------------------------------------- 1 | using AppAny.HotChocolate.FluentValidation; 2 | using FirebaseAdmin; 3 | using FirebaseAdmin.Auth; 4 | using FirebaseAdminAuthentication.DependencyInjection.Extensions; 5 | using FirebaseAdminAuthentication.DependencyInjection.Models; 6 | using FluentValidation.AspNetCore; 7 | using Google.Apis.Auth.OAuth2; 8 | using GraphQLDemo.API.DataLoaders; 9 | using GraphQLDemo.API.Schema; 10 | using GraphQLDemo.API.Schema.Mutations; 11 | using GraphQLDemo.API.Schema.Queries; 12 | using GraphQLDemo.API.Schema.Subscriptions; 13 | using GraphQLDemo.API.Services; 14 | using GraphQLDemo.API.Services.Courses; 15 | using GraphQLDemo.API.Services.Instructors; 16 | using GraphQLDemo.API.Validators; 17 | using Microsoft.AspNetCore.Builder; 18 | using Microsoft.AspNetCore.Hosting; 19 | using Microsoft.AspNetCore.Http; 20 | using Microsoft.EntityFrameworkCore; 21 | using Microsoft.Extensions.Configuration; 22 | using Microsoft.Extensions.DependencyInjection; 23 | using Microsoft.Extensions.Hosting; 24 | using System; 25 | using System.Collections.Generic; 26 | using System.Linq; 27 | using System.Threading.Tasks; 28 | 29 | namespace GraphQLDemo.API 30 | { 31 | public class Startup 32 | { 33 | private readonly IConfiguration _configuration; 34 | 35 | public Startup(IConfiguration configuration) 36 | { 37 | _configuration = configuration; 38 | } 39 | 40 | public void ConfigureServices(IServiceCollection services) 41 | { 42 | services.AddFluentValidation(); 43 | services.AddTransient(); 44 | services.AddTransient(); 45 | 46 | services.AddGraphQLServer() 47 | .AddQueryType() 48 | .AddMutationType() 49 | .AddSubscriptionType() 50 | .AddType() 51 | .AddType() 52 | .AddTypeExtension() 53 | .AddTypeExtension() 54 | .AddTypeExtension() 55 | .AddTypeExtension() 56 | .AddFiltering() 57 | .AddSorting() 58 | .AddProjections() 59 | .AddAuthorization() 60 | .AddFluentValidation(o => 61 | { 62 | o.UseDefaultErrorMapper(); 63 | }); 64 | 65 | services.AddSingleton(FirebaseApp.Create(new AppOptions() 66 | { 67 | Credential = GoogleCredential.FromJson(_configuration.GetValue("FIREBASE_CONFIG")) 68 | })); 69 | services.AddFirebaseAuthentication(); 70 | services.AddAuthorization( 71 | o => o.AddPolicy("IsAdmin", 72 | p => p.RequireClaim(FirebaseUserClaimType.EMAIL, "singletonsean4@gmail.com"))); 73 | 74 | services.AddInMemorySubscriptions(); 75 | 76 | string connectionString = _configuration.GetConnectionString("default"); 77 | services.AddPooledDbContextFactory(o => o.UseSqlServer(connectionString).LogTo(Console.WriteLine)); 78 | 79 | services.AddScoped(); 80 | services.AddScoped(); 81 | services.AddScoped(); 82 | services.AddScoped(); 83 | } 84 | 85 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 86 | { 87 | if (env.IsDevelopment()) 88 | { 89 | app.UseDeveloperExceptionPage(); 90 | } 91 | 92 | app.UseRouting(); 93 | 94 | app.UseAuthentication(); 95 | 96 | app.UseWebSockets(); 97 | 98 | app.UseEndpoints(endpoints => 99 | { 100 | endpoints.MapGraphQL(); 101 | }); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Validators/CourseTypeInputValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using GraphQLDemo.API.Schema.Mutations; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace GraphQLDemo.API.Validators 9 | { 10 | public class CourseTypeInputValidator : AbstractValidator 11 | { 12 | public CourseTypeInputValidator() 13 | { 14 | RuleFor(c => c.Name) 15 | .MinimumLength(3) 16 | .MaximumLength(50) 17 | .WithMessage("Course name must be between 3 and 50 characters.") 18 | .WithErrorCode("COURSE_NAME_LENGTH"); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/Validators/InstructorTypeInputValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using GraphQLDemo.API.Schema.Mutations; 3 | 4 | namespace GraphQLDemo.API.Validators 5 | { 6 | public class InstructorTypeInputValidator : AbstractValidator 7 | { 8 | public InstructorTypeInputValidator() 9 | { 10 | RuleFor(i => i.FirstName).NotEmpty(); 11 | RuleFor(i => i.LastName).NotEmpty(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "FIREBASE_CONFIG_PATH": "./firebase-config.json", 10 | "ConnectionStrings": { 11 | "default": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=graphql-demo;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.Client/.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "strawberryshake.tools": { 6 | "version": "11.3.2", 7 | "commands": [ 8 | "dotnet-graphql" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.Client/.graphqlrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": "schema.graphql", 3 | "documents": "**/*.graphql", 4 | "extensions": { 5 | "strawberryShake": { 6 | "name": "GraphQLDemoClient", 7 | "url": "http://localhost:5000/graphql", 8 | "namespace": "GraphQLDemo.Client", 9 | "dependencyInjection": true, 10 | "strictSchemaValidation": true, 11 | "hashAlgorithm": "md5", 12 | "useSingleFile": true, 13 | "requestStrategy": "Default", 14 | "outputDirectoryName": "Generated", 15 | "noStore": false, 16 | "emitGeneratedCode": true, 17 | "razorComponents": false, 18 | "records": { 19 | "inputs": false, 20 | "entities": false 21 | }, 22 | "transportProfiles": [ 23 | { 24 | "default": "Http", 25 | "subscription": "WebSocket" 26 | } 27 | ] 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.Client/GraphQLDemo.Client.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net5.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | PreserveNewest 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.Client/GraphQLDemo.Client.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | 6 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.Client/Mutations/CreateCourseMutation.graphql: -------------------------------------------------------------------------------- 1 | mutation CreateCourse($courseInput: CourseTypeInput) { 2 | createCourse(courseInput: $courseInput) { 3 | id 4 | name 5 | subject 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.Client/Mutations/DeleteCourseMutation.graphql: -------------------------------------------------------------------------------- 1 | mutation DeleteCourse($id: Uuid!) { 2 | deleteCourse(id: $id) 3 | } 4 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.Client/Mutations/UpdateCourseMutation.graphql: -------------------------------------------------------------------------------- 1 | mutation UpdateCourse($id: Uuid!, $courseInput: CourseTypeInput) { 2 | updateCourse(courseInput: $courseInput, id: $id) { 3 | id 4 | name 5 | subject 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.Client/Program.cs: -------------------------------------------------------------------------------- 1 | using Firebase.Auth; 2 | using GraphQLDemo.Client.Scripts; 3 | using GraphQLDemo.Client.Stores; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using StrawberryShake; 8 | using System; 9 | using System.Linq; 10 | using System.Net.Http.Headers; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | 14 | namespace GraphQLDemo.Client 15 | { 16 | class Program 17 | { 18 | static void Main(string[] args) 19 | { 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureServices((context, services) => 22 | { 23 | string graphqlApiUrl = context.Configuration.GetValue("GRAPHQL_API_URL"); 24 | 25 | string httpGraphQLApiUrl = $"http://{graphqlApiUrl}"; 26 | string webSocketsGraphQLApiUrl = $"ws://{graphqlApiUrl}"; 27 | 28 | services 29 | .AddGraphQLDemoClient() 30 | .ConfigureHttpClient((services, c) => { 31 | c.BaseAddress = new Uri(httpGraphQLApiUrl); 32 | 33 | TokenStore tokenStore = services.GetRequiredService(); 34 | c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenStore.AccessToken); 35 | }) 36 | .ConfigureWebSocketClient(c => c.Uri = new Uri(webSocketsGraphQLApiUrl)); 37 | 38 | services.AddHostedService(); 39 | 40 | services.AddSingleton(); 41 | 42 | string firebaseApiKey = context.Configuration.GetValue("FIREBASE_API_KEY"); 43 | services.AddSingleton(new FirebaseAuthProvider(new FirebaseConfig(firebaseApiKey))); 44 | 45 | services.AddTransient(); 46 | services.AddTransient(); 47 | services.AddTransient(); 48 | services.AddTransient(); 49 | }) 50 | .Build() 51 | .Run(); 52 | } 53 | } 54 | 55 | public class Startup : IHostedService 56 | { 57 | private readonly SearchScript _searchScript; 58 | 59 | public Startup(SearchScript searchScript) 60 | { 61 | _searchScript = searchScript; 62 | } 63 | 64 | public async Task StartAsync(CancellationToken cancellationToken) 65 | { 66 | await _searchScript.Run(); 67 | } 68 | 69 | public Task StopAsync(CancellationToken cancellationToken) 70 | { 71 | return Task.CompletedTask; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.Client/Queries/CourseByIdQuery.graphql: -------------------------------------------------------------------------------- 1 | query GetCourseById($id: Uuid!) { 2 | courseById(id: $id) { 3 | id 4 | name 5 | instructor { 6 | firstName 7 | } 8 | students { 9 | id 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.Client/Queries/CoursesQuery.graphql: -------------------------------------------------------------------------------- 1 | query GetCourses($first: Int, $after: String, $last: Int, $before: String, $where: CourseTypeFilterInput, $order: [CourseTypeSortInput!]) { 2 | courses ( 3 | first: $first 4 | last: $last 5 | before: $before 6 | after: $after 7 | order: $order 8 | where: $where 9 | ) { 10 | nodes { 11 | id 12 | name 13 | subject 14 | instructor { 15 | id 16 | firstName 17 | lastName 18 | salary 19 | } 20 | } 21 | pageInfo { 22 | startCursor 23 | endCursor 24 | hasNextPage 25 | hasPreviousPage 26 | } 27 | totalCount 28 | } 29 | } -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.Client/Queries/InstructionsQuery.graphql: -------------------------------------------------------------------------------- 1 | query GetInstructions { 2 | instructions 3 | } -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.Client/Queries/SearchQuery.graphql: -------------------------------------------------------------------------------- 1 | query Search($term: String!){ 2 | search(term: $term) { 3 | __typename 4 | ... on CourseType { 5 | id 6 | name 7 | subject 8 | } 9 | ... on InstructorType { 10 | id 11 | firstName 12 | lastName 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.Client/Scripts/CreateCourseScript.cs: -------------------------------------------------------------------------------- 1 | using StrawberryShake; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace GraphQLDemo.Client.Scripts 9 | { 10 | public class CreateCourseScript 11 | { 12 | private readonly IGraphQLDemoClient _client; 13 | 14 | public CreateCourseScript(IGraphQLDemoClient client) 15 | { 16 | _client = client; 17 | } 18 | 19 | public async Task Run() 20 | { 21 | CourseTypeInput courseInput = new CourseTypeInput() 22 | { 23 | Name = "Algebra", 24 | Subject = Subject.Mathematics, 25 | InstructorId = Guid.Parse("b0920fe9a1a54468b9faa5fe295dfc2d") 26 | }; 27 | 28 | IOperationResult createCourseResult = await _client.CreateCourse.ExecuteAsync(courseInput); 29 | 30 | Console.WriteLine(createCourseResult.Errors); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.Client/Scripts/GetCoursesScript.cs: -------------------------------------------------------------------------------- 1 | using StrawberryShake; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace GraphQLDemo.Client.Scripts 9 | { 10 | public class GetCoursesScript 11 | { 12 | private readonly IGraphQLDemoClient _client; 13 | 14 | public GetCoursesScript(IGraphQLDemoClient client) 15 | { 16 | _client = client; 17 | } 18 | 19 | public async Task Run() 20 | { 21 | ConsoleKey key; 22 | 23 | int? first = 5; 24 | string after = null; 25 | 26 | int? last = null; 27 | string before = null; 28 | 29 | do 30 | { 31 | IOperationResult coursesResult = await _client.GetCourses.ExecuteAsync( 32 | first, after, last, before, null, 33 | new List() 34 | { 35 | new CourseTypeSortInput() 36 | { 37 | Name = SortEnumType.Asc 38 | } 39 | }); 40 | 41 | Console.WriteLine($"{"Name",-10} | {"Subject",-10}"); 42 | Console.WriteLine(); 43 | 44 | foreach (IGetCourses_Courses_Nodes course in coursesResult.Data.Courses.Nodes) 45 | { 46 | Console.WriteLine($"{course.Name,-10} | {course.Subject,-10}"); 47 | } 48 | 49 | IGetCourses_Courses_PageInfo pageInfo = coursesResult.Data.Courses.PageInfo; 50 | 51 | if(pageInfo.HasPreviousPage) 52 | { 53 | Console.WriteLine("Press 'A' to move to the previous page."); 54 | } 55 | 56 | if(pageInfo.HasNextPage) 57 | { 58 | Console.WriteLine("Press 'D' to move to the next page."); 59 | } 60 | 61 | Console.WriteLine("Press 'Enter' to exit."); 62 | 63 | key = Console.ReadKey().Key; 64 | 65 | if(key == ConsoleKey.A && pageInfo.HasPreviousPage) 66 | { 67 | last = 5; 68 | before = pageInfo.StartCursor; 69 | 70 | first = null; 71 | after = null; 72 | } 73 | else if (key == ConsoleKey.D && pageInfo.HasNextPage) 74 | { 75 | first = 5; 76 | after = pageInfo.EndCursor; 77 | 78 | last = null; 79 | before = null; 80 | } 81 | } while (key != ConsoleKey.Enter); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.Client/Scripts/LoginScript.cs: -------------------------------------------------------------------------------- 1 | using Firebase.Auth; 2 | using GraphQLDemo.Client.Stores; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace GraphQLDemo.Client.Scripts 10 | { 11 | public class LoginScript 12 | { 13 | private readonly FirebaseAuthProvider _firebaseAuthProvider; 14 | private readonly TokenStore _tokenStore; 15 | 16 | public LoginScript(FirebaseAuthProvider firebaseAuthProvider, TokenStore tokenStore) 17 | { 18 | _firebaseAuthProvider = firebaseAuthProvider; 19 | _tokenStore = tokenStore; 20 | } 21 | 22 | public async Task Run() 23 | { 24 | Console.WriteLine("Enter your email:"); 25 | string email = Console.ReadLine(); 26 | 27 | Console.WriteLine("Enter your password:"); 28 | string password = Console.ReadLine(); 29 | 30 | FirebaseAuthLink firebaseAuthLink = await _firebaseAuthProvider.SignInWithEmailAndPasswordAsync(email, password); 31 | _tokenStore.AccessToken = firebaseAuthLink.FirebaseToken; 32 | 33 | Console.WriteLine("Successfully logged in."); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.Client/Scripts/SearchScript.cs: -------------------------------------------------------------------------------- 1 | using StrawberryShake; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace GraphQLDemo.Client.Scripts 9 | { 10 | public class SearchScript 11 | { 12 | private readonly IGraphQLDemoClient _client; 13 | 14 | public SearchScript(IGraphQLDemoClient client) 15 | { 16 | _client = client; 17 | } 18 | 19 | public async Task Run() 20 | { 21 | Console.WriteLine("Enter a search term:"); 22 | string term = Console.ReadLine(); 23 | IOperationResult searchResult = await _client.Search.ExecuteAsync(term); 24 | 25 | IEnumerable courses = searchResult.Data.Search.OfType(); 26 | Console.WriteLine("COURSES"); 27 | foreach (ISearch_Search_CourseType course in courses) 28 | { 29 | Console.WriteLine(course.Name); 30 | } 31 | 32 | IEnumerable instructors = searchResult.Data.Search.OfType(); 33 | Console.WriteLine(); 34 | Console.WriteLine("INSTRUCTORS"); 35 | foreach (ISearch_Search_InstructorType instructor in instructors) 36 | { 37 | Console.WriteLine($"{instructor.FirstName} {instructor.LastName}"); 38 | } 39 | 40 | Console.WriteLine(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.Client/Stores/TokenStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace GraphQLDemo.Client.Stores 8 | { 9 | public class TokenStore 10 | { 11 | public string AccessToken { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.Client/Subscriptions/CourseCreatedSubscription.graphql: -------------------------------------------------------------------------------- 1 | subscription CourseCreated { 2 | courseCreated { 3 | id 4 | name 5 | subject 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.Client/Subscriptions/CourseUpdatedSubscription.graphql: -------------------------------------------------------------------------------- 1 | subscription CourseUpdated($courseId: Uuid!) { 2 | courseUpdated(courseId: $courseId) { 3 | id 4 | name 5 | subject 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.Client/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "GRAPHQL_API_URL": "localhost:5000/graphql", 3 | "FIREBASE_API_KEY": "AIzaSyAADKY3ZMNdqjjWSSK3bEKuF5XUJifOHiA" 4 | } -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.Client/schema.extensions.graphql: -------------------------------------------------------------------------------- 1 | scalar _KeyFieldSet 2 | 3 | directive @key(fields: _KeyFieldSet!) on SCHEMA | OBJECT 4 | 5 | directive @serializationType(name: String!) on SCALAR 6 | 7 | directive @runtimeType(name: String!) on SCALAR 8 | 9 | directive @enumValue(value: String!) on ENUM_VALUE 10 | 11 | directive @rename(name: String!) on INPUT_FIELD_DEFINITION | INPUT_OBJECT | ENUM | ENUM_VALUE 12 | 13 | extend schema @key(fields: "id") -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.Client/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | mutation: Mutation 4 | subscription: Subscription 5 | } 6 | 7 | enum ApplyPolicy { 8 | BEFORE_RESOLVER 9 | AFTER_RESOLVER 10 | } 11 | 12 | type Query { 13 | search(term: String): [SearchResult] 14 | instructions: String @deprecated(reason: "This query is deprecated.") 15 | courses(first: Int after: String last: Int before: String where: CourseTypeFilterInput order: [CourseTypeSortInput!]): CourseTypeConnection 16 | courseById(id: Uuid!): CourseType 17 | } 18 | 19 | type Mutation { 20 | createCourse(courseInput: CourseTypeInput): CourseResult 21 | updateCourse(id: Uuid! courseInput: CourseTypeInput): CourseResult 22 | deleteCourse(id: Uuid!): Boolean! 23 | } 24 | 25 | type Subscription { 26 | courseCreated: CourseResult 27 | courseUpdated(courseId: Uuid!): CourseResult 28 | } 29 | 30 | type CourseType { 31 | instructor: InstructorType! 32 | creator: UserType 33 | id: Uuid! 34 | name: String 35 | subject: Subject! 36 | instructorId: Uuid! 37 | students: [StudentType] 38 | creatorId: String 39 | } 40 | 41 | type InstructorType { 42 | id: Uuid! 43 | firstName: String 44 | lastName: String 45 | salary: Float! 46 | } 47 | 48 | input CourseTypeFilterInput { 49 | and: [CourseTypeFilterInput!] 50 | or: [CourseTypeFilterInput!] 51 | id: ComparableGuidOperationFilterInput 52 | name: StringOperationFilterInput 53 | subject: SubjectOperationFilterInput 54 | instructorId: ComparableGuidOperationFilterInput 55 | creatorId: StringOperationFilterInput 56 | } 57 | 58 | input CourseTypeSortInput { 59 | name: SortEnumType 60 | subject: SortEnumType 61 | creatorId: SortEnumType 62 | } 63 | 64 | "A connection to a list of items." 65 | type CourseTypeConnection { 66 | "Information to aid in pagination." 67 | pageInfo: PageInfo! 68 | "A list of edges." 69 | edges: [CourseTypeEdge!] 70 | "A flattened list of the nodes." 71 | nodes: [CourseType] 72 | totalCount: Int! 73 | } 74 | 75 | input ComparableGuidOperationFilterInput { 76 | eq: Uuid 77 | neq: Uuid 78 | in: [Uuid!] 79 | nin: [Uuid!] 80 | gt: Uuid 81 | ngt: Uuid 82 | gte: Uuid 83 | ngte: Uuid 84 | lt: Uuid 85 | nlt: Uuid 86 | lte: Uuid 87 | nlte: Uuid 88 | } 89 | 90 | input StringOperationFilterInput { 91 | and: [StringOperationFilterInput!] 92 | or: [StringOperationFilterInput!] 93 | eq: String 94 | neq: String 95 | contains: String 96 | ncontains: String 97 | in: [String] 98 | nin: [String] 99 | startsWith: String 100 | nstartsWith: String 101 | endsWith: String 102 | nendsWith: String 103 | } 104 | 105 | input SubjectOperationFilterInput { 106 | eq: Subject 107 | neq: Subject 108 | in: [Subject!] 109 | nin: [Subject!] 110 | } 111 | 112 | enum SortEnumType { 113 | ASC 114 | DESC 115 | } 116 | 117 | "Information about pagination in a connection." 118 | type PageInfo { 119 | "Indicates whether more edges exist following the set defined by the clients arguments." 120 | hasNextPage: Boolean! 121 | "Indicates whether more edges exist prior the set defined by the clients arguments." 122 | hasPreviousPage: Boolean! 123 | "When paginating backwards, the cursor to continue." 124 | startCursor: String 125 | "When paginating forwards, the cursor to continue." 126 | endCursor: String 127 | } 128 | 129 | "An edge in a connection." 130 | type CourseTypeEdge { 131 | "A cursor for use in pagination." 132 | cursor: String! 133 | "The item at the end of the edge." 134 | node: CourseType 135 | } 136 | 137 | scalar Uuid 138 | 139 | type UserType { 140 | id: String 141 | username: String 142 | photoUrl: String 143 | } 144 | 145 | input CourseTypeInput { 146 | name: String 147 | subject: Subject! 148 | instructorId: Uuid! 149 | } 150 | 151 | type CourseResult { 152 | id: Uuid! 153 | name: String 154 | subject: Subject! 155 | instructorId: Uuid! 156 | } 157 | 158 | union SearchResult = CourseType | InstructorType 159 | 160 | enum Subject { 161 | MATHEMATICS 162 | SCIENCE 163 | HISTORY 164 | } 165 | 166 | type StudentType { 167 | id: Uuid! 168 | firstName: String 169 | lastName: String 170 | gpa: Float! 171 | } 172 | 173 | directive @authorize("The name of the authorization policy that determines access to the annotated resource." policy: String "Roles that are allowed to access the annotated resource." roles: [String!] "Defines when when the resolver shall be executed.By default the resolver is executed after the policy has determined that the current user is allowed to access the field." apply: ApplyPolicy! = BEFORE_RESOLVER) repeatable on SCHEMA | OBJECT | FIELD_DEFINITION -------------------------------------------------------------------------------- /GraphQLDemo/GraphQLDemo.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31424.327 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQLDemo.API", "GraphQLDemo.API\GraphQLDemo.API.csproj", "{79B1BDFA-5506-4C63-91C2-0AE4DB95C400}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQLDemo.Client", "GraphQLDemo.Client\GraphQLDemo.Client.csproj", "{F8DE44D3-2942-4588-8516-767C52AA1BAE}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {79B1BDFA-5506-4C63-91C2-0AE4DB95C400}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {79B1BDFA-5506-4C63-91C2-0AE4DB95C400}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {79B1BDFA-5506-4C63-91C2-0AE4DB95C400}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {79B1BDFA-5506-4C63-91C2-0AE4DB95C400}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {F8DE44D3-2942-4588-8516-767C52AA1BAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {F8DE44D3-2942-4588-8516-767C52AA1BAE}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {F8DE44D3-2942-4588-8516-767C52AA1BAE}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {F8DE44D3-2942-4588-8516-767C52AA1BAE}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {4F397443-DDE5-47A3-A5E3-3D6E16C4260C} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Demo (.NET) 2 | 3 | Full stack GraphQL demo with Hot Chocolate and Strawberry Shake. 4 | 5 | ## Episode Branches 6 | 7 | The following links go to the state of this repository after the corresponding episode. For example, the "Part 2" link under the "API" section will show the state of this repository after part 2 of the API series. 8 | 9 | ## API 10 | 11 | - [Part 1](https://github.com/SingletonSean/graphql-demo-net/tree/server/ep-1) 12 | - [Part 2](https://github.com/SingletonSean/graphql-demo-net/tree/server/ep-2) 13 | - [Part 3](https://github.com/SingletonSean/graphql-demo-net/tree/server/ep-3) 14 | - [Part 4](https://github.com/SingletonSean/graphql-demo-net/tree/server/ep-4) 15 | - [Part 5.1](https://github.com/SingletonSean/graphql-demo-net/tree/server/ep-5.1) 16 | - [Part 5.2](https://github.com/SingletonSean/graphql-demo-net/tree/server/ep-5.2) 17 | - [Part 6](https://github.com/SingletonSean/graphql-demo-net/tree/server/ep-6) 18 | - [Part 7.1](https://github.com/SingletonSean/graphql-demo-net/tree/server/ep-7.1) 19 | - [Part 7.2](https://github.com/SingletonSean/graphql-demo-net/tree/server/ep-7.2) 20 | - [Part 7.3](https://github.com/SingletonSean/graphql-demo-net/tree/server/ep-7.3) 21 | - [Part 7.4](https://github.com/SingletonSean/graphql-demo-net/tree/server/ep-7.4) 22 | - [Part 8.1](https://github.com/SingletonSean/graphql-demo-net/tree/server/ep-8.1) 23 | - [Part 8.2](https://github.com/SingletonSean/graphql-demo-net/tree/server/ep-8.2) 24 | - [Part 8.3](https://github.com/SingletonSean/graphql-demo-net/tree/server/ep-8.3) 25 | - [Part 9](https://github.com/SingletonSean/graphql-demo-net/tree/server/ep-9) 26 | - [Part 10](https://github.com/SingletonSean/graphql-demo-net/tree/server/ep-10) 27 | - [Part 11](https://github.com/SingletonSean/graphql-demo-net/tree/server/ep-11) 28 | - [Part 12](https://github.com/SingletonSean/graphql-demo-net/tree/server/ep-12) 29 | 30 | ## Client 31 | 32 | - [Part 1](https://github.com/SingletonSean/graphql-demo-net/tree/client/ep-1) 33 | - [Part 2](https://github.com/SingletonSean/graphql-demo-net/tree/client/ep-2) 34 | - [Part 3](https://github.com/SingletonSean/graphql-demo-net/tree/client/ep-3) 35 | - [Part 4](https://github.com/SingletonSean/graphql-demo-net/tree/client/ep-4) 36 | - [Part 5](https://github.com/SingletonSean/graphql-demo-net/tree/client/ep-5) 37 | - [Part 6](https://github.com/SingletonSean/graphql-demo-net/tree/client/ep-6) 38 | - [Part 7](https://github.com/SingletonSean/graphql-demo-net/tree/client/ep-7) 39 | --------------------------------------------------------------------------------