web-dev-qa-db-ja.com

ServiceStack Request DTOの設計

私は、Microsoft TechnologiesでWebアプリケーションを開発するのに使用される.Net開発者です。 REST Webサービスのアプローチを理解するために自分自身を教育しようとしています。これまでのところ、ServiceStackフレームワークが大好きです。

しかし、WCFで慣れ親しんでいる方法でサービスを記述することに気付くことがあります。だから私は私を悩ませている質問があります。

2つのリクエストDTOがあるため、次のような2つのサービスがあります。

[Route("/bookinglimit", "GET")]
[Authenticate]
public class GetBookingLimit : IReturn<GetBookingLimitResponse>
{
    public int Id { get; set; }
}
public class GetBookingLimitResponse
{
    public int Id { get; set; }
    public int ShiftId { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
    public int Limit { get; set; }

    public ResponseStatus ResponseStatus { get; set; }
}

[Route("/bookinglimits", "GET")]
[Authenticate]
public class GetBookingLimits : IReturn<GetBookingLimitsResponse>
{      
    public DateTime Date { get; set; }
}
public class GetBookingLimitsResponse
{
    public List<GetBookingLimitResponse> BookingLimits { get; set; }
    public ResponseStatus ResponseStatus { get; set; }
}

これらのリクエストDTOで見られるように、私はほぼすべてのサービスに対して同様のリクエストDTOを持っていますが、これはDRYではないようです。

そのため、GetBookingLimitResponse内のリストでGetBookingLimitsResponseクラスを使用しようとしました。ResponseStatus内のGetBookingLimitResponseクラスは、GetBookingLimitsサービス。

また、次のようなこれらのリクエストのサービス実装もあります。

public class BookingLimitService : AppServiceBase
{
    public IValidator<AddBookingLimit> AddBookingLimitValidator { get; set; }

    public GetBookingLimitResponse Get(GetBookingLimit request)
    {
        BookingLimit bookingLimit = new BookingLimitRepository().Get(request.Id);
        return new GetBookingLimitResponse
        {
            Id = bookingLimit.Id,
            ShiftId = bookingLimit.ShiftId,
            Limit = bookingLimit.Limit,
            StartDate = bookingLimit.StartDate,
            EndDate = bookingLimit.EndDate,
        };
    }

    public GetBookingLimitsResponse Get(GetBookingLimits request)
    {
        List<BookingLimit> bookingLimits = new BookingLimitRepository().GetByRestaurantId(base.UserSession.RestaurantId);
        List<GetBookingLimitResponse> listResponse = new List<GetBookingLimitResponse>();

        foreach (BookingLimit bookingLimit in bookingLimits)
        {
            listResponse.Add(new GetBookingLimitResponse
                {
                    Id = bookingLimit.Id,
                    ShiftId = bookingLimit.ShiftId,
                    Limit = bookingLimit.Limit,
                    StartDate = bookingLimit.StartDate,
                    EndDate = bookingLimit.EndDate
                });
        }


        return new GetBookingLimitsResponse
        {
            BookingLimits = listResponse.Where(l => l.EndDate.ToShortDateString() == request.Date.ToShortDateString() && l.StartDate.ToShortDateString() == request.Date.ToShortDateString()).ToList()
        };
    }
}

ご覧のように、ここでも検証機能を使用したいので、DTOのすべてのリクエストに対して検証クラスを作成する必要があります。そのため、同様のサービスを1つのサービスにグループ化することで、サービス番号を低く抑える必要があると感じています。

しかし、クライアントがその要求に必要とする以上の情報を送信する必要があるという私の頭の中に浮かぶ質問はありますか?

私はWCFの人のように考えて書いた現在のコードに満足していないので、私の考え方が変わるはずだと思います。

誰かが私に続く正しい方向を示すことができます。

39
mustafasturan

ServiceStack でメッセージベースのサービスを設計するときに考慮する必要がある違いを味わうために、WCF/WebApiとServiceStackのアプローチを比較する例をいくつか示します。

WCF vs ServiceStack API Design

WCFでは、Webサービスを通常のC#メソッド呼び出しと考えることをお勧めします。例:

public interface IWcfCustomerService
{
    Customer GetCustomerById(int id);
    List<Customer> GetCustomerByIds(int[] id);
    Customer GetCustomerByUserName(string userName);
    List<Customer> GetCustomerByUserNames(string[] userNames);
    Customer GetCustomerByEmail(string email);
    List<Customer> GetCustomerByEmails(string[] emails);
}

これは、 新しいAPI を使用したServiceStackでの同じサービスコントラクトの外観です。

public class Customers : IReturn<List<Customer>> 
{
   public int[] Ids { get; set; }
   public string[] UserNames { get; set; }
   public string[] Emails { get; set; }
}

覚えておくべき重要な概念は、クエリ全体(別名リクエスト)が、サーバーメソッドのシグネチャではなく、リクエストメッセージ(つまり、リクエストDTO)でキャプチャされることです。メッセージベースの設計を採用することの明白な直接的な利点は、単一のサービス実装により、上記のRPC呼び出しの任意の組み合わせを1つのリモートメッセージで実現できることです。

WebApi vs ServiceStack API Design

同様に、WebApiは、WCFが行う同様のC#に似たRPC Apiを促進します。

public class ProductsController : ApiController 
{
    public IEnumerable<Product> GetAllProducts() {
        return products;
    }

    public Product GetProductById(int id) {
        var product = products.FirstOrDefault((p) => p.Id == id);
        if (product == null)
        {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }
        return product;
    }

    public Product GetProductByName(string categoryName) {
        var product = products.FirstOrDefault((p) => p.Name == categoryName);
        if (product == null)
        {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }
        return product;
    }

    public IEnumerable<Product> GetProductsByCategory(string category) {
        return products.Where(p => string.Equals(p.Category, category,
                StringComparison.OrdinalIgnoreCase));
    }

    public IEnumerable<Product> GetProductsByPriceGreaterThan(decimal price) {
        return products.Where((p) => p.Price > price);
    }
}

ServiceStackメッセージベースのAPI設計

ServiceStackでは、メッセージベースのデザインを保持することをお勧めします。

public class FindProducts : IReturn<List<Product>> {
    public string Category { get; set; }
    public decimal? PriceGreaterThan { get; set; }
}

public class GetProduct : IReturn<Product> {
    public int? Id { get; set; }
    public string Name { get; set; }
}

public class ProductsService : Service 
{
    public object Get(FindProducts request) {
        var ret = products.AsQueryable();
        if (request.Category != null)
            ret = ret.Where(x => x.Category == request.Category);
        if (request.PriceGreaterThan.HasValue)
            ret = ret.Where(x => x.Price > request.PriceGreaterThan.Value);            
        return ret;
    }

    public Product Get(GetProduct request) {
        var product = request.Id.HasValue
            ? products.FirstOrDefault(x => x.Id == request.Id.Value)
            : products.FirstOrDefault(x => x.Name == request.Name);

        if (product == null)
            throw new HttpError(HttpStatusCode.NotFound, "Product does not exist");

        return product;
    }
}

再びリクエストDTOでリクエストの本質をキャプチャします。メッセージベースの設計では、5つの個別のRPC WebAPIサービスを2つのメッセージベースのServiceStackサービスに凝縮することもできます。

呼び出しセマンティクスおよび応答タイプごとのグループ化

この例ではCall SemanticsおよびResponse Typesに基づいて2つの異なるサービスにグループ化されています。

各リクエストDTOのすべてのプロパティは、FindProductsの場合と同じセマンティクスを持ち、各プロパティはフィルタ(例:AND)のように機能し、GetProductではコンビネータ(例:OR)のように機能します。また、サービスはIEnumerable<Product>およびProduct戻り値の型を返しますが、これらは型付きAPIの呼び出しサイトで異なる処理を必要とします。

WCF/WebAPI(および他のRPCサービスフレームワーク)では、クライアント固有の要件がある場合は常に、その要求に一致する新しいサーバー署名をコントローラーに追加します。ただし、ServiceStackのメッセージベースのアプローチでは、この機能がどこに属し、既存のサービスを強化できるかどうかを常に検討する必要があります。また、クライアント固有の要件をgeneric wayでサポートする方法を考えて、同じサービスが他の将来の潜在的なユースケースに役立つようにする必要があります。

GetBooking Limitsサービスのリファクタリング

上記の情報を使用して、サービスのリファクタリングを開始できます。異なる結果を返す2つの異なるサービスがあるため、 GetBookingLimitは1つのアイテムを返し、GetBookingLimitsは多くのアイテムを返します。これらは異なるサービスに保持する必要があります。

サービス操作とタイプの区別

ただし、サービスごとに一意であり、サービスのリクエストをキャプチャするために使用されるサービス操作(リクエストDTOなど)と、返されるDTOタイプとの間には明確な分割が必要です。要求DTOは通常アクションであるため、動詞です。一方、DTOタイプはエンティティ/データコンテナであるため、名詞です。

一般的な応答を返す

新しいAPIでは、ServiceStack応答 はResponseStatus プロパティを必要としなくなりました。これが存在しない場合、汎用ErrorResponse DTOが代わりにクライアントでスローおよびシリアル化されるためです。これにより、応答にResponseStatusプロパティを含める必要がなくなります。それで、私はあなたの新しいサービスの契約を次のようにリファクタリングすると言いました:

[Route("/bookinglimits/{Id}")]
public class GetBookingLimit : IReturn<BookingLimit>
{
    public int Id { get; set; }
}

public class BookingLimit
{
    public int Id { get; set; }
    public int ShiftId { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
    public int Limit { get; set; }
}

[Route("/bookinglimits/search")]
public class FindBookingLimits : IReturn<List<BookingLimit>>
{      
    public DateTime BookedAfter { get; set; }
}

GETリクエストの場合、コードが少ないため、あいまいでない場合はルート定義から除外する傾向があります。

一貫した命名法を維持する

ワードを予約する必要がありますGet一意のフィールドまたはプライマリキーフィールドでクエリを実行するサービス、つまり、指定された値がフィールド(IDなど)に一致する場合、それはGets 1結果のみです。フィルターのように機能し、目的の範囲内に収まる複数の一致結果を返す検索サービスの場合、FindまたはSearch動詞を使用して、これが事実であることを知らせます。

自己記述的なサービス契約を目指す

また、各フィールド名を説明するようにしてください。これらのプロパティはpublic APIの一部であり、それが何をするかについて自己記述的でなければなりません。例えば。サービス契約(Request DTOなど)を見るだけでは、Dateが何をするのかわかりませんが、BookedAfterと仮定しましたが、BookedBeforeまたはBookedOnは、その日に行われた予約のみを返した場合。

これの利点は、 タイプされた.NETクライアント の呼び出しサイトが読みやすくなることです。

Product product = client.Get(new GetProduct { Id = 1 });

List<Product> results = client.Get(
    new FindBookingLimits { BookedAfter = DateTime.Today });

サービス実装

リクエストDTOから[Authenticate]属性を削除しました。サービス実装で一度指定するだけで、次のようになります。

[Authenticate]
public class BookingLimitService : AppServiceBase 
{ 
    public BookingLimit Get(GetBookingLimit request) { ... }

    public List<BookingLimit> Get(FindBookingLimits request) { ... }
}

エラー処理と検証

検証を追加する方法については、 C#exceptions をスローするオプションがあり、独自のカスタマイズを適用します。それ以外の場合は、組み込みの Fluent Validation しかし、AppHostの1行ですべてを配線できるので、それらをサービスに注入する必要はありません。例:

container.RegisterValidators(typeof(CreateBookingValidator).Assembly);

バリデータはノータッチで侵襲性がないため、レイヤー化されたアプローチを使用して追加し、サービス実装またはDTOクラスを変更せずにそれらを維持できます。余分なクラスが必要なため、GETの検証は最小限である傾向があり、C#例外のスローに必要なボイラープレートが少なくなる傾向があるため、副作用(POST/PUTなど)の操作でのみ使用します。したがって、あなたが持つことができるバリデータの例は、最初に予約を作成するときです:

public class CreateBookingValidator : AbstractValidator<CreateBooking>
{
    public CreateBookingValidator()
    {
        RuleFor(r => r.StartDate).NotEmpty();
        RuleFor(r => r.ShiftId).GreaterThan(0);
        RuleFor(r => r.Limit).GreaterThan(0);
    }
}

個別のCreateBookingおよびUpdateBooking DTOを使用する代わりに、ユースケースに応じて、両方に同じリクエストDTOを再使用します。その場合、StoreBookingと名前を付けます。

88
mythz

ResponseStatusプロパティは 不要になった なので、「Reponse Dtos」は不要のようです。ただし、SOAPを使用する場合は、対応するResponseクラスが必要になる場合があります。 Response Dtosを削除すると、BookLimitをResponseオブジェクトに押し込む必要がなくなります。また、ServiceStackのTranslateTo()も役立ちます。

以下は、あなたが投稿したものを単純化しようとする方法です... YMMV。

BookingLimitのDTOを作成する-これは、他のすべてのシステムに対するBookingLimitの表現になります。

public class BookingLimitDto
{
    public int Id { get; set; }
    public int ShiftId { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
    public int Limit { get; set; }
}

リクエストとDtoは 非常に重要

[Route("/bookinglimit", "GET")]
[Authenticate]
public class GetBookingLimit : IReturn<BookingLimitDto>
{
    public int Id { get; set; }
}

[Route("/bookinglimits", "GET")]
[Authenticate]
public class GetBookingLimits : IReturn<List<BookingLimitDto>>
{
    public DateTime Date { get; set; }
}

Reponseオブジェクトを返さなくなりました...ちょうどBookingLimitDto

public class BookingLimitService : AppServiceBase 
{ 
    public IValidator AddBookingLimitValidator { get; set; }

    public BookingLimitDto Get(GetBookingLimit request)
    {
        BookingLimitDto bookingLimit = new BookingLimitRepository().Get(request.Id);
        //May need to bookingLimit.TranslateTo<BookingLimitDto>() if BookingLimitRepository can't return BookingLimitDto

        return bookingLimit; 
    }

    public List<BookingLimitDto> Get(GetBookingLimits request)
    {
        List<BookingLimitDto> bookingLimits = new BookingLimitRepository().GetByRestaurantId(base.UserSession.RestaurantId);
        return
            bookingLimits.Where(
                l =>
                l.EndDate.ToShortDateString() == request.Date.ToShortDateString() &&
                l.StartDate.ToShortDateString() == request.Date.ToShortDateString()).ToList();
    }
} 
10
paaschpa