如何通过结合 EntityFramework Core 和 GraphQL.NET 实现通用查询?





5.00/5 (15投票s)
本文将向您展示如何将数据库架构暴露给 API,然后从中进行查询。简单的查询操作无需样板代码。快来阅读吧。
引言
目前,我一直在为必须反复实现我的数据模型中任何实体的查询问题而苦苦挣扎。我认为如果我们可以将所有实体暴露给 API(或者有办法限制某些敏感实体),那就太棒了,然后其他人可以简单地查询它(就像一个通用查询,以避免在系统中反复实现查询功能时编写样板代码)。
我花了很长时间研究 OData 项目(实际上是解析器部分),但并不满意。也许是因为它太复杂,而且需要预先定义很多架构。而且,它只被 .NET 生态系统使用(Microsoft Dynamic CRM、Microsoft Sharepoint… 曾使用过)。
我想要一个可以很好地工作并适应其他生态系统的解决方案,例如前端、移动设备或物联网设备可以轻松地消费数据输出。这就是我选择 Facebook 的 GraphQL 的原因。关于这一点,我认为我不需要过多解释它为什么在这些时候如此酷。有关更多信息,我强烈建议您阅读这篇文章 2017:GraphQL 年度总结。
在本文中,我将向您展示我的 POC 项目,该项目试图结合 Entity Framework Core 和 GraphQL.NET。我们可以使用 gql DSL 语言 从前端进行查询,并查询数据库中的每个实体。
感谢 Joe McBride 和 Stef Heyenrath 提供如此出色的库(y)。
必备组件
- .NET Core 2.0 SDK
- Entity Framework Core 2.x
- GraphQL.NET 库
- System.Linq.Dynamic.Core 库
- Swagger UI
- Visual Studio 2017
数据库架构
图(Graph)Schema 模型
我们如下定义数据库列的模型
public class ColumnMetadata
{
public string ColumnName { get; set }
public string DataType { get; set; }
}
然后,我们对表做同样的事情,如下所示
public class TableMetadata
{
public string TableName { get; set; }
public string AssemblyFullName { get; set; }
public IEnumerable<ColumnMetadata> Columns { get; set; }
}
因为我们对数据库中的表进行了前缀(例如,dbo.crm_Tasks
),所以我们需要一个映射表,它将帮助我们解析友好名称(例如,在 UI 中查询时使用 `tasks`)。
public interface ITableNameLookup
{
bool InsertKeyName(string friendlyName);
string GetFriendlyName(string correctName);
}
public class TableNameLookup : ITableNameLookup
{
private IDictionary<string, string=""> _lookupTable = new Dictionary<string, string="">();
public bool InsertKeyName(string correctName)
{
if(!_lookupTable.ContainsKey(correctName))
{
var friendlyName = CanonicalName(correctName);
_lookupTable.Add(correctName, friendlyName);
return true;
}
return false;
}
public string GetFriendlyName(string correctName)
{
if (!_lookupTable.TryGetValue(correctName, out string value))
throw new Exception($"Could not get {correctName} out of the list.");
return value;
}
private string CanonicalName(string correctName)
{
var index = correctName.LastIndexOf("_");
var result = correctName.Substring(
index + 1,
correctName.Length - index - 1);
return Char.ToLowerInvariant(result[0]) + result.Substring(1);
}
}
现在,我们将所有 Schema 模型放入数据库元数据中,如下所示
public interface IDatabaseMetadata
{
void ReloadMetadata();
IEnumerable<tablemetadata> GetTableMetadatas();
}
public sealed class DatabaseMetadata : IDatabaseMetadata
{
private readonly DbContext _dbContext;
private readonly ITableNameLookup _tableNameLookup;
private string _databaseName;
private IEnumerable<TableMetaData> _tables;
public DatabaseMetadata(DbContext dbContext, ITableNameLookup tableNameLookup)
{
_dbContext = dbContext;
_tableNameLookup = tableNameLookup;
_databaseName = _dbContext.Database.GetDbConnection().Database;
if (_tables == null)
ReloadMetadata();
}
public IEnumerable<TableMetaData> GetTableMetadatas()
{
if (_tables == null)
return new List<TableMetaData>();
return _tables;
}
public void ReloadMetadata()
{
_tables = FetchTableMetaData();
}
private IReadOnlyList<TableMetaData> FetchTableMetaData()
{
var metaTables = new List<TableMetaData>();
foreach (var entityType in _dbContext.Model.GetEntityTypes())
{
var tableName = entityType.Relational().TableName;
metaTables.Add(new TableMetadata
{
TableName = tableName,
AssemblyFullName = entityType.ClrType.FullName,
Columns = GetColumnsMetadata(entityType)
});
_tableNameLookup.InsertKeyName(tableName);
}
return metaTables;
}
private IReadOnlyList<ColumnMetaData> GetColumnsMetadata(IEntityType entityType)
{
var tableColumns = new List<ColumnMetaData>();
foreach (var propertyType in entityType.GetProperties())
{
var relational = propertyType.Relational();
tableColumns.Add(new ColumnMetadata
{
ColumnName = relational.ColumnName,
DataType = relational.ColumnType
});
}
return tableColumns;
}
}
我们在代码中拥有 Schema 模型,它将在应用程序启动时填充 EfCore 实体的所有 Schema 信息。
图(Graph)类型
现在,我们如下定义 GraphQL 的类型
public class TableType : ObjectGraphType<object>
{
public QueryArguments TableArgs
{
get; set;
}
private IDictionary<string, Type> _databaseTypeToSystemType;
protected IDictionary<string, Type> DatabaseTypeToSystemType
{
get
{
if (_databaseTypeToSystemType == null)
{
_databaseTypeToSystemType = new Dictionary<string, type> {
{ "uniqueidentifier", typeof(String) },
{ "char", typeof(String) },
{ "nvarchar", typeof(String) },
{ "int", typeof(int) },
{ "decimal", typeof(decimal) },
{ "bit", typeof(bool) }
};
}
return _databaseTypeToSystemType;
}
}
public TableType(TableMetadata tableMetadata)
{
Name = tableMetadata.TableName;
foreach (var tableColumn in tableMetadata.Columns)
{
InitGraphTableColumn(tableColumn);
}
}
private void InitGraphTableColumn(ColumnMetadata columnMetadata)
{
var graphQLType =
(ResolveColumnMetaType(columnMetadata.DataType)).GetGraphTypeFromType(true);
var columnField = Field(
graphQLType,
columnMetadata.ColumnName
);
columnField.Resolver = new NameFieldResolver();
FillArgs(columnMetadata.ColumnName);
}
private void FillArgs(string columnName)
{
if (TableArgs == null)
{
TableArgs = new QueryArguments(
new QueryArgument<StringGraphType>()
{
Name = columnName
}
);
}
else
{
TableArgs.Add(new QueryArgument<StringGraphType> { Name = columnName });
}
TableArgs.Add(new QueryArgument<IdGraphType> { Name = "id" });
TableArgs.Add(new QueryArgument<IdGraphType> { Name = "first" });
TableArgs.Add(new QueryArgument<IdGraphType> { Name = "offset" });
}
private Type ResolveColumnMetaType(string dbType)
{
if (DatabaseTypeToSystemType.ContainsKey(dbType))
return DatabaseTypeToSystemType[dbType];
return typeof(String);
}
}
以上这段代码将帮助应用程序识别数据类型以及一些查询参数,如分页、投影… 请参阅本文的最后一部分,我将向您展示查询结果。
图(Graph)解析器
为了让 GraphQL 理解我们想要填充的数据库 Schema,我们需要创建一些解析器,如下所示
public class NameFieldResolver : IFieldResolver
{
public object Resolve(ResolveFieldContext context)
{
var source = context.Source;
if (source == null)
{
return null;
}
var name = Char.ToUpperInvariant(context.FieldAst.Name[0]) +
context.FieldAst.Name.Substring(1);
var value = GetPropValue(source, name);
if (value == null)
{
throw new InvalidOperationException($"Expected to find property
{context.FieldAst.Name} on {context.Source.GetType().Name}
but it does not exist.");
}
return value;
}
private static object GetPropValue(object src, string propName)
{
return src.GetType().GetProperty(propName).GetValue(src, null);
}
}
以及数据库 Schema 中每个字段的解析器
public class MyFieldResolver : IFieldResolver
{
private TableMetadata _tableMetadata;
private DbContext _dbContext;
public MyFieldResolver(TableMetadata tableMetadata, DbContext dbContext)
{
_tableMetadata = tableMetadata;
_dbContext = dbContext;
}
public object Resolve(ResolveFieldContext context)
{
var queryable = _dbContext.Query(_tableMetadata.AssemblyFullName);
if (context.FieldName.Contains("_list"))
{
var first = context.Arguments["first"] != null ?
context.GetArgument("first", int.MaxValue) :
int.MaxValue;
var offset = context.Arguments["offset"] != null ?
context.GetArgument("offset", 0) :
0;
return queryable
.Skip(offset)
.Take(first)
.ToDynamicList<object>();
}
else
{
var id = context.GetArgument<guid>("id");
return queryable.FirstOrDefault($"Id == @0", id);
}
}
}
您是否在上面的代码(粗体文本颜色)中看到了动态 LINQ?感谢 System.Linq.Dynamic.Core
库,没有它,我们需要做更多的工作。还有一件事使得程序集名称的查询是
public static class DbContextExtensions
{
public static IQueryable Query(this DbContext context, string entityName) =>
context.Query(context.Model.FindEntityType(entityName).ClrType);
static readonly MethodInfo SetMethod = typeof(DbContext).GetMethod(nameof(DbContext.Set));
public static IQueryable Query(this DbContext context, Type entityType) =>
(IQueryable)SetMethod.MakeGenericMethod(entityType).Invoke(context, null);
}
您可以参考 此链接 了解解决方案。
图(Graph)查询
现在是时候为我们的应用程序定义查询了。有两点需要注意,如下所示
- 如果我们想查询像
{ tasks (id: “<id here>”) {id, name} }
这样的实体,那么我们基于到目前为止定义的数据库 Schema 进行指向,并且结果将只在输出中包含一条记录。 - 但是,如果我们想查询实体列表,如
{ tasks_list (offset:1, first:10) { id, name } }
,查询第 1 页并获取 10 条记录,那么结果应该是一个记录列表。
如上所示,我们需要为数据库中的每个实体定义 2 个字段。例如,如果您的 DbContext
中有 10 个实体,那么 GraphQL 定义中将有 10 x 2 = 20 个字段。我将向您展示代码
public class GraphQLQuery : ObjectGraphType<object>
{
private IDatabaseMetadata _dbMetadata;
private ITableNameLookup _tableNameLookup;
private DbContext _dbContext;
public GraphQLQuery(
DbContext dbContext,
IDatabaseMetadata dbMetadata,
ITableNameLookup tableNameLookup)
{
_dbMetadata = dbMetadata;
_tableNameLookup = tableNameLookup;
_dbContext = dbContext;
Name = "Query";
foreach (var metaTable in _dbMetadata.GetTableMetadatas())
{
var tableType = new TableType(metaTable);
var friendlyTableName = _tableNameLookup.GetFriendlyName(metaTable.TableName);
AddField(new FieldType
{
Name = friendlyTableName,
Type = tableType.GetType(),
ResolvedType = tableType,
Resolver = new MyFieldResolver(metaTable, _dbContext),
Arguments = new QueryArguments(
tableType.TableArgs
)
});
// lets add key to get list of current table
var listType = new ListGraphType(tableType);
AddField(new FieldType
{
Name = $"{friendlyTableName}_list",
Type = listType.GetType(),
ResolvedType = listType,
Resolver = new MyFieldResolver(metaTable, _dbContext),
Arguments = new QueryArguments(
tableType.TableArgs
)
});
}
}
}
我们基于先前步骤中获得的 Schema 来定义 GraphQL 查询的字段。到目前为止,这对您有意义吗?
图(Graph)控制器
本文的最后一步是定义控制器,以便我们可以运行应用程序。它非常简单,如下所示
[Route("graphql/api/query")]
public class GraphQLController : Controller
{
private readonly Schema _graphQLSchema;
public GraphQLController(Schema schema)
{
_graphQLSchema = schema;
}
[HttpPost]
public async Task<string> Get([FromQuery]
string query = "{ tasks_list(offset:1, first:10) { id, name } }")
{
var result = await new DocumentExecuter().ExecuteAsync(
new ExecutionOptions()
{
Schema = _graphQLSchema,
Query = query
}
).ConfigureAwait(false);
if (result.Errors?.Count > 0)
{
return result.Errors.ToString();
}
var json = new DocumentWriter(indent: true).Write(result.Data);
return json;
}
}
不要忘记将所有内容注册到 IOC 容器中,如下所示
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddMyGraphQL(this IServiceCollection services)
{
services.AddScoped<ITableNameLookup, TableNameLookup>();
services.AddScoped<IDatabaseMetaData, DatabaseMetaData>();
services.AddScoped((resolver) =>
{
var dbContext = resolver.GetRequiredService<ApplicationDbcontext>();
var metaDatabase = resolver.GetRequiredService<IDatabaseMetaData>();
var tableNameLookup = resolver.GetRequiredService<ITablenameLookup>();
var schema = new Schema { Query = new GraphQLQuery
(dbContext, metaDatabase, tableNameLookup) };
schema.Initialize();
return schema;
});
return services;
}
}
整合起来
完成上述所有代码后,其结构如下
这只是一个 POC 项目,因此我将来会进行更多重构。按 F5 运行它,我将向您展示它是如何工作的。
让我们输入上面的一些 GraphQL 查询,然后点击 Try it out!
数据库 Schema 自动加载如下
它构建了所有的 GraphQL
字段如下
对于输入 { tasks_list(offset:1, first:10) { id, name } }
,您将收到
如果我将其更改为 { tasks(id: “621CFF32-A15D-4622–9938–0028EA0C3FEE”) { name, id, taskStatus } }
,它应该是
今天就到这里。:) 让我知道您的感受。
源代码
所有源代码都可以在 CRMCore.Module.GraphQL 中找到。如果您喜欢,请给我一个星,这样我将有更多动力将其做得更好,为社区做出贡献。
关注点
本文只是我为查询端完成的 POC。现在您知道我们可以动态使用 .NET 中的一些库,以便在现实世界中使查询更加动态和灵活。但是,我没有在这篇文章中提到的一些注意事项如下
- 实体的身份验证和授权尚未提及。
- 也许我们可以隐藏一些东西,而不是像这样全部暴露给外界。
- 我将在其他文章中继续研究变异(mutation)端以及
GraphQL
的其他概念。另外,下次我将与前端(使用 Apollo client 库的 React/Redux)集成。 - 与当前实体相关的子关系解决方案(我还没有解决方案)。
- 您可以随意命名,并在评论中告诉我,这样我以后可以改进它。
其他阅读
- http://graphql-dotnet.github.io
- https://dev-blog.apollodata.com/2017-the-year-in-graphql-124a050d04c6
- https://github.com/chentsulin/awesome-graphql#lib-dotnet
- https://github.com/nreco/data/tree/master/examples/SqliteDemo.GraphQLApi
- https://github.com/landmarkhw/Dapper.GraphQL
- https://github.com/JacekKosciesza/StarWars
- https://github.com/JuergenGutsch/GraphQlDemo
历史
- 2018 年 1 月 9 日:修正了拼写错误
- 2018 年 1 月 8 日:撰写了文章