Sunday 22 April 2018

Estratégia de versionamento de aplicativos


suporte de middleware.
Servidor Oracle Weblogic.
Tagged with versionamento de aplicativos
Recurso de Reimplementação de Produção no Weblogic.
Versão do aplicativo no Weblogic.
O servidor Weblogic suporta a implantação de novas versões de aplicativos já em execução sem afetar a solicitação para o aplicativo implementado atual, ou seja, sem reiniciar os servidores. A estratégia foi incluída pelo Weblogic como & # 8220; Reimplementação de Produção & # 8220 ;.
Ele fornece um modelo de implantação lado a lado, no qual você pode enviar aplicativos alterados para o Weblogic como uma nova versão do webapp sem interromper as solicitações de clientes existentes para a versão antiga do aplicativo da Web. Os clientes existentes continuarão usando o aplicativo antigo até que a sessão seja concluída e novos clientes sejam direcionados para a nova versão do aplicativo. Você também pode fornecer um bloco de expiração após o qual o Weblogic enviará todas as solicitações do cliente para a nova versão do webapp regardles de qualquer sessão existente do cliente para a versão antiga. Depois que o aplicativo antigo não for mais necessário, ele poderá ser removido manualmente, removendo-o do console Weblogic.
Existem certas limitações para esse recurso também. Às vezes, quando ocorrem mudanças nos recursos dependentes do aplicativo, como alteração no ponto final de back-end ou alteração no banco de dados de back-end (basicamente, alterações nos recursos JDBC), você não pode manter a versão antiga do aplicativo atendendo à solicitação do cliente. Aqui o recurso de Reimplementação de Produção não funcionará.
Para usar aplicativos de reimplementação de produção, é necessário um número de versão. Número da versão são strings feitas a partir da lista de caracteres válidos fornecidos pelo Weblogic.
Lista de caracteres válidos para o número da versão:
O Weblogic permite apenas duas versões de um aplicativo de cada vez. Portanto, certifique-se de que somente uma versão do aplicativo seja implantada antes de prosseguir com a reimplementação de produção.
Especificando um identificador de aplicativo: Identificador ou Versão pode ser especificado como uma cadeia de identificador exclusivo no arquivo MANIFEST. MF dentro do archive de aplicativo [EAR ou WAR] ou pode ser especificado no momento da implementação. O utilitário weblogic. Deployer fornece o recurso para fornecer - appversion no momento da implementação, reimplementação, desimplementação de um aplicativo.
Vamos tentar isso especificando o identificador no momento da implementação via weblogic. Deployer utility.
No exemplo a seguir, estaríamos implantando um aplicativo em formato explodido para o primeiro identificador de aplicativo de fornecimento de horário como "versão1.0". Note, se você estiver usando o & # 8220; nostage & # 8221; como modo de palco, então, decida sobre uma maneira de armazenar um arquivo de aplicativo de versão diferente, já que após a implementação da nova versão, se algum problema for observado, você pode simplesmente retornar à versão antiga.
Eu prefiro a montagem de armazenamento definida como,
$ & lt; diretório-base & gt; / nostage / aplicativos / & lt; nome-do-aplicativo & gt; / & lt; versão & gt; / & lt; arquivo de aplicativos reais & gt;
Use o utilitário de linha de comando abaixo para implantar o aplicativo com o controle de versão.
Defina o ambiente Weblogic executando setWLSEnv. sh ou setWLSEnv. cmd (para Windows). & gt; java weblogic. Deployer - adminurl t3: //127.0.0.1: 7001 - usuário weblogic - password s3cur3p0rt @ l - deploy - name testapp - source c: \ Users \ rk0039403 \ Documents \ nostage \ aplicativos \ testapp \ version1.0 \ testapp - nostage - appversion version1.0 Uma mensagem apareceria especificando a implementação concluída com a versão do aplicativo especificada. O mesmo que você pode fazer a verificação cruzada do Admin Console, onde o aplicativo implantado será exibido com o identificador de versão. Agora, para testar o recurso de reimplementação de produção, implantaremos o aplicativo modificado com o identificador de versão como & # 8220; versão2.0 & # 8221; e tempo de aposentadoria como 300 segundos. & gt; java weblogic. Deployer - adminurl t3: //127.0.0.1: 7001 - usuário weblogic - password s3cur3p0rt @ l - redeploy - name testapp - source C: \ Users \ rk0039403 \ Documentos \ nostage \ applications \ testapp \ version2.0 \ testapp - retiretimeout 300 - appversion version2.0 Aparece uma mensagem especificando a nova versão do aplicativo implementado. Como definimos o tempo limite de aposentadoria para o aplicativo antigo como 300 segundos, o Weblogic ainda encaminhará a solicitação do cliente existente para a versão antiga do aplicativo. Você pode ver no Admin Console que temos duas versões de aplicativos implantadas e ambas no estado ativo. Com a conclusão do tempo de aposentadoria de 300 segundos, o Weblogic irá aposentar o aplicativo antigo, que poderá ser removido manualmente mais tarde. Verifique se há logs do servidor que confirmem que o aplicativo está sendo retirado após o tempo limite.
Há uma certa limitação neste recurso, que deve ser cuidadosamente verificado pelo projetista para decidir se o aplicativo pode ser implantado usando a redistribuição de produção & # 8220; & # 8221; característica.
A estratégia de reafectação de produção é suportada por:
Módulos do Aplicativo Web (WAR) independente e Aplicativos Corporativos (EARs) cujos clientes acessam o aplicativo por meio de um aplicativo da Web (HTTP). Aplicativos corporativos que são acessados ​​por mensagens JMS de entrada de um destino JMS global. Todos os tipos de serviços da Web, incluindo serviços da Web conversacionais e confiáveis.
A reimplementação de produção não é suportada por:
Módulos EJB ou RAR independentes. Se você tentar usar a reimplementação de produção com esses módulos, o WebLogic Server rejeitará a solicitação de reimplementação.

pedaços glitchy.
notas de tecnologia.
Siga o blog por email.
Coautoria e controle de versão no SharePoint 2013.
Atualmente, usamos o sistema de checkout de arquivos, mas há situações em que a coautoria pode ser muito mais eficiente do ponto de vista do usuário. Por exemplo, uma equipe pode compartilhar uma única pasta de trabalho do Excel na qual eles inserem os status de seus projetos semanais em suas planilhas designadas nessa pasta de trabalho. Para fazer isso, cada pessoa precisa verificar o arquivo e fazer o check-in novamente para que a próxima pessoa possa atualizá-lo. Todos os usuários são impedidos de editar esse arquivo até que o usuário atual seja feito. Que dor quando você só precisa atualizar esse arquivo para que você possa ir para casa para o dia. E, em seguida, há momentos em que um usuário se esquece de verificar o arquivo de volta e, em seguida, deixa a empresa. O bom é que o administrador do SharePoint tem a capacidade de verificar o arquivo, mas geralmente só faz isso quando alguém o solicita.
Eu queria analisar o impacto do recurso de co-autoria no desempenho e no armazenamento. Especificamente, eu queria ver como isso funciona com o controle de versão. No momento, temos um limite padrão de 5 versões por arquivo, mas os usuários podem solicitar mais versões, se necessário (recentemente, foi solicitado por um gerente de desenvolvimento para torná-lo ilimitado, mas isso não seria bom da nossa perspectiva).
A funcionalidade de coautoria é o estado padrão no SharePoint 2013 e no SharePoint Online. O controle de versão está desativado por padrão.
Como o controle de versão funciona com a verificação de arquivos?
Quando você faz check-out de um arquivo de uma biblioteca que tem o controle de versão ativado, uma nova versão é criada toda vez que você faz check-in. E, se versões principais e secundárias estiverem ativadas, você pode decidir, no check-in, qual tipo da versão que você está verificando. Nas bibliotecas onde o check-out é necessário, as versões são criadas apenas no momento do check-in.
Nas bibliotecas em que o checkout não é necessário, uma nova versão é criada na primeira vez que você salva após abrir o arquivo. Cada salvamento subseqüente sobrescreve a versão criada com o primeiro salvamento. Se você fechar o aplicativo e reabrir o documento, o primeiro salvamento, mais uma vez, produzirá uma versão. Isso pode fazer com que o número de versões prolifere muito rapidamente.
IMPORTANTE: Se você for coautor de um documento, não o registre, a menos que tenha uma boa razão para impedir que outras pessoas trabalhem no documento. Isso ajudará a manter baixo o número de versões.
Office 2013: Word, PowerPoint, OneNote, Visio Office 2010: Word, PowerPoint, Servidor do OneNote Office Web Apps: Word, PowerPoint, OneNote, Excel.
O aplicativo cliente do Excel 2013 não oferece suporte a pastas de trabalho de coautoria no SharePoint 2013 ou no SharePoint Online. Além disso, os usuários do 2007 e versões anteriores do PowerPoint e do Word não podem usar a coautoria.
Reduz a sobrecarga do compartilhamento de documentos por meio de anexos de e-mail. Ajuda os usuários a acompanhar versões e edições de vários autores. Nenhuma edição de usuário é perdida, pois todas elas estão trabalhando em um documento central armazenado no servidor. Os usuários não estão bloqueados em um documento.
Os clientes do Office não enviam ou baixam informações de coautoria do servidor até que mais de um autor esteja editando. Quando um único usuário está editando um documento, o impacto no desempenho se assemelha ao das versões anteriores do SharePoint. Os clientes do Office são configurados para reduzir o impacto do servidor, reduzindo a frequência de ações de sincronização relacionadas à coautoria quando o servidor está sob carga pesada ou quando um usuário não está editando ativamente o documento.
Considerações importantes sobre planejamento para coautoria no SharePoint 2013 e no SharePoint Online.
Permissões - Para que vários usuários possam editar o mesmo documento, os usuários precisam editar as permissões para a biblioteca de documentos em que o documento está armazenado. A maneira mais simples de garantir isso é conceder a todos os usuários acesso ao site do SharePoint onde os documentos são armazenados. Versionamento - Por padrão, esse recurso está desativado no SharePoint 2013. O SharePoint 2013 suporta dois tipos de controle de versão (principal e secundário). Número de versões - Para evitar o uso de espaço em disco desnecessário, o administrador deve definir o número máximo de versões para um número razoável em bibliotecas de documentos. Período de controle de versão - O período de controle de versão determina com que frequência os produtos do SharePoint criarão uma versão de um documento que esteja sendo coautoria. Definir esse período como um valor baixo capturará as versões com mais frequência, mas provavelmente exigirá mais armazenamento do servidor. Check out - Quando um usuário faz o check out de um documento para edição, o documento é bloqueado para edição por esse usuário. Isso impede a coautoria. Por padrão, o recurso Exigir check-out não está ativado.

Seu versionamento de API está errado, e é por isso que decidi fazer isso de três formas diferentes e erradas.
No final, decidi que a maneira mais justa e equilibrada era irritar todo mundo igualmente. É claro que estou falando sobre versionamento de APIs e não desde as ótimas guias # x201C versus espaços & # x201D; debate tenho visto tantas crenças fortes em campos totalmente diferentes.
Isso foi ótimo. Quando eu construí Eu fui pwned? (HIBP) no final de novembro, foi concebido para ser um serviço simples e rápido que algumas pessoas usariam. Eu acho que é justo dizer que os dois primeiros pontos foram alcançados, mas não o último. Não era um "número um", na verdade, no final da primeira semana, era mais do que o Google Analytics poderia suportar. O ponto é que você não pode sempre prever o futuro quando você escreve sua API e em algum momento você pode precisar mudar algo que as pessoas já dependem.
Mas aqui está o problema & # x2013; toda vez que você começa a falar sobre qualquer coisa relacionada a APIs via HTTP, isso acontece:
Todo caminho que você vira, há diferentes filosoficos sobre o caminho certo & # x201D; e muito para trás e para frente no REST, o que é RESTful, o que não é e se é importante. Vamos falar sobre as alterações da API, o impacto sobre as versões, por que há tantas idéias divergentes sobre como isso deve ser feito e, por fim, por que nenhuma das brincadeiras é tão importante quanto realmente fazer as coisas.
Puxando mais dados de violação.
Tendo em mente que a mesma API é usada para o recurso de pesquisa no site e agora também por terceiros criando tudo, de aplicativos de smartphone a ferramentas de teste de penetração, a resposta acima funcionou bem no começo, mas foi limitada. Por exemplo, esta resposta não funciona tão bem:
Por quê? Porque & # x201C; BattlefieldHeroes & # x201D; é o Pascal-cased que é ótimo para combiná-lo com classes CSS codificadas (embora provavelmente não seja uma boa abordagem de longo prazo) e por ter um & # x201C; stable & # x201D; nome para se referir a (eu não vou alterá-lo, mesmo que haja uma segunda violação), mas não é adequado para exibição como um título. Tudo isso sai do Armazenamento de Tabelas do Azure e eu entro no SQL Azure para extrair dados relacionais que realmente descrevem a violação. Um dos atributos nesse armazenamento relacional é o nome que você vê acima.
O que eu realmente queria fazer era algo mais assim:
Pegue? Para o ponto anterior sobre o nome da violação, que ainda está lá no atributo name, mas agora temos um título também. Isto é o que você mostra para as pessoas & # x2013; & # x201C; Battlefield Heroes & # x201D; & # x2013; mas mais importante, se o Gawker for penhorado novamente, posso nomear a violação de algo como Gawker2014 e o título pode ser algo amigável ao longo das linhas de Gawker (Ataque Eletrônico do Exército Sírio) & # x201D ;. Ele segmenta o que é estável e previsível daquilo que não é e significa que as pessoas podem criar dependências, como imagens ou outros recursos, no atributo name.
Os outros dados devem ser bem claros: a data da violação, quando foi adicionada ao sistema, o número de contas pwned, a descrição da violação (novamente, isso pode mudar se o palavreado precisar ser ajustado) e & # x201C; DataClasses & # x201D ;. Uma das coisas que muitas pessoas estavam pedindo era uma descrição do que estava comprometido na brecha, então agora há um monte de atributos que podem ser adicionados através de uma coleção sobre a violação em si. Eu já estou mostrando isso abaixo de cada violação na página de sites da Pwned (essa é outra razão pela qual eu posso agora ajustar algumas das descrições).
Esta é uma mudança urgente. Enquanto o sentimento da API é o mesmo & # x2013; forneça um nome de conta, receba de volta uma lista de violações & # x2013; não há mais uma matriz de strings de nomes de violações. Se eu simplesmente substituísse a API antiga por essa, as coisas iriam quebrar. APIs. Devo. Evoluir.
O software evolui, as APIs devem ser versionadas.
Vamos ser sinceros sobre isso: o mundo segue em frente. A API para o HIBP durou cerca de 2 meses, não porque foi mal projetada, mas porque o serviço se tornou descontrolado e inesperadamente bem-sucedido. Eu gosto desse tipo de problema, e você também deveria.
Agora eu tive uma escolha; ou eu poderia me contentar com o que eu tinha e privar as pessoas de uma maneira melhor, eu poderia adicionar ao serviço existente de uma forma não violenta ou eu poderia criar uma nova versão (embora expondo a mesma entidade de uma maneira diferente) e construir É a melhor maneira de saber como; sem bytes desnecessários, modelados corretamente (até que eu decida que uma nova versão é mais correta) e uma boa representação da entidade que eu estou finalmente tentando entrar em consumidores & # x2019; aplicativos.
Não há nada de errado em introduzir uma nova versão de uma API quando é a coisa mais sensata a ser feita. Por todos os meios, faça o seu melhor para obtê-lo & # x201C; direita & # x201D; desde o primeiro dia, mas fazê-lo com a expectativa de que "certo" & # x201D; é um estado temporário. É por isso que precisamos estar aptos para a versão.
Os vários campos de versionamento.
Certo, então quão difícil pode ser esse negócio de versionamento? Quero dizer, deve ser um exercício simples, certo? O problema é que isso é muito filosófico, mas em vez de ficar atolado nisso por enquanto, deixe-me delinear as três escolas comuns de pensamento em termos de como elas são praticamente implementadas:
URL: basta digitar a versão da API no URL, por exemplo: haveibeenpwned / api / v2 / breachedaccount / foo Cabeçalho de solicitação personalizada: você usa o mesmo URL de antes, mas adiciona um cabeçalho como & # x201C; api-version: 2 & # x201D; Aceitar cabeçalho: você modifica o cabeçalho de aceitação para especificar a versão, por exemplo, & # x201C; Accept: application / vnd. haveibeenpwned. v2 + json & # x201D;
Tem havido muitas, muitas coisas escritas sobre isso e eu vou linkar para eles no final do post, mas aqui está a versão abreviada:
Os URLs são uma droga porque devem representar a entidade: na verdade, eu concordo com isso na medida em que a entidade que estou recuperando é uma conta violada, não uma versão da conta violada. Semanticamente, não é realmente correto, mas é fácil de usar! Cabeçalhos de pedidos personalizados são uma droga porque não é realmente uma forma semântica de descrever o recurso: A especificação HTTP nos dá um meio de solicitar a natureza que gostaríamos do recurso representado por meio do cabeçalho de aceitação, por que reproduzir? esta? Aceitar cabeçalhos chupados porque são mais difíceis de testar: não posso mais apenas fornecer a alguém um URL e dizer: "##201C; aqui, clique em" & # x201D ;, em vez disso, eles devem construir cuidadosamente a solicitação e configurar o cabeçalho de aceitação adequadamente .
Os vários argumentos a favor e contra cada abordagem tendem a ir de & # x201C; este é o & # x2018; certo & # x2019; maneira de fazer isso, mas é menos prático & # x201D; através de & # x201C; Esta é a maneira mais fácil de criar algo consumível que, portanto, faz com que seja "# & # x2018; right & # x2019; & # x201D ;. Há muita discussão sobre hipermídia, negociação de conteúdo, o que é & nbsp; REST & # x201D; e todo tipo de outras questões. Infelizmente, isso muitas vezes é filosófico e perde a visão de qual deve ser o objetivo real: construir um software que funcione e particularmente para uma API, tornando-a facilmente consumível.
É sobre ter um contrato estável, estúpido!
Mais importante do que todas as reclamações e delírios sobre como fazer isso dessa maneira ou daquela maneira é dar estabilidade às pessoas. Se eles investem seu esforço suado escrevendo código para consumir sua API, então é melhor que você não a interrompa mais adiante.
Honestamente, os debates sobre o que é o & # X201C; RESTful & # x201D; contra o que não é como se o próprio termo ditasse seu sucesso é apenas louco. Transforme essa discussão em "Aqui estão as razões práticas pelas quais isso faz sentido, e é isso que pode acontecer se você não o fizer", e eu farei de tudo. O problema é que até mesmo as vozes da razão dentro das discussões barulhentas deixam dúvidas quanto ao que realmente é a melhor abordagem e, portanto, eu alcancei um compromisso & # x2026;
Aqui estão 3 maneiras erradas de consumir a API de HIBP que você pode escolher agora.
Ok, agora que estamos claramente estabelecidos, mas você está errado, eu gostaria de dar a você a escolha de escolher qualquer uma das três formas erradas. Espere & # x2013; o que?! É assim: no entanto, eu implemento a API, ela será muito difícil de consumir, muito acadêmica, muito provavelmente falha no proxy ou algo do tipo. Em vez de escolher um caminho errado, decidi dar-lhe todas as 3 formas erradas e pode escolher aquele que é o menos errado para você.
Caminho errado 2 - cabeçalho de solicitação personalizada:
Inevitavelmente, alguém vai me dizer que fornecer 3 maneiras erradas é a coisa errada a fazer. Não significaria mais código kludge para manter? Não, isso significa simplesmente que a implementação da API da Web subjacente é decorada com dois atributos:
O primeiro é simplesmente uma restrição de roteamento que implementa o RouteFactoryAttribute. Eu passo na rota e passo a versão que pode mapear para aquela rota, então a implementação procura a presença de um & # x201C; api-version & # x201D; cabeçalho ou um cabeçalho de aceitação correspondente a esse padrão:
Se a versão especificada em uma dessas combina com a especificada na restrição de roteamento, então, é o método que será invocado. Esta é uma adaptação simples desta amostra no CodePlex.
O segundo atributo que decora o método GetV2 acima é cortesia da Web API 2 e do recurso de roteamento de atributos. É claro que sempre poderíamos fazer roteamento na API da Web, mas isso geralmente era definido globalmente. O roteamento de atributos como esse traz a definição de rota para o contexto em que ela é aplicada e facilita a visualização da ação do controlador que será chamada por qual rota. Isso também significa que as implementações de todas as três formas erradas de chamar a API estão reunidas em um único local.
Então, em suma, não, isso não cria um monte de kludge e é muito fácil de manter. Cada uma das três abordagens retornará exatamente o mesmo resultado e, o mais importante, elas permanecerão estáveis ​​e não serão alteradas de forma alguma e, no final das contas, será a melhor opção. importante, independentemente de qual opção você escolher. Toda a implementação agora também está claramente documentada na página da API do site.
Mas e se você não especificar uma versão?
Você sabe o pouco onde eu disse que você não pode quebrar o que já está lá fora? Sim, isso significa que se você fizer o que faz agora, # x2013; não especifique uma versão & # x2013; então você começa o que você recebe agora. Em outras palavras, nenhum pedido para uma versão específica significa que você obtém a versão 1.
Estou bem com isso, independentemente de ter atingido este ponto por padrão. Eu sei que algumas pessoas sempre gostam de retornar a versão mais recente se um número não for especificado, mas IMHO que quebra todo o contrato estável & # x201D; # x201D; objetivo; o que você obtém da API hoje pode ser completamente diferente do que você recebe amanhã se eu revisá-lo. Isso seria uma droga e quebraria as coisas.
Você tem 3 opções, mas minha preferência pessoal é & # x2026;
Eu tenho o luxo de controlar tanto a API quanto o consumidor primário do site da HIBP. Dado que eu forneci 3 opções para consumir a API, qual delas eu mesmo uso?
Eu fui com o favorito filosófico que é especificá-lo através do cabeçalho de aceitação. Eu não acho que isso é certo e os outros estão errados, ao contrário, eu acho que isso faz mais sentido por duas razões principais:
Concordo que o URL não deve mudar: se concordarmos que o URL representa o recurso, a menos que estejamos tentando representar versões diferentes do próprio recurso, não, não acredito que o URL deva mudar. As brechas para foo são sempre as brechas para foo e eu não acho que só porque eu mudo os dados retornados para foo que a localização de foo deve mudar. Concordo que os cabeçalhos de aceitação descrevem como você deseja os dados: Esta é uma semântica da especificação de HTTP e assim como a semântica dos verbos de solicitação faz muito sentido (isto é, estamos obtendo ou colocando ou excluindo ou postando), O mesmo acontece com a maneira como o cliente gostaria que o conteúdo fosse representado.
De maneira nenhuma isso significa que eu acho que os outros dois estão errados e, francamente, não há melhor maneira de compartilhar a API com alguém do que dizer: "Aqui, clique aqui", mas quando eu puder facilmente construir o pedido e gerenciar os cabeçalhos, eu fui com esta rota.
Na verdade, pensando nisso, eu também uso a versão na rota do domínio. Por quê? Apenas através do processo de escrever esta API eu estava constantemente me comunicando com as pessoas sobre as formas de consultá-las (mais sobre isso mais tarde) e os atributos que ela retorna. Ser capaz de passar por um e-mail e dizer "Ei, aqui está o que eu estou pensando" ############################################################ e eles simplesmente clicam e obtêm resultados é inestimável. Este é o ponto que os proponentes da abordagem de versionamento de URLs fazem com toda a razão: você simplesmente não pode fazer isso quando você está dependente de cabeçalhos.
Ah, e no caso de você estar me checando, no momento em que escrevo, eu ainda não rolei o site para a v2 da API. Agora que os dados de violação são extraídos na API quando ocorre uma pesquisa, isso significa que eu tenho o luxo de não carregar todas as violações na origem na carga inicial (isso nunca será sustentável à medida que o conjunto de dados se expande). Isso salvará um monte de tráfego de saída e acelerará as coisas para as pessoas em termos de obter o site carregado, mas isso também significa um pouco mais de trabalho do meu jeito. Fique ligado.
No encerramento.
Claramente, eu tenho sido um pouco irônico aqui com relação a tudo estar errado, mas honestamente, quanto mais você lê sobre isso e quanto mais perguntas você faz, mais errado todo caminho parece de uma maneira ou de outra. Na verdade, eu sei muito bem que existem aspectos da minha implementação que serão referidos como "errados" e "x201D". (Eu posso pensar em pelo menos um par) e, naturalmente, eu estou me preparando para o potencial ataque de feedback para esse efeito. A coisa é, porém, cada uma dessas opções funciona e, francamente, para todos os efeitos práticos, eles funcionam tão bem quanto os outros.
Se eu puder deixar outras pessoas pensando em como atualizar suas APIs com um pensamento final: ninguém usará sua API até que você a tenha criado. Pare de procrastinar. Nenhuma dessas opções é "ruim", "# x201C; ruim & # x201D; em qualquer sentido tangível, eles são apenas diferentes. Eles são todos facilmente consumíveis, todos eles retornam o mesmo resultado e nenhum deles é susceptível de ter qualquer impacto real sobre o sucesso do seu projeto.
Referências.
Stack Overflow: Práticas recomendadas para o versionamento de API? (ótima pergunta, ótimas respostas, fechada como "não construtiva", eu assumo porque "Bill the Lizard & # x201D; saiu do lado errado da cama naquela manhã) Blog do Lexical Scope: How are REST APIs com versão? (boa comparação de práticas de controle de versão entre serviços, ainda que alguns anos atrás) CodePlex: Exemplo de restrição de roteamento (vinculado na página da API da Web da Microsoft como exemplo de APIs de controle de versão, adicionando um cabeçalho personalizado) CodeBetter: Versionamento RESTful Serviços (muito pragmáticos e uma boa descrição das várias maneiras pelas quais uma API pode mudar) Blog de Vinay Sahni: Práticas recomendadas para projetar uma API RESTful pragmática (ele está argumentando sobre o versionamento de URL por causa de & # x201C ; explorabilidade do navegador & # x201D;) Lans Pivot: versionamento de API (boa visão das opiniões conflitantes existentes) Web Stack of Love: Versões de API da Web ASP com Tipos de Mídia (bom passo-a-passo para criar um aplicativo para dar suporte a versões por negociação de conteúdo)
Oi, sou Troy Hunt, escrevo este blog, crio cursos para a Pluralsight e sou diretor regional da Microsoft e MVP que viaja pelo mundo falando em eventos e treinando profissionais de tecnologia.
Oi, sou Troy Hunt, escrevo este blog, crio cursos para a Pluralsight e sou diretor regional da Microsoft e MVP que viaja pelo mundo falando em eventos e treinando profissionais de tecnologia.
Próximos eventos.
Eu normalmente faço workshops particulares em torno destes, eis os próximos eventos públicos em que estarei:
Não tem a Pluralsight? Que tal um teste gratuito de 10 dias? Isso vai te dar acesso a milhares de cursos, dentre os quais dezenas de meus incluem:
"O Cloud Never Goes Down", os SLAs do Azure e outras trivialidades de disponibilidade.
Veja como Bell foi hackeado - injeção de SQL, golpe a golpe.
Inscreva-se agora!
Copyright 2018, caça de Troy.
Esta obra está licenciada sob uma licença Creative Commons Atribuição 4.0 Internacional. Em outras palavras, compartilhe generosamente, mas forneça atribuição.
Aviso Legal.
As opiniões expressas aqui são minhas e podem não refletir as das pessoas com quem trabalho, meus amigos, minha esposa, as crianças etc. A menos que eu esteja citando alguém, elas são apenas minhas opiniões.
Publicado com o Ghost.
Este site funciona inteiramente no Ghost e é possível graças ao seu apoio gentil. Leia mais sobre porque eu escolhi usar o Ghost.

Estratégia de versão do aplicativo
Há alguma prática ou melhores práticas conhecidas para a versão da API REST do serviço da Web?
Percebi que a AWS faz o controle de versão pela URL do terminal. Esta é a única maneira ou existem outras maneiras de alcançar o mesmo objetivo? Se existem várias maneiras, quais são os méritos de cada maneira?
fechado como principalmente baseado em opinião por templatetypedef, amon, George Stocker & # 9830; 6 de março de 14 às 13:22.
Muitas boas perguntas geram algum grau de opinião com base na experiência de um especialista, mas as respostas a essa pergunta tendem a ser quase inteiramente baseadas em opiniões, e não em fatos, referências ou conhecimentos específicos. Se esta questão pode ser reformulada para ajustar as regras no centro de ajuda, edite a questão.
bloqueado por animuson & # 9830; 8 de março de 16 às 3:40.
Esta questão existe porque tem significado histórico, mas não é considerada uma boa pergunta sobre o assunto para este site, então, por favor, não a use como prova de que você pode fazer perguntas semelhantes aqui. Esta questão e suas respostas estão congeladas e não podem ser alteradas. Mais informações: centro de ajuda.
Esta é uma boa e uma pergunta complicada. O tópico de design de URI é ao mesmo tempo a parte mais proeminente de uma API REST e, portanto, um compromisso potencialmente de longo prazo com os usuários dessa API.
Como a evolução de um aplicativo e, em menor extensão, de sua API é um fato da vida e é até similar à evolução de um produto aparentemente complexo como uma linguagem de programação, o design de URI deve ter menos restrições naturais e deve ser preservado ao longo do tempo . Quanto maior o tempo de vida do aplicativo e da API, maior o comprometimento com os usuários do aplicativo e da API.
Por outro lado, outro fato da vida é que é difícil prever todos os recursos e seus aspectos que seriam consumidos por meio da API. Felizmente, não é necessário projetar a API inteira que será usada até o Apocalypse. É suficiente definir corretamente todos os endpoints de recursos e o esquema de endereçamento de cada recurso e instância de recurso.
Com o tempo, você pode precisar adicionar novos recursos e novos atributos a cada recurso específico, mas o método que os usuários da API seguem para acessar determinados recursos não deve ser alterado quando um esquema de endereçamento de recursos se tornar público e, portanto, final.
Este método se aplica à semântica do verbo HTTP (por exemplo, PUT deve sempre atualizar / substituir) e códigos de status HTTP que são suportados em versões anteriores da API (eles devem continuar a funcionar para que os clientes da API que trabalharam sem intervenção humana possam continuar trabalhando) Curtiu isso).
Além disso, como a incorporação da versão da API na URI interromperia o conceito de hipermídia como o mecanismo do estado do aplicativo (declarado na dissertação de doutorado de Roy T. Fieldings) por ter um endereço de recurso / URI que mudaria com o tempo, concluiria que a API As versões não devem ser mantidas em URIs de recursos por um longo tempo, o que significa que os URIs de recursos dos quais os usuários da API podem depender devem ser permalinks.
Claro, é possível incorporar a versão da API no URI de base, mas apenas para usos razoáveis ​​e restritos, como a depuração de um cliente de API que funciona com a nova versão da API. Essas APIs com versão devem ser limitadas no tempo e estar disponíveis apenas para grupos limitados de usuários da API (como durante betas fechados). Caso contrário, você se compromete onde não deveria.
Algumas considerações sobre a manutenção de versões da API com data de expiração. Todas as plataformas / linguagens de programação comumente usadas para implementar serviços da Web (Java, PHP, Perl, Rails, etc.) permitem a fácil vinculação de pontos de extremidade de serviços da Web a um URI de base. Dessa forma, é fácil reunir e manter uma coleção de arquivos / classes / métodos separados em diferentes versões da API.
A partir do POV dos usuários da API, também é mais fácil trabalhar e vincular-se a uma determinada versão da API quando isso é óbvio, mas apenas por tempo limitado, ou seja, durante o desenvolvimento.
A partir do POV do desenvolvedor da API, é mais fácil manter diferentes versões da API em paralelo usando sistemas de controle de origem que trabalham predominantemente em arquivos como a menor unidade de versão (código-fonte).
No entanto, com as versões da API claramente visíveis no URI, há uma ressalva: também é possível objetar essa abordagem, já que o histórico da API se torna visível / aparente no design da URI e, portanto, está sujeito a alterações ao longo do tempo que contraria as diretrizes do REST. Concordo!
A maneira de contornar essa objeção razoável é implementar a versão mais recente da API sob URI de base da API sem versão. Nesse caso, os desenvolvedores de clientes da API podem optar por:
desenvolver contra o mais recente (comprometendo-se a manter o aplicativo protegendo-o de eventuais alterações de API que possam quebrar seu cliente de API mal projetado).
ligar a uma versão específica da API (que se torna aparente), mas apenas por um tempo limitado.
For example, if API v3.0 is the latest API version, the following two should be aliases (i. e. behave identically to all API requests):
In addition, API clients that still try to point to the old API should be informed to use the latest previous API version, if the API version they're using is obsolete or not supported anymore . So accessing any of the obsolete URIs like these:
should return any of the 30x HTTP status codes that indicate redirection that are used in conjunction with Location HTTP header that redirects to the appropriate version of resource URI which remain to be this one:
There are at least two redirection HTTP status codes that are appropriate for API versioning scenarios:
301 Moved permanently indicating that the resource with a requested URI is moved permanently to another URI (which should be a resource instance permalink that does not contain API version info). This status code can be used to indicate an obsolete/unsupported API version, informing API client that a versioned resource URI been replaced by a resource permalink .
302 Found indicating that the requested resource temporarily is located at another location, while requested URI may still supported. This status code may be useful when the version-less URIs are temporarily unavailable and that a request should be repeated using the redirection address (e. g. pointing to the URI with APi version embedded) and we want to tell clients to keep using it (i. e. the permalinks).
The URL should NOT contain the versions. The version has nothing to do with "idea" of the resource you are requesting. You should try to think of the URL as being a path to the concept you would like - not how you want the item returned. The version dictates the representation of the object, not the concept of the object. As other posters have said, you should be specifying the format (including version) in the request header.
If you look at the full HTTP request for the URLs which have versions, it looks like this:
The header contains the line which contains the representation you are asking for ("Accept: application/xml"). That is where the version should go. Everyone seems to gloss over the fact that you may want the same thing in different formats and that the client should be able ask for what it wants. In the above example, the client is asking for ANY XML representation of the resource - not really the true representation of what it wants. The server could, in theory, return something completely unrelated to the request as long as it was XML and it would have to be parsed to realize it is wrong.
A better way is:
Further, lets say the clients think the XML is too verbose and now they want JSON instead. In the other examples you would have to have a new URL for the same customer, so you would end up with:
(or something similar). When in fact, every HTTP requests contains the format you are looking for:
Using this method, you have much more freedom in design and are actually adhering to the original idea of REST. You can change versions without disrupting clients, or incrementally change clients as the APIs are changed. If you choose to stop supporting a representation, you can respond to the requests with HTTP status code or custom codes. The client can also verify the response is in the correct format, and validate the XML.
One last example to show how putting the version in the URL is bad. Lets say you want some piece of information inside the object, and you have versioned your various objects (customers are v3.0, orders are v2.0, and shipto object is v4.2). Here is the nasty URL you must supply in the client:
We found it practical and useful to put the version in the URL. It makes it easy to tell what you're using at a glance. We do alias /foo to /foo/(latest versions) for ease of use, shorter / cleaner URLs, etc, as the accepted answer suggests.
Keeping backwards compatibility forever is often cost-prohibitive and/or very difficult. We prefer to give advanced notice of deprecation, redirects like suggested here, docs, and other mechanisms.
I agree that versioning the resource representation better follows the REST approach. but, one big problem with custom MIME types (or MIME types that append a version parameter) is the poor support to write to Accept and Content-Type headers in HTML and JavaScript.
For example, it is not possible IMO to POST with the following headers in HTML5 forms, in order to create a resource:
This is because the HTML5 enctype attribute is an enumeration, therefore anything other than the usual application/x-www-formurlencoded , multipart/form-data and text/plain are invalid.
. nor am I sure it is supported across all browsers in HTML4 (which has a more lax encytpe attribute, but would be a browser implementation issue as to whether the MIME type was forwarded)
Because of this I now feel the most appropriate way to version is via the URI, but I accept that it is not the 'correct' way.
Put your version in the URI. One version of an API will not always support the types from another, so the argument that resources are merely migrated from one version to another is just plain wrong. It's not the same as switching format from XML to JSON. The types may not exist, or they may have changed semantically.
Versions are part of the resource address. You're routing from one API to another. It's not RESTful to hide addressing in the header.
There are a few places you can do versioning in a REST API:
As noted, in the URI. This can be tractable and even esthetically pleasing if redirects and the like are used well.
In the Accepts: header, so the version is in the filetype. Like 'mp3' vs 'mp4'. This will also work, though IMO it works a bit less nicely than.
In the resource itself. Many file formats have their version numbers embedded in them, typically in the header; this allows newer software to 'just work' by understanding all existing versions of the filetype while older software can punt if an unsupported (newer) version is specified. In the context of a REST API, it means that your URIs never have to change, just your response to the particular version of data you were handed.
I can see reasons to use all three approaches:
if you like doing 'clean sweep' new APIs, or for major version changes where you want such an approach. if you want the client to know before it does a PUT/POST whether it's going to work or not. if it's okay if the client has to do its PUT/POST to find out if it's going to work.
Versioning your REST API is analogous to the versioning of any other API. Minor changes can be done in place, major changes might require a whole new API. The easiest for you is to start from scratch every time, which is when putting the version in the URL makes most sense. If you want to make life easier for the client you try to maintain backwards compatibility, which you can do with deprecation (permanent redirect), resources in several versions etc. This is more fiddly and requires more effort. But it's also what REST encourages in "Cool URIs don't change".
In the end it's just like any other API design. Weigh effort against client convenience. Consider adopting semantic versioning for your API, which makes it clear for your clients how backwards compatible your new version is.

Version Your App.
Your app must be versioned Set the version in the app's Gradle build files How you version your apps affects how users upgrade Determine your versioning strategy early in the development process, including considerations for future releases.
In this document.
Versioning is a critical component of your app upgrade and maintenance strategy. Versioning is important because:
Users need to have specific information about the app version that is installed on their devices and the upgrade versions available for installation. Other apps — including other apps that you publish as a suite — need to query the system for your app's version, to determine compatibility and identify dependencies. Services through which you will publish your app(s) may also need to query your app for its version, so that they can display the version to users. A publishing service may also need to check the app version to determine compatibility and establish upgrade/downgrade relationships.
The Android system uses your app's version information to protect against downgrades. The system does not use app version information to enforce restrictions on upgrades or compatibility of third-party apps. Your app must enforce any version restrictions and should tell users about them.
The Android system does enforce system version compatibility as expressed by the minSdkVersion setting in the build files. This setting allows an app to specify the minimum system API with which it is compatible. For more information see Specifying Minimum System API Version.
Set Application Version Information.
To define the version information for your app, set values for the version settings in the Gradle build files. These values are then merged into your app's manifest file during the build process.
Note: If your app defines the app version directly in the <manifest> element, the version values in the Gradle build file will override the settings in the manifest. Additionally, defining these settings in the Gradle build files allows you to specify different values for different versions of your app. For greater flexibility and to avoid potential overwriting when the manifest is merged, you should remove these attributes from the <manifest> element and define your version settings in the Gradle build files instead.
Two settings are available, and you should always define values for both of them:
versionCode — An integer used as an internal version number. This number is used only to determine whether one version is more recent than another, with higher numbers indicating more recent versions. This is not the version number shown to users; that number is set by the versionName setting, below. The Android system uses the versionCode value to protect against downgrades by preventing users from installing an APK with a lower versionCode than the version currently installed on their device.
The value is an integer so that other apps can programmatically evaluate it, for example to check an upgrade or downgrade relationship. You can set the value to any integer you want, however you should make sure that each successive release of your app uses a greater value. You cannot upload an APK to the Play Store with a versionCode you have already used for a previous version.
Note: In some specific situations, you might wish to upload a version of your app with a lower versionCode than the most recent version. For example, if you are publishing multiple APKs, you might have pre-set versionCode ranges for specific APKs. For more about assigning versionCode values for multiple APKs, see Multiple APK Support.
Typically, you would release the first version of your app with versionCode set to 1, then monotonically increase the value with each release, regardless of whether the release constitutes a major or minor release. This means that the versionCode value does not necessarily have a strong resemblance to the app release version that is visible to the user (see versionName , below). Apps and publishing services should not display this version value to users.
Warning: The greatest value Google Play allows for versionCode is 2100000000.
The value is a string so that you can describe the app version as a <major>.<minor>.<point> string, or as any other type of absolute or relative version identifier. The versionName has no purpose other than to be displayed to users.
You can define default values for these settings by including them in the defaultConfig <> block, nested inside the android <> block of your module's build. gradle file. You can then override these default values for different versions of your app by defining separate values for individual build types or product flavors. The following build. gradle file shows the versionCode and versionName settings in the defaultConfig <> block, as well as the productFlavors <> block.
In the defaultConfig <> block of this example, the versionCode value indicates that the current APK contains the second release of the app, and the versionName string specifies that it will appear to users as version 1.1. This build. gradle file also defines two product flavors, "demo" and "full." Since the "demo" product flavor defines versionName as "1.1-demo", the "demo" build uses this versionName instead of the default value. The "full" product flavor block does not define versionName , so it uses the default value of "1.1".
The Android framework provides an API to let you query the system for version information about your app. To obtain version information, use the getPackageInfo(java. lang. String, int) method of PackageManager .
Note: When you use Instant Run, Android Studio automatically sets the versionCode to MAXINT and the versionName to "INSTANTRUN" .
Specify API Level Requirements.
If your app requires a specific minimum version of the Android platform, you can specify that version requirement as API level settings in the app's build. gradle file. During the build process, these settings are merged into your app's manifest file. Specifying API level requirements ensures that your app can only be installed on devices that are running a compatible version of the Android platform.
Note: If you specify API level requirements directly in your app's manifest file, the corresponding settings in the build files will override the settings in the manifest file. Additionally, defining these settings in the Gradle build files allows you to specify different values for different versions of your app. For greater flexibility and to avoid potential overwriting when the manifest is merged, you should remove these attributes from the <uses-sdk> element and define your API level settings in the Gradle build files instead.
There are two API level settings available:
minSdkVersion — The minimum version of the Android platform on which the app will run, specified by the platform's API level identifier. targetSdkVersion — Specifies the API level on which the app is designed to run. In some cases, this allows the app to use manifest elements or behaviors defined in the target API level, rather than being restricted to using only those defined for the minimum API level.
To specify default API level requirements in a build. gradle file, add one or more of the settings above to the defaultConfig <> block, nested inside the android <> block. You can also override these default values for different versions of your app by adding the settings to build types or product flavors. The following build. gradle file specifies default minSdkVersion and targetSdkVersion settings in the defaultConfig <> block and overrides minSdkVersion for one product flavor.
When preparing to install your app, the system checks the value of these settings and compares them to the system version. If the minSdkVersion value is greater than the system version, the system prevents the installation of the app.
If you do not specify these settings, the system assumes that your app is compatible with all platform versions.
For more information, see the <uses-sdk> manifest element documentation and the API Levels document. For Gradle build settings, see Configure Build Variants.
Except as noted, this content is licensed under Creative Commons Attribution 2.5. For details and restrictions, see the Content License.
Get the latest Android developer news and tips that will help you find success on Google Play.
You have successfully signed up for the latest Android developer news and tips.
Следите за новостями от Google Developers в WeChat.
Browse this site in ?
You requested a page in , but your language preference for this site is .
Would you like to change your language preference and browse this site in ? If you want to change your language preference later, use the language menu at the bottom of each page.
This class requires API level or higher.
This doc is hidden because your selected API level for the documentation is . You can change the documentation API level with the selector above the left navigation.
For more information about specifying the API level your app requires, read Supporting Different Platform Versions.

Application versioning strategy


Is there any guideline or standard best practice how to version a software you develop in your spare time for fun, but nevertheless will be used by some people? I think it's necessary to version such software so that you know about with version one is talking about (e. g. for bug fixing, support, and so on).
But where do I start the versioning? 0.0.0? or 0.0? And then how to I increment the numbers? major release. minor change? and shouldn't any commit to a version control system be another version? or is this only for versions which are used in a productive manner?
closed as primarily opinion-based by bummi, animuson ♦ Aug 2 '13 at 15:43.
Many good questions generate some degree of opinion based on expert experience, but answers to this question will tend to be almost entirely based on opinions, rather than facts, references, or specific expertise. Se esta questão pode ser reformulada para ajustar as regras no centro de ajuda, edite a questão.
locked by Flexo ♦ Mar 21 '17 at 21:32.
This question exists because it has historical significance, but it is not considered a good, on-topic question for this site , so please do not use it as evidence that you can ask similar questions here. This question and its answers are frozen and cannot be changed. More info: help center.
12 Respostas.
You should start with version 1, unless you know that the first version you "release" is incomplete in some way.
As to how you increment the versions, that's up to you, but use the major, minor, build numbering as a guide.
It's not necessary to have every version you commit to source control as another version - you'll soon have a very large version number indeed. You only need to increment the version number (in some way) when you release a new version to the outside world.
So If you make a major change move from version 1.0.0.0 to version 2.0.0.0 (you changed from WinForms to WPF for example). If you make a smaller change move from 1.0.0.0 to 1.1.0.0 (you added support for png files). If you make a minor change then go from 1.0.0.0 to 1.0.1.0 (you fixed some bugs).
If you really want to get detailed use the final number as the build number which would increment for every checkin/commit (but I think that's going too far).
I would use x. y.z kind of versioning.
x - major release.
y - minor release.
z - build number.
I basically follow this pattern:
when it's ready I branch the code in the source repo, tag 0.1.0 and create the 0.1.0 branch, the head/trunk becomes 0.2.0-snapshot or something similar.
I add new features only to the trunk, but backport fixes to the branch and in time I release from it 0.1.1, 0.1.2, .
I declare version 1.0.0 when the product is considered feature complete and doesn't have major shortcomings.
from then on - everyone can decide when to increment the major version.
I use this rule for my applications:
. y = feature number, 0-9. Increase this number if the change contains new features with or without bug fixes. z = hotfix number, 0-
. Increase this number if the change only contains bug fixes.
For new application, the version number starts with 1.0.0. If the new version contains only bug fixes, increase the hotfix number so the version number will be 1.0.1. If the new version contains new features with or without bug fixes, increase the feature number and reset the hotfix number to zero so the version number will be 1.1.0. If the feature number reaches 9, increase the main version number and reset the feature and hotfix number to zero (2.0.0 etc)
We use a. b.c. d where.
a - major (incremented on delivery to client) b - minor (incremented on delivery to client) c - revision (incremented on internal releases) d - build (incremented by cruise control)
Yet another example for the A. B.C approach is the Eclipse Bundle Versioning. Eclipse bundles rather have a fourth segment:
In Eclipse, version numbers are composed of four (4) segments: 3 integers and a string respectively named major. minor. service. qualifier . Each segment captures a different intent:
the major segment indicates breakage in the API the minor segment indicates "externally visible" changes the service segment indicates bug fixes and the change of development stream the qualifier segment indicates a particular build.
There is also the date versioning scheme, eg: YYYY. MM , YY. MM , YYYYMMDD.
It is quite informative because a first look gives an impression about the release date. But i prefer the x. y.z scheme, because i always want to know a product's exact point in its life cycle (Major. minor. release)
The basic answer is "It depends".
What is your objective in versioning? Many people use version. revision. build and only advertise version. revision to the world as that's a release version rather than a dev version. If you use the check-in 'version' then you'll quickly find that your version numbers become large.
If you are planning your project then I'd increment revision for releases with minor changes and increment version for releases with major changes, bug fixes or functionality/features. If you are offering beta or nightly build type releases then extend the versioning to include the build and increment that with every release.
Still, at the end of the day, it's up to you and it has to make sense to you.
As Mahesh says: I would use x. y.z kind of versioning.
x - major release y - minor release z - build number.
you may want to add a datetime, maybe instead of z.
You increment the minor release when you have another release. The major release will probably stay 0 or 1, you change that when you really make major changes (often when your software is at a point where its not backwards compatible with previous releases, or you changed your entire framework)
You know you can always check to see what others are doing. Open source software tend to allow access to their repositories. For example you could point your SVN browser to svn. doctrine-project and take a look at the versioning system used by a real project.
Version numbers, tags, it's all there.
We follow a. b.c approach like:
increament 'a' if there is some major changes happened in application. Like we upgrade 1.1 application to 3.5.
increament 'b' if there is some minor changes like any new CR or Enhancement is implemented.
increament 'c' if there is some defects fixes in the code.
I start versioning at the lowest (non hotfix) segement. I do not limit this segment to 10. Unless you are tracking builds then you just need to decide when you want to apply an increment. If you have a QA phase then that might be where you apply an increment to the lowest segment and then the next segement up when it passes QA and is released. Leave the topmost segment for Major behavior/UI changes.
If you are like me you will make it a hybrid of the methods so as to match the pace of your software's progression.
I think the most accepted pattern a. b.c. or a. b.c. d especially if you have QA/Compliance in the mix. I have had so much flack around date being a regular part of versions that I gave it up for mainstream.
I do not track builds so I like to use the a. b.c pattern unless a hotfix is involved. When I have to apply a hotfix then I apply parameter d as a date with time. I adopted the time parameter as d because there is always the potential of several in a day when things really blow up in production. I only apply the d segment (YYYYMMDDHHNN) when I'm diverging for a production fix.
I personally wouldn't be opposed to a software scheme of va. b revc where c is YYYYMMDDHHMM or YYYYMMDD.
All that said. If you can just snag a tool to configure and run with it will keep you from the headache having to marshall the opinion facet of versioning and you can just say "use the tool". because everyone in the development process is typically so compliant.

No comments:

Post a Comment