ServiceStack Request DTO design

Soy un desarrollador de .Net usado para desarrollar aplicaciones web en Microsoft Technologies. Estoy tratando de educarme a mí mismo para comprender el enfoque REST para los servicios web. Hasta ahora estoy amando el marco de ServiceStack.

Pero a veces me encuentro escribiendo servicios de una manera a la que estoy acostumbrado con WCF. Entonces tengo una pregunta que me molesta.

Tengo 2 solicitudes de DTO para 2 servicios como estos:

[Route("/bookinglimit", "GET")] [Authenticate] public class GetBookingLimit : IReturn { 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 { public DateTime Date { get; set; } } public class GetBookingLimitsResponse { public List BookingLimits { get; set; } public ResponseStatus ResponseStatus { get; set; } } 

Como se ve en estas solicitudes de DTO, tengo solicitudes similares de DTO casi para todos los servicios y esto parece no ser SECO.

Traté de utilizar la clase GetBookingLimitResponse en una lista dentro de GetBookingLimitsResponse por esa razón ResponseStatus dentro de la clase GetBookingLimitResponse está duplicada en caso de que tenga un error en el servicio GetBookingLimits .

También tengo implementaciones de servicios para estas solicitudes, como:

 public class BookingLimitService : AppServiceBase { public IValidator 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 bookingLimits = new BookingLimitRepository().GetByRestaurantId(base.UserSession.RestaurantId); List listResponse = new List(); 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() }; } } 

Como ve, también quiero usar la función de validación aquí, así que tengo que escribir clases de validación para cada DTO de solicitud que tengo. Así que tengo la sensación de que debo mantener mi número de servicio bajo al agrupar servicios similares en un solo servicio.

Pero la pregunta aquí que aparece en mi mente es que debería enviar más información que la necesitada por el cliente para esa solicitud.

Creo que mi forma de pensar debería cambiar porque no estoy contento con el código actual que escribí pensando como un tipo WCF.

¿Puede alguien mostrarme la dirección correcta para seguir?

Para darle una idea de las diferencias que debe tener en cuenta al diseñar servicios basados ​​en mensajes en ServiceStack , proporcionaré algunos ejemplos que comparan el enfoque de WCF / WebApi frente a ServiceStack:

WCF vs ServiceStack API Design

WCF lo alienta a pensar en los servicios web como llamadas normales al método C #, por ejemplo:

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

Así es como se vería el mismo contrato de servicio en ServiceStack con la nueva API :

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

El concepto importante a tener en cuenta es que toda la consulta (también conocida como Solicitud) se captura en el Mensaje de solicitud (es decir, solicitud de DTO) y no en las firmas de método del servidor. El beneficio obvio inmediato de adoptar un diseño basado en mensajes es que cualquier combinación de las llamadas RPC anteriores puede cumplirse en 1 mensaje remoto, mediante una única implementación de servicio.

WebApi vs ServiceStack API Design

Del mismo modo, WebApi promueve una RPC Api similar a C # que WCF hace:

 public class ProductsController : ApiController { public IEnumerable 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 GetProductsByCategory(string category) { return products.Where(p => string.Equals(p.Category, category, StringComparison.OrdinalIgnoreCase)); } public IEnumerable GetProductsByPriceGreaterThan(decimal price) { return products.Where((p) => p.Price > price); } } 

ServiceStack basado en mensajes de diseño de API

Mientras que ServiceStack lo alienta a conservar un diseño basado en mensajes:

 public class FindProducts : IReturn> { public string Category { get; set; } public decimal? PriceGreaterThan { get; set; } } public class GetProduct : IReturn { 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; } } 

Nuevamente capturando la esencia de la Solicitud en la solicitud DTO. El diseño basado en mensajes también puede condensar 5 servicios RAP WebAPI separados en dos ServiceStack basados ​​en mensajes.

Grupo por llamada Semántica y tipos de respuesta

Se agrupa en 2 servicios diferentes en este ejemplo basado en la semántica de llamadas y los tipos de respuesta :

Cada propiedad en cada Request DTO tiene la misma semántica que para FindProducts cada propiedad actúa como un filtro (por ejemplo, AND) mientras que en GetProduct actúa como un combinador (por ejemplo, un quirófano). Los Servicios también devuelven IEnumerable y los tipos de devolución de Product que requerirán un manejo diferente en los sitios de llamada de las API Typed.

En WCF / WebAPI (y otros marcos de servicios de RPC) siempre que tenga un requisito específico del cliente, deberá agregar una nueva firma de servidor en el controlador que coincida con esa solicitud. En el enfoque basado en mensajes de ServiceStack, sin embargo, siempre debe estar pensando a dónde pertenece esta característica y si puede mejorar los servicios existentes. También debería pensar cómo puede respaldar el requisito específico del cliente de una manera genérica para que el mismo servicio pueda beneficiar otros posibles casos de uso futuros.

Servicios de Re-factoring GetBooking Limits

Con la información anterior podemos comenzar a volver a factorizar sus servicios. Dado que tiene 2 servicios diferentes que arrojan resultados diferentes, por ejemplo, GetBookingLimit devuelve 1 elemento y GetBookingLimits devuelve muchos, deben mantenerse en diferentes servicios.

Distinguir operaciones de servicio vs tipos

Sin embargo, debe tener una división limpia entre sus operaciones de servicio (por ejemplo, solicitud de DTO), que es única por servicio y se utiliza para capturar la solicitud de los servicios y los tipos de DTO que devuelven. Los DTO de solicitud suelen ser acciones, por lo que son verbos, mientras que los tipos de DTO son entidades / contenedores de datos, por lo que son sustantivos.

Devolver respuestas genéricas

En la API nueva, las respuestas de ServiceStack ya no requieren una propiedad ResponseStatus , ya que si no existe, el ErrorResponse DTO genérico se ErrorResponse y se serializará en el cliente. Esto te libera de tener tus respuestas que contienen propiedades ResponseStatus . Dicho esto, volvería a factorizar el contrato de sus nuevos servicios a:

 [Route("/bookinglimits/{Id}")] public class GetBookingLimit : IReturn { 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> { public DateTime BookedAfter { get; set; } } 

Para las solicitudes GET, tiendo a dejarlos fuera de la definición de ruta cuando no son ambiguos ya que es menos código.

Mantenga una nomenclatura consistente

Debe reservar la palabra Acceder a los servicios que consultan en campos únicos o de Claves primarias, es decir, cuando un valor suministrado coincide con un campo (por ejemplo, Id) solo obtiene 1 resultado. Para los servicios de búsqueda que actúan como un filtro y devuelven múltiples resultados coincidentes que se encuentran dentro de un rango deseado, uso los verbos Find o Search para indicar que este es el caso.

Objetivo para los contratos de servicio autodescriptivos

También trate de ser descriptivo con cada uno de sus nombres de campo, estas propiedades son parte de su API pública y deberían describirse a sí mismas en cuanto a lo que hace. Por ejemplo, solo mirando el contrato de servicio (por ejemplo, solicitud de DTO) no tenemos idea de qué fecha hace, he supuesto BookedAfter , pero también podría haber sido BookedBefore o BookedOn si solo devolvió las reservas realizadas ese día.

El beneficio de esto ahora es que los sitios de llamada de sus clientes .NET tipados se vuelven más fáciles de leer:

 Product product = client.Get(new GetProduct { Id = 1 }); List results = client.Get( new FindBookingLimits { BookedAfter = DateTime.Today }); 

Implementación del servicio

Eliminé el atributo [Authenticate] de sus DTO de solicitud, ya que puede especificarlo una vez en la implementación del Servicio, que ahora se ve así:

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

Manejo y validación de errores

Para obtener información sobre cómo agregar validación, tiene la opción de simplemente lanzar excepciones de C # y aplicarles sus propias personalizaciones, de lo contrario, tiene la opción de usar la Validación de Fluidez incorporada, pero no necesita insertarlas en su servicio. como puede conectarlos a todos con una sola línea en su AppHost, por ejemplo:

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

Los validadores son sin contacto e invasivos, lo que significa que puede agregarlos mediante un enfoque estratificado y mantenerlos sin modificar la implementación del servicio o las clases de DTO. Como requieren una clase adicional, solo los usaría en operaciones con efectos secundarios (por ejemplo, POST / PUT) ya que los GET tienden a tener una validación mínima y arrojar una Excepción de C # requiere menos placa de caldera. Así que un ejemplo de un validador que podría tener es la primera vez que crea una reserva:

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

Dependiendo del caso de uso en lugar de tener DTO CreateBooking y UpdateBooking separado, volvería a usar el mismo Request DTO para ambos, en cuyo caso denominaré StoreBooking .

La ‘respuesta Dtos’ parece innecesaria ya que la propiedad ResponseStatus ya no es necesaria. . Sin embargo, creo que aún puede necesitar una clase de respuesta correspondiente si usa SOAP. Si elimina el Dtos de respuesta, ya no necesita insertar BookLimit en objetos de respuesta. Además, TranslateTo () de ServiceStack podría ayudar también.

A continuación se muestra cómo trataría de simplificar lo que publicaste … YMMV.

Haga un DTO para BookingLimit: Esta será la representación de BookingLimit para todos los demás sistemas.

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

Las solicitudes y Dtos son muy importantes

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

Ya no devuelve objetos de respuesta … solo el 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() if BookingLimitRepository can't return BookingLimitDto return bookingLimit; } public List Get(GetBookingLimits request) { List bookingLimits = new BookingLimitRepository().GetByRestaurantId(base.UserSession.RestaurantId); return bookingLimits.Where( l => l.EndDate.ToShortDateString() == request.Date.ToShortDateString() && l.StartDate.ToShortDateString() == request.Date.ToShortDateString()).ToList(); } }