HotChocolate Kurulum ve Query Tasarımı¶
HotChocolate, .NET için güçlü bir GraphQL sunucusudur; yanlış kurulum ve query tasarımı performans sorunlarına ve güvenlik açıklarına yol açar.
1. Tüm Entity’leri Doğrudan Expose Etmek¶
❌ Yanlış Kullanım: Veritabanı entity’lerini direkt GraphQL şemasına eklemek.
public class Query
{
[UseDbContext(typeof(AppDbContext))]
public IQueryable<User> GetUsers([ScopedService] AppDbContext context)
=> context.Users; // Tüm alanlar (password hash dahil) expose olur
}
✅ İdeal Kullanım: DTO/projection ile sadece gerekli alanları döndürün.
public class Query
{
public async Task<IEnumerable<UserDto>> GetUsers(
[Service] IUserService service, CancellationToken ct)
=> await service.GetAllAsync(ct);
}
[ObjectType("User")]
public class UserType : ObjectType<UserDto>
{
protected override void Configure(IObjectTypeDescriptor<UserDto> descriptor)
{
descriptor.Field(u => u.Id).Type<NonNullType<IdType>>();
descriptor.Field(u => u.FullName).Type<NonNullType<StringType>>();
descriptor.Field(u => u.Email).Type<NonNullType<StringType>>();
// PasswordHash gibi hassas alanlar yok
}
}
2. Query Depth Sınırı Koymamak¶
❌ Yanlış Kullanım: Sınırsız derinlikte query’lere izin vermek.
builder.Services
.AddGraphQLServer()
.AddQueryType<Query>();
// Saldırgan: { users { orders { products { reviews { user { orders { ... } } } } } } }
// Sonsuz derinlikte sorgu sunucuyu çökertir
✅ İdeal Kullanım: Query depth ve complexity sınırları ekleyin.
builder.Services
.AddGraphQLServer()
.AddQueryType<Query>()
.AddMutationType<Mutation>()
.AddMaxExecutionDepthRule(5)
.AddProjections()
.AddFiltering()
.AddSorting()
.SetRequestOptions(_ => new RequestExecutorOptions
{
ExecutionTimeout = TimeSpan.FromSeconds(10)
});
3. Mutation’ları Doğru Yapılandırmamak¶
❌ Yanlış Kullanım: Mutation’da input validation ve hata yönetimi yapmamak.
public class Mutation
{
public async Task<Product> CreateProduct(string name, decimal price,
[Service] AppDbContext db)
{
var product = new Product { Name = name, Price = price };
db.Products.Add(product);
await db.SaveChangesAsync();
return product;
}
}
✅ İdeal Kullanım: Input type, payload pattern ve validation kullanın.
public record CreateProductInput(string Name, decimal Price, int CategoryId);
public class CreateProductPayload
{
public ProductDto? Product { get; init; }
public IReadOnlyList<UserError>? Errors { get; init; }
}
public record UserError(string Message, string Code);
public class Mutation
{
public async Task<CreateProductPayload> CreateProduct(
CreateProductInput input,
[Service] IProductService service)
{
if (string.IsNullOrWhiteSpace(input.Name))
return new CreateProductPayload
{
Errors = new[] { new UserError("Ürün adı boş olamaz", "INVALID_NAME") }
};
var product = await service.CreateAsync(input);
return new CreateProductPayload { Product = product };
}
}
4. Authorization Eklememek¶
❌ Yanlış Kullanım: Tüm query ve mutation’lara anonim erişim.
public class Query
{
public IQueryable<Order> GetOrders([Service] AppDbContext db)
=> db.Orders; // Herkes tüm siparişleri görebilir
}
✅ İdeal Kullanım: HotChocolate authorization ile erişim kontrolü yapın.
builder.Services
.AddGraphQLServer()
.AddAuthorization()
.AddQueryType<Query>();
public class Query
{
[Authorize]
public async Task<IEnumerable<OrderDto>> GetMyOrders(
[Service] IOrderService service,
[GlobalState("currentUserId")] int userId,
CancellationToken ct)
=> await service.GetByUserIdAsync(userId, ct);
[Authorize(Roles = new[] { "Admin" })]
public async Task<IEnumerable<OrderDto>> GetAllOrders(
[Service] IOrderService service, CancellationToken ct)
=> await service.GetAllAsync(ct);
}
5. DataLoader Kullanmamak¶
❌ Yanlış Kullanım: İlişkili veride N+1 sorgu problemi.
public class OrderType : ObjectType<OrderDto>
{
protected override void Configure(IObjectTypeDescriptor<OrderDto> descriptor)
{
descriptor.Field("customer")
.ResolveWith<OrderResolvers>(r => r.GetCustomer(default!, default!));
}
}
public class OrderResolvers
{
public async Task<CustomerDto> GetCustomer(
[Parent] OrderDto order, [Service] AppDbContext db)
=> await db.Customers.FindAsync(order.CustomerId); // Her order için ayrı sorgu
}
✅ İdeal Kullanım: DataLoader ile batch yükleme yapın.
public class CustomerBatchDataLoader : BatchDataLoader<int, CustomerDto>
{
private readonly IServiceProvider _services;
public CustomerBatchDataLoader(
IServiceProvider services,
IBatchScheduler batchScheduler,
DataLoaderOptions? options = null)
: base(batchScheduler, options) => _services = services;
protected override async Task<IReadOnlyDictionary<int, CustomerDto>> LoadBatchAsync(
IReadOnlyList<int> keys, CancellationToken ct)
{
await using var scope = _services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var customers = await db.Customers
.Where(c => keys.Contains(c.Id))
.ToDictionaryAsync(c => c.Id, c => new CustomerDto(c.Id, c.Name), ct);
return customers;
}
}
// Kullanım
descriptor.Field("customer")
.ResolveWith<OrderResolvers>(r => r.GetCustomer(default!, default!));
public async Task<CustomerDto> GetCustomer(
[Parent] OrderDto order, CustomerBatchDataLoader loader)
=> await loader.LoadAsync(order.CustomerId);