私は、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の人のように考えて書いた現在のコードに満足していないので、私の考え方が変わるはずだと思います。
誰かが私に続く正しい方向を示すことができます。
ServiceStack でメッセージベースのサービスを設計するときに考慮する必要がある違いを味わうために、WCF/WebApiとServiceStackのアプローチを比較する例をいくつか示します。
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は、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では、メッセージベースのデザインを保持することをお勧めします。
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でサポートする方法を考えて、同じサービスが他の将来の潜在的なユースケースに役立つようにする必要があります。
上記の情報を使用して、サービスのリファクタリングを開始できます。異なる結果を返す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
と名前を付けます。
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();
}
}