API プロジェクト向けのクエリ ビルダーのサポート
概要
このガイドでは、ユーザー定義のクエリを有効にするために、API プロジェクトにクエリ ビルダー モデルとユーティリティを実装する方法について説明します。実装には、コントローラーの作成、クエリ ビルダー モデルの定義、およびこれらのクエリから SQL ステートメントを実行および生成するためのユーティリティの追加が含まれます。結果として得られるカスタマイズにより、ユーザーはサブクエリのサポート、フィルタリング、論理演算子、フィールド選択を使用して複雑なクエリを構築し、データ ソースに対して効率的に実行できるようになります。
実装オプション
API プロジェクトでクエリ ビルダー サポートを実装するには、2 つの方法があります。
| 方法 | 最適な用途 | 作業レベル |
|---|---|---|
NuGet パッケージ (Infragistics.QueryBuilder.Executor) |
迅速な統合、標準的なユース ケース | 低 |
| 手動での実装 | 完全なカスタマイズ、複雑な要件 | 中~高 |
オプション 1: Infragistics.QueryBuilder.Executor NuGet パッケージの使用 (推奨)
Infragistics.QueryBuilder.Executor パッケージは、最小限の構成で既存の API プロジェクトに直接組み込むことができる、すぐに使用できるクラス、条件、およびメソッドを提供します。
前提条件
- .NET 8 または .NET 9
- Microsoft.EntityFrameworkCore
- AutoMapper
- 既存の ASP.NET Core API プロジェクト
手順 1: パッケージのインストール
dotnet add package Infragistics.QueryBuilder.Executor
手順 2: サービスの構成
Program.cs または Startup.cs で、DbContext と結果 DTO を使用して QueryBuilder サービスを登録します。
// Program.cs
builder.Services.AddQueryBuilder<MyDbContext, MyResultDto>();
// Required for SwaggerUI compatibility
builder.Services.AddControllers().AddNewtonsoftJson(o =>
o.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore);
手順 3: クエリ エンドポイントの公開
クエリ ビルダー機能を公開するためにエンドポイント ルーティングを構成します。
app.UseEndpoints(endpoints =>
{
endpoints.UseQueryBuilder<MyDbContext, MyResultDto>("/api/query");
});
手順 4: クエリの送信
API がクエリ ペイロードを受け入れる準備が整いました。以下はリクエストの例です。
{
"Entity": "Users",
"ReturnFields": ["Id", "Name", "Email"],
"Operator": "And",
"FilteringOperands": [
{
"FieldName": "IsActive",
"Condition": { "Name": "equals" },
"SearchVal": true
},
{
"FieldName": "CreatedDate",
"Condition": { "Name": "after" },
"SearchVal": "2024-01-01"
}
]
}
手順 5: SQL の生成 (オプション)
診断や分析のために、クエリ モデルから SQL を生成できます。
var sql = SqlGenerator.GenerateSql(query);
Console.WriteLine(sql);
// Output: SELECT Id, Name, Email FROM Users WHERE IsActive = 1 AND CreatedDate > '2024-01-01'
オプション 2: 手動での実装
完全なカスタマイズや複雑なエンティティ マッピングが必要なプロジェクトでは、クエリ ビルダーを手動で実装できます。実装は NorthwindAPI REST API プロジェクトで示されています。
前提条件
- .NET Core または .NET API プロジェクト。
- LINQ、Entity Framework、またはその他の ORM を使用したデータ クエリの基礎知識。
- C# および ASP.NET Core コントローラーの基本的な理解。
手順 1: QueryBuilderController の作成
QueryBuilderController は、クエリ リクエストを処理するためのエントリ ポイントとして機能します。クライアントから Query オブジェクトを受け取り、QueryExecutor を使用して処理し、結果を返します。
Controllers ディレクトリに QueryBuilderController.cs という名前のファイルを作成します。
[ApiController]
[Route("[controller]")]
public class QueryBuilderController : ControllerBase
{
private readonly DataContext dataContext;
private readonly IMapper mapper;
private readonly ILogger<QueryBuilderController> logger;
public QueryBuilderController(DataContext dataContext, IMapper mapper, ILogger<QueryBuilderController> logger)
{
this.dataContext = dataContext;
this.mapper = mapper;
this.logger = logger;
}
[HttpPost("ExecuteQuery")]
[Consumes("application/json")]
[Produces("application/json")]
public ActionResult<QueryBuilderResult> ExecuteQuery(Query query)
{
var sanitizedEntity = query.Entity.Replace("\r", string.Empty).Replace("\n", string.Empty);
logger.LogInformation("Executing query for entity: {Entity}", sanitizedEntity);
var t = query.Entity.ToLower(CultureInfo.InvariantCulture);
return Ok(new Dictionary<string, object[]?>
{
{
t,
t switch
{
"addresses" => dataContext.Addresses.Run<AddressDb, AddressDto>(query, mapper),
"categories" => dataContext.Categories.Run<CategoryDb, CategoryDto>(query, mapper),
"products" => dataContext.Products.Run<ProductDb, ProductDto>(query, mapper),
"regions" => dataContext.Regions. Run<RegionDb, RegionDto>(query, mapper),
"territories" => dataContext.Territories. Run<TerritoryDb, TerritoryDto>(query, mapper),
"employees" => dataContext.Employees. Run<EmployeeDb, EmployeeDto>(query, mapper),
"customers" => dataContext.Customers.Run<CustomerDb, CustomerDto>(query, mapper),
"orders" => dataContext.Orders.Run<OrderDb, OrderDto>(query, mapper),
"orderdetails" => dataContext.OrderDetails.Run<OrderDetailDb, OrderDetailDto>(query, mapper),
"shippers" => dataContext.Shippers.Run<ShipperDb, ShipperDto>(query, mapper),
"suppliers" => dataContext.Suppliers.Run<SupplierDb, SupplierDto>(query, mapper),
_ => throw new InvalidOperationException($"Unknown entity {t}"),
}
},
});
}
}
完全な例については、NorthwindAPI QueryBuilderController を参照してください。
手順 2: クエリ ビルダー モデルの定義
クエリ ビルダーは、クエリ構造を表すためのモデル郡に依存します。これらを QueryBuilder ディレクトリまたは名前空間に配置します。
FilterType 列挙型
フィルターを組み合わせるための論理演算子を定義します。
public enum FilterType
{
And = 0,
Or = 1,
}
Query クラス
クエリ全体の構造を表します。
public class Query
{
public string Entity { get; set; }
public string[] ReturnFields { get; set; }
public FilterType Operator { get; set; }
public QueryFilter[] FilteringOperands { get; set; }
}
| プロパティ | 説明 |
|---|---|
Entity |
クエリの対象となるエンティティ/テーブル (例: "Products")。 |
ReturnFields |
結果に含めるフィールド (例: ["Name", "Price"]、またはすべてを表す ["*"])。 |
Operator |
フィルターを組み合わせるための論理演算子 (And または Or)。 |
FilteringOperands |
条件を定義する QueryFilter オブジェクトの配列。 |
QueryFilter クラス
個別のフィルタリング条件を表します。
public class QueryFilter
{
// Basic condition
public string? FieldName { get; set; }
public bool? IgnoreCase { get; set; }
public QueryFilterCondition? Condition { get; set; }
public object? SearchVal { get; set; }
public Query? SearchTree { get; set; }
// And/Or
public FilterType? Operator { get; set; }
public QueryFilter[] FilteringOperands { get; set; }
}
QueryFilterCondition クラス
使用可能なフィルタリング条件を定義します。
public class QueryFilterCondition
{
public string Name { get; set; }
public bool IsUnary { get; set; }
public string IconName { get; set; }
}
詳細な実装は、NorthwindAPI QueryBuilder モデルを参照してください。
手順 3: QueryExecutor の実装
QueryExecutor クラスは、Query オブジェクトを処理し、LINQ 式に変換して IQueryable データ ソースに対して実行します。
private static Expression BuildConditionExpression<TEntity>(
DataContext db,
IQueryable<TEntity> source,
QueryFilter filter,
ParameterExpression parameter)
{
if (filter. FieldName is not null && filter.Condition is not null)
{
var property = source.ElementType.GetProperty(
filter.FieldName,
BindingFlags. IgnoreCase | BindingFlags.Public | BindingFlags. Instance)
?? throw new InvalidOperationException($"Property '{filter.FieldName}' not found");
var field = Expression.Property(parameter, property);
var targetType = property.PropertyType;
var searchValue = GetSearchValue(filter.SearchVal, targetType);
Expression condition = filter.Condition.Name switch
{
"null" => Expression.Equal(field, Expression.Constant(null)),
"notNull" => Expression.NotEqual(field, Expression.Constant(null)),
"empty" => Expression.Equal(field, Expression.Constant(string.Empty)),
"notEmpty" => Expression.NotEqual(field, Expression.Constant(string.Empty)),
"equals" => Expression.Equal(field, searchValue),
"doesNotEqual" => Expression.NotEqual(field, searchValue),
"inQuery" => BuildInExpression(db, filter.SearchTree, field),
"notInQuery" => Expression.Not(BuildInExpression(db, filter.SearchTree, field)),
"contains" => CallContains(field, searchValue),
"doesNotContain" => Expression.Not(CallContains(field, searchValue)),
"startsWith" => CallStartsWith(field, searchValue),
"endsWith" => CallEndsWith(field, searchValue),
"greaterThan" => Expression.GreaterThan(field, searchValue),
"lessThan" => Expression.LessThan(field, searchValue),
"greaterThanOrEqualTo" => Expression.GreaterThanOrEqual(field, searchValue),
"lessThanOrEqualTo" => Expression.LessThanOrEqual(field, searchValue),
"before" => Expression.LessThan(field, searchValue),
"after" => Expression.GreaterThan(field, searchValue),
"today" => BuildTodayExpression(field),
"yesterday" => BuildYesterdayExpression(field),
"thisMonth" => BuildThisMonthExpression(field),
"lastMonth" => BuildLastMonthExpression(field),
"nextMonth" => BuildNextMonthExpression(field),
"thisYear" => BuildThisYearExpression(field),
"lastYear" => BuildLastYearExpression(field),
"nextYear" => BuildNextYearExpression(field),
"true" => Expression.Equal(field, Expression.Constant(true)),
"false" => Expression.Equal(field, Expression.Constant(false)),
_ => throw new NotImplementedException($"Condition not implemented: {filter.Condition. Name}"),
};
return condition;
}
else
{
// Handle nested filter groups
var subexpressions = filter.FilteringOperands?
.Select(f => BuildConditionExpression(db, source, f, parameter))
.ToArray();
if (subexpressions == null || ! subexpressions.Any())
return Expression. Constant(true);
return filter.Operator == FilterType.And
? subexpressions. Aggregate(Expression.AndAlso)
: subexpressions.Aggregate(Expression.OrElse);
}
}
サポートされているフィルター条件
どちらの実装オプションも、Angular クエリ ビルダー コンポーネント (IgxQueryBuilderComponent) に対応する以下のフィルター条件をサポートしています。
文字列の条件
| 条件 | 説明 |
|---|---|
contains |
フィールドに検索値が含まれる |
doesNotContain |
フィールドに検索値が含まれない |
startsWith |
フィールドが検索値で始まる |
endsWith |
フィールドが検索値で終わる |
equals |
フィールドが検索値と等しい |
doesNotEqual |
フィールドが検索値と等しくない |
empty |
フィールドが空 |
notEmpty |
フィールドが空でない |
null |
フィールドが null |
notNull |
フィールドが null でない |
数値の条件
| 条件 | 説明 |
|---|---|
equals |
フィールドが検索値と等しい |
doesNotEqual |
フィールドが検索値と等しくない |
greaterThan |
フィールドが検索値より大きい |
lessThan |
フィールドが検索値より小さい |
greaterThanOrEqualTo |
フィールドが検索値より大きい |
lessThanOrEqualTo |
フィールドが検索値より小さい |
empty |
フィールドが空/ゼロ |
notEmpty |
フィールドが空/ゼロでない |
null |
フィールドが null |
notNull |
フィールドが null でない |
日付/時刻の条件
| 条件 | 説明 |
|---|---|
equals |
フィールドが検索値と等しい |
doesNotEqual |
フィールドが検索値と等しくない |
before |
フィールドが検索値より前である |
after |
フィールドが検索値より後である |
today |
フィールドが今日の日付と一致する |
yesterday |
フィールドが昨日の日付と一致する |
thisMonth |
フィールドが今月内である |
lastMonth |
フィールドが先月内である |
nextMonth |
フィールドが来月内である |
thisYear |
フィールドが今年内である |
lastYear |
フィールドが昨年内である |
nextYear |
フィールドが来年内である |
null |
フィールドが null |
notNull |
フィールドが null でない |
ブール値の条件
| 条件 | 説明 |
|---|---|
true |
フィールドが true である |
false |
フィールドが false である |
null |
フィールドが null |
notNull |
フィールドが null でない |
サブクエリ条件
| 条件 | 説明 |
|---|---|
inQuery |
フィールド値がサブクエリ結果に存在する |
notInQuery |
フィールド値がサブクエリ結果に存在しない |
Angular フロントエンドとの統合
クエリ ビルダー API は、Ignite UI for Angular クエリ ビルダー コンポーネント (IgxQueryBuilderComponent) とシームレスに連携するように設計されています。このコンポーネントは、期待される API ペイロード形式と一致するクエリ構造を生成します。
主要な Angular コンポーネント
IgniteUI/igniteui-angular リポジトリから:
import {
FilteringExpressionsTree,
FilteringLogic,
IgxQueryBuilderComponent,
IExpressionTree,
IgxNumberFilteringOperand,
IgxStringFilteringOperand,
IgxBooleanFilteringOperand,
IgxDateFilteringOperand,
IgxTimeFilteringOperand,
IgxDateTimeFilteringOperand
} from 'igniteui-angular';
例: クエリ式ツリーの作成
// エンティティとフィールドの定義
const entities = [
{
name: 'Products',
fields: [
{ field: 'productId', dataType: 'number' },
{ field: 'productName', dataType: 'string' },
{ field: 'unitPrice', dataType: 'number' },
{ field: 'discontinued', dataType: 'boolean' },
{ field: 'categoryId', dataType: 'number' }
]
},
{
name: 'OrderDetails',
fields: [
{ field: 'productId', dataType: 'number' },
{ field: 'quantity', dataType: 'number' },
{ field: 'discount', dataType: 'number' }
]
}
];
// サブクエリを含む複雑なクエリの構築
const innerTree = new FilteringExpressionsTree(
FilteringLogic. And,
undefined,
'OrderDetails',
['productId']
);
innerTree.filteringOperands. push({
fieldName: 'quantity',
condition: IgxNumberFilteringOperand. instance().condition('greaterThanOrEqualTo'),
conditionName: 'greaterThanOrEqualTo',
searchVal: 10
});
const mainTree = new FilteringExpressionsTree(
FilteringLogic.And,
undefined,
'Products',
['*']
);
mainTree.filteringOperands.push({
fieldName: 'productId',
condition: IgxStringFilteringOperand.instance().condition('inQuery'),
conditionName: 'inQuery',
searchTree: innerTree
});
mainTree.filteringOperands.push({
fieldName: 'discontinued',
condition: IgxBooleanFilteringOperand.instance().condition('false'),
conditionName: 'false'
});
クエリ ビルダー コンポーネントの使用
<igx-query-builder
#queryBuilder
[entities]="entities"
[expressionTree]="expressionTree"
(expressionTreeChange)="onQueryChange($event)">
<igx-query-builder-header [title]="'Product Query Builder'">
</igx-query-builder-header>
</igx-query-builder>
例: サブクエリを含む複雑なクエリ
クエリ ペイロード
{
"filteringOperands": [
{
"fieldName": "categoryId",
"condition": {
"name": "equals",
"isUnary": false,
"iconName": "filter_equal"
},
"conditionName": "equals",
"searchVal": 1,
"searchTree": null,
"ignoreCase": true
},
{
"fieldName": "productId",
"condition": {
"name": "inQuery",
"isUnary": false,
"isNestedQuery": true,
"iconName": "in"
},
"conditionName": "inQuery",
"searchVal": null,
"searchTree": {
"filteringOperands": [
{
"fieldName": "quantity",
"condition": {
"name": "greaterThanOrEqualTo",
"isUnary": false,
"iconName": "filter_greater_than_or_equal"
},
"conditionName": "greaterThanOrEqualTo",
"searchVal": 10,
"searchTree": null,
"ignoreCase": true
},
{
"fieldName": "quantity",
"condition": {
"name": "lessThanOrEqualTo",
"isUnary": false,
"iconName": "filter_less_than_or_equal"
},
"conditionName": "lessThanOrEqualTo",
"searchVal": 15,
"searchTree": null,
"ignoreCase": true
}
],
"operator": 0,
"entity": "orderDetails",
"returnFields": [
"productId"
]
},
"ignoreCase": true
}
],
"operator": 0,
"entity": "products",
"returnFields": [
"productId",
"productName",
"supplierId",
"categoryId",
"quantityPerUnit",
"unitPrice",
"unitsInStock",
"unitsOnOrder",
"reorderLevel",
"discontinued"
]
}
生成された SQL 出力
SELECT *
FROM products
WHERE categoryId = {{selectedCategory}}
AND productId IN (
SELECT productId
FROM orderDetails
WHERE quantity >= {{quantityGreaterThan}}
AND quantity <= {{quantityLessThan}}
)
API エンドポイント
POST https://data-northwind.appbuilder.dev/QueryBuilder/ExecuteQuery
Content-Type: application/json
ローカライゼーション サポート
Angular クエリ ビルダー コンポーネントには、フィルター条件ラベル用の組み込みローカライゼーション サポートが含まれています。igniteui-angular リポジトリは、以下を含む複数の言語のリソース文字列を提供しています。
- 英語 (EN) - デフォルト
- フランス語 (FR)
- ドイツ語 (DE)
- スペイン語 (ES)
- イタリア語 (IT)
- ポルトガル語 (PT)
- ポーランド語 (PL)
- スウェーデン語 (SV)
- その他
リソース文字列の例:
export const QueryBuilderResourceStringsEN: IQueryBuilderResourceStrings = {
igx_query_builder_filter_contains: 'Contains',
igx_query_builder_filter_doesNotContain: 'Does Not Contain',
igx_query_builder_filter_startsWith: 'Starts With',
igx_query_builder_filter_endsWith: 'Ends With',
igx_query_builder_filter_equals: 'Equals',
igx_query_builder_filter_doesNotEqual: 'Does Not Equal',
igx_query_builder_filter_greaterThan: 'Greater Than',
igx_query_builder_filter_lessThan: 'Less Than',
// ... more strings
};
その他の参考資料
- Infragistics. QueryBuilder. Executor NuGet パッケージ
- NorthwindAPI QueryBuilderController
- NorthwindAPI QueryBuilder モデル
- Ignite UI for Angular Query Builder コンポーネント
- クエリ ビルダーの使用方法のステップ バイス テップ ガイド
