ServiceStack Solicitud DTO diseño


Soy un desarrollador de.Net utilizado para desarrollar aplicaciones web en Tecnologías de Microsoft. Estoy tratando de educarme para comprender el enfoque REST para los servicios web. Hasta ahora me encanta el marco ServiceStack.

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

Tengo 2 DTO de solicitud por lo que 2 servicios como estos:

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

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

Traté de usar la clase GetBookingLimitResponse en una lista dentro de GetBookingLimitsResponse por esa razón ResponseStatus dentro de GetBookingLimitResponse la clase se duplica en caso de que tenga un error en el servicio GetBookingLimits.

También tengo implementaciones de servicio para estas solicitudes como:

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

Como ves, 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 tenga. Así que tengo la sensación de que debo mantener mi número de servicio bajo agrupando similares servicios en un solo servicio.

Pero la pregunta aquí que aparece en mi mente que debo enviar más información de la que el cliente necesita 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 de WCF.

Puede alguien mostrarme la dirección correcta a seguir.

Author: jgauffin, 2013-04-10

2 answers

Para darle una idea de las diferencias que debe tener en cuenta al diseñar servicios basados en mensajes en ServiceStack Proporcionaré algunos ejemplos comparando WCF / WebAPI vs el enfoque de ServiceStack:

WCF vs ServiceStack API Design

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

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

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

public class Customers : IReturn<List<Customer>> 
{
   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, DTO de solicitud) y no en las firmas del método del servidor. El beneficio inmediato obvio de adoptar un diseño basado en mensajes es que cualquier combinación de las llamadas RPC anteriores se puede cumplir en 1 mensaje remoto, mediante una sola implementación de servicio.

Diseño de API de WebAPI vs ServiceStack

Asimismo WebAPI promueve un similar Api RPC similar a C#que hace 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);
    }
}

ServiceStack Diseño de API basado en mensajes

Mientras ServiceStack te anima a conservar un Diseño basado en Mensajes:

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

Capturando de nuevo la esencia de la Solicitud en el DTO de solicitud. El diseño basado en mensajes también es capaz de condensar 5 servicios WebAPI RPC separados en 2 ServiceStack basados en mensajes.

Agrupar por Semántica de Llamada y Tipos de Respuesta

Está agrupado en 2 servicios diferentes en este ejemplo basado en Semántica de Llamadas y Tipos de respuesta :

Cada propiedad en cada DTO de solicitud tiene la misma semántica que para FindProducts cada propiedad actúa como un Filtro (por ejemplo, un AND) mientras que en GetProduct actúa como un combinador (por ejemplo, un OR). Los Servicios también devuelven IEnumerable<Product> y Product tipos de retorno que requerirán un manejo diferente en los sitios de llamadas de las API escritas.

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

Re-factorización de los límites de GetBooking servicios

Con la información anterior podemos comenzar a volver a factorizar sus servicios. Dado que tiene 2 servicios diferentes que devuelven 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, Solicitar DTO) que es única por servicio y se utiliza para capturar la solicitud de los Servicios, y los tipos de DTO que devolver. Los DTO de solicitud suelen ser acciones, por lo que son verbos, mientras que los tipos DTO son entidades/contenedores de datos, por lo que son sustantivos.

Devuelve respuestas genéricas

En la Nueva API, ServiceStack responses ya no requiere una propiedad ResponseStatus ya que si no existe, el DTO genérico ErrorResponse será lanzado y serializado en el cliente. Esto le libera de que sus respuestas contengan propiedades ResponseStatus. Con eso dicho me re-factor el contrato de su nuevo servicios a:

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

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

Mantener una nomenclatura coherente

Debe reservar la palabra Get en servicios que consultan campos de Claves únicas o 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úa como un filtro y devuelve múltiples resultados coincidentes rango deseado Utilizo los verbos Find o Search para indicar que este es el caso.

Trata de que los Contratos de Servicios se describan por sí mismos

También trate de ser descriptivo con cada uno de sus nombres de campo, estas propiedades son parte de su API pública y deben ser auto-descriptivas en cuanto a lo que hace. Por ejemplo, con solo mirar el Contrato de Servicio (por ejemplo, Solicitar DTO) no tenemos idea de lo que Date hace, he asumido BookedAfter , pero también podría haber sido reservado antes de o Reservado en si solo devolviera las reservas realizadas ese día.

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

Product product = client.Get(new GetProduct { Id = 1 });

List<Product> results = client.Get(
    new FindBookingLimits { BookedAfter = DateTime.Today });

Implementación de servicios

He eliminado el atributo [Authenticate] de sus DTOs de solicitud, ya que puede especificarlo una vez en la implementación del servicio, que ahora se ve como:

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

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

Manejo de errores y Validación

Para obtener información sobre cómo agregar validación, tiene la opción de simplemente lanzar excepciones de C# y aplicar sus propias personalizaciones a ellas, de lo contrario, tiene la opción de usar la validación fluida incorporada pero no necesita inyectarlas en su servicio, ya que puede cablearlas todas con una sola línea en su AppHost, por ejemplo:

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

Los validadores no son táctiles e invasivos, lo que significa que puede agregarlos utilizando un enfoque por capas y mantenerlos sin modificar la implementación del servicio o las clases DTO. Dado que requieren una clase adicional, solo los usaría en operaciones con efectos secundarios (por ejemplo, POST/PUT), ya que GETs' tiende a tener una validación mínima y lanzar 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<CreateBooking>
{
    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 CreateBooking y UpdateBooking DTO separados, reutilizaría el mismo DTO de solicitud para ambos, en cuyo caso se llamaría StoreBooking.

 86
Author: mythz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-04-23 11:23:06

Los 'Reponse Dtos' parecen innecesarios ya que la propiedad ResponseStatus ya no es necesaria.. Sin embargo, creo que todavía puede necesitar una clase de respuesta coincidente si usa SOAP. Si elimina los Dto de respuesta, ya no tendrá que introducir BookLimit en los objetos de respuesta. Además, TranslateTo() de ServiceStack también podría ayudar.

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

Hacer 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 los Dto son muy importantes

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

Ya no devuelve objetos Reponse...solo el límite de reservas

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();
    }
} 
 10
Author: paaschpa,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:54:57