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();
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}.";
}
}
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();
}
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);
}
}
public class Query
{
public IQueryable<Document> GetDocuments(AppDbContext context)
{
return context.Documents.Include(a => a.Tags).Include(a => a.Metadata);
}
}
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();
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();
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;
}
}
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>();
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;
}
}
}
[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);
}
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);
}
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();
// 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>>();
}
}
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;
}
}
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;
}
}
//[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);
//}
}
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();
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);
});
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;
}
}
//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" } };
[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" } };
}
[assembly: DataLoaderDefaults(
AccessModifier = DataLoaderAccessModifier.Public,
ServiceScope = DataLoaderServiceScope.Default)]
public class GlobalStateUserNameAttribute : GlobalStateAttribute
{
public GlobalStateUserNameAttribute() : base("UserName")
{
}
}
public class GlobalStatePasswordAttribute : GlobalStateAttribute
{
public GlobalStatePasswordAttribute() : base("Password")
{
}
}
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);
}
}
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;
}
}
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
};
}
}
// 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
};
}
}
[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; }
}
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");
}
}
builder.Services
.AddGraphQLServer()
.AddProjections()
.AddSorting()
.AddFiltering()
.AddHttpRequestInterceptor<HttpRequestInterceptor>()
.AddTypeExtension<DocumentViewsExtendObjecType>()
.AddType<DocumentType>()
.AddMutationType<Mutation>()
.AddTypeExtension<QuestionMutations>()
.AddSubscriptionType<DocumentSubscriptions>()
.AddQueryType<Query>()
.AddMutationConventions()
.AddInMemorySubscriptions()
.RegisterDbContextFactory<AppDbContext>();
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);
}
}
}
}
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; }
}
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
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>>();
}
}
// 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;
}
}
}
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.
};
}
}
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();
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);
}
}