Compartilhar via


ASP.NET

Topshelf e Katana: Uma Web unificada e arquitetura de serviços

Wes McClure

Baixar o código de exemplo

Usar o IIS para hospedar os aplicativos da Web do ASP.NET tem sido o padrão de fato por mais de uma década. Criar tais aplicativos é um processo relativamente simples, mas implantá-los não o é. A implantação exige conhecimento auxiliar das hierarquias de configuração do aplicativo e nuances do histórico do IIS, e o tedioso provisionamento de sites, aplicativos e diretórios virtuais. Muitas das partes essenciais da infraestrutura geralmente terminam residindo fora do aplicativo em componentes do IIS configurados manualmente.

Quando os aplicativos ultrapassam simples solicitações da Web e precisam dar suporte à solicitações de execução longa, trabalhos recorrentes e outros trabalhos de processamento, eles se tornam difíceis para executar com o IIS. Geralmente, a solução é criar um Serviço do Windows separado para hospedar estes componentes. Porém, isso exige um processo de implantação totalmente separado, dobrando o esforço envolvido. A última gota é fazer com que os processos de serviço e Web se comuniquem. O que poderia ser um aplicativo simples rapidamente se torna extremamente complexo.

A Figura 1 mostra como esta arquitetura normalmente se parece. A camada da Web é responsável por gerenciar solicitações rápidas e fornecer uma interface de usuário ao sistema. As solicitações de longa execução são delegadas ao serviço, que também manipula trabalhos recorrentes e processamento. Além disso, o serviço fornece status sobre o trabalho atual e futuro para a camada da Web ser incluída na interface do usuário.

Traditional Separated Web and Service Architecture
Figura 1 Web separada tradicional e arquitetura de serviços

Uma nova abordagem

Felizmente, as novas tecnologias estão surgindo e isso pode tornar o desenvolvimento e implantação de aplicativos de serviços e da Web muito mais simples. Graças ao projeto Katana (katanaproject.codeplex.com) e as especificações fornecidas pelo OWIN (owin.org), agora é possível hospedar internamente os aplicativos Web, eliminando o IIS, e ainda fornecer suporte a vários dos componentes do ASP.NET obsoletos como o WebApi e SignalR. A Web hospedada internamente pode ser incorporada em um aplicativo do console rudimentar juntamente com o Topshelf (topshelf-project.com) para criar um serviço do Windows com facilidade. Como resultado, os componentes de serviço e da Web podem viver lado a lado no mesmo processo, conforme exibido na Figura 2. Isto elimina a sobrecarga de desenvolver camadas de comunicação irrelevantes, separar projetos e procedimentos de implantação separados.

Unified Web and Service Architecture with Katana and Topshelf
Figura 2 Web unificada e arquitetura de serviços com Katana e Topshelf

Este recurso não é totalmente novo. O Topshelf já existe a anos, ajudando a simplificar o desenvolvimento do Windows Service, e também existem várias estruturas Web hospedadas internamente de código-fonte aberto, como o Nancy. No entanto, até o OWIN surgir no projeto Katana, nada se mostrou mais promissor do que se tornar o padrão alternativo de fato para hospedar aplicativos Web no IIS. Além do mais, o Nancy e outros componentes de código-fonte aberto funcionam com o projeto Katana, permitindo que você organize uma estrutura flexível, eclética.

O Topshelf parece opcional, mas esse não é o caso. Sem a capacidade de simplificar o desenvolvimento de serviços, o desenvolvimento da Web hospedada internamente pode ser extremamente inconveniente. O Topshelf simplifica o desenvolvimento de serviços ao tratá-lo como um aplicativo do console e abstraindo o fato de que ele será hospedado como um serviço. Quando for o momento de implantar, o Topshelf trata automaticamente de instalar e iniciar o aplicativo como um Serviço do Windows—tudo sem a sobrecarga de lidar com o InstallUtil; as nuances de um projeto de serviço e dos componentes do serviço; e anexar depuradores aos serviços quando algo dá errado. O Topshelf também permite vários parâmetros, como o nome do serviço, a ser especificado em código ou configurado durante a instalação por meio da linha de comando.

Para ilustrar como unificar os componentes de serviço e Web com o Katana e Topshelf, eu criarei um aplicativo de mensagens SMS simples. Começarei com uma API para receber mensagens e colocá-las na fila para enviar. Isto demonstrará como é fácil gerenciar solicitações de execução longa. Então, eu adicionarei um método de consulta da API para retornar a contagem de mensagens pendentes, mostrando que também é fácil consultar o status do serviço dos componentes da Web.

Depois, eu adicionarei uma interface administrativa para demonstrar que os componentes da Web ainda podem fornecer a criação de interfaces da Web ricas. Para completar o processamento de mensagem, eu adicionarei um componente para enviar mensagens a medida em que elas são colocadas na fila, para demonstrar os componentes de serviço incluídos.

E para destacar uma das melhores partes da arquitetura, eu criarei um script psake para expor a simplicidade das implantações.

Para focar nos benefícios combinados do Katana e Topshelf, eu não vou entrar em detalhes sobre nenhum dos projetos. Consulte “Introdução ao projeto Katana” (bit.ly/1h9XaBL) e “Criar os serviços do Windows facilmente com o Topshelf” (bit.ly/1h9XReh) para saber mais.

Um aplicativo de console é tudo que você precisa para começar

O Topshelf existe para desenvolver e implantar um serviço do Windows do ponto inicial de um aplicativo de console simples. Para começar com o aplicativo de mensagens SMS, eu criei um aplicativo de console do C# e depois instalei o pacote Topshelf NuGet do Package Manager Console.

Quando o aplicativo de console inicia, eu preciso configurar o Topshelf HostFactory para abstrair o host do aplicativo como um console no desenvolvimento e como um serviço na produção:

private static int Main() { var exitCode = HostFactory.Run(host => { }); return (int) exitCode; }

O HostFactory retornará um código de saída, que é útil durante a instalação do serviço para detectar e interromper uma falha. O configurador do host fornece um método de serviço para especificar um tipo personalizado que representa o ponto de entrada para seu código de aplicativo. O Topshelf refere-se a isto como o serviço que está hospedando, porque o Topshelf é uma estrutura para simplificar a criação do Windows Service:

host.Service<SmsApplication>(service => { });

Depois, eu criei um tipo SmsApplication para conter a lógica para criar um servidor da Web hospedado internamente e os componentes de serviço do Windows tradicionais. No mínimo, este tipo personalizado conterá o comportamento para executar quando o aplicativo inicia ou para:

public class SmsApplication { public void Start() { } public void Stop() { } }

Porque eu escolhi usar um objeto CLS antigo simples (POCO) para o tipo de serviço, eu forneci uma expressão lambda para o Topshelf construir uma instância do tipo SmsApplication, e eu especificar os métodos de início e parada:

service.ConstructUsing(() => new SmsApplication()); service.WhenStarted(a => a.Start()); service.WhenStopped(a => a.Stop());

O Topshelf permite que vários parâmetros de serviço sejam configurados em códigos, então eu uso o SetDescription, SetDisplayName e SetServiceName para descrever e nomear o serviço que será instalado na produção:

host.SetDescription("An application to manage sending sms messages and provide message status."); host.SetDisplayName("Sms Messaging"); host.SetServiceName("SmsMessaging"); host.RunAsNetworkService();

Finalmente, eu uso o RunAsNetworkService para instruir o Topshelf a configurar o serciço para executar como a conta do serviço de rede. Você pode alterar isto para qualquer conta que se ajuste ao seu ambiente. Para mais opções de serviço, confira a documentação de configuração do Topshelf em bit.ly/1rAfMiQ.

Executar o serviço como um aplicativo de console é tão simples como iniciar o executável. A Figura 3 mostra o resultado de iniciar e parar o executável SMS. Porque este é um aplicativo de console, quando você inicia seu aplicativo no Visual Studio, você enfrentará o mesmo comportamento.

Running the Service as a Console Application
Figura 3 Executando o serviço como um aplicativo de console

Incorporando uma API

Com os detalhes técnicos do Topshelf no lugar eu posso começar a trabalhar na API do aplicativo. O projeto Katana fornece componentes para hospedar internamente um pipeline do OWIN, então eu instalo o pacote Microsoft.Owin.SelfHost para incorporar os componentes hospedados internamente. Este pacote referencia vários pacotes, dois dos quais são importantes para hospedagem interna. Primeiro, o Microsoft.Owin.Hosting fornece um conjunto de componentes para hospedar e executar um pipeline do OWIN. Segundo, o Microsoft.Owin.Host.HttpListener fornece uma implementação de um servidor HTTP.

Dentro do SmsApplication, eu criei um aplicativo Web hospedado internamente usando o tipo WebApp fornecido pelo pacote hospedado:

protected IDisposable WebApplication; public void Start() { WebApplication = WebApp.Start<WebPipeline>("http://localhost:5000"); }

O método WebApp Start exige dois parâmetros, um parâmetro genérico para especificar um tipo que configurará o pipeline do OWIN e uma URL para ouvir solicitações. O aplicativo Web é um recurso descartável. Quando a instância SmsApplication é parada, eu descarto o aplicativo Web:

public void Stop() { WebApplication.Dispose(); }

Um dos benefícios de usar o OWIN é que eu posso aproveitar uma variedade de componentes familiares. Primeiro, eu usarei o WebApi para criar a API. Preciso instalar o pacote Microsoft.AspNet.WebApi.Owin para incorporar o WebApi no pipeline do OWIN. Então, eu criarei o tipo WebPipeline para configurar o pipeline do OWIN e inserir o middleware WebApi. Além disso, eu configurarei o WebApi para usar o roteamento de atributo:

public class WebPipeline { public void Configuration(IAppBuilder application) { var config = new HttpConfiguration(); config.MapHttpAttributeRoutes(); application.UseWebApi(config); } }

E agora eu posso criar um método API para receber mensagens e colocá-las na fila para serem enviadas:

public class MessageController : ApiController { [Route("api/messages/send")] public void Send([FromUri] SmsMessageDetails message) { MessageQueue.Messages.Add(message); } }

O SmsMessageDetails contém a carga da mensagem. A ação de envio adiciona a mensagem para uma fila para ser processada de forma assíncrona posteriormente. O MessageQueue é um BlockingCollection global. Em um aplicativo real isto pode significar que você precisa decompor em outras preocupações como durabilidade e escalabilidade:

public static readonly BlockingCollection<SmsMessageDetails> Messages;

Em uma arquitetura de serviço e Web separada, distribuindo o processamento assíncrono de solicitações de execução longa, como enviar uma mensagem, exige uma comunicação entre a Web e os processos de serviço. E adicionar métodos API para consultar o status do serviço significa ainda mais sobrecarga de comunicação. Uma abordagem unificada torna o compartilhamento das informações de status entre a Web e os componentes de serviço simples. Para demonstrar isto, eu adiciono uma consulta para a API:

[Route("api/messages/pending")] public int PendingCount() { return MessageQueue.Messages.Count; }

Construindo uma interface de usuário rica

As APIs são convenientes, porém aplicativos Web hospedados internamente que ainda precisam fornecer suporte a uma interface visual. No futuro, eu suspeito, que um derivado ou a estrutura do ASP.NET MVC estará disponível como o middleware do OWIN. Por enquanto, o Nancy é compatível e tem um pacote para dar suporte ao núcleo do mecanismo de exibição do Razor.

Eu instalarei o pacote Nancy.Owin para adicionar suporte para o Nancy, e o Nancy.Viewengines.Razor para incorporar o mecanismo de exibição do Razor. Para conectar o Nancy no pipeline do OWIN, eu preciso registrá-lo após o registro para o WebApi para que ele não capture as rotas que fiz o mapeamento para a API. Por padrão, o Nancy retorna um erro se um recurso não for encontrado, enquanto o WebApi passa as solicitações nas quais ele não pode lidar de volta para o pipeline:

application.UseNancy();

Para saber mais sobre como usar o Nancy com um pipeline do OWIN, consulte o “Hosting Nancy with OWIN” em bit.ly/1gqjIye.

Para construir uma interface com status administrativo, eu adicionei um módulo do Nancy e fiz um mapa de uma rota do status para renderizar a exibição do status, passando a contagem de mensagem pendente como o modelo de exibição:

public class StatusModule : NancyModule { public StatusModule() { Get["/status"] = _ => View["status", MessageQueue.Messages.Count]; } }

A exibição não é muito glamorosa neste momento, apenas uma simples contagem de mensagens pendentes:

    <h2>Status</h2> There are <strong>@Model</strong> messages pending.

Vou dar um pouco mais vida a exibição com um Bootstrap navbar simples, conforme mostrado na Figura 4. Usando o Bootstrap necessita de uma hospedagem de conteúdo estático para a folha de estilo do Bootstrap.

Administrative Status Page
Figura 4 Página de status administrativo

Eu posso usar o Nancy para a hospedagem de um conteúdo estático, mas a vantagem do OWIN é misturar e combinar o middleware, então ao invés eu usarei o pacote recém lançado Microsoft.Owin.StaticFiles, que faz parte do projeto Katana. O pacote StaticFiles fornece o middleware para servir o arquivo. Eu o adicionarei para iniciar o pipeline do OWIN para que o arquivo estático do Nancy que está servindo não seja ativado.

application.UseFileServer(new FileServerOptions { FileSystem = new PhysicalFileSystem("static"), RequestPath = new PathString("/static") });

O parâmetro FileSystem informa ao servidor de arquivo onde procurar por arquivos para servir. Estou usando uma pasta denominada estática. O RequestPath especifica o prefixo da rota para ouvir solicitações para este conteúdo. Neste caso, eu escolho refletir o nome estático, mas estes não devem corresponder. Eu uso o link a seguir no layout para referenciar a folha de estilo do bootstrap (naturalmente, eu coloco a folha de estilo do booststrap em uma pasta CSS dentro da pasta estática):

    <link rel="stylesheet" href="/static/css/bootstrap.min.css">

Uma palavra sobre o conteúdo estático e as exibições

Antes de avançar, quero lhe falar sobre uma dica que achei útil ao desenvolver um aplicativo Web hospedado internamente. Normalmente, você definiria o conteúdo estático e as exibições do MVC para serem copiados para o diretório de saída para que os componentes da Web hospedados internamente possam encontrá-los em relação à assembly executada no momento. Isto não é apenas uma carga e fácil de esquecer, alterar as exibições e o conteúdo estático requer a recompilação do aplicativo—o que absolutamente destrói a produtividade. No entanto, eu recomendo não copiar o conteúdo estático e as exibições para o diretório de saída; ao invés, configure o middleware como o Nancy e o FileServer para mapear para as pastas de desenvolvimento.

Por padrão, o diretório de saída de depuração de um aplicativo de console é bin/Debug, então no desenvolvimento eu digo para o FileServer olhar dois diretórios acima do diretório atual para encontrar a pasta estática que contém a folha de estilo do bootstrap:

FileSystem = new PhysicalFileSystem(IsDevelopment() ? "../../static" : "static")

Depois, para dizer para o Nancy onde olhar em busca de exibições, eu criarei um NancyPathProvider personalizado:

public class NancyPathProvider : IRootPathProvider { public string GetRootPath() { return WebPipeline.IsDevelopment() ? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"..\..\") : AppDomain.CurrentDomain.BaseDirectory; } }

Novamente, eu usei a mesma verificação para olhar dois diretórios acima do diretório base se estou executando no modo de desenvolvimento no Visual Studio. Deixei a implementação do IsDevelopment para você decidir; pode ser uma configuração simples ou você pode escrever um código para detectar quando o aplicativo foi lançado a partir do Visual Studio.

Para registrar este provedor de caminho raiz personalizado, eu criei um NancyBootstrapper personalizado e substituí a propriedade RootPathProvider padrão para criar uma instância do NancyPathProvider:

public class NancyBootstrapper : DefaultNancyBootstrapper { protected override IRootPathProvider RootPathProvider { get { return new NancyPathProvider(); } } }

E quando eu adicionei o Nancy ao pipeline do OWIN, eu passo um instância do NancyBootstrapper nas opções:

application.UseNancy(options => options.Bootstrapper = new NancyBootstrapper());

Enviar mensagens

Receber mensagens é a metade do trabalho, mas o aplicativo ainda precisa de um processo para enviá-las. Este é um processo que tradicionalmente viveria em um serviço isolado. Nesta solução unificada eu posso simplesmente adicionar um SmsSender que inicie quando o aplicativo iniciar. Eu adicionarei isto ao método Start SmsApplication (em um aplicativo real, você deve adicionar o recurso para parar e descartar este recurso):

public void Start() { WebApplication = WebApp.Start<WebPipeline>("http://localhost:5000"); new SmsSender().Start(); }

Dentro do método Start no SmsSender, eu inicio uma tarefa de execução longa para enviar mensagens:

public class SmsSender { public void Start() { Task.Factory.StartNew(SendMessages, TaskCreationOptions.LongRunning); } }

Quando a ação de envio do WebApi recebe uma mensagem, ele adiciona a uma fila de mensagem que é uma coleção de blocos. Eu crio o método SendMessages para o bloco até a mensagem chegar. Isto é possível graças as abstrações por trás do GetConsumingEnumerable. Quando um conjunto de mensagens chega, ele começa imediatamente a enviá-las:

private static void SendMessages() { foreach (var message in MessageQueue.Messages.GetConsumingEnumerable()) { Console.WriteLine("Sending: " + message.Text); } }

Seria trivial criar múltiplas instâncias do SmsSender para expandir a capacidade de enviar mensagens. Em um aplicativo real, você desejaria passar um CancellationToken para um GetConsumingEnumerable para parar com segurança a enumeração. Se você deseja saber mais sobre coleções de blocos, você encontrará boas informações em bit.ly/QgiCM7 e bit.ly/1m6sqlI.

Implantações tranquilas

Desenvolver um serviço combinado e um aplicativo Web é muito simples e direto, graças ao Katana e Topshelf. Um dos excelentes benefícios desta combinação poderosa é um processo de implantação ridiculamente simples. Vou lhe mostrar uma implantação de duas etapas simples usando o psake (github.com/psake/psake). Isto não pretende ser um script robusto para o uso de produção real; eu apenas quero demonstrar como o processo é verdadeiramente simples, independentemente de qual ferramenta você usa.

A primeira etapa é criar o aplicativo. Eu crio uma tarefa de construção que chamarei de msbuild com o caminho para a solução e crio uma compilação de versão (a saída terminará no bin/Release):

properties { $solution_file = "Sms.sln" } task build { exec { msbuild $solution_file /t:Clean /t:Build /p:Configuration=Release /v:q } }

A segunda etapa é implantar o aplicativo como um serviço. Eu crio uma tarefa de implantação que depende da tarefa de construção e declara um diretório de entrega para manter um caminho para o local da instalação. Para simplicidade eu apenas implanto para um diretório local. Em seguida, eu crio uma variável executável para apontar para o aplicativo de console executável no diretório de entrega:

task deploy -depends build { $delivery_directory = "C:\delivery" $executable = join-path $delivery_directory 'Sms.exe'

Primeiro, a tarefa de implantação verificará se o diretório de entrega existe. Se ela encontrar o diretório de entrega, ela irá supor que o serviço já foi implantado. Neste caso, a tarefa de implantação desinstalará o serviço e removerá o diretório de entrega:

if (test-path $delivery_directory) { exec { & $executable uninstall } rd $delivery_directory -rec -force }

Depois, a tarefa de implantação copia a saída de compilação para o diretório de entrega para implantar o novo código, e depois copia as exibições e pastas estáticas no diretório de entrega:

copy-item 'Sms\bin\Release' $delivery_directory -force -recurse -verbose copy-item 'Sms\views' $delivery_directory -force -recurse -verbose copy-item 'Sms\static' $delivery_directory -force -recurse –verbose

Finalmente, a tarefa de implantação instalará e iniciará o serviço:

exec { & $executable install start }

Quando você implanta o serviço, certifique-se de que sua implementação IsDevelopment retorne false ou você receberá uma exceção de Acesso Negado se o servidor do arquivo não conseguir encontrar a pasta estática. Também, às vezes reinstalar o serviço em cada implantação pode ser problemático. Outra tática é parar, atualizar e depois iniciar o serviço se já estiver instalado.

Como você pode ver, a implantação é ridiculamente simples. O IIS e o InstallUtil são completamente removidos da equação; há um processo de implantação ao invés de dois; e não tem a necessidade de se preocupar sobre como a Web ou a camada de serviços irão se comunicar. Esta tarefa de implantação pode ser executada repetidamente conforme você constrói sua Web unificada e aplicativo de serviços!

Olhando para o futuro

A melhor maneira de determinar se este modelo combinado funcionará para você é encontrar um projeto de baixo risco e experimentá-lo. É tão simples de desenvolver e implantar um aplicativo com esta arquitetura. Haverá uma curva de aprendizagem ocasional (se você usar o Nancy para MVC, por exemplo). Mas a grande vantagem de usar o OWIN, mesmo se a abordagem combinada não funcionar, é que você pode ainda hospedar o pipeline do OWIN dentro do IIS usando o host ASP.NET (Microsoft.Owin.Host.SystemWeb). Experimente e veja o que você acha.

Wes McClure aproveita seu conhecimento para ajudar os clientes a entregar rapidamente software de alta qualidade para aumentar de forma exponencial o valor que eles criam para o cliente. Ele gosta de afalar sobre tudo relacionado ao desenvolvimento de software, é um autor do Pluralsight e escreve sobre suas experiências em devblog.wesmcclure.com. Entre em contato com ele no email wes.mcclure@gmail.com.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Howard Dierking (Microsoft), Damian Hickey, Chris Patterson (RelayHealth), Chris Ross (Microsoft) e Travis Smith
Howard Dierking é gerente de programas da equipe de ferramentas e estruturas do Windows Azure, na qual o seu foco está em ASP.NET, NuGet e APIs Web. Anteriormente, Dierking trabalhou como editor-chefe da MSDN Magazine e também comandou o programa de certificação de desenvolvedores para o Microsoft Learning. Antes de fazer parte da Microsoft, ele foi desenvolvedor e arquiteto de aplicativos por dez anos, com foco em sistemas distribuídos.

Chris Ross é um engenheiro de design de software na Microsoft, onde ele se foca em todas as coisas relacionadas à rede e ao OWIN.

Chris Patterson é um arquiteto para RelayHealth, a empresa de conectividade da McKesson Corporation, e é responsável pela arquitetura e desenvolvimento de aplicativos e serviços que aceleram a prestação de cuidados de saúde conectando pacientes, provedores, farmacêuticos e instituições financeiras. Chris é o principal contribuinte para o Topshelf e MassTransit, e recebeu o prêmio Most Valuable Professional da Microsoft pelas suas contribuições técnicas à comunidade.

Damian Hickey é um desenvolvedor de software com um foco em aplicativos baseados em DDD\CQRS\ES. Ele é um defensor do software de código-fonte aberto .NET e contribui para vários projetos, como o Nancy, NEventStore e outros. Geralmente, ele fala quando as pessoas estão incomodadas de ouvir e também publica blogs em http://dhickey.ie. Entre em contato com ele no email dhickey@gmail.com / @randompunter

Travis Smith é um desenvolvedor defensor para Atlassian e a Atlassian Marketplace. Travis ajuda a promover uma Web aberta, poliglotismo e tecnologias da Web emergentes. Travis é um contribuinte para vários projetos de código-fonte aberto incluindo o Topshelf e MassTransit. Travis pode ser encontrado em eventos de desenvolvedores falando com paixão sobre a criação de excelentes software ou na internet em http://travisthetechie.com.