Hot Chocolate 是 .NET 平台下的一个开源组件库, 您可以使用它创建 GraphQL 服务, 它消除了构建成熟的 GraphQL 服务的复杂性, Hot Chocolate 可以连接任何服务或数据源,并创建一个有凝聚力的服务,为您的消费者提供统一的 API。
我会在 .NET 应用中使用 Hot Chocolate 组件来构建 GraphQL 服务, 让我们开始吧!
创建服务
- 创建一个GraphQL服务
安装nuget包:
HotChocolate.AspNetCore // GraphQL - HotChocolate实现包
HotChocolate.Data.EntityFramework //HotChocolate-IQueryable 实现包
HotChocolate.Subscriptions.Redis //redis订阅
Microsoft.EntityFrameworkCore.SqlServer //orm ef sqlServer
Microsoft.EntityFrameworkCore.Tools //ef 工具
builder.Services.AddGraphQLServer()
使用Hot Chocolate
- 新增一个Query
builder.Services.AddQueryType<MyQuery>()//一个Query,所有Query写在一起
public class MyQuery
{
[UseOffsetPaging]
[UseProjection]
[UseFiltering]
[UseSorting]
public IQueryable<Superhero> GetSuperheroes([Service] ApplicationDbContext context) =>
context.Superheroes;
[UseOffsetPaging]
[UseProjection]
[UseFiltering]
[UseSorting]
public IQueryable<Movie> GetMovies([Service] ApplicationDbContext context) =>
context.Movies;
[UseOffsetPaging]
[UseProjection]
[UseFiltering]
[UseSorting]
public IQueryable<Superpower> GetSuperpowers([Service] ApplicationDbContext context) =>
context.Superpowers;
}
- 使用特性 搜索、排序、投影
builder.Services.AddProjections()
.AddFiltering()
.AddSorting()
.SetPagingOptions(new PagingOptions
{
MaxPageSize = 10000,
DefaultPageSize = 10,
IncludeTotalCount = true
});
- 创建多个Query,通过ExtendObjectType("Query")关联
.AddQueryType(q => q.Name("Query"))
.AddTypeExtension<SuperheroQuery>()
.AddTypeExtension<MovieQuery>()
.AddTypeExtension<SuperpowerQuery>()
[ExtendObjectType("Query")]
public class SuperheroQuery
{
[UseOffsetPaging]
[UseProjection]//始终显示的数据字段,无论是否查询该字段
[UseFiltering]
[UseSorting]
[GraphQLDescription("获取超级英雄集合")]
public IQueryable<Superhero> GetSuperheroes([Service] ApplicationDbContext context) =>
context.Superheroes;
}
- 创建Mutation&自定义错误信息
.AddMutationType(m => m.Name("Mutation"))
.AddTypeExtension<SuperheroMutation>()
[ExtendObjectType("Mutation")]
public class SuperheroMutation
{
public async Task<Boolean> AddSuperheroAsync(SuperheroDto superhero, [Service] ISuperheroRepository _repository, [Service] ITopicEventSender sender)
{
var (Success, KeyId) = await _repository.AddSuperheroAsync(superhero);
await sender.SendAsync("SuperheroModified", superhero);
return Success;
}
[Error(typeof(NameTakenException))]
public async Task<Boolean> UpdateSuperheroAsync([ID] Guid Id, string name, [Service] ISuperheroRepository _repository, [Service] ITopicEventSender sender)
{
var Success = await _repository.UpdateSuperheroAsync(Id, name);
await sender.SendAsync("SuperheroModified", new SuperheroDto
{
Id = Id,
Name = name,
Description = "",
Height = 0,
Movies = null,
Superpowers = null
});
return Success;
}
}
public class NameTakenException : Exception
{
public NameTakenException(string username)
: base($"The name {username} is already taken.")
{
}
}
- 创建指令
.AddDirectiveType<ToUpperDirectiveType>()
.AddType<toLowerDirective>()
/// <summary>
/// 转大写指令
/// </summary>
public class ToUpperDirectiveType : DirectiveType
{
protected override void Configure(
IDirectiveTypeDescriptor descriptor)
{
descriptor.Name("toupper");
//descriptor.Argument("name").Type<NonNullType<StringType>>();
descriptor.Location(DirectiveLocation.Field);
//https://chillicream.com/docs/hotchocolate/v13/execution-engine/field-middleware/#field-middleware-as-a-class
//中间件
descriptor.Use((next, directive) =>
{
return async context =>
{
await next(context);
if (context.Result is string str)
{
context.Result = str.ToUpper();
}
else
{
context.ReportError("Bad Request.");
context.OperationResult.SetResultState(WellKnownContextData.HttpStatusCode, 500);
}
};
});
}
}
/// <summary>
/// 转小写指令
/// 属性模式
/// </summary>
[DirectiveType(DirectiveLocation.Field)]
[toLowerDirectiveMiddleware]
public class toLowerDirective
{
}
/// <summary>
/// 指令中间件
/// </summary>
public class toLowerDirectiveMiddlewareAttribute : DirectiveTypeDescriptorAttribute
{
protected override async void OnConfigure(IDescriptorContext context, IDirectiveTypeDescriptor descriptor, Type type)
{
descriptor.Use((next, directive) =>
{
return async context =>
{
await next(context);
if (context.Result is string str)
{
context.Result = str.ToLower();
}
else
{
context.ReportError("Bad Request.");
context.OperationResult.SetResultState(WellKnownContextData.HttpStatusCode, 500);
}
};
});
}
}
- 创建订阅(通过webSocket方式)
.AddInMemorySubscriptions()
//.AddRedisSubscriptions((sp) => ConnectionMultiplexer.Connect("127.0.0.1:6379,password=Michael,defaultDatabase=2"))
.AddSubscriptionType(q => q.Name("Subscription"))
.AddTypeExtension<SuperheroSubscribe>()
//.AddSubscriptionType<SuperheroSubscribe>()//指定一个订阅类,所有订阅写在一起
[ExtendObjectType("Subscription")]
public class SuperheroSubscribe
{
[Subscribe]
[Topic("SuperheroUpdated")]
public async Task<SuperheroDto> SuperheroUpdated([EventMessage] SuperheroDto superherodto, [Service] ISuperheroRepository _repository)
{
var ret = await _repository.UpdateSuperheroAsync(superherodto.Id, $"{superherodto.Name}_{DateTime.Now.ToString("yyMMdd")}");
superherodto.Description = "Subscribe-SuperheroModified";
return superherodto;
}
#region 混合模式(订阅逻辑和解析器分离)
/// <summary>
/// 数据逻辑处理
/// </summary>
/// <param name="receiver"></param>
/// <returns></returns>
public async IAsyncEnumerable<SuperheroDto> SubscribeToSuperheroDto(
[Service] ITopicEventReceiver receiver, [Service] ISuperheroRepository _repository)
{
yield return new SuperheroDto { Id = Guid.NewGuid(), Name = $"Name-{DateTime.Now.ToString("HHmmss")}" };
//return ISourceStream<SuperheroDto>
var source = await receiver.SubscribeAsync<SuperheroDto>("SuperheroModified");
Task.Delay(3000);
await foreach (SuperheroDto superherodto in source.ReadEventsAsync())
{
superherodto.Name = $"{superherodto.Name}_{DateTime.Now.ToString("HHmmss")}";
var ret = await _repository.UpdateSuperheroAsync(superherodto.Id, superherodto.Name);
yield return superherodto;
}
}
/// <summary>
/// 订阅
/// 服务端必须开启websocket
/// 订阅人监听websocket
/// </summary>
/// <param name="superherodto"></param>
/// <param name="_repository"></param>
/// <returns></returns>
[Topic("SuperheroModified")]
[Subscribe(With = nameof(SubscribeToSuperheroDto))]
public async Task<SuperheroDto> SuperheroModified([EventMessage] SuperheroDto superherodto)
{
return superherodto;
}
#endregion
}
必须先链接WebSocket,Mutation事件才会推送 ,GraphQL语法
subscription{
superheroUpdated {
id
name
description
}
}
Promgram 整体配置
var builder = WebApplication.CreateBuilder(args);
Microsoft.Extensions.Configuration.ConfigurationManager configuration = builder.Configuration;
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddGraphQLServer()
.AddInMemorySubscriptions()
//.AddRedisSubscriptions((sp) => ConnectionMultiplexer.Connect("127.0.0.1:6379,password=Michael,defaultDatabase=2"))
.AddSubscriptionType(q => q.Name("Subscription"))
.AddTypeExtension<SuperheroSubscribe>()
//.AddSubscriptionType<SuperheroSubscribe>()//指定一个订阅类,所有订阅写在一起
//.AddTypeExtensionsFromFile("./Stitching.graphql")
//.ModifyOptions(options =>
//{
// /*
// * code-first模式-Explicit:显示绑定(手动绑定字段,或者使用ObjectType<T>-override Configure手动设置)
// * 显示绑定 必须所有数据都要声明包括 Tsortinput,Tinput,TOperationFilterInput等。。。
// * Annotation-based模式-Implicit:隐式绑定(默认展示所有字段,或者使用ObjectType<T>-override Configure手动设置)
// * GraphQLIgnoreAttribute 可过滤不需要的字段
// */
// options.DefaultBindingBehavior = BindingBehavior.Explicit;
//})
.AddType<SuperheroType>()
.AddType<MovieType>()
.AddType<SuperpowerType>()
//.AddQueryType<MyQuery>()//一个Query,所有Query写在一起
.AddQueryType(q => q.Name("Query"))
.AddTypeExtension<SuperheroQuery>()
.AddTypeExtension<MovieQuery>()
.AddTypeExtension<SuperpowerQuery>()
.AddMutationType(m => m.Name("Mutation"))
.AddTypeExtension<SuperheroMutation>()
.AddProjections()
.AddFiltering()
.AddSorting()
.SetPagingOptions(new PagingOptions
{
MaxPageSize = 10000,
DefaultPageSize = 10,
IncludeTotalCount = true
})
.AddDirectiveType<MyDirectiveType>()
.AddDirectiveType<ToUpperDirectiveType>()
.AddType<toLowerDirective>()
;
string appRoot = builder.Environment.ContentRootPath;
Environment.SetEnvironmentVariable("AppDataDirectory", System.IO.Path.Combine(appRoot, "App_Data"));
var SqlServerConnStr = Environment.ExpandEnvironmentVariables(configuration.GetConnectionString("SqlServer"));
// Add Application Db Context options
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(SqlServerConnStr));
// Register custom services for the superheroes
builder.Services.AddScoped<ISuperheroRepository, SuperheroRepository>();
builder.Services.AddScoped<ISuperpowerRepository, SuperpowerRepository>();
builder.Services.AddScoped<IMovieRepository, MovieRepository>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
//使用GraphQL-Subscription 必须开启websocket
app.UseWebSockets();
//https://blog.christian-schou.dk/how-to-implement-graphql-in-asp-net-core/
app.MapGraphQL();
app.Run();
- https://localhost:7199/graphql/
通过Strawberry Shake,自动链接GraphQL服务,创建客户端
Introduction - Strawberry Shake - ChilliCream GraphQL Platform
NSwagStudio,通过Swagger.json 文档创建 TypeScript Client、CSharp Client、CSharp Controller
NSwagStudio · RicoSuter/NSwag Wiki · GitHub