Zaklęcia GraphQL dla magów ASP.NET Core: Jak zaczarować swoje API

Cezary Walenciuk

Zaklęcia GraphQL
dla magów ASP.NET Core:
Jak zaczarować swoje API

@walenciukC

Speaker
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW

ASP.NET Program.cs Hello World

                    
                        var builder = WebApplication.CreateBuilder(args);

                        builder.Services
                            .AddGraphQLServer()
                            .AddQueryType<Query>();
                        
                        var app = builder.Build();
                        app.MapGraphQL();
                        
                        app.MapGet("/", () =>
                        { return Results.Redirect($"/graphql", permanent: true); });
                        
                        app.Run();
                    
                

Hello World Query Hello

                    

                        public class Query
                        {
                            public string Hello(string name = "World",
                                int a = 0, int b = 0)
                            {
                                int hour = DateTime.UtcNow.Hour;
                                string timeMessage = hour == 0 ? "midnight" :
                                       hour == 12 ? "noon" :
                                       $"{(hour > 12 ? hour - 12 : hour)}" +
                                       $"{(hour >= 12 ? "PM" : "AM")}";
                        
                                string calMessage = a == 0 && b == 0 ? "" :
                                    $"The sum of {a} and {b} is {a + b} and ";
                        
                                return $"Hello, {name}! {calMessage} " +
                                    $"It's {timeMessage}.";
                            }
                        }
                    
                
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW

Klasy reprezentujące relacje w EF Core

                    









                        public class Document
                        {
                            public int Id { get; set; }
                            public required string Title { get; set; }
                            public string? Content { get; set; }
                            public string? Author { get; set; }
                            public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
                            public DateTime ModifiedAt { get; set; } = DateTime.UtcNow;
                            public List<Tag> Tags { get; set; } = new();
                            public List<Metadata> Metadata { get; set; } = new();
                        }
                        
                        public class Metadata
                        {
                            public int Id { get; set; }
                            public required string FieldName { get; set; }
                            public string? Value { get; set; }
                        
                            public int DocumentId { get; set; }
                            public Document Document { get; set; } = null!;
                        }
                        
                        public class Tag
                        {
                            public int Id { get; set; }
                            public required string Name { get; set; }
                            public List<Document> Documents { get; set; } = new();
                        }

                                                                                    
                    
                

Konfiguracja AppDbContext dla tej aplikacji

                    

                        public class AppDbContext : DbContext
                        {
                            public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
                        
                            public DbSet<Document> Documents => Set<Document>();
                            public DbSet<Models.Tag> Tags => Set<Models.Tag>();
                            public DbSet<Metadata> Metadata => Set<Metadata>();
                        
                            protected override void OnModelCreating(ModelBuilder modelBuilder)
                            {
                                // Configure many-to-many relationship between Documents and Tags
                                modelBuilder.Entity<Document>()
                                    .HasMany(d => d.Tags)
                                    .WithMany(t => t.Documents)
                                    .UsingEntity(j => j.ToTable("DocumentTags"));
                        
                                // Configure one-to-many relationship between Document and Metadata
                                modelBuilder.Entity<Document>()
                                    .HasMany(d => d.Metadata)
                                    .WithOne(m => m.Document)
                                    .HasForeignKey(m => m.DocumentId)
                                    .OnDelete(DeleteBehavior.Cascade);
                            }
                        }                                                       
                    
                

Query dla tego projektu

                    

                        public class Query
                        {
                            public IQueryable<Document> GetDocuments(AppDbContext context)
                            {
                                return context.Documents.Include(a => a.Tags).Include(a => a.Metadata);
                            }
                        }                                                                       
                    
                

Program CS

                    

                        var builder = WebApplication.CreateBuilder(args);

                        // Add services to the container
                        builder.Services.AddDbContextFactory<AppDbContext>(options =>
                            options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
                        
                        // Register GraphQL services
                        builder.Services
                            .AddGraphQLServer()
                            .AddTypes(typeof(Query))
                            .RegisterDbContextFactory<AppDbContext>();
                        
                        var app = builder.Build();
                        
                        app.MapGraphQL();
                        
                        app.Run();
                                                                                         
                    
                
blog
CW
blog
CW
blog
CW

Pełny przykład demo z EF Core : Program.cs

                    

                        var builder = WebApplication.CreateBuilder(args);

                        // Add services to the container
                        builder.Services.AddDbContextFactory<AppDbContext>(options =>
                            options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
                        
                        // Register GraphQL services
                        builder.Services
                            .AddGraphQLServer()
                            .AddTypes(typeof(Query))
                            .AddProjections()
                            .AddSorting()
                            .AddFiltering()
                            .AddType<DocumentType>()
                            .AddType<TagType>()
                            .AddType<MetadataType>()
                            .RegisterDbContextFactory<AppDbContext>();
                        
                        
                        var app = builder.Build();
                        
                        app.MapGraphQL();
                        
                        app.Run();                                                   
                    
                

Pełny przykład demo z EF Core : Query

                    
                        public class Query
                        {
                            [UsePaging]
                            [UseProjection]
                            [UseFiltering]
                            [UseSorting]
                            public IQueryable<Document> GetDocuments(AppDbContext context,
                                IResolverContext resolver)
                            {
                                var all = context.Documents.Include(a => a.Tags).Include(a => a.Metadata);
                                return resolver.ArgumentKind("order") is ValueKind.Null
                                ? all.OrderBy(t => t.Title) : all;
                            }
                        
                        }                                              
                    
                
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW

Ukrywanie pól i ich dodowanie

                    












                        public class DocumentType : ObjectType<Document>
                        {
                            protected override void Configure(IObjectTypeDescriptor<Document> descriptor)
                            {
                                descriptor.Description("Represents a document in the system");
                        
                                descriptor.Field(d => d.Id).Description("The unique identifier of the document");
                                descriptor.Field(d => d.Title).Description("The title of the document");
                                descriptor.Field(d => d.Content).Description("The content of the document");
                                descriptor.Field(d => d.Author).Description("The author of the document");
                                //descriptor.Field(d => d.CreatedAt).Description("When the document was created");
                                //descriptor.Field(d => d.ModifiedAt).Description("When the document was last modified");
                                descriptor.Field(d => d.Tags).Description("The tags associated with the document");
                                descriptor.Field(d => d.Metadata).Description("The metadata associated with the document");
                            }
                        }

                        builder.Services
                        .AddGraphQLServer()
                        .AddTypes(typeof(Query))
                        .AddProjections()
                        .AddSorting()
                        .AddFiltering()
                        .AddType<DocumentType>()
                        .AddType<TagType>()
                        .AddType<MetadataType>()
                        .RegisterDbContextFactory<AppDbContext>();
                        
                    
                
blog
CW
blog
CW
blog
CW
blog
CW

DocumentRepository z Dapper

                    


























































































































































                        public class DocumentRepository
                        {
                            private readonly string _connectionString;
                        
                            public DocumentRepository(string connectionString)
                            {
                                _connectionString = connectionString;
                            }
                        
                            private IDbConnection CreateConnection()
                            {
                                return new SqliteConnection(_connectionString);
                            }
                        
                            public async Task<IEnumerable<Document>> GetDocumentsAsync(string? orderBy = null,
                                string? direction = "ASC", int? skip = null, int? take = null)
                            {
                                using var connection = CreateConnection();
                        
                                // Base query
                                var sql = "SELECT d.*, t.*, m.* FROM Documents d " +
                                          "LEFT JOIN DocumentTags dt ON d.Id = dt.DocumentsId " +
                                          "LEFT JOIN Tags t ON dt.TagsId = t.Id " +
                                          "LEFT JOIN Metadata m ON d.Id = m.DocumentId";
                        
                                // Add ordering
                                if (!string.IsNullOrEmpty(orderBy))
                                {
                                    sql += $" ORDER BY d.{orderBy} {direction}";
                                }
                                else
                                {
                                    sql += " ORDER BY d.Title ASC";
                                }
                        
                                // Add pagination
                                if (skip.HasValue && take.HasValue)
                                {
                                    sql += $" LIMIT {take.Value} OFFSET {skip.Value}";
                                }
                        
                                // Use Dapper's QueryAsync with a multi-mapping approach
                                var documentDictionary = new Dictionary<int, Document>();
                        
                                await connection.QueryAsync<Document, Tag, Metadata, Document>(
                                    sql,
                                    (document, tag, metadata) =>
                                    {
                                        if (!documentDictionary.TryGetValue(document.Id, out var documentEntry))
                                        {
                                            documentEntry = document;
                                            documentEntry.Tags = new List<Tag>();
                                            documentEntry.Metadata = new List<Metadata>();
                                            documentDictionary.Add(document.Id, documentEntry);
                                        }
                        
                                        if (tag != null && tag.Id != 0)
                                        {
                                            if (!documentEntry.Tags.Any(t => t.Id == tag.Id))
                                            {
                                                documentEntry.Tags.Add(tag);
                                            }
                                        }
                        
                                        if (metadata != null && metadata.Id != 0)
                                        {
                                            if (!documentEntry.Metadata.Any(m => m.Id == metadata.Id))
                                            {
                                                documentEntry.Metadata.Add(metadata);
                                            }
                                        }
                        
                                        return documentEntry;
                                    },
                                    splitOn: "Id,Id");
                        
                                return documentDictionary.Values;
                            }
                        
                            public async Task<Document?> GetDocumentByIdAsync(int id)
                            {
                                using var connection = CreateConnection();
                        
                                var sql = "SELECT d.*, t.*, m.* FROM Documents d " +
                                          "LEFT JOIN DocumentTags dt ON d.Id = dt.DocumentsId " +
                                          "LEFT JOIN Tags t ON dt.TagsId = t.Id " +
                                          "LEFT JOIN Metadata m ON d.Id = m.DocumentId " +
                                          "WHERE d.Id = @Id";
                        
                                Document? result = null;
                                await connection.QueryAsync<Document, Tag, Metadata, Document>(
                                    sql,
                                    (document, tag, metadata) =>
                                    {
                                        if (result == null)
                                        {
                                            result = document;
                                            result.Tags = new List<Tag>();
                                            result.Metadata = new List<Metadata>();
                                        }
                        
                                        if (tag != null && tag.Id != 0 && !result.Tags.Any(t => t.Id == tag.Id))
                                        {
                                            result.Tags.Add(tag);
                                        }
                        
                                        if (metadata != null && metadata.Id != 0 && !result.Metadata.Any(m => m.Id == metadata.Id))
                                        {
                                            result.Metadata.Add(metadata);
                                        }
                        
                                        return result;
                                    },
                                    new { Id = id },
                                    splitOn: "Id,Id");
                        
                                return result;
                            }
                        
                            public async Task<int> CreateDocumentAsync(Document document)
                            {
                                using var connection = CreateConnection();
                                connection.Open();
                                using var transaction = connection.BeginTransaction();
                        
                                try
                                {
                                    // Insert document
                                    var sql = @"
                                        INSERT INTO Documents (Title, Content, Author, CreatedAt, ModifiedAt)
                                        VALUES (@Title, @Content, @Author, @CreatedAt, @ModifiedAt);
                                        SELECT last_insert_rowid();";
                        
                                    var documentId = await connection.ExecuteScalarAsync<int>(sql, document, transaction);
                        
                                    // Insert tags relationships
                                    if (document.Tags.Any())
                                    {
                                        foreach (var tag in document.Tags)
                                        {
                                            // Insert tag if it doesn't exist
                                            var existingTagId = await connection.ExecuteScalarAsync<int?>(
                                                "SELECT Id FROM Tags WHERE Name = @Name", new { tag.Name }, transaction);
                        
                                            var tagId = existingTagId ?? await connection.ExecuteScalarAsync<int>(
                                                "INSERT INTO Tags (Name) VALUES (@Name); SELECT last_insert_rowid();",
                                                new { tag.Name }, transaction);
                        
                                            // Create relationship
                                            await connection.ExecuteAsync(
                                                "INSERT INTO DocumentTags (DocumentId, TagId) VALUES (@DocumentId, @TagId)",
                                                new { DocumentId = documentId, TagId = tagId }, transaction);
                                        }
                                    }
                        
                                    // Insert metadata
                                    if (document.Metadata.Any())
                                    {
                                        foreach (var meta in document.Metadata)
                                        {
                                            await connection.ExecuteAsync(
                                                "INSERT INTO Metadata (FieldName, Value, DocumentId) VALUES (@FieldName, @Value, @DocumentId)",
                                                new { meta.FieldName, meta.Value, DocumentId = documentId }, transaction);
                                        }
                                    }
                        
                                    transaction.Commit();
                                    return documentId;
                                }
                                catch
                                {
                                    transaction.Rollback();
                                    throw;
                                }
                            }
                        }
                    
                
blog
CW

Confluent.Kafka Consumer

                    



 
                        [UseOffsetPaging]
                        [UseSorting(typeof(DocumentSortInputType))]
                        [UseFiltering(typeof(UserFilterType))]
                        public async Task<IEnumerable<Document>> GetDocumentsAsync(
                            [Service] DocumentRepository repository,
                             IResolverContext resolver)
                        {
                            var selections = resolver.GetSelectedField();
                            List<string> fields = new List<string>();
                            foreach (var parent in selections.GetFields())
                            {
                                foreach (var innerCh in parent.GetFields())
                                {
                                    fields.Add(innerCh.Field.Name);
                                }
                            }
                    
                            var filterDic = resolver.ArgumentValue<IReadOnlyDictionary<string, object>?>
                                ("where");
                    
                            string? orderBy = null;
                            string? direction = "ASC";
                            int? skip = null;
                            int? take = null;
                    
                            return await repository.GetDocumentsAsync(orderBy, direction, skip, take);
                        }
                    
                
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW

Redis GraphQL Przykład

                    
                        using HotChocolate.Resolvers;
                        using Redis.OM;
                        using Redis.OM.Contracts;
                        using Redis.OM.Searching;
                        
                        namespace GraphQLBlog;
                        
                        [ExtendObjectType("Query")]
                        public class Query
                        {
                            private readonly IRedisCollection<Book> _books;
                        
                            public Query(IRedisConnectionProvider provider)
                            {
                                _books = provider.RedisCollection<Book>();
                            }
                        
                            [UsePaging(IncludeTotalCount = true, MaxPageSize = 10)]
                            [UseProjection]
                            [UseFiltering]
                            public IQueryable<Book> GetBook(IResolverContext context)
                                 => _books.Filter(context);
                        }
                    
                

Redis GraphQL Przykład Program.cs

                    

                        using GraphQLBlog;
                        using Redis.OM;
                        using Redis.OM.Contracts;
                        
                        var builder = WebApplication.CreateBuilder(args);
                        
                        builder.Services.AddSingleton<IRedisConnectionProvider>
                            (new RedisConnectionProvider("redis://localhost:6379"));
                        builder.Services.AddHostedService<StartupService>();
                        
                        builder.Services.AddGraphQLServer()
                            .AddProjections()
                            .AddFiltering()
                            .AddQueryType(d => d.Name("Query"))
                            .AddType<Query>();
                        
                        var app = builder.Build();
                        app.MapGraphQL();
                        app.Run();
                        
                    
                
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW

ProductType przykład bardziej złożonego typu

                    

                        // GraphQL Schema Types
                        public class ProductType : ObjectType<Product>
                        {
                            protected override void Configure(IObjectTypeDescriptor<Product> descriptor)
                            {
                                descriptor.Field(p => p.Id)
                                    .Type<NonNullType<IntType>>();
                                descriptor.Field(p => p.Name)
                                    .Type<NonNullType<StringType>>();
                                descriptor.Field(p => p.Price)
                                    .Type<NonNullType<DecimalType>>();
                                descriptor.Field(p => p.Description)
                                    .Type<StringType>();
                        
                                // Use separate resolvers for Details and Reviews so that
                                // they can be deferred or streamed.
                                descriptor.Field("details")
                                    .ResolveWith<ProductResolvers>(r => r.GetDetails(default))
                                    .Type<ProductDetailsType>();
                                descriptor.Field("reviews")
                                    .ResolveWith<ProductResolvers>(r => r.GetReviews(default))
                                    .Type<ListType<ReviewType>>();
                            }
                        }
                        
                    
                
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW

REST API do dema

                    





































                        using Microsoft.EntityFrameworkCore;
                        using Scalar.AspNetCore;
                        
                        var builder = WebApplication.CreateBuilder(args);
                        // Add services to the container
                        builder.Services.AddDbContext<ApplicationDbContext>(options =>
                            options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")
                            ?? "Data Source=documentViews.db"));
                        
                        builder.Services.AddScoped<IDocumentViewService, DocumentViewService>();
                        builder.Services.AddOpenApi();
                        
                        // Add endpoint mapping
                        builder.Services.AddEndpointsApiExplorer();
                        var app = builder.Build();
                        
                        if (app.Environment.IsDevelopment())
                        {
                            app.MapOpenApi();
                            app.UseReDoc(c =>
                            {
                                c.SpecUrl("/openapi/v1.json");
                            });
                            app.MapScalarApiReference();
                        }
                        
                        app.UseHttpsRedirection();
                        
                        app.MapGroup("/views")
                            .MapDocumentViewEndpoints();
                        
                        app.Run();
                        
                        public static class DocumentViewEndpointExtensions
                        {
                            public static IEndpointRouteBuilder MapDocumentViewEndpoints(this IEndpointRouteBuilder endpoints)
                            {
                                endpoints.MapGet("/id/{documentId}/span/{timeFrameS:regex(^(week|month|threemonths)$)}",
                                    async (string documentId, string timeFrameS, IDocumentViewService service) =>
                                {
                                    if (!Enum.TryParse<TimeFrame>(timeFrameS, true, out var timeFrame))
                                    {
                                        return Results.BadRequest
                                        ("Invalid time frame. Please use 'threemonths', 'week', or 'month'.");
                                    }
                                    var result = await service.GetDocumentViewsOverTimeAsync(documentId, timeFrame);
                                    return Results.Ok(result);
                                });
                        
                                endpoints.MapGet("/span/{timeFrameS:regex(^(week|month|threemonths)$)}",
                                async (string timeFrameS, IDocumentViewService service) =>
                                {
                                    if (!Enum.TryParse<TimeFrame>(timeFrameS, true, out var timeFrame))
                                    {
                                        return Results.BadRequest
                                        ("Invalid time frame. Please use 'threemonths', 'week', or 'month'.");
                                    }
                                    var result = await service.GetDocumentsViewsOverTimeAsync(timeFrame);
                                    return Results.Ok(result);
                                });
                        
                                return endpoints;
                            }
                        }
                        
                    
                
blog
CW

Endpointy do REST API

                    


                        public static class DocumentViewEndpointExtensions
                        {
                            public static IEndpointRouteBuilder MapDocumentViewEndpoints(this IEndpointRouteBuilder endpoints)
                            {
                                endpoints.MapGet("/id/{documentId}/span/{timeFrameS:regex(^(week|month|threemonths)$)}",
                                    async (string documentId, string timeFrameS, IDocumentViewService service) =>
                                {
                                    if (!Enum.TryParse<TimeFrame>(timeFrameS, true, out var timeFrame))
                                    {
                                        return Results.BadRequest
                                        ("Invalid time frame. Please use 'threemonths', 'week', or 'month'.");
                                    }
                                    var result = await service.GetDocumentViewsOverTimeAsync(documentId, timeFrame);
                                    return Results.Ok(result);
                                });
                        
                                endpoints.MapGet("/span/{timeFrameS:regex(^(week|month|threemonths)$)}",
                                async (string timeFrameS, IDocumentViewService service) =>
                                {
                                    if (!Enum.TryParse<TimeFrame>(timeFrameS, true, out var timeFrame))
                                    {
                                        return Results.BadRequest
                                        ("Invalid time frame. Please use 'threemonths', 'week', or 'month'.");
                                    }
                                    var result = await service.GetDocumentsViewsOverTimeAsync(timeFrame);
                                    return Results.Ok(result);
                                });
                        
                                return endpoints;
                            }
                        }
                    
                
blog
CW

DocumentViewsExtendObjecType [ExtendObjectType(typeof(Document))]

                    


















                        //[Node]
                        [ExtendObjectType(typeof(Document))]
                        public sealed class DocumentViewsExtendObjecType
                        {
                            public async Task<DocumentViews> GetViewChange(ChangeSpan span,
                                [Parent] Document parent,
                                [Service] IHttpClientFactory clientFactory,
                                CancellationToken cancellationToken)
                            {
                                using HttpClient client = clientFactory.CreateClient("documentViews");
                        
                                using var message = new HttpRequestMessage
                                    (HttpMethod.Get, $"/views/id/{parent.Id}/span/{span.ToString().ToLower()}");
                        
                                var response = await client.SendAsync(message, cancellationToken);
                                var content = await response.Content.ReadAsByteArrayAsync(cancellationToken);
                                var json = JsonDocument.Parse(content);
                        
                                var options = new JsonSerializerOptions
                                {
                                    PropertyNameCaseInsensitive = true
                                };
                        
                                var list = json.Deserialize<List<DocumentViewRecord>>(options);
                        
                                if (list == null || list.Count == 0)
                                {
                                    return new DocumentViews(null, null, 0);
                                }
                        
                                return new DocumentViews(list.GetDateFirst(),
                                    list.GetDateLast(), list.Sum(a => a.Views));
                            }
                        
                            //[NodeResolver]
                            //public static async Task<Document> GetDocumentByIdAsync(
                            //[ID] int id,
                            //[Service] AppDbContext context,
                            //CancellationToken cancellationToken)
                            //{
                            //    return await context.Documents.FirstOrDefaultAsync(t => t.Id == id, cancellationToken);
                            //}
                        }
                    
                

Program.cs coraz bardziej rośnie

                    

                        using Microsoft.EntityFrameworkCore;
                        using System.Net.Http.Headers;
                        
                        var builder = WebApplication.CreateBuilder(args);
                        
                        // Add services to the container
                        builder.Services.AddDbContextFactory<AppDbContext>(options =>
                            options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
                        
                        builder.Services.AddHttpClient("documentViews", client =>
                        {
                            client.BaseAddress = new Uri("https://localhost:2222");
                            client.DefaultRequestHeaders.Accept.Clear();
                            client.DefaultRequestHeaders.Accept.
                                Add(new MediaTypeWithQualityHeaderValue("application/json"));
                        });
                        
                        
                        // Register GraphQL services
                        builder.Services
                            .AddGraphQLServer()
                            .AddProjections()
                            .AddSorting()
                            .AddFiltering()
                            .AddTypeExtension<DocumentViewsExtendObjecType>()
                            .AddType<DocumentType>()
                            .AddTypes(typeof(Query))
                            .RegisterDbContextFactory<AppDbContext>();
                        
                        var app = builder.Build();
                        
                        app.MapGraphQL();
                        
                        app.Run();
                    
                
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW

Ten endpoint pobierze batchowo wszystkie dane 1 zapytaniem

                    
                        endpoints.MapGet("/span/{timeFrameS:regex(^(week|month|threemonths)$)}",
                        async (string timeFrameS, IDocumentViewService service) =>
                        {
                            if (!Enum.TryParse<TimeFrame>(timeFrameS, true, out var timeFrame))
                            {
                                return Results.BadRequest
                                ("Invalid time frame. Please use 'threemonths', 'week', or 'month'.");
                            }
                            var result = await service.GetDocumentsViewsOverTimeAsync(timeFrame);
                            return Results.Ok(result);
                        });
                    
                
blog
CW

ViewsByKeyDataLoader czyli DataLoader jak klasa

                    

































                        public class ViewsByKeyDataLoader : BatchDataLoader<KeyAndSpan, DocumentViews>
                        {
                            private readonly IHttpClientFactory _clientFactory;
                        
                            public ViewsByKeyDataLoader(
                                IBatchScheduler batchScheduler,
                                IHttpClientFactory clientFactory,
                                DataLoaderOptions? options = null)
                                : base(batchScheduler, options)
                            {
                                _clientFactory = clientFactory;
                            }
                        
                            private int NumberOfRequests { get; set; }
                        
                            protected override async Task<IReadOnlyDictionary<KeyAndSpan, DocumentViews>> LoadBatchAsync(
                                IReadOnlyList<KeyAndSpan> keysAndSpans,
                                CancellationToken cancellationToken)
                            {
                                NumberOfRequests++;
                        
                                using HttpClient client = _clientFactory.CreateClient("documentViews");
                        
                                var map = new Dictionary<KeyAndSpan, DocumentViews>();
                        
                                foreach (var group in keysAndSpans.GroupBy(k => k.Span))
                                {
                                    // Prepare the request for the current span.
                                    using var message = new HttpRequestMessage(
                                        HttpMethod.Get, $"/views/span/{group.Key.ToString().ToLower()}");
                        
                                    using var response = await client.SendAsync(message, cancellationToken);
                                    response.EnsureSuccessStatusCode();
                        
                                    var content = await response.Content.ReadAsByteArrayAsync(cancellationToken);
                                    using var json = JsonDocument.Parse(content);
                        
                                    var options = new JsonSerializerOptions
                                    {
                                        PropertyNameCaseInsensitive = true
                                    };
                        
                                    var list = json.Deserialize<List<DocumentViewRecord>>(options);
                        
                                    foreach (var recordGroup in list.GroupBy(r => r.DocumentId))
                                    {
                                        var sublist = recordGroup.ToList();
                        
                                        var documentViews = new DocumentViews(
                                            sublist.GetDateFirst(),
                                            sublist.GetDateLast(),
                                            sublist.Sum(a => a.Views));
                        
                                        var key = new KeyAndSpan(recordGroup.Key, group.Key);
                                        map.Add(key, documentViews);
                                    }
                                }
                        
                                return map;
                            }
                        }
                    
                
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW

Prosty przykład DataLoader From Hotchocolate.Types.Analyzers

                    
                        //This will create TestDataLoader
                        [DataLoader(name: "Test")]
                        public static async Task<Dictionary<int, string>> GetTestIdAsync(
                        IReadOnlyList<int> ids,
                        CancellationToken ct)
                        => new Dictionary<int, string>() { { 1, "1" } };
                    
                

ViewsByKeyFrom_Hotchocolate_Types_Analyzers_DataLoader

                    





































                        


                        [ExtendObjectType(typeof(Document))]
                        public class DocumentViewsExtendObjecType
                        {
                        
                            public static async Task<DocumentViews> GetViewChange(ChangeSpan span,
                             [Parent] Document parent,
                             ViewsByKeyFrom_Hotchocolate_Types_Analyzers_DataLoader loader,
                             CancellationToken cancellationToken)
                            {
                                return await loader.LoadAsync(new KeyAndSpan(parent.Id, span), cancellationToken);
                            }
                        
                        
                            [DataLoader(name: "ViewsByKeyFrom_Hotchocolate_Types_Analyzers_")]
                            public static async Task<IReadOnlyDictionary<KeyAndSpan, DocumentViews>> GetViewsByKey(
                                IReadOnlyList<KeyAndSpan> keysAndSpans,
                                [Service] IHttpClientFactory clientFactory,
                                CancellationToken cancellationToken)
                            {
                        
                                using HttpClient client = clientFactory.CreateClient("documentViews");
                                var map = new Dictionary<KeyAndSpan, DocumentViews>();
                        
                                foreach (var span in keysAndSpans.GroupBy(t => t.Span))
                                {
                                    using var message = new HttpRequestMessage
                                         (HttpMethod.Get, $"/views/span/{span.Key.ToString().ToLower()}");
                        
                                    using var response = await client.SendAsync(message, cancellationToken);
                                    response.EnsureSuccessStatusCode();
                        
                                    var content = await response.Content.ReadAsByteArrayAsync(cancellationToken);
                                    var json = JsonDocument.Parse(content);
                        
                                    var options = new JsonSerializerOptions
                                    {
                                        PropertyNameCaseInsensitive = true
                                    };
                        
                                    var list = json.Deserialize<List<DocumentViewRecord>>(options);
                        
                                    foreach (var recordByDocumentId in list.GroupBy(a => a.DocumentId))
                                    {
                                        var sublist = recordByDocumentId.Select(a => a).ToList();
                        
                                        var documentViews = new DocumentViews(sublist.GetDateFirst(),
                                            sublist.GetDateLast(), sublist.Sum(a => a.Views));
                                        var keyAndSpan = new KeyAndSpan(recordByDocumentId.Key, span.Key);
                        
                                        map.Add(keyAndSpan, documentViews);
                                    }
                                }
                        
                                return map;
                            }
                        
                            //This will create TestDataLoader
                            [DataLoader(name: "Test")]
                            public static async Task<Dictionary<int, string>> GetTestIdAsync(
                            IReadOnlyList<int> ids,
                            CancellationToken ct)
                            => new Dictionary<int, string>() { { 1, "1" } };
                        }
                    
                

Ustawianie opcji generowanego kodu w Properties/ModuleInfo.cs

                    
                        [assembly: DataLoaderDefaults(
                            AccessModifier = DataLoaderAccessModifier.Public,
                            ServiceScope = DataLoaderServiceScope.Default)]
                    
                
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW

Confluent.Kafka Consumer

                    
                        public class GlobalStateUserNameAttribute : GlobalStateAttribute
                        {
                            public GlobalStateUserNameAttribute() : base("UserName")
                            {
                            }
                        }
                        
                        public class GlobalStatePasswordAttribute : GlobalStateAttribute
                        {
                            public GlobalStatePasswordAttribute() : base("Password")
                            {
                            }
                        }
                    
                

HttpRequestInterceptor do ustawianie GlobalState

                    







                        public class HttpRequestInterceptor : DefaultHttpRequestInterceptor
                        {
                            public override ValueTask OnCreateAsync(HttpContext context,
                                IRequestExecutor requestExecutor,
                                OperationRequestBuilder requestBuilder,
                                CancellationToken cancellationToken)
                            {
                                // Retrieve the Authorization header
                                if (context.Request.Headers.TryGetValue("Authorization", out var authHeader))
                                {
                                    // Check if the header starts with "Basic "
                                    if (authHeader.ToString().StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
                                    {
                                        // Extract the Base64-encoded credentials
                                        var encodedCredentials = authHeader.ToString().Substring("Basic ".Length).Trim();
                        
                                        // Decode the Base64 string
                                        var credentialBytes = Convert.FromBase64String(encodedCredentials);
                                        var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':', 2);
                        
                                        if (credentials.Length == 2)
                                        {
                                            var username = credentials[0];
                                            var password = credentials[1];
                        
                                            // Store the username as global state so that resolvers can access it
                                            requestBuilder.AddGlobalState("UserName", username);
                                            requestBuilder.AddGlobalState("Password", password);
                                        }
                                    }
                                }
                        
                                return base.OnCreateAsync(context, requestExecutor, requestBuilder, cancellationToken);
                            }
                        }
                    
                
blog
CW

Confluent.Kafka Consumer

                    




































                        public class Mutation
                        {
                            [Error<NotAuthenticatedException>]
                            [Error<DocumentDoesntExist>]
                            [Error<UserDoesNotExistException>]
                            [Error<WrongPasswordException>]
                            [UseMutationConvention]
                            public async Task<Watchlist> AddDocumentToWatchlistAsync(
                                [Service] WatchlistRepository repository,
                                int documentId,
                                [GlobalStateUserName] string? userName,
                                [GlobalStatePassword] string? password,
                                CancellationToken cancellationToken)
                            {
                                int userId = CheckUser(userName, password);
                                return await repository.AddDocumentToWatchlistAsync
                                    (documentId, userId, cancellationToken);
                            }
                        
                            [Error<NotAuthenticatedException>]
                            [Error<DocumentDoesntExist>]
                            [Error<UserDoesNotExistException>]
                            [Error<WatchlistItemDoesNotExistException>]
                            [Error<WrongPasswordException>]
                            [UseMutationConvention]
                            public async Task<bool> RemoveDocumentFromWatchlistAsync(
                                [Service] WatchlistRepository repository,
                                int documentId,
                                [GlobalStateUserName] string? userName,
                                [GlobalStatePassword] string? password,
                                CancellationToken cancellationToken)
                            {
                                int userId = CheckUser(userName, password);
                                return await repository.RemoveDocumentFromWatchlistAsync
                                    (documentId, userId, cancellationToken);
                            }
                        
                            private int CheckUser(string? userName, string? password)
                            {
                                if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password))
                                {
                                    throw new NotAuthenticatedException();
                                }
                        
                                if (password != "password")
                                {
                                    throw new WrongPasswordException();
                                }
                        
                                var userId = Users.GetId(userName.ToLower());
                        
                                if (userId is null)
                                {
                                    throw new UserDoesNotExistException();
                                }
                        
                                return userId.Value;
                            }
                        
                        }
                    
                
blog
CW

WatchlistRepository

                    









































































                        public class WatchlistRepository
                        {
                            private readonly AppDbContext _dbContext;
                        
                            public WatchlistRepository(AppDbContext dbContext)
                            {
                                _dbContext = dbContext;
                            }
                        
                            public async Task<Watchlist> AddDocumentToWatchlistAsync(
                                int documentId, int userId, CancellationToken cancellationToken)
                            {
                                if (!await _dbContext.Documents.AnyAsync(d => d.Id == documentId, cancellationToken))
                                {
                                    throw new DocumentDoesntExist();
                                }
                        
                                // Check if the document is already in the watchlist for this user
                                var existingWatchlist = await _dbContext.Watchlists
                                    .FirstOrDefaultAsync
                                        (w => w.DocumentId == documentId && w.UserId == userId, cancellationToken);
                        
                                if (existingWatchlist != null)
                                {
                                    return existingWatchlist; // Already in watchlist, return existing
                                }
                        
                                var watchlist = new Watchlist
                                {
                                    DocumentId = documentId,
                                    UserId = userId,
                                };
                        
                                _dbContext.Watchlists.Add(watchlist);
                                await _dbContext.SaveChangesAsync(cancellationToken);
                                return watchlist;
                            }
                        
                            public async Task<bool> RemoveDocumentFromWatchlistAsync(
                                int documentId, int userId, CancellationToken cancellationToken)
                            {
                                if (!await _dbContext.Documents.
                                    AnyAsync(d => d.Id == documentId, cancellationToken))
                                {
                                    throw new DocumentDoesntExist();
                                }
                        
                                var watchlistItem = await _dbContext.Watchlists
                                    .FirstOrDefaultAsync(w => w.DocumentId == documentId && w.UserId == userId, cancellationToken);
                        
                                if (watchlistItem == null)
                                {
                                    throw new WatchlistItemDoesNotExistException();
                                }
                        
                                _dbContext.Watchlists.Remove(watchlistItem);
                                await _dbContext.SaveChangesAsync(cancellationToken);
                        
                                return true;
                            }
                        
                        }
                        
                        // Updated exception classes with consistent naming
                        public class NotAuthenticatedException : Exception
                        {
                            public NotAuthenticatedException() : base("User is not authenticated") { }
                        }
                        
                        public class WrongPasswordException : Exception
                        {
                            public WrongPasswordException() : base("Wrong password") { }
                        }
                        
                        public class UserDoesNotExistException : Exception
                        {
                            public UserDoesNotExistException() : base("User does not exist") { }
                        }
                        
                        public class DocumentDoesntExist : Exception
                        {
                            public DocumentDoesntExist() : base("Document does not exist") { }
                        }
                        
                        public class WatchlistItemDoesNotExistException : Exception
                        {
                            public WatchlistItemDoesNotExistException() : base("Watchlist item does not exist") { }
                        }
                        
                        public static class Users
                        {
                            public static int? GetId(string name)
                            {
                                return name switch
                                {
                                    "cezary" => 1,
                                    "john" => 2,
                                    _ => null
                                };
                            }
                        }
                    
                

NotAuthenticatedException WrongPasswordException UserDoesNotExistException WatchlistItemDoesNotExistException

                    










                        // Updated exception classes with consistent naming
                        public class NotAuthenticatedException : Exception
                        {
                            public NotAuthenticatedException() : base("User is not authenticated") { }
                        }
                        
                        public class WrongPasswordException : Exception
                        {
                            public WrongPasswordException() : base("Wrong password") { }
                        }
                        
                        public class UserDoesNotExistException : Exception
                        {
                            public UserDoesNotExistException() : base("User does not exist") { }
                        }
                        
                        public class DocumentDoesntExist : Exception
                        {
                            public DocumentDoesntExist() : base("Document does not exist") { }
                        }
                        
                        public class WatchlistItemDoesNotExistException : Exception
                        {
                            public WatchlistItemDoesNotExistException() : base("Watchlist item does not exist") { }
                        }
                        
                        public static class Users
                        {
                            public static int? GetId(string name)
                            {
                                return name switch
                                {
                                    "cezary" => 1,
                                    "john" => 2,
                                    _ => null
                                };
                            }
                        }
                    
                
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW

QuestionMutations startujemy z wysłaniem wiadomości

                    



                        [ExtendObjectType(typeof(Mutation))]
                        public class QuestionMutations
                        {
                            [UseMutationConvention]
                            public async Task<bool> SendQuestionAboutDocumentAsync(
                                QuestionToLLMAboutDocument question,
                                  [Service] ITopicEventSender eventSender,
                                CancellationToken cancellationToken)
                            {
                                await eventSender.SendAsync("questions", question);
                                return true;
                            }
                        }

                        public class QuestionToLLMAboutDocument
                        {
                            public QuestionToLLMAboutDocument(int documentId, string question)
                            {
                                DocumentId = documentId;
                                Question = question;
                            }

                            public int DocumentId { get; set; }
                            public string Question { get; set; }
                        }

                    
                

DocumentSubscriptions czyli odbiór wiadomości

                    













              
                        public class DocumentSubscriptions
                        {
                            [Topic("questions")]
                            [Subscribe]
                            public LLMAnswer OnReciveQuestion(
                                [EventMessage] QuestionToLLMAboutDocument question)
                            {
                                return new LLMAnswer("ANSWER", question.Question, question.DocumentId);
                            }
                        
                        }

                        public class LLMAnswer
                        {
                            public LLMAnswer(string answer, string question, int documentId)
                            {
                                Answer = answer;
                                Question = question;
                                DocumentId = documentId;
                            }

                            public string Answer { get; set; }
                            public string Question { get; set; }
                            public int DocumentId { get; set; }
                        }

                        public class LLMAnswerType : ObjectType<LLMAnswer>
                        {
                            protected override void Configure(IObjectTypeDescriptor<LLMAnswer> descriptor)
                            {
                                descriptor.Field(t => t.Answer).Description("The answer to the question");
                                descriptor.Field(t => t.Question).Description("The question");
                                descriptor.Field(t => t.DocumentId).Description("The document ID");
                            }
                        }
                    
                

Program.cs puchnie

                    
                        builder.Services
                            .AddGraphQLServer()
                            .AddProjections()
                            .AddSorting()
                            .AddFiltering()
                            .AddHttpRequestInterceptor<HttpRequestInterceptor>()
                            .AddTypeExtension<DocumentViewsExtendObjecType>()
                            .AddType<DocumentType>()
                            .AddMutationType<Mutation>()
                            .AddTypeExtension<QuestionMutations>()
                            .AddSubscriptionType<DocumentSubscriptions>()
                            .AddQueryType<Query>()
                            .AddMutationConventions()
                            .AddInMemorySubscriptions()
                            .RegisterDbContextFactory<AppDbContext>();
                    
                

BackgroundServiceDocumentTaskPublisher

                    





















































                        public class BackgroundServiceDocumentTaskPublisher : BackgroundService
                        {
                            private readonly IServiceScopeFactory _scopeFactory;
                            private readonly ITopicEventSender _eventSender;
                            private readonly Random _random = new Random();
                            private readonly string[] _taskTypes = new[]
                            {
                                "Review",
                                "Analyze",
                                "Approve",
                                "Reject",
                                "Archive",
                                "Share",
                                "Update",
                                "Translate"
                            };
                        
                            public BackgroundServiceDocumentTaskPublisher
                                (IServiceScopeFactory scopeFactory, ITopicEventSender eventSender)
                            {
                                _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
                                _eventSender = eventSender ?? throw new ArgumentNullException(nameof(eventSender));
                            }
                        
                            protected override async Task ExecuteAsync(CancellationToken stoppingToken)
                            {
                                while (!stoppingToken.IsCancellationRequested)
                                {
                                    try
                                    {
                                        // Create a new scope for each operation
                                        using (var scope = _scopeFactory.CreateScope())
                                        {
                                            // Get the dbContext from the scope
                                            var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
                        
                                            // Get document IDs from the database
                                            var documentIds = await dbContext.Documents
                                                .Select(d => d.Id)
                                                .ToListAsync(stoppingToken);
                        
                                            if (documentIds.Any())
                                            {
                                                // Choose a random document ID
                                                int randomDocumentId = documentIds[_random.Next(documentIds.Count)];
                        
                                                // Create a random task
                                                string randomTask = _taskTypes[_random.Next(_taskTypes.Length)];
                        
                                                // Create a new document task
                                                var documentTask = new DocumentTask(randomDocumentId, randomTask);
                        
                                                // Send the task to the topic
                                                await _eventSender.SendAsync("DocumentTasked", documentTask, stoppingToken);
                        
                                                Console.WriteLine
                                                    ($"Published task: {randomTask} for document ID: {randomDocumentId}");
                                            }
                                            else
                                            {
                                                Console.WriteLine
                                                    ("No documents found in the database.");
                                            }
                                        }
                        
                                        // Wait for some time before publishing the next task
                                        // Random interval between 1 and 4 seconds
                                        int delaySeconds = _random.Next(1, 4);
                                        await Task.Delay(TimeSpan.FromSeconds(delaySeconds), stoppingToken);
                                    }
                                    catch (Exception ex)
                                    {
                                        Console.WriteLine($"Error in DocumentTaskPublisher: {ex.Message}");
                        
                                        // Wait before retrying
                                        await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
                                    }
                                }
                            }
                        }
                    
                

Odczyt z kolejki IAsyncEnumerable

                    











































                        public class DocumentSubscriptions
                        {
                            [Subscribe(With = nameof(OnDocumentTaskAsync))]
                            public RecivedDocumentTask OnDocumentCreateTask(
                                [EventMessage] DocumentTask documentTask,
                                CancellationToken ct)
                            {
                                return new RecivedDocumentTask(documentTask.DocumentId,
                                    documentTask.Task,
                                    TimeOnly.FromDateTime(DateTime.Now));
                            }
                        
                            public static async IAsyncEnumerable<DocumentTask> OnDocumentTaskAsync(
                                [Service] AppDbContext dbContext,
                                [Service] ITopicEventReceiver eventReceiver,
                                [EnumeratorCancellation] CancellationToken ct)
                            {
                                // Subscribe to the correct topic that matches where DocumentTask objects are published
                                ISourceStream<DocumentTask> sourceStream = await eventReceiver.SubscribeAsync<DocumentTask>(
                                    "DocumentTasked",
                                    ct);
                        
                                // Read events from the stream and yield them to subscribers
                                await foreach (DocumentTask item in sourceStream.ReadEventsAsync().WithCancellation(ct))
                                {
                                    if (item != null && !string.IsNullOrEmpty(item.Task) && item.DocumentId > 0)
                                    {
                                        yield return item;
                                    }
                                }
                            }
                        }
                        
                        public class RecivedDocumentTaskType : ObjectType<RecivedDocumentTask>
                        {
                            protected override void Configure(IObjectTypeDescriptor<RecivedDocumentTask> descriptor)
                            {
                                descriptor.Field(t => t.DocumentId).Description("The document ID");
                                descriptor.Field(t => t.Task).Description("The task");
                                descriptor.Field(t => t.Time).Description("The time the task was created");
                        
                            }
                        }
                        
                        public class RecivedDocumentTask
                        {
                            public RecivedDocumentTask(int documentId, string task, TimeOnly time)
                            {
                                DocumentId = documentId;
                                Task = task;
                                Time = time;
                            }
                            public int DocumentId { get; set; }
                            public string Task { get; set; }
                            public TimeOnly Time { get; set; }
                        }
                        
                        public class DocumentTask
                        {
                            public DocumentTask(int documentId, string task)
                            {
                                DocumentId = documentId;
                                Task = task;
                            }
                        
                            public int DocumentId { get; set; }
                            public string Task { get; set; }
                        }
                        
                        
                    
                
blog
CW
blog
CW
blog
CW
blog
CW

Modele do tego przykładu

                    










                        public class Product
                        {
                            public int Id { get; set; }
                            public string Name { get; set; }
                            public decimal Price { get; set; }
                            public string Description { get; set; }
                            // Details and Reviews are no longer populated in the query resolver
                            // but resolved on-demand via field resolvers.
                            public ProductDetails Details { get; set; }
                            public List<Review> Reviews { get; set; }
                        }
                        
                        public class ProductDetails
                        {
                            public string Manufacturer { get; set; }
                            public DateTime ManufactureDate { get; set; }
                            public string CountryOfOrigin { get; set; }
                            public string MaterialInfo { get; set; }
                            public string Dimensions { get; set; }
                            public string TechnicalSpecifications { get; set; }
                        }
                        
                        public class Review
                        {
                            public int Id { get; set; }
                            public string AuthorName { get; set; }
                            public int Rating { get; set; }
                            public string Comment { get; set; }
                            public DateTime CreatedAt { get; set; }
                        }
                        
                    
                

GraphQL Schema Types

                    


































                        // GraphQL Schema Types
                        public class ProductType : ObjectType<Product>
                        {
                            protected override void Configure
                                (IObjectTypeDescriptor<Product> descriptor)
                            {
                                descriptor.Field(p => p.Id)
                                    .Type<NonNullType<IntType>>();
                                descriptor.Field(p => p.Name)
                                    .Type<NonNullType<StringType>>();
                                descriptor.Field(p => p.Price)
                                    .Type<NonNullType<DecimalType>>();
                                descriptor.Field(p => p.Description)
                                    .Type<StringType>();
                        
                                // Use separate resolvers for Details and Reviews so that
                                // they can be deferred or streamed.
                                descriptor.Field("details")
                                    .ResolveWith<ProductResolvers>(r => r.GetDetails(default))
                                    .Type<ProductDetailsType>();
                                descriptor.Field("reviews")
                                    .ResolveWith<ProductResolvers>(r => r.GetReviews(default))
                                    .Type<ListType<ReviewType>>();
                            }
                        }
                        
                        
                        public class ProductDetailsType : ObjectType<ProductDetails>
                        {
                            protected override void Configure
                                (IObjectTypeDescriptor<ProductDetails> descriptor)
                            {
                                descriptor.Field(d => d.Manufacturer).Type<StringType>();
                                descriptor.Field(d => d.ManufactureDate).Type<DateTimeType>();
                                descriptor.Field(d => d.CountryOfOrigin).Type<StringType>();
                                descriptor.Field(d => d.MaterialInfo).Type<StringType>();
                                descriptor.Field(d => d.Dimensions).Type<StringType>();
                                descriptor.Field(d => d.TechnicalSpecifications).Type<StringType>();
                            }
                        }
                        
                        
                        public class ReviewType : ObjectType<Review>
                        {
                            protected override void Configure(IObjectTypeDescriptor<Review> descriptor)
                            {
                                descriptor.Field(r => r.Id).Type<NonNullType<IntType>>();
                                descriptor.Field(r => r.AuthorName).Type<NonNullType<StringType>>();
                                descriptor.Field(r => r.Rating).Type<NonNullType<IntType>>();
                                descriptor.Field(r => r.Comment).Type<StringType>();
                                descriptor.Field(r => r.CreatedAt).Type<NonNullType<DateTimeType>>();
                            }
                        }
                        
                    
                

ProductResolvers

                    
































                        // Field resolvers for deferred and streamed fields.
                        public class ProductResolvers
                        {
                            // Simulate slow database access for details (e.g. 14 seconds delay)
                            public async Task<ProductDetails> GetDetails([Parent] Product product)
                            {
                                await Task.Delay(14000);
                                return new ProductDetails
                                {
                                    Manufacturer = "TechCorp",
                                    ManufactureDate = new DateTime(2024, 3, 15),
                                    CountryOfOrigin = "Japan",
                                    MaterialInfo = "Aluminum and glass",
                                    Dimensions = "6.1 x 2.8 x 0.3 inches",
                                    TechnicalSpecifications = "CPU: 3.2 GHz, RAM: 8GB, Storage: 256GB"
                                };
                            }
                        
                            // Simulate streaming reviews with a 2s delay for each review.
                            public async IAsyncEnumerable<Review> GetReviews([Parent] Product product)
                            {
                                var reviews = new List<Review>
                                    {
                                        new Review 
                                        { Id = 1, AuthorName = "Alice", Rating = 5, 
                                            Comment = "Excellent product!", 
                                            CreatedAt = DateTime.Now.AddDays(-5) },
                                        new Review 
                                        { Id = 2, AuthorName = "Bob", Rating = 4, 
                                            Comment = "Good value for money", 
                                            CreatedAt = DateTime.Now.AddDays(-10) },
                                        new Review 
                                        { Id = 3, AuthorName = "Charlie", Rating = 3, 
                                            Comment = "Average performance", 
                                            CreatedAt = DateTime.Now.AddDays(-15) },
                                        new Review 
                                        { Id = 4, AuthorName = "David", Rating = 5, 
                                            Comment = "Exceeded expectations", 
                                            CreatedAt = DateTime.Now.AddDays(-20) },
                                        new Review 
                                        { Id = 5, AuthorName = "Eve", Rating = 2, 
                                            Comment = "Not worth the price", 
                                            CreatedAt = DateTime.Now.AddDays(-25) }
                                    };
                        
                                foreach (var review in reviews)
                                {
                                    await Task.Delay(2000);
                                    yield return review;
                                }
                            }
                        }
                    
                

Query dla przykładu z @defer i @stream

                    
                        public class Query
                        {
                            // The product field takes an id argument.
                            public Product GetProduct(int id)
                            {
                                return new Product
                                {
                                    Id = id,
                                    Name = "SmartPhone X",
                                    Price = 999.99m,
                                    Description = "Latest smartphone with cutting-edge features"
                                    // Details and Reviews are intentionally not populated here.
                                };
                            }
                        }
                        
                    
                
blog
CW

Aplikacja ASP.NET Core

                    

                        var builder = WebApplication.CreateBuilder(args);

                        // Register GraphQL services
                        builder.Services.AddGraphQLServer()
                            .ModifyOptions(o =>
                            {
                                o.EnableDefer = true;
                                o.EnableStream = true;
                            }).AddQueryType<Query>()
                                    .AddType<ProductType>()
                                    .AddType<ProductDetailsType>()
                                    .AddType<ReviewType>();
                        
                        var app = builder.Build();
                        
                        app.MapGraphQL();
                        app.MapGet("/", () =>
                        { return Results.Redirect($"/graphql", permanent: true); });
                        app.Run();
                        
                    
                
blog
CW

Jak odczytyać w C# zapytanie GraphQL?

                    











































































































































































































































































































































































                        using _9GraphQLDemoOfDeferAndStream.QueriesAndTypes;
                        using _9GraphQLDemoOfDeferAndStream.QueriesAndTypes.ObjectType;
                        using HotChocolate;
                        using HotChocolate.Execution;
                        using Microsoft.Extensions.DependencyInjection;
                        using System.Text.Json;
                        
                        var services = new ServiceCollection();
                        
                        services.AddSingleton<ProductResolvers>();
                        
                        services.AddGraphQLServer()
                            .ModifyOptions(o =>
                            {
                                o.EnableDefer = true;
                                o.EnableStream = true;
                            }).AddQueryType<Query>()
                                    .AddType<ProductType>()
                                    .AddType<ProductDetailsType>()
                                    .AddType<ReviewType>();
                        
                        
                        var serviceProvider = services.BuildServiceProvider();
                        
                        var executorResolver = serviceProvider.GetRequiredService<IRequestExecutorResolver>();
                        var executor = await executorResolver.GetRequestExecutorAsync();
                        
                        Console.ReadLine();
                        
                        Console.WriteLine("Running GraphQL query with @defer and @stream...");
                        
                        //Client query: the @defer directive tells the server to return the 'details'
                        // field later, and the @stream directive streams each review as it becomes available.
                        var query = @"
                        query GetProduct {
                          product(id: 1) {
                            id
                            name
                            price
                            description
                            ...DeferredDetails @defer
                            reviews @stream {
                              id
                              authorName
                              rating
                              comment
                              createdAt
                            }
                          }
                        }
                        
                        fragment DeferredDetails on Product {
                          details {
                            manufacturer
                            manufactureDate
                            countryOfOrigin
                            materialInfo
                            dimensions
                            technicalSpecifications
                          }
                        }
                        ";
                        
                        var finalResult = new Dictionary<string, object>();
                        
                        var result = await executor.ExecuteAsync(query);
                        
                        if (result is IResponseStream responseStream)
                        {
                            await foreach (var incrementalResult in responseStream.ReadResultsAsync())
                            {
                                Console.WriteLine($"Received update at {DateTime.Now:HH:mm:ss}");
                        
                                var json = incrementalResult.ToJson();
                                Console.WriteLine(json);
                        
                                UpdateFinalJson(finalResult, json);
                            }
                        
                            WriteFinalJson(finalResult);
                        }
                        else
                        {
                            // In case the result isn't incremental, just output the full JSON.
                            Console.WriteLine();
                        }
                        
                        Console.WriteLine("\nQuery execution completed.");
                        
                        
                        void UpdateFinalJson(Dictionary<string, object> finalResult, string json)
                        {
                            using (var doc = JsonDocument.Parse(json))
                            {
                                var root = doc.RootElement;
                        
                                // Merge the top-level "data" object into finalResult.
                                if (root.TryGetProperty("data", out var dataProp))
                                {
                                    var dataObj = JsonElementToObject(dataProp) as Dictionary<string, object>;
                                    if (!finalResult.ContainsKey("data"))
                                    {
                                        finalResult["data"] = dataObj;
                                    }
                                    else
                                    {
                                        MergeDictionaries(finalResult["data"] as Dictionary<string, object>, dataObj);
                                    }
                                }
                        
                                // Process any incremental patch entries.
                                if (root.TryGetProperty("incremental", out var incrementalProp) && incrementalProp.ValueKind == JsonValueKind.Array)
                                {
                                    foreach (var inc in incrementalProp.EnumerateArray())
                                    {
                                        // Each patch has a "path" (an array of keys/indexes)
                                        // and either "data" (an object patch) or "items" (for arrays).
                                        ApplyIncrementalPatch(finalResult["data"] as Dictionary<string, object>, inc);
                                    }
                                }
                            }
                        }
                        
                        static void WriteFinalJson(Dictionary<string, object> finalResult)
                        {
                            var finalJson = JsonSerializer.Serialize(finalResult, new JsonSerializerOptions { WriteIndented = true });
                            Console.WriteLine("\n========Final Merged JSON=========\n");
                            Console.WriteLine(finalJson);
                        }
                        
                        /// <summary>
                        /// Recursively merges dictionary source into target.
                        /// </summary>
                        void MergeDictionaries(Dictionary<string, object> target, Dictionary<string, object> source)
                        {
                            foreach (var kv in source)
                            {
                                if (kv.Value is Dictionary<string, object> srcDict)
                                {
                                    if (target.TryGetValue(kv.Key, out var existing) && existing is Dictionary<string, object> targetDict)
                                    {
                                        MergeDictionaries(targetDict, srcDict);
                                    }
                                    else
                                    {
                                        target[kv.Key] = srcDict;
                                    }
                                }
                                else if (kv.Value is List<object> srcList)
                                {
                                    // For arrays, simply override (or you can merge element-by-element if needed)
                                    target[kv.Key] = srcList;
                                }
                                else
                                {
                                    target[kv.Key] = kv.Value;
                                }
                            }
                        }
                        
                        /// <summary>
                        /// Applies an incremental patch (with a "path" and "data" or "items") to the target dictionary.
                        /// The patch path is relative to the "data" object.
                        /// </summary>
                        void ApplyIncrementalPatch(Dictionary<string, object> target, JsonElement incremental)
                        {
                            if (!incremental.TryGetProperty("path", out var pathElement))
                                return;
                        
                            var path = new List<object>();
                            foreach (var item in pathElement.EnumerateArray())
                            {
                                if (item.ValueKind == JsonValueKind.Number && item.TryGetInt32(out int index))
                                {
                                    path.Add(index);
                                }
                                else if (item.ValueKind == JsonValueKind.String)
                                {
                                    path.Add(item.GetString());
                                }
                            }
                        
                            if (incremental.TryGetProperty("data", out var dataElement) && dataElement.ValueKind != JsonValueKind.Null)
                            {
                                MergeAtPath(target, path, dataElement);
                            }
                            else if (incremental.TryGetProperty("items", out var itemsElement) && itemsElement.ValueKind == JsonValueKind.Array)
                            {
                                // Assume a single item in "items" array.
                                var enumerator = itemsElement.EnumerateArray();
                                if (enumerator.MoveNext())
                                {
                                    SetAtPath(target, path, enumerator.Current);
                                }
                            }
                        }
                        
                        /// <summary>
                        /// Recursively merges a JSON patch (patchData) into the object at the given path.
                        /// </summary>
                        void MergeAtPath(object currentObj, List<object> path, JsonElement patchData)
                        {
                            // Base case: no more segments, merge patchData into currentObj if it is a dictionary.
                            if (path.Count == 0)
                            {
                                if (currentObj is Dictionary<string, object> dict)
                                {
                                    foreach (var prop in patchData.EnumerateObject())
                                    {
                                        dict[prop.Name] = JsonElementToObject(prop.Value);
                                    }
                                }
                                return;
                            }
                        
                            var segment = path[0];
                            path.RemoveAt(0);
                        
                            if (segment is string key)
                            {
                                if (currentObj is Dictionary<string, object> dict)
                                {
                                    if (!dict.TryGetValue(key, out var next))
                                    {
                                        // Look ahead: if the next segment is an int, create a list; else create a dictionary.
                                        next = (path.Count > 0 && path[0] is int)
                                            ? (object)new List<object>()
                                            : new Dictionary<string, object>();
                                        dict[key] = next;
                                    }
                                    MergeAtPath(next, path, patchData);
                                }
                                else
                                {
                                    throw new Exception("Expected a dictionary for a string path segment.");
                                }
                            }
                            else if (segment is int index)
                            {
                                if (currentObj is List<object> list)
                                {
                                    // Ensure the list is large enough.
                                    while (list.Count <= index)
                                    {
                                        list.Add(null);
                                    }
                                    if (path.Count > 0)
                                    {
                                        if (list[index] == null)
                                        {
                                            // Look ahead: if the next segment is an int, create a list; else a dictionary.
                                            list[index] = (path[0] is int)
                                                ? (object)new List<object>()
                                                : new Dictionary<string, object>();
                                        }
                                        MergeAtPath(list[index], path, patchData);
                                    }
                                    else
                                    {
                                        // Final segment: assign the value.
                                        list[index] = JsonElementToObject(patchData);
                                    }
                                }
                                else
                                {
                                    throw new Exception("Expected a list at an integer path segment.");
                                }
                            }
                        }
                        
                        /// <summary>
                        /// Converts a JsonElement to a .NET object recursively (Dictionary, List, or primitive).
                        /// </summary>
                        object JsonElementToObject(JsonElement element)
                        {
                            switch (element.ValueKind)
                            {
                                case JsonValueKind.Object:
                                    var dict = new Dictionary<string, object>();
                                    foreach (var prop in element.EnumerateObject())
                                    {
                                        dict[prop.Name] = JsonElementToObject(prop.Value);
                                    }
                                    return dict;
                                case JsonValueKind.Array:
                                    var list = new List<object>();
                                    foreach (var item in element.EnumerateArray())
                                    {
                                        list.Add(JsonElementToObject(item));
                                    }
                                    return list;
                                case JsonValueKind.String:
                                    return element.GetString();
                                case JsonValueKind.Number:
                                    if (element.TryGetInt64(out long l))
                                        return l;
                                    return element.GetDouble();
                                case JsonValueKind.True:
                                case JsonValueKind.False:
                                    return element.GetBoolean();
                                case JsonValueKind.Null:
                                    return null;
                                default:
                                    return element.ToString();
                            }
                        }
                        
                        
                        /// <summary>
                        /// Sets a value at the given path in the target object.
                        /// </summary>
                        void SetAtPath(Dictionary<string, object> current, List<object> path, JsonElement value)
                        {
                            if (path.Count == 0)
                                return;
                        
                            var segment = path[0];
                            if (path.Count == 1)
                            {
                                if (segment is string key)
                                {
                                    current[key] = JsonElementToObject(value);
                                }
                                else if (segment is int)
                                {
                                    throw new Exception("Expected a dictionary but found an integer key.");
                                }
                                return;
                            }
                        
                            if (segment is string keySegment)
                            {
                                if (!current.TryGetValue(keySegment, out var next))
                                {
                                    // Decide whether to create a list or dictionary based on the next path segment.
                                    next = path[1] is int ? (object)new List<object>() : new Dictionary<string, object>();
                                    current[keySegment] = next;
                                }
                                if (next is Dictionary<string, object> nextDict)
                                {
                                    path.RemoveAt(0);
                                    SetAtPath(nextDict, path, value);
                                }
                                else if (next is List<object> nextList)
                                {
                                    path.RemoveAt(0);
                                    SetAtPathInList(nextList, path, value);
                                }
                            }
                        }
                        
                        /// <summary>
                        /// Helper method for setting a value in a list given a path.
                        /// </summary>
                        void SetAtPathInList(List<object> list, List<object> path, JsonElement value)
                        {
                            if (path.Count == 0)
                                return;
                        
                            if (!(path[0] is int index))
                                throw new Exception("Expected an integer index in list path.");
                        
                            if (path.Count == 1)
                            {
                                while (list.Count <= index)
                                    list.Add(null);
                                list[index] = JsonElementToObject(value);
                                return;
                            }
                        
                            while (list.Count <= index)
                                list.Add(null);
                        
                            if (list[index] == null)
                            {
                                list[index] = path[1] is int ? (object)new List<object>() : new Dictionary<string, object>();
                            }
                        
                            if (list[index] is Dictionary<string, object> dict)
                            {
                                path.RemoveAt(0);
                                SetAtPath(dict, path, value);
                            }
                            else if (list[index] is List<object> innerList)
                            {
                                path.RemoveAt(0);
                                SetAtPathInList(innerList, path, value);
                            }
                        }
                                                
                    
                
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW