Introdução
Esse artigo tem como objetivo demonstrar de maneira prática, como armazenar dados estruturados e não estruturados utilizando o Blob e o Tables, ambas funcionalidades do Azure Storage.
Porém, antes de colocarmos a mão na massa, vamos entender de maneira breve o que é cada uma dessas funcionalidades.
Blob File
O Blob é um serviço geralmente utilizado para armazenar grandes quantidades de dados não estruturados, tais como texto ou binários que podem ser acessados através de uma requisição HTTP ou HTTPS.
Tables
Tables é o serviço que armazena dados NoSQL na nuvem de maneira estruturada, fornecendo um repositório chave/atributo. Vale lembrar que as tables não possuem schema.
Bom, agora sem mais delongas, vamos para a parte prática.
Criando nosso Storage no Azure
Nessa primeira parte, vamos acessar o portal do Microsoft Azure através do link: https://portal.azure.com/ e efetuar o nosso login.
Feito isso, vamos utilizar a busca do Azure para achar o serviço Storage Account, conforme nos mostra a figura seguir e selecioná-lo.
Agora, vamos preencher o Name e o Resource Group (caso já possua um, não é necessário criar outro) para criar nossa Storage Account, conforme nos mostra a figura abaixo:
Depois de finalizado o processo de criação da nossa Storage Account, clique sobre ela e em seguida, clique na opção Access Keys que fica logo abaixo de Settings. Feito isso, copie sua Connection String clicando no botão ao lado, conforme a seguir.
Agora vamos começar a segunda parte, que é no Visual Studio.
Criando o Projeto
Vamos criar um projeto do tipo ASP.NET Core Web.API em branco, conforme a figura acima.
Definindo as configurações do Projeto
Clique sobre o projeto que acabamos de criar e clique em propriedades (Windows) ou Options (Mac), acesse as configurações de execução Default e crie uma nova variável de ambiente – que eu vou chamar de CONNECTION_AZURE – e como valor, vamos passar aquela Connection String que copiamos do nosso ambiente Azure passos atrás, conforme mostra a próxima figura.
Depois, na aba ASP.NET Core vamos definir nossa url padrão como index.html, e em seguida vamos clicar em OK.
Agora vamos voltar a nossa solution, criar uma pasta chamada Entities e dentro dela vamos criar uma classe chamada Pessoa, com o seguinte código:
using Microsoft.WindowsAzure.Storage.Table; namespace Imasters.Entities { public class Pessoa : TableEntity { public Pessoa() { } public Pessoa(string nome, string email) { this.PartitionKey = nome; this.RowKey = email; } public string FotoUrl { get; set; } } }
Observe que precisamos herdar a classe TableEntity para mapearmos os objetos para o C#, além disso criamos um construtor personalizado usando nome e e-mail como chave e atributo para que sejam únicos na tabela.
Agora vamos criar uma pasta chamada Interfaces, e dentro dela criar a interface IImasterInterface.cs com o código abaixo:
using System.Collections.Generic; using System.Threading.Tasks; using Imasters.Entities; using Microsoft.AspNetCore.Http; namespace Imasters.Interfaces { public interface IImasterInterface { Task<string> UploadBlobFileAsync(IFormFile file); Task Save(string name, string email, string documentUrl); Task<List<Pessoa>> List(); } }
No código acima criamos os métodos de Upload, Salvar e Listar que serão implementados logo abaixo pelo nosso serviço.
No serviço, primeiramente vamos criar uma pasta Services e em seguida vamos criar uma classe chamada ImasterService.cs com o seguinte código (comentários no próprio código para melhor entendimento):
using System; using System.Collections.Generic; using System.Threading.Tasks; using Imasters.Entities; using Imasters.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.WindowsAzure.Storage; using Microsoft.WindowsAzure.Storage.Table; namespace Imasters.Services { public class ImasterService : IImasterInterface { /// <summary> /// Uploads the BLOB file async. /// </summary> /// <returns>Metódo que efetua o upload da foto da pessoa no Blob Storage.</returns> /// <param name="file">File.</param> public async Task<string> UploadBlobFileAsync(IFormFile file) { //Recupera a Connection String da variável de ambiente que definimos no inicio do projeto. var storageAccount = CloudStorageAccount.Parse(Environment.GetEnvironmentVariable("CONNECTION_AZURE")); //Variável que representa o endpoint do blob na Storage Account. var blobClient = storageAccount.CreateCloudBlobClient(); //Tenta recuperar o container em que será salvo o Blob, se não existir ele cria. Obs: O nome do container não deve conter letras maisucúlas. var container = blobClient.GetContainerReference("meucontainer"); await container.CreateIfNotExistsAsync(); //Recupera a referência do endereço do blob e faz o upload do mesmo. var blockBlob = container.GetBlockBlobReference(file.FileName); using (var fileStream = file.OpenReadStream()) { await blockBlob.UploadFromStreamAsync(fileStream); } return blockBlob.Uri.ToString(); } /// <summary> /// Save the specified name, email and documentUrl. /// </summary> /// <returns>Método que salva os dados cadastrados nas tabelas do Storage do Azure.</returns> /// <param name="name">Name.</param> /// <param name="email">Email.</param> /// <param name="documentUrl">Document URL.</param> public async Task Save(string name, string email, string documentUrl) { //Recupera a Connection String da variável de ambiente que definimos no inicio do projeto. var storageAccount = CloudStorageAccount.Parse(Environment.GetEnvironmentVariable("CONNECTION_AZURE")); //Cria o client para a tabela. var tableClient = storageAccount.CreateCloudTableClient(); //Tenta recuperar o objeto que representará a tabela pessoa, se não existir ele cria. Obs: O nome da tabela não deve conter letras maisucúlas. var table = tableClient.GetTableReference("pessoa"); await table.CreateIfNotExistsAsync(); //Cria o objeto pessoa. var person = new Pessoa(name, email); person.FotoUrl = documentUrl; //Efetua a inserção na tabela. var tableOperation = TableOperation.Insert(person); await table.ExecuteAsync(tableOperation); } /// <summary> /// Metódo que irá listar todos os cadastros efetuados. /// </summary> /// <returns>The list.</returns> public async Task<List<Pessoa>> List() { //Recupera a Connection String da variável de ambiente que definimos no inicio do projeto. var storageAccount = CloudStorageAccount.Parse(Environment.GetEnvironmentVariable("CONNECTION_AZURE")); //Cria o client para a tabela. var tableClient = storageAccount.CreateCloudTableClient(); //Recupera o objeto que representará a tabela pessoa. var table = tableClient.GetTableReference("pessoa"); //Inicializa a variável que receberá os dados. var tableQuery = new TableQuery<Pessoa>(); //Inicializa a variavel que representa o token para null para garantir que se comece no inicio da tabela. TableContinuationToken continuationToken = null; //Inicializa a variável que receberá o resultado. TableQuerySegment<Pessoa> tableQueryResult = null; //Lê toda a tabela. do { tableQueryResult = await table.ExecuteQuerySegmentedAsync(tableQuery, continuationToken); continuationToken = tableQueryResult.ContinuationToken; } while (continuationToken != null); return tableQueryResult.Results; } } }
Neste próximo passo, vamos criar um Controller que servirá para orquestrar nossas rotas, e para isso, na pasta Controllers já previamente criada na inicialização do projeto, crie o ImasterController.cs com o seguinte código:
using System.Collections.Generic; using System.Threading.Tasks; using Imasters.Entities; using Imasters.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Imasters.Controllers { [Route("api/[controller]")] public class ImasterController : Controller { private IImasterInterface _imasterService; public ImasterController(IImasterInterface imasterService) { _imasterService = imasterService; } [HttpPost] public async Task<IActionResult> Post(string name, string email, IFormFile file) { var url = await _imasterService.UploadBlobFileAsync(file); await _imasterService.Save(name, email, url); return Redirect("../index.html"); } [HttpGet] public async Task<List<Pessoa>> Get() { var people = await _imasterService.List(); return people; } } }
Como vocês podem observar o código acima, ele é bem simples e apenas chama os métodos previamente implementados no ImasterService.cs.
Agora, para finalizar a parte “backend” do projeto vamos adicionar a injeção de dependência e habilitar nosso projeto para ler arquivos estáticos (CSS, Imagens e etc). Para isso, no arquivo Startup.cs no método ConfigureServices adicione a seguinte linha:
services.AddScoped<IImasterInterface, ImasterService>();
E no método Configure adicione as seguintes linhas:
app.UseDefaultFiles(); app.UseStaticFiles();
Agora finalmente finalizamos todo o nosso “backend”, faltando apenas uma página HTML para o formulário e outra página HTML para a listagem dos dados que seguem abaixo:
Index.html
<form class="form-labels-on-top" method="post" enctype="multipart/form-data" action="api/imaster"> <div class="form-title-row"> <h1>Cadastro de Pessoas</h1> </div> <div class="form-row"> <label> <span>Nome</span> <input type="text" name="name"> </label> </div> <div class="form-row"> <label> <span>Email</span> <input type="email" name="email"> </label> </div> <div class="form-row"> <label> <span>Foto</span> <input type="file" name="file" /> </label> </div> <div class="form-row"> <button type="submit">Salvar</button> </div> </form>
List.html
<div class="main-content"> <div class="list"> <div class="row list-title-row"> <div class="col-md-4"><strong>Nome</strong></div> <div class="col-md-4"><strong>Email</strong></div> <div class="col-md-4"><strong>Documento</strong></div> </div> <div id="result"></div> </div> </div> <script> $(document).ready(function () { var uri = 'api/imaster'; $(document).ready(function () { $.getJSON(uri).done(function (data) { $.each(data, function (key, value) { var valido; if (value.isValid) valido = "Sim"; else valido = "Não"; $("#result").append("<div class='row list-row'>"); $("#result").append("<div class='col-md-3'>" + value.partitionKey + "</div>"); $("#result").append("<div class='col-md-3'>" + value.rowKey + "</div>"); $("#result").append("<div class='col-md-4'><img src='" + value.fotoUrl +"' width='120' style='margin-top: -30px;' /></div>"); $("#result").append("</div>"); }); }); }); }); </script>
Agora é só rodar o projeto e efetuar um cadastro. Porém, você verá que a imagem não vai aparecer e acontecerá um erro de permissão. Isso porque, por padrão, qualquer container criado é Private, portanto você deve alterar isso para Public para que você veja a imagem corretamente conforme mostra na figura abaixo:
Obviamente o Azure fornece opções de segurança para controlarmos acesso de nossos arquivos dentro de um Storage, mas acho que esse assunto fica para um próximo artigo.
Espero que tenham gostado e até a próxima!