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

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

Я пытался использовать 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.

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

2 ответов


чтобы дать вам вкус различий, о которых вы должны думать при разработке услуг на основе сообщений в ServiceStack я приведу несколько примеров, сравнивающих подход WCF / WebApi против ServiceStack:

WCF vs ServiceStack API Design

WCF рекомендует вам думать о веб-службах как о обычных вызовах метода C#, e.g:

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 запрос) захватывается в сообщении запроса (т. е. запрос DTO), а не в сигнатурах метода сервера. Очевидным непосредственным преимуществом принятия дизайна на основе сообщений является то, что любая комбинация вышеуказанных вызовов RPC может быть выполнена в 1 удаленном сообщении с помощью одной реализации службы.

веб-API против ServiceStack API-интерфейс Дизайн

дополнительно веб-API способствует похож на C#-как API-интерфейс RPC, что 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 каждое свойство действует как фильтр (например, и) в то время как в GetProduct он действует как комбинатор (например, OR). Услуги и вернуть IEnumerable<Product> и Product возвращаемые типы, которые требуют различной обработки в сайты вызовов типизированных API.

в WCF / WebAPI (и других фреймворках служб RPC) всякий раз, когда у вас есть клиентское требование, вы добавляете новую подпись сервера на контроллере, который соответствует этому запросу. Однако в подходе на основе сообщений ServiceStack вы всегда должны думать о том, где эта функция принадлежит и можете ли вы улучшить существующие службы. Вы также должны подумать о том, как вы можете поддерживать клиентское требование в родовые пути чтобы та же услуга могла принести пользу другим будущим потенциальным случаям использования.

Перефакторинг Getbooking ограничивает услуги

С информацией выше мы можем начать перефакторинг ваших услуг. Поскольку у вас есть 2 разных сервиса, которые возвращают разные результаты, например GetBookingLimit возвращает 1 пункт и GetBookingLimits возвращает много, они должны храниться в разных службах.

различать операции Службы и типы

вы должны однако имейте чистое разделение между вашими операциями службы (например, запросом DTO), который уникален для каждой службы и используется для захвата запроса служб, и типами DTO, которые они возвращают. Запросы DTOs обычно являются действиями, поэтому они являются глаголами, а типы DTO-сущностями / контейнерами данных, поэтому они являются существительными.

возврат общих ответов

в новом API ответы ServiceStack больше не требуется ResponseStatus свойство, так как если оно не существует generic 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), оно только получает 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] атрибут из вашего запроса DTOs, так как вы можете вместо этого просто указать его один раз в реализации службы, которая теперь выглядит так:

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

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

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

ниже я бы попытался упростить то, что вы опубликовали...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; }
}

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