Ответ 1
Чтобы подчеркнуть различия, о которых вы должны думать при разработке служб, основанных на сообщениях, в ServiceStack Я приведу несколько примеров сравнение подхода WCF/WebApi и ServiceStack:
Проект WCF vs ServiceStack API
WCF рекомендует вам рассматривать веб-службы как обычные вызовы методов С#, например:
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);
}
Это то же самое соглашение с Сервисом, что и в ServiceStack с Новый API:
public class Customers : IReturn<List<Customer>>
{
public int[] Ids { get; set; }
public string[] UserNames { get; set; }
public string[] Emails { get; set; }
}
Важное понятие, о котором следует помнить, состоит в том, что весь запрос (aka Request) фиксируется в сообщении запроса (т.е. запросе DTO), а не в сигнатурах метода сервера. Очевидным непосредственным преимуществом принятия дизайна на основе сообщений является то, что любая комбинация вышеупомянутых вызовов RPC может быть выполнена в 1 удаленном сообщении с помощью единой службы.
WebApi vs ServiceStack API Design
Аналогично, WebApi продвигает аналогичный С# -подобный RPC Api, который выполняет WCF:
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);
}
}
Дизайн API на основе сообщений ServiceStack
Пока 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 на основе сообщений.
Групповая семантика и типы ответов
В этом примере он сгруппирован в 2 разные службы на основе Семантики вызовов и Типы ответов:
Каждое свойство в каждом запросе DTO имеет ту же семантику, что и для FindProducts
, каждое свойство действует как фильтр (например, AND), а в GetProduct
действует как комбинатор (например, OR). Сервисы также возвращают типы возвращаемого типа IEnumerable<Product>
и Product
, которые потребуют различной обработки на сайтах запросов Typed API.
В WCF/WebAPI (и других инфраструктурах служб RPC) всякий раз, когда у вас есть требование для конкретного клиента, вы должны добавить новую подпись сервера на контроллере, которая соответствует этому запросу. Однако в сервисе ServiceStack, основанном на сообщениях, вы всегда должны думать о том, где находится эта функция, и можете ли вы улучшить существующие сервисы. Вам также следует подумать о том, как вы можете поддержать требование, специфичное для клиента, в общем, чтобы тот же сервис мог принести пользу другим будущим потенциальным случаям использования.
Рефакторинг служб GetBooking Limits
С приведенной выше информацией мы можем начать рефакторинг ваших услуг. Поскольку у вас есть две разные службы, которые возвращают разные результаты, например. GetBookingLimit
возвращает 1 элемент, а GetBookingLimits
возвращает много, их нужно хранить в разных службах.
Различать служебные операции и типы
Однако у вас должно быть четкое разделение между вашими сервисными операциями (например, Request DTO), которое является уникальным для каждой службы и используется для захвата запроса Services и возвращаемых типов 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 я стараюсь оставить их вне определения маршрута, когда они не являются двусмысленными, поскольку это меньше кода.
Сохранять согласованную номенклатуру
Вы должны зарезервировать слово Получить для служб, которые запрашивают уникальные или первичные ключи, т.е. когда поставляемое значение соответствует полю (например, Id), оно только Получает 1 результат. Для поисковых служб, которые действуют как фильтр и возвращают несколько совпадающих результатов, которые попадают в желаемый диапазон, я использую либо Найти, либо Поиск, чтобы сообщить, что это так.
Цель для самостоятельного описания Сервисных контрактов
Также старайтесь описать каждое из ваших имен полей, эти свойства являются частью вашего публичного API и должны быть самоописательными в отношении того, что он делает. Например. Просто взглянув на Сервисный контракт (например, запрос DTO), мы не знаем, что делает Дата, я предположил BookedAfter, но он также мог бы быть BookedBefore или BookedOn, если он только возвратил заказы, сделанные в этот день.
Преимущество этого теперь в том, что читаемые типизированные клиенты .NET становятся более удобными для чтения:
Product product = client.Get(new GetProduct { Id = 1 });
List<Product> results = client.Get(
new FindBookingLimits { BookedAfter = DateTime.Today });
Реализация службы
Я удалил атрибут [Authenticate]
из ваших запросов DTO, так как вы можете просто указать его один раз в реализации службы, который теперь выглядит следующим образом:
[Authenticate]
public class BookingLimitService : AppServiceBase
{
public BookingLimit Get(GetBookingLimit request) { ... }
public List<BookingLimit> Get(FindBookingLimits request) { ... }
}
Обработка и проверка ошибок
Для получения информации о том, как добавить подтверждение, вы либо имеете возможность просто выбросить исключения С# и применить к ним свои собственные настройки, в противном случае у вас есть возможность использовать встроенную Fluent Validation, но вам не нужно вводить их в свою службу, так как вы можете связать их с помощью одного в вашем AppHost, например:
container.RegisterValidators(typeof(CreateBookingValidator).Assembly);
Валидаторы - это бесконтактный и инвазивный свободный смысл, который вы можете добавить с помощью многоуровневого подхода и поддерживать их без изменения реализации службы или классов DTO. Поскольку они требуют дополнительного класса, я бы использовал их только при работе с побочными эффектами (например, POST/PUT), так как GETs имеют тенденцию к минимальной проверке, а выброс С# Exception требует меньше котельной пластины. Итак, пример валидатора, который у вас может быть, - это когда вы сначала создаете бронирование:
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
.