zukucode
主にWEB関連の情報を技術メモとして発信しています。

ASP.NETに導入したMediatRのラッパークラスを作成する

ASP.NETにMediatRを導入するASP.NETWebAPIのプロジェクトにMediatRを導入しましたが、今回はラッパークラスを作成し、もっと汎用的に実装できるように修正します。

処理は主に検索系Queryと更新系Commandの2つに分けられるので汎用的なクラスを2パターン作成します。

目的

外部ライブラリを直接参照すると、外部ライブラリの仕様変更があると、参照しているファイルをすべて修正する必要が出てくるため、このようなラッパークラスを作成することがあります。

よくあるケースとして、データベースのアクセスライブラリのラッパークラスがイメージしやすいかと思います。

また、MediatRについては、抽象化しておくことによりパイプライン(IPipelineBehavior)の実装がやりやすくなります。

インターフェースの作成

出力クラスを抽象化した、IResultというインターフェースを作成します。

全て(QueryCommand両方)の出力クラスはこのインターフェースを継承するようにします。

IResult.cs
public interface IResult
{
}

さらに、Query用とCommand用でそれぞれインターフェースを作成します。

IQueryResult.cs
public interface IQueryResult : IResult
{
}
ICommandResult.cs
public interface ICommandResult : IResult
{
}

次に、入力クラスを抽象化したインターフェースをQuery用とCommand用でそれぞれ作成します。

入力クラスはIRequest<出力クラス>のインターフェースを継承して作成しますが、出力クラスには上記で作成したIQueryResultICommandResultを指定します。

IQuery.cs
using MediatR;

public interface IQuery : IRequest<IQueryResult>
{
}
ICommand.cs
using MediatR;

public interface ICommand : IRequest<ICommandResult>
{
}

次に、ハンドラクラスを抽象化したクラスをQuery用とCommand用でそれぞれ作成します。

IRequestHandler<入力クラス, 出力クラス>の形式で指定しますが、出力クラスはIQueryResultまたはICommandResultを指定します。

入力クラスはジェネリック型とし、IQueryまたはICommandを継承したクラスを指定できるようにします。

QueryHandler.cs
using MediatR;

public abstract class QueryHandler<TQuery> : IRequestHandler<TQuery, IQueryResult> where TQuery : class, IQuery
{
    public abstract Task<IQueryResult> Handle(TQuery request, CancellationToken cancellationToken);
}
CommandHandler.cs
using MediatR;

public abstract class CommandHandler<TQuery> : IRequestHandler<TQuery, ICommandResult> where TQuery : class, ICoomand
{
    public abstract Task<ICommandResult> Handle(TQuery request, CancellationToken cancellationToken);
}

出力クラスはIQueryResultまたはICommandResult固定としましたが、不都合がある場合は出力クラスもジェネリック型で受け取れるようにしてください。

※後述のコントローラーの修正に影響します。

各処理の修正

ASP.NETにMediatRを導入するで実装した各クラスを上記の抽象化したインターフェースなどを継承するように修正します。

Queryのみ記載していますが、Commandの場合も考え方は同じです。

UserFindByIdQuery.c#
using MediatR;

public class UserFindByIdQuery : IRequest<UserFindByIdQueryResult>
public class UserFindByIdQuery : IQuery
{
    public UserFindByIdQuery(string id)
    {
        Id = id;
    }

    public string Id { get; }
}
UserFindByIdQuery.c#
public class UserFindByIdQueryResult
public class UserFindByIdQueryResult : IQueryResult
{
    public UserFindByIdQueryResult(string id, string  name)
    {
        Id = id;
        Name = name;
    }

    public string Id { get; }
    public string Name { get; }
}
UserFindByIdQueryHandler.cs
using MediatR;

public class UserFindByIdQueryHandler : IRequestHandler<UserFindByIdQuery, UserFindByIdQueryResult>
public class UserFindByIdQueryHandler : QueryHandler<UserFindByIdQuery>
{
    private readonly IUserQueryService _queryService;

    public UserFindByIdQueryHandler(IUserQueryService queryService)
    {
        _queryService = queryService;
    }

    public async Task<UserFindByIdQueryResult> Handle(UserFindByIdQuery query, CancellationToken cancellationToken)
    public override async Task<IQueryResult> Handle(UserFindByIdQuery query, CancellationToken cancellationToken)
    {
        return new UserFindByIdQueryResult("1", "ユーザーテスト");
        // 実際は以下のようにDBなどから取得することになります。
        // return await _queryService.FindById(query.Id);
    }
}

各ファイルでusing MediatR;が不要になることを確認できます。(直接MediatRを使用しない)

コントローラーの修正

出力クラスがUserFindByIdQueryResultという具象クラスではなく、IQueryResultという抽象クラス(インターフェース)になったので、UserFindByIdQueryResultをレスポンスするには、以下のように型変換が必要になります。

[HttpGet("{id}")]
public async Task<UserFindByIdQueryResult> FindById(string id)
{
    return await Mediator.Send(new UserFindByIdQuery(id));
    return (UserFindByIdQueryResult)await Mediator.Send(new UserFindByIdQuery(id));
}

型変換ではなく、コントローラーのレスポンスはIActionResultという抽象化したインターフェースで返却することができますので、以下のように修正します。

[HttpGet("{id}")]
public async Task<UserFindByIdQueryResult> FindById(string id)
public async Task<IActionResult> FindById(string id)
{
    return await Mediator.Send(new UserFindByIdQuery(id));
   return Ok(await Mediator.Send(new UserFindByIdQuery(id)));
}

UserFindByIdQueryResultという具象クラスをレスポンスするのではなく、IQueryResultという抽象クラス(インターフェース)をそのままレスポンスする形になります。

Okファンクションはステータスコード200で、引数の値をレスポンスします。

ほかにもNoContentBadRequestなど、レスポンス用のファンクションがあります。

なお、実際のレスポンスデータには具象クラスのデータが入っています。

UserFindByIdQueryResultの値(IdName)を参照する場合は型変換が必要になりますが、Api層では基本的にはApplication層の結果をそのまま返却することが多いです。(UserFindByIdQueryResultの値(IdName)を参照して、いろいろと処理を行うのはApplication層の責務です)

swaggerなどで、レスポンスの型を明示する場合は以下のようにします。

[HttpGet("{id}")]
[ProducesResponseType(typeof(UserFindByIdQueryResult), StatusCodes.Status200OK)]
public async Task<IActionResult> FindById(string id)
{
   return Ok(await Mediator.Send(new UserFindByIdQuery(id)));
}

以上で、抽象化したインターフェースを使用して、処理を実装することができました。



関連記事