跳到主要内容

OData (Open Data Protocol)

OData (Open Data Protocol) は、RESTful API を標準化し、データのクエリと操作を統一的に行うためのオープンスタンダードプロトコルです。

OData とは

概要

OData は OASIS (Organization for the Advancement of Structured Information Standards) によって標準化された、Web 経由でデータを公開・消費するためのプロトコルです。

主な特徴:

  • HTTP プロトコルをベースとした RESTful API
  • 標準化されたクエリ構文による柔軟なデータ取得
  • メタデータによるスキーマの自己記述
  • JSON および XML フォーマットのサポート
  • CRUD 操作の統一的なインターフェース

OData のバージョン

  • OData v2: 広く採用された初期バージョン(現在はレガシー)
  • OData v4: 現在の標準バージョン(2014年に OASIS 標準として承認)
  • OData v4.01: 最新の仕様(2020年承認)

OData の主要な機能

1. 統一的なリソースアドレス指定

OData では、リソースを URL で表現します。

# コレクション全体
GET https://api.example.com/odata/Products

# 特定のエンティティ
GET https://api.example.com/odata/Products(1)

# ナビゲーションプロパティ
GET https://api.example.com/odata/Products(1)/Category

# プロパティ値の直接取得
GET https://api.example.com/odata/Products(1)/Name/$value

2. 標準化されたクエリオプション

OData の最大の特徴は、URL クエリパラメータを使用した強力なデータフィルタリングとクエリ機能です。

$filter (フィルタリング)

データをフィルタリングして取得します。

# 価格が100以上の製品
GET /odata/Products?$filter=Price ge 100

# 名前に"Pro"を含む製品
GET /odata/Products?$filter=contains(Name, 'Pro')

# 複数条件(AND/OR)
GET /odata/Products?$filter=Price lt 50 and Category/Name eq 'Electronics'

# 日付フィルタ
GET /odata/Orders?$filter=OrderDate ge 2024-01-01T00:00:00Z

比較演算子:

演算子意味
eq等しいPrice eq 100
ne等しくないStatus ne 'Cancelled'
gtより大きいQuantity gt 10
ge以上Price ge 50
ltより小さいStock lt 5
le以下Discount le 0.2

論理演算子:

演算子意味
andAND条件Price gt 10 and Price lt 100
orOR条件Category eq 'Books' or Category eq 'Music'
notNOT条件not (Status eq 'Inactive')

文字列関数:

関数説明
contains(field, 'value')部分一致contains(Name, 'Phone')
startswith(field, 'value')前方一致startswith(Email, 'admin')
endswith(field, 'value')後方一致endswith(Email, '@example.com')
length(field)文字列長length(Name) gt 10
tolower(field)小文字変換tolower(Name) eq 'product'
toupper(field)大文字変換toupper(Code) eq 'ABC'

算術関数:

# 割引後の価格が100以下
GET /odata/Products?$filter=(Price mul (1 sub Discount)) le 100

# 在庫が閾値の50%以下
GET /odata/Products?$filter=Stock le (Threshold div 2)

日付・時刻関数:

関数説明
year(field)年を取得year(CreatedDate) eq 2024
month(field)月を取得month(CreatedDate) eq 12
day(field)日を取得day(CreatedDate) eq 25
hour(field)時を取得hour(CreatedTime) ge 9
minute(field)分を取得minute(CreatedTime) eq 30
now()現在時刻CreatedDate lt now()

$select (プロパティ選択)

取得するプロパティを指定します(パフォーマンス向上)。

# 特定のフィールドのみ取得
GET /odata/Products?$select=Id,Name,Price

# ネストしたプロパティの選択
GET /odata/Products?$select=Name,Category/Name

$expand (関連エンティティの展開)

ナビゲーションプロパティを展開して、関連データを一度に取得します。

# カテゴリ情報を含めて取得
GET /odata/Products?$expand=Category

# 複数の関連エンティティを展開
GET /odata/Orders?$expand=Customer,OrderItems

# ネストした展開
GET /odata/Orders?$expand=OrderItems($expand=Product)

# 展開したエンティティをフィルタリング
GET /odata/Orders?$expand=OrderItems($filter=Quantity gt 5)

# 展開と選択の組み合わせ
GET /odata/Products?$expand=Category($select=Name)&$select=Name,Price

$orderby (並び替え)

結果を並び替えます。

# 昇順(デフォルト)
GET /odata/Products?$orderby=Price

# 降順
GET /odata/Products?$orderby=Price desc

# 複数フィールドで並び替え
GET /odata/Products?$orderby=Category/Name,Price desc

# 関数を使った並び替え
GET /odata/Products?$orderby=length(Name) desc

$top と $skip (ページネーション)

結果のページネーションを実装します。

# 最初の10件を取得
GET /odata/Products?$top=10

# 最初の10件をスキップして次の10件を取得
GET /odata/Products?$skip=10&$top=10

# ページネーション(3ページ目、1ページ20件)
GET /odata/Products?$skip=40&$top=20

$count (カウント)

結果の総数を取得します。

# カウントのみ取得
GET /odata/Products/$count

# データと一緒にカウントを取得
GET /odata/Products?$count=true&$top=10

# フィルタ後のカウント
GET /odata/Products/$count?$filter=Price gt 100

$search (全文検索)

全文検索を実行します(OData v4 以降)。

# 単純な検索
GET /odata/Products?$search=laptop

# AND検索
GET /odata/Products?$search=laptop AND gaming

# OR検索
GET /odata/Products?$search=laptop OR desktop

# フレーズ検索
GET /odata/Products?$search="high performance"

$apply (集計・グループ化)

データの集計とグループ化を行います(OData v4 以降の拡張機能)。

# カテゴリごとの製品数
GET /odata/Products?$apply=groupby((Category/Name), aggregate($count as Total))

# カテゴリごとの平均価格
GET /odata/Products?$apply=groupby((Category/Name), aggregate(Price with average as AvgPrice))

# 複数の集計
GET /odata/Orders?$apply=groupby((Customer/Name), aggregate(TotalAmount with sum as TotalSales, $count as OrderCount))

3. メタデータとサービスドキュメント

OData サービスは、スキーマ情報をメタデータとして公開します。

サービスドキュメント

GET https://api.example.com/odata/

レスポンス例(JSON):

{
"@odata.context": "https://api.example.com/odata/$metadata",
"value": [
{
"name": "Products",
"kind": "EntitySet",
"url": "Products"
},
{
"name": "Categories",
"kind": "EntitySet",
"url": "Categories"
},
{
"name": "Orders",
"kind": "EntitySet",
"url": "Orders"
}
]
}

メタデータドキュメント

GET https://api.example.com/odata/$metadata

レスポンス例(XML - CSDL形式):

<?xml version="1.0" encoding="UTF-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
<edmx:DataServices>
<Schema Namespace="ProductService" xmlns="http://docs.oasis-open.org/odata/ns/edm">

<!-- エンティティ型定義 -->
<EntityType Name="Product">
<Key>
<PropertyRef Name="Id"/>
</Key>
<Property Name="Id" Type="Edm.Int32" Nullable="false"/>
<Property Name="Name" Type="Edm.String" MaxLength="100"/>
<Property Name="Price" Type="Edm.Decimal" Precision="18" Scale="2"/>
<Property Name="Stock" Type="Edm.Int32"/>
<Property Name="CategoryId" Type="Edm.Int32"/>
<NavigationProperty Name="Category" Type="ProductService.Category"/>
</EntityType>

<EntityType Name="Category">
<Key>
<PropertyRef Name="Id"/>
</Key>
<Property Name="Id" Type="Edm.Int32" Nullable="false"/>
<Property Name="Name" Type="Edm.String" MaxLength="50"/>
<NavigationProperty Name="Products" Type="Collection(ProductService.Product)"/>
</EntityType>

<!-- エンティティコンテナ -->
<EntityContainer Name="Container">
<EntitySet Name="Products" EntityType="ProductService.Product">
<NavigationPropertyBinding Path="Category" Target="Categories"/>
</EntitySet>
<EntitySet Name="Categories" EntityType="ProductService.Category">
<NavigationPropertyBinding Path="Products" Target="Products"/>
</EntitySet>
</EntityContainer>

</Schema>
</edmx:DataServices>
</edmx:Edmx>

4. CRUD 操作

Create (作成) - POST

POST https://api.example.com/odata/Products
Content-Type: application/json

{
"Name": "New Product",
"Price": 99.99,
"Stock": 50,
"CategoryId": 1
}

レスポンス(201 Created):

{
"@odata.context": "https://api.example.com/odata/$metadata#Products/$entity",
"Id": 123,
"Name": "New Product",
"Price": 99.99,
"Stock": 50,
"CategoryId": 1
}

Read (読取) - GET

GET https://api.example.com/odata/Products(123)

レスポンス(200 OK):

{
"@odata.context": "https://api.example.com/odata/$metadata#Products/$entity",
"Id": 123,
"Name": "New Product",
"Price": 99.99,
"Stock": 50,
"CategoryId": 1
}

Update (更新) - PUT / PATCH

PUT - 完全更新(すべてのプロパティを指定):

PUT https://api.example.com/odata/Products(123)
Content-Type: application/json

{
"Name": "Updated Product",
"Price": 89.99,
"Stock": 45,
"CategoryId": 1
}

PATCH - 部分更新(変更するプロパティのみ指定):

PATCH https://api.example.com/odata/Products(123)
Content-Type: application/json

{
"Price": 79.99,
"Stock": 40
}

Delete (削除) - DELETE

DELETE https://api.example.com/odata/Products(123)

レスポンス(204 No Content)

5. バッチリクエスト

複数の操作を一度に実行できます。

POST https://api.example.com/odata/$batch
Content-Type: multipart/mixed; boundary=batch_36522ad7-fc75-4b56-8c71-56071383e77b

--batch_36522ad7-fc75-4b56-8c71-56071383e77b
Content-Type: application/http
Content-Transfer-Encoding: binary

GET Products(1) HTTP/1.1
Host: api.example.com
Accept: application/json

--batch_36522ad7-fc75-4b56-8c71-56071383e77b
Content-Type: application/http
Content-Transfer-Encoding: binary

GET Products(2) HTTP/1.1
Host: api.example.com
Accept: application/json

--batch_36522ad7-fc75-4b56-8c71-56071383e77b--

OData のデータ型

基本データ型(Primitive Types)

OData型説明
Edm.String文字列"Hello"
Edm.Int3232ビット整数123
Edm.Int6464ビット整数9223372036854775807
Edm.Decimal10進数99.99
Edm.Double倍精度浮動小数点3.14159
Edm.Boolean真偽値true / false
Edm.DateTime日時(v2)2024-12-23T10:30:00
Edm.DateTimeOffsetタイムゾーン付き日時(v4)2024-12-23T10:30:00+09:00
Edm.Date日付のみ(v4)2024-12-23
Edm.TimeOfDay時刻のみ(v4)10:30:00
Edm.Duration期間PT12H30M
Edm.GuidGUID12345678-1234-1234-1234-123456789012
Edm.BinaryバイナリデータT0RhdGE= (Base64)

複合型(Complex Types)

<ComplexType Name="Address">
<Property Name="Street" Type="Edm.String"/>
<Property Name="City" Type="Edm.String"/>
<Property Name="PostalCode" Type="Edm.String"/>
<Property Name="Country" Type="Edm.String"/>
</ComplexType>

<EntityType Name="Customer">
<Key>
<PropertyRef Name="Id"/>
</Key>
<Property Name="Id" Type="Edm.Int32" Nullable="false"/>
<Property Name="Name" Type="Edm.String"/>
<Property Name="BillingAddress" Type="ProductService.Address"/>
<Property Name="ShippingAddress" Type="ProductService.Address"/>
</EntityType>

コレクション型(Collection Types)

<EntityType Name="Product">
<Property Name="Tags" Type="Collection(Edm.String)"/>
<Property Name="Dimensions" Type="Collection(Edm.Double)"/>
</EntityType>

OData のベストプラクティス

1. OData を使用すべき場面

✅ 適している場面

  1. 複雑なクエリ要件がある

    • クライアントが動的にフィルタ、ソート、ページネーションを行う必要がある
    • 様々な条件での検索が必要なダッシュボードやレポート画面
  2. データ探索とアドホッククエリ

    • データアナリストやパワーユーザーが自由にデータを探索する
    • BI ツールとの統合
  3. 標準化されたインターフェースが必要

    • 複数のクライアント(Web、モバイル、デスクトップ)で共通のAPIを使用
    • サードパーティ統合
  4. Microsoft エコシステム

    • SharePoint、Dynamics 365、Power Platform との統合
    • .NET アプリケーション(ASP.NET Core OData ライブラリ)
  5. メタデータ駆動型アプリケーション

    • 動的にUIを生成する
    • スキーマ情報から自動的にクライアントコードを生成

❌ 適していない場面

  1. 単純なCRUD操作のみ

    • OData のオーバーヘッドが不要
    • シンプルな REST API で十分
  2. パフォーマンスが最重要

    • OData のクエリ解析とメタデータのオーバーヘッド
    • GraphQL や専用エンドポイントの方が効率的な場合も
  3. 非常に特殊なクエリロジック

    • OData の標準クエリでは表現できない複雑なビジネスロジック
    • カスタムエンドポイントの方が適切
  4. リアルタイム通信

    • WebSocket や SignalR の方が適している
    • OData は HTTP ベースのリクエスト/レスポンス
  5. バイナリデータの大量転送

    • ファイルアップロード/ダウンロードには専用エンドポイントを使用

2. クエリのパフォーマンス最適化

$select を使用して必要なフィールドのみ取得

# ❌ 悪い例: すべてのフィールドを取得
GET /odata/Products

# ✅ 良い例: 必要なフィールドのみ
GET /odata/Products?$select=Id,Name,Price

$top でページサイズを制限

# ❌ 悪い例: 無制限に取得
GET /odata/Products

# ✅ 良い例: ページサイズを制限
GET /odata/Products?$top=20

深いネストの $expand を避ける

# ❌ 悪い例: 深くネストした展開
GET /odata/Orders?$expand=Customer($expand=Orders($expand=OrderItems($expand=Product)))

# ✅ 良い例: 必要な階層のみ展開
GET /odata/Orders?$expand=Customer,OrderItems($expand=Product;$select=Name,Price)

インデックスされたフィールドでフィルタリング

# ✅ データベースインデックスを活用
GET /odata/Products?$filter=CategoryId eq 5 and Price gt 100

3. セキュリティのベストプラクティス

クエリの複雑さを制限

サーバー側で以下の制限を設定:

  • 最大 $top 値: 例えば 100 や 1000
  • 最大 $expand の深さ: 例えば 2階層まで
  • $filter の複雑さ: ネストした条件の最大数

ASP.NET Core での実装例:

services.AddControllers()
.AddOData(opt => opt
.SetMaxTop(100)
.Expand().Select().Filter().OrderBy().Count()
.SetMaxExpansionDepth(2));

認証・認可の実装

// エンティティレベルのフィルタリング
[Authorize]
[EnableQuery]
public IQueryable<Order> GetOrders()
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
// ログインユーザーの注文のみ返す
return _context.Orders.Where(o => o.UserId == userId);
}

// 詳細な認可チェック
[Authorize]
public async Task<IActionResult> Get([FromODataUri] int key)
{
var order = await _context.Orders.FindAsync(key);
if (order == null)
return NotFound();

var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (order.UserId != userId && !User.IsInRole("Admin"))
return Forbid();

return Ok(order);
}

機密データの保護

// メタデータから機密プロパティを除外
public class UserDto
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
// パスワードハッシュは公開しない
}

// または属性で制御
public class User
{
public int Id { get; set; }
public string Name { get; set; }

[Ignore] // OData メタデータから除外
public string PasswordHash { get; set; }
}

SQL インジェクション対策

  • OData ライブラリは通常、パラメータ化されたクエリを使用するため安全
  • カスタムフィルタ関数を実装する際は、必ずパラメータ化を使用
// ✅ 安全: パラメータ化されたクエリ
_context.Products.Where(p => p.CategoryId == categoryId);

// ❌ 危険: 文字列連結(使用しない)
_context.Products.FromSqlRaw($"SELECT * FROM Products WHERE CategoryId = {categoryId}");

// ✅ 安全: パラメータを使用
_context.Products.FromSqlRaw("SELECT * FROM Products WHERE CategoryId = {0}", categoryId);

4. エラーハンドリング

OData は標準的なエラー形式を定義しています。

{
"error": {
"code": "InvalidQuery",
"message": "The query specified in the URI is not valid.",
"target": "$filter",
"details": [
{
"code": "UnknownProperty",
"message": "Property 'InvalidField' does not exist.",
"target": "InvalidField"
}
],
"innererror": {
"trace": "...",
"context": "..."
}
}
}

サーバー側での実装例:

public class ODataErrorHandler
{
public IActionResult HandleODataException(ODataException ex)
{
var error = new ODataError
{
ErrorCode = "BadRequest",
Message = ex.Message,
Target = ex.Target,
Details = ex.Details?.Select(d => new ODataErrorDetail
{
ErrorCode = d.ErrorCode,
Message = d.Message,
Target = d.Target
}).ToList()
};

return new BadRequestObjectResult(new { error });
}
}

5. バージョニング戦略

URL パスによるバージョニング

GET https://api.example.com/odata/v1/Products
GET https://api.example.com/odata/v2/Products

カスタムヘッダーによるバージョニング

GET https://api.example.com/odata/Products
API-Version: 2.0

クエリパラメータによるバージョニング

GET https://api.example.com/odata/Products?api-version=2.0

実装例

ASP.NET Core での実装

1. パッケージのインストール

dotnet add package Microsoft.AspNetCore.OData

2. エンティティモデルの定義

// Models/Product.cs
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; }
}

// Models/Category.cs
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Product> Products { get; set; }
}

3. DbContext の設定

// Data/ApplicationDbContext.cs
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}

public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>()
.HasOne(p => p.Category)
.WithMany(c => c.Products)
.HasForeignKey(p => p.CategoryId);
}
}

4. OData の設定(Program.cs)

using Microsoft.AspNetCore.OData;
using Microsoft.OData.ModelBuilder;

var builder = WebApplication.CreateBuilder(args);

// EDM モデルの構築
var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Product>("Products");
modelBuilder.EntitySet<Category>("Categories");

// OData サービスの登録
builder.Services.AddControllers()
.AddOData(options => options
.Select() // $select を有効化
.Filter() // $filter を有効化
.OrderBy() // $orderby を有効化
.Expand() // $expand を有効化
.Count() // $count を有効化
.SetMaxTop(100) // $top の最大値を設定
.AddRouteComponents("odata", modelBuilder.GetEdmModel()));

builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

var app = builder.Build();

app.UseRouting();
app.UseAuthorization();
app.MapControllers();

app.Run();

5. コントローラーの実装

// Controllers/ProductsController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Routing.Controllers;

[Route("odata/[controller]")]
public class ProductsController : ODataController
{
private readonly ApplicationDbContext _context;

public ProductsController(ApplicationDbContext context)
{
_context = context;
}

// GET: odata/Products
[EnableQuery] // OData クエリを有効化
public IQueryable<Product> Get()
{
return _context.Products;
}

// GET: odata/Products(5)
[EnableQuery]
public SingleResult<Product> Get([FromRoute] int key)
{
return SingleResult.Create(_context.Products.Where(p => p.Id == key));
}

// POST: odata/Products
public async Task<IActionResult> Post([FromBody] Product product)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

_context.Products.Add(product);
await _context.SaveChangesAsync();

return Created(product);
}

// PATCH: odata/Products(5)
public async Task<IActionResult> Patch([FromRoute] int key, [FromBody] Delta<Product> delta)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

var product = await _context.Products.FindAsync(key);
if (product == null)
{
return NotFound();
}

delta.Patch(product);
await _context.SaveChangesAsync();

return Updated(product);
}

// DELETE: odata/Products(5)
public async Task<IActionResult> Delete([FromRoute] int key)
{
var product = await _context.Products.FindAsync(key);
if (product == null)
{
return NotFound();
}

_context.Products.Remove(product);
await _context.SaveChangesAsync();

return NoContent();
}
}

6. カスタムクエリオプションの実装

// カスタムフィルタの追加
public class CustomProductsController : ODataController
{
private readonly ApplicationDbContext _context;

public CustomProductsController(ApplicationDbContext context)
{
_context = context;
}

[EnableQuery]
[HttpGet("odata/Products/InStock")]
public IQueryable<Product> GetInStockProducts()
{
// 在庫があるものだけ返す
return _context.Products.Where(p => p.Stock > 0);
}

[EnableQuery]
[HttpGet("odata/Products/LowStock")]
public IQueryable<Product> GetLowStockProducts([FromQuery] int threshold = 10)
{
// 在庫が少ないものを返す
return _context.Products.Where(p => p.Stock <= threshold);
}
}

クライアント側の実装(TypeScript)

// OData クライアントクラス
class ODataClient {
private baseUrl: string;

constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}

// クエリビルダー
async query<T>(
entitySet: string,
options?: {
filter?: string;
select?: string[];
expand?: string[];
orderby?: string;
top?: number;
skip?: number;
count?: boolean;
}
): Promise<{ value: T[]; '@odata.count'?: number }> {
const params = new URLSearchParams();

if (options?.filter) params.append('$filter', options.filter);
if (options?.select) params.append('$select', options.select.join(','));
if (options?.expand) params.append('$expand', options.expand.join(','));
if (options?.orderby) params.append('$orderby', options.orderby);
if (options?.top) params.append('$top', options.top.toString());
if (options?.skip) params.append('$skip', options.skip.toString());
if (options?.count) params.append('$count', 'true');

const url = `${this.baseUrl}/${entitySet}?${params.toString()}`;
const response = await fetch(url);

if (!response.ok) {
throw new Error(`OData query failed: ${response.statusText}`);
}

return await response.json();
}

// 単一エンティティの取得
async getById<T>(entitySet: string, id: number | string): Promise<T> {
const url = `${this.baseUrl}/${entitySet}(${id})`;
const response = await fetch(url);

if (!response.ok) {
throw new Error(`OData get failed: ${response.statusText}`);
}

return await response.json();
}

// 作成
async create<T>(entitySet: string, entity: Partial<T>): Promise<T> {
const url = `${this.baseUrl}/${entitySet}`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(entity),
});

if (!response.ok) {
throw new Error(`OData create failed: ${response.statusText}`);
}

return await response.json();
}

// 更新
async update<T>(
entitySet: string,
id: number | string,
entity: Partial<T>
): Promise<void> {
const url = `${this.baseUrl}/${entitySet}(${id})`;
const response = await fetch(url, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(entity),
});

if (!response.ok) {
throw new Error(`OData update failed: ${response.statusText}`);
}
}

// 削除
async delete(entitySet: string, id: number | string): Promise<void> {
const url = `${this.baseUrl}/${entitySet}(${id})`;
const response = await fetch(url, {
method: 'DELETE',
});

if (!response.ok) {
throw new Error(`OData delete failed: ${response.statusText}`);
}
}
}

// 使用例
interface Product {
Id: number;
Name: string;
Price: number;
Stock: number;
CategoryId: number;
Category?: Category;
}

interface Category {
Id: number;
Name: string;
Products?: Product[];
}

const client = new ODataClient('https://api.example.com/odata');

// 複雑なクエリの実行
async function fetchProducts() {
const result = await client.query<Product>('Products', {
filter: "Price gt 50 and contains(Name, 'Pro')",
select: ['Id', 'Name', 'Price'],
expand: ['Category($select=Name)'],
orderby: 'Price desc',
top: 20,
skip: 0,
count: true,
});

console.log(`Total count: ${result['@odata.count']}`);
console.log('Products:', result.value);
}

// ページネーション
async function fetchProductsPage(page: number, pageSize: number = 20) {
const skip = (page - 1) * pageSize;

const result = await client.query<Product>('Products', {
orderby: 'Id',
top: pageSize,
skip: skip,
count: true,
});

return {
items: result.value,
total: result['@odata.count'],
page: page,
pageSize: pageSize,
totalPages: Math.ceil((result['@odata.count'] || 0) / pageSize),
};
}

// CRUD 操作
async function crudOperations() {
// 作成
const newProduct = await client.create<Product>('Products', {
Name: 'New Product',
Price: 99.99,
Stock: 50,
CategoryId: 1,
});
console.log('Created:', newProduct);

// 読取
const product = await client.getById<Product>('Products', newProduct.Id);
console.log('Retrieved:', product);

// 更新
await client.update<Product>('Products', newProduct.Id, {
Price: 89.99,
Stock: 45,
});
console.log('Updated');

// 削除
await client.delete('Products', newProduct.Id);
console.log('Deleted');
}

OData と他のAPIスタイルの比較

OData vs REST

側面OData標準的なREST
標準化厳密に標準化されている柔軟、慣例ベース
クエリ機能強力な組み込みクエリカスタム実装が必要
メタデータ自動的に提供される通常は提供されない
学習曲線やや複雑シンプル
柔軟性標準に従う必要がある自由度が高い
適用シーン複雑なクエリ、企業アプリ一般的なWebアプリ

OData vs GraphQL

側面ODataGraphQL
クエリ言語URL クエリパラメータ専用のクエリ言語
型システムEDM(Entity Data Model)GraphQLスキーマ
過剰取得/不足取得$select/$expand で制御クエリで正確に指定
変更操作標準的なHTTP動詞Mutation
リアルタイムなしSubscription
ツールエコシステム成熟(Microsoft系)非常に豊富

セキュリティの考慮事項

1. 認証と認可

// JWT ベアラートークン認証
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
};
});

// エンティティレベルの認可
[Authorize(Roles = "Admin,User")]
[EnableQuery]
public IQueryable<Product> Get()
{
return _context.Products;
}

2. レート制限

// ASP.NET Core のレート制限ミドルウェア
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.User.Identity?.Name ?? httpContext.Request.Headers.Host.ToString(),
factory: partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 100,
QueueLimit = 0,
Window = TimeSpan.FromMinutes(1)
}));
});

app.UseRateLimiter();

3. CORS 設定

builder.Services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigin",
builder => builder
.WithOrigins("https://yourdomain.com")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});

app.UseCors("AllowSpecificOrigin");

4. 入力検証

public class ProductValidator : AbstractValidator<Product>
{
public ProductValidator()
{
RuleFor(p => p.Name)
.NotEmpty().WithMessage("Product name is required")
.MaximumLength(100).WithMessage("Name must not exceed 100 characters");

RuleFor(p => p.Price)
.GreaterThan(0).WithMessage("Price must be greater than 0");

RuleFor(p => p.Stock)
.GreaterThanOrEqualTo(0).WithMessage("Stock cannot be negative");
}
}

// コントローラーで使用
public async Task<IActionResult> Post([FromBody] Product product)
{
var validator = new ProductValidator();
var validationResult = await validator.ValidateAsync(product);

if (!validationResult.IsValid)
{
return BadRequest(validationResult.Errors);
}

_context.Products.Add(product);
await _context.SaveChangesAsync();

return Created(product);
}

まとめ

OData の主な利点

  • 標準化: 一貫性のある API 設計
  • 強力なクエリ: 柔軟なフィルタリング、ソート、ページネーション
  • メタデータ: 自己記述型のスキーマ
  • ツールサポート: 豊富なライブラリとツール
  • 生産性: 定型的なコードの削減

OData の課題

  • 複雑性: 学習曲線がやや急
  • オーバーヘッド: メタデータとクエリ処理のコスト
  • 柔軟性の制約: 標準に従う必要がある
  • パフォーマンス: 不適切なクエリによる問題

推奨される使用シナリオ

  1. 企業向けアプリケーション: 複雑なデータクエリとレポート
  2. データ探索ツール: ダッシュボード、BI統合
  3. Microsoft エコシステム: SharePoint、Dynamics、Power Platform
  4. API の標準化: 組織全体で一貫したAPI設計
  5. 自動コード生成: メタデータからのクライアント生成

参考リンク