web-dev-qa-db-fra.com

ServiceStack Request DTO design

Je suis un développeur .Net utilisé pour développer une application web sur les technologies Microsoft. J'essaie de m'éduquer pour comprendre REST pour les services Web. Jusqu'à présent, j'aime le cadre ServiceStack.

Mais parfois, je me retrouve à écrire des services d'une manière à laquelle je suis habitué avec WCF. J'ai donc une question qui me dérange.

J'ai 2 demandes de DTO donc 2 services comme ceux-ci:

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

Comme on le voit sur ces DTO de demande, j'ai des DTO de demande similaires presque pour tous les services et cela ne semble pas SEC.

J'ai essayé d'utiliser la classe GetBookingLimitResponse dans une liste à l'intérieur GetBookingLimitsResponse pour cette raison ResponseStatus à l'intérieur GetBookingLimitResponse la classe est dupliquée au cas où j'aurais une erreur sur GetBookingLimits service.

J'ai également des implémentations de service pour ces demandes comme:

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

Comme vous le voyez, je veux également utiliser la fonction de validation ici, donc je dois écrire des classes de validation pour chaque demande DTO que j'ai. J'ai donc le sentiment que je devrais garder mon numéro de service bas en regroupant des services similaires en un seul service.

Mais la question ici qui me vient à l'esprit que dois-je envoyer plus d'informations que le client n'en a besoin pour cette demande?

Je pense que ma façon de penser devrait changer parce que je ne suis pas satisfait du code actuel que j'ai écrit en pensant comme un gars de la WCF.

Quelqu'un peut-il me montrer la bonne direction à suivre.

39
mustafasturan

Pour vous donner une idée des différences auxquelles vous devez penser lors de la conception de services basés sur des messages dans ServiceStack je vais fournir quelques exemples comparant l'approche WCF/WebApi vs ServiceStack:

WCF vs conception API ServiceStack

WCF vous encourage à considérer les services Web comme des appels de méthode C # normaux, par exemple:

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

Voici à quoi ressemblerait le même contrat de service dans ServiceStack avec Nouvelle API :

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

Le concept important à garder à l'esprit est que la requête entière (alias Request) est capturée dans le message Request (c'est-à-dire Request DTO) et non dans les signatures de méthode serveur. L'avantage immédiat évident de l'adoption d'une conception basée sur les messages est que toute combinaison des appels RPC ci-dessus peut être satisfaite dans 1 message distant, par une seule implémentation de service.

WebApi vs conception de l'API ServiceStack

De même, WebApi promeut une API RPC similaire à C # que WCF fait:

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

Conception d'API basée sur les messages ServiceStack

Alors que ServiceStack vous encourage à conserver une conception basée sur les messages:

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

Capturer à nouveau l'essence de la demande dans le DTO de la demande. La conception basée sur les messages peut également condenser 5 services RPC WebAPI distincts en 2 services ServiceStack basés sur des messages.

Grouper par sémantique d'appel et types de réponse

Il est regroupé en 2 services différents dans cet exemple basé sur la sémantique des appels et les types de réponse :

Chaque propriété dans chaque DTO de demande a la même sémantique que pour FindProducts chaque propriété agit comme un filtre (par exemple un ET) tandis que dans GetProduct elle agit comme un combinateur (par exemple un OU). Les Services renvoient également IEnumerable<Product> et Product renvoient des types qui nécessiteront une gestion différente dans les sites d'appel des API typées.

Dans WCF/WebAPI (et d'autres infrastructures de services RPC) chaque fois que vous avez une exigence spécifique au client, vous devez ajouter une nouvelle signature de serveur sur le contrôleur qui correspond à cette demande. Dans l'approche basée sur les messages de ServiceStack, vous devez toujours penser à la place de cette fonctionnalité et si vous pouvez améliorer les services existants. Vous devez également réfléchir à la manière dont vous pouvez prendre en charge l'exigence spécifique au client d'une manière générique afin que le même service puisse bénéficier à d'autres futurs cas d'utilisation potentiels. .

Refactorisation des services GetBooking Limits

Avec les informations ci-dessus, nous pouvons commencer à refactoriser vos services. Puisque vous avez 2 services différents qui retournent des résultats différents, par exemple GetBookingLimit renvoie 1 élément et GetBookingLimits en renvoie plusieurs, ils doivent être conservés dans différents services.

Distinguer les opérations de service des types

Vous devez cependant avoir une répartition nette entre vos opérations de service (par exemple, demande de DTO), qui est unique par service et est utilisée pour capturer la demande des services, et les types de DTO qu'ils renvoient. Les DTO de demande sont généralement des actions, donc ce sont des verbes, tandis que les types de DTO sont des entités/conteneurs de données, donc ce sont des noms.

Renvoyer des réponses génériques

Dans la nouvelle API, ServiceStack répond ne nécessite plus de propriété ResponseStatus car s'il n'existe pas, le DTO générique ErrorResponse sera jeté et sérialisé sur le client à la place. Cela vous évite que vos réponses contiennent des propriétés ResponseStatus. Cela dit, je remettrais en cause le contrat de vos nouveaux services pour:

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

Pour les demandes GET, j'ai tendance à les laisser en dehors de la définition de Route quand elles ne sont pas ambiguës car c'est moins de code.

Gardez une nomenclature cohérente

Vous devez réserver le mot Get sur les services qui interrogent des champs de clés uniques ou primaires, c'est-à-dire lorsqu'une valeur fournie correspond à un champ (par exemple Id), elle Obtient 1 résultat. Pour les services de recherche qui agissent comme un filtre et renvoient plusieurs résultats correspondants qui se situent dans la plage souhaitée, j'utilise soit Rechercher ou Recherchez les verbes pour signaler que c'est le cas.

Viser des contrats de service auto-descriptifs

Essayez également d'être descriptif avec chacun de vos noms de champs, ces propriétés font partie de votre API publique et devraient être auto-descriptives quant à ce qu'elle fait. Par exemple. Juste en regardant le contrat de service (par exemple, demander le DTO), nous n'avons aucune idée de ce que la date fait, j'ai supposé BookedAfter , mais il aurait également pu être BookedBefore ou BookedOn si elle n'a renvoyé que les réservations effectuées ce jour-là.

L'avantage de ceci est maintenant que les sites d'appel de vos clients .NET typés deviennent plus faciles à lire:

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

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

Mise en œuvre des services

J'ai supprimé le [Authenticate] attribut de vos DTO de demande, car vous pouvez simplement le spécifier une seule fois sur l'implémentation du service, qui ressemble maintenant à:

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

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

Traitement et validation des erreurs

Pour plus d'informations sur la façon d'ajouter la validation, vous avez la possibilité de simplement lancer des exceptions C # et de leur appliquer vos propres personnalisations, sinon vous avez la possibilité d'utiliser la fonction intégrée Validation Fluent mais vous n'avez pas besoin de les injecter dans votre service car vous pouvez tous les câbler avec une seule ligne dans votre AppHost, par exemple:

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

Les validateurs sont sans contact et sans invasion, ce qui signifie que vous pouvez les ajouter en utilisant une approche en couches et les maintenir sans modifier l'implémentation du service ou les classes DTO. Comme ils nécessitent une classe supplémentaire, je ne les utiliserais que pour des opérations avec des effets secondaires (par exemple, POST/PUT) car les GET ont tendance à avoir une validation minimale et lever une exception C # nécessite moins de plaque de chaudière. Ainsi, un exemple de validateur que vous pourriez avoir est lors de la première création d'une réservation:

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

Selon le cas d'utilisation au lieu d'avoir des DTO CreateBooking et UpdateBooking distincts, je réutiliserais le même DTO de demande pour les deux, auquel cas je nommerais StoreBooking.

88
mythz

Les 'Reponse Dtos' semblent inutiles car la propriété ResponseStatus est n'est plus nécessaire. . Cependant, je pense que vous pouvez toujours avoir besoin d'une classe Response correspondante si vous utilisez SOAP. Si vous supprimez les Dtos de réponse, vous n'avez plus besoin d'insérer BookLimit dans les objets Response. De plus, TranslateTo () de ServiceStack pourrait également vous aider.

Voici comment j'essaierais de simplifier ce que vous avez publié ... YMMV.

Faire un DTO pour BookingLimit - Ce sera la représentation de BookingLimit à tous les autres systèmes.

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

Les demandes et les Dtos sont très importants

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

Ne renvoyant plus d'objets Reponse ... juste le 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();
    }
} 
10
paaschpa