ServiceStack Запросить дизайн DTO

Я разработчик .Net, используемый для разработки веб-приложения в Microsoft Technologies. Я пытаюсь воспитывать себя, чтобы понять подход REST для веб-сервисов. Пока я люблю структуру 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, который у меня есть. Поэтому у меня возникает ощущение, что я должен поддерживать низкий уровень обслуживания, группируя подобные службы в одну службу.

Но вопрос, который возникает у меня в голове, что я должен отправить больше информации, чем клиент, для этого запроса?

Я думаю, что мой образ мышления должен измениться, потому что я не доволен текущим кодом, который я написал, думая, как парень WCF.

Может ли кто-нибудь показать мне правильное направление, чтобы следовать.

Ответ 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.

Ответ 2

"Отклик Dtos" кажется ненужным, поскольку свойство ResponseStatus больше не требуется.. Хотя, я думаю, вам может понадобиться соответствующий класс Response, если вы используете SOAP. Если вы удалите Response Dtos, вам больше не нужно будет перетаскивать BookLimit в объекты Response. Кроме того, ServiceStack TranslateTo() также может помочь.

Ниже я попытаюсь упростить то, что вы разместили... YMMV.

Сделайте DTO для BookingLimit - это будет представление 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; }
}

Запросы и Dtos очень важны

[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; }
}

Больше не возвращать объекты Ответа... просто 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();
    }
}