Upload de imagem para o Azure Storage com C#
O upload de imagem no Azure Storage resolve um problema que quase toda aplicação enfrenta: onde guardar as fotos e arquivos que os usuários enviam. Neste artigo, você vê por que o banco de dados e o disco do servidor não são boas opções e como implementar o envio com C# em poucas linhas.
Em boa parte das aplicações que desenvolvemos, temos o envio de imagens. Hoje, com as facilidades do frontend, é muito simples tratar uma imagem, recortar, aplicar efeitos e depois enviar para o servidor.
O que antes acontecia todo no backend, hoje se divide entre os dois lados. Porém, ainda temos o trabalho de capturar e armazenar essa imagem, mas onde?
No banco de dados não!
Quando comecei o balta.io (na época era ABT, André Baltieri Treinamentos), optei pelo mínimo de infraestrutura possível. Eu hospedava o site, a API e o app em um lugar só, na mesma máquina, sem escalonamento nem nada.
Nessa época, eu salvava as imagens em um banco de dados. Afinal, elas vinham da tela como string (Base64), então era simples.
O problema é que o banco cresce demais em tamanho. Além disso, as imagens não podem ser reaproveitadas, porque não têm uma URL, e sempre que precisamos de uma imagem temos que ir até o banco.
Fora isso, a thread não finaliza enquanto você não envia todas as informações para a tela. Nesse caso, se você tem imagens grandes, segura a thread da API e a conexão com o banco abertas enquanto o tráfego acontece.
Há também um custo de backup. Cada cópia do banco carrega todas as imagens junto, o que deixa as rotinas de backup e restauração mais lentas e caras.
Acabou que existem maneiras MUITO melhores de armazenar imagens, como vou mostrar aqui.
Na mesma máquina não!
Bom, se não é legal colocar no banco de dados, então vamos salvar em disco, correto? Bem... quase.
Sempre que você salva uma imagem em disco, consome I/O dessa máquina. Além disso, as requisições para essa imagem vão consumir recursos que podem ser vitais para a sua API.
Como se tudo isso não bastasse, salvar localmente (na mesma máquina) não escala!
Um dos modelos de escalonamento que mais usamos hoje é o horizontal, em que temos várias máquinas menores. Nesse cenário, se nada estiver em uso, pagamos apenas por uma máquina pequena, que fica ali em stand-by.
Isso é diferente de manter uma máquina mais poderosa, que custa mais caro. Sendo assim, máquinas são criadas e destruídas a todo tempo.
Quando nasce, cada máquina se baseia em uma imagem do sistema, e adivinha só: essa imagem contém apenas o SO. Então, um novo build e deploy do seu app acontece e a máquina fica disponível.
Ou seja, nada que estava na memória ou no disco das máquinas anteriores vem junto. Logo, se você salvou alguma imagem em uma máquina, ela ficou lá, apenas lá, e desaparece junto com a máquina assim que ela sair de uso.
Por que fazer upload de imagem no Azure Storage
Se não recomendamos salvar no banco e salvar em disco não escala, para onde podemos correr? Para um storage, e no nosso caso, o Azure Storage.
Como o nome já diz, os storages existem para isso. Eles armazenam arquivos e oferecem backup, geolocalização, CDN e muito mais.
Os storages ficam na mesma região dos nossos apps, porém rodam de forma independente. Isso significa que não afetam o I/O nem o processamento da aplicação e estão sempre disponíveis.
Pode cair máquina, pode subir máquina, e o storage permanece lá. Por isso, o upload de imagem no Azure Storage combina tão bem com o escalonamento horizontal.
Os storages também têm segurança. Por exemplo, você pode exigir um token para acessar um arquivo, e todo arquivo tem uma URL, o que o torna rastreável e reutilizável.
Para finalizar, os storages estão entre os recursos mais baratos do Azure. Segundo a documentação oficial do Azure Blob Storage, o serviço é otimizado justamente para armazenar grandes volumes de dados não estruturados, como imagens e documentos.
Containers e blobs
No Azure Storage, o serviço que guarda arquivos se chama Blob Storage. Dentro de uma conta de armazenamento, você organiza os arquivos em containers, que funcionam como pastas.
Cada arquivo, por sua vez, é um blob. Assim, a URL de uma imagem segue o formato https://{account}.blob.core.windows.net/{container}/{blob}.
Esse modelo simples é o que torna o upload de imagem no Azure Storage tão direto. Você escolhe o container, define o nome do blob e envia os bytes.
Upload de imagem no Azure Storage em Base64
Para o upload de imagem no Azure Storage, os storages aceitam o envio de arquivos como array de bytes. Porém, no frontend é comum trabalharmos com Base64 quando falamos de imagens.
Existem várias bibliotecas para tratamento de imagem, desde recorte até efeitos. A maioria delas devolve a imagem como Base64.
Se você já tiver o array de bytes, fica até mais fácil, porque pulamos um passo no código. No entanto, na maioria das vezes você vai terminar com uma string.
Em termos de performance, strings pesam mais que arrays de bytes na transferência. De fato, o Base64 aumenta o tamanho do conteúdo em cerca de um terço.
Então, se o seu foco é performance, talvez valha a pena converter para binário antes de enviar para a API. Para este artigo, porém, vamos seguir com o cenário mais comum, em que a API recebe o Base64.
Criando o método de upload de imagem no Azure Storage
Eu particularmente crio um serviço para o upload de imagem no Azure Storage, abstraindo o Azure da minha API. Isso é importante para futuras mudanças na aplicação.
Como o envio de imagem está estritamente ligado à infraestrutura, e ela pode mudar, recomendo que você faça o mesmo. Ou seja, crie um método separado, em um serviço, na camada de infraestrutura da sua API.
Esse método vai fazer o envio da imagem e retornar a URL dela.
public string UploadBase64Image(string base64Image, string container) { }
Antes de escrever o corpo do método, instale o pacote oficial do SDK do Azure para blobs. Ele traz a classe BlobClient, que usaremos mais adiante:
dotnet add package Azure.Storage.Blobs
Gerando o nome do arquivo
Ao tentar enviar uma imagem que já existe no storage, você pode se deparar com um erro ou acabar forçando a sobrescrita dela. Na prática, o método Upload do BlobClient lança uma exceção quando o blob já existe, a menos que você peça a sobrescrita.
No upload de imagem no Azure Storage, eu particularmente gosto de gerar nomes aleatórios para evitar esse tipo de problema. Além disso, quando há cache envolvido, como em uma CDN na frente do storage, sobrescrever uma imagem pode levar horas para refletir a mudança.
public string UploadBase64Image(string base64Image, string container) {
// Gera um nome aleatório para a imagem
var fileName = Guid.NewGuid().ToString() + ".jpg";
}
Nesse caso, eu já aproveito para definir o formato da imagem, que sempre será JPG. Contudo, você pode extrair essa informação do Base64 recebido, caso queira salvar no formato correto.
Preparando a imagem
Sempre que uma imagem chega no formato Base64, ela traz primeiro o tipo da imagem, seguido pela codificação, neste caso Base64. Um exemplo é o prefixo data:image/jpeg;base64,.
Porém, para concluir o upload de imagem no Azure Storage esse prefixo não é necessário, e ele gera um erro na conversão. Temos então que remover essa parte.
Para isso, utilizei uma expressão regular que limpa essas informações e já deixa tudo pronto para o envio.
public string UploadBase64Image(string base64Image, string container) {
// Gera um nome aleatório para a imagem
var fileName = Guid.NewGuid().ToString() + ".jpg";
// Remove o prefixo data:image/...;base64,
var data = new Regex(@"^data:image\/[a-z]+;base64,").Replace(base64Image, "");
}
Lembre de importar o namespace System.Text.RegularExpressions para usar a classe Regex.
Gerando o array de bytes
Conforme comentei, para enviar o arquivo precisamos de um array de bytes. Felizmente, o próprio C# resolve isso com o método Convert.FromBase64String.
public string UploadBase64Image(string base64Image, string container) {
// Gera um nome aleatório para a imagem
var fileName = Guid.NewGuid().ToString() + ".jpg";
// Remove o prefixo data:image/...;base64,
var data = new Regex(@"^data:image\/[a-z]+;base64,").Replace(base64Image, "");
// Converte a string Base64 em um array de bytes
byte[] imageBytes = Convert.FromBase64String(data);
}
Se a string recebida não for um Base64 válido, esse método lança uma FormatException. Por isso, vale validar a entrada na API e devolver um erro amigável ao cliente.
Fazendo o upload de imagem no Azure Storage
O primeiro passo do upload de imagem no Azure Storage é definir o destino. Como vimos, no Azure Storage temos os blobs e os containers, que representam o arquivo e a "pasta", respectivamente.
Neste caso, recebemos o container por parâmetro, e é importante frisar que ele precisa existir. Caso contrário, a operação retorna um erro.
Com tudo definido, podemos usar um MemoryStream para ler o array de bytes e fazer o upload para o Azure.
public string UploadBase64Image(string base64Image, string container) {
// Gera um nome aleatório para a imagem
var fileName = Guid.NewGuid().ToString() + ".jpg";
// Remove o prefixo data:image/...;base64,
var data = new Regex(@"^data:image\/[a-z]+;base64,").Replace(base64Image, "");
// Converte a string Base64 em um array de bytes
byte[] imageBytes = Convert.FromBase64String(data);
// Define o blob no qual a imagem será armazenada
var blobClient = new BlobClient("YOUR_CONNECTION_STRING", container, fileName);
// Envia a imagem
using (var stream = new MemoryStream(imageBytes)) {
blobClient.Upload(stream);
}
}
O construtor do BlobClient recebe a connection string da conta, o nome do container e o nome do blob. Em seguida, o método Upload cria o blob com o conteúdo do stream.
Obtendo a URL após o upload de imagem no Azure Storage
Por fim, tudo que precisamos fazer é retornar a URL da imagem que acabamos de enviar.
public string UploadBase64Image(string base64Image, string container) {
// Gera um nome aleatório para a imagem
var fileName = Guid.NewGuid().ToString() + ".jpg";
// Remove o prefixo data:image/...;base64,
var data = new Regex(@"^data:image\/[a-z]+;base64,").Replace(base64Image, "");
// Converte a string Base64 em um array de bytes
byte[] imageBytes = Convert.FromBase64String(data);
// Define o blob no qual a imagem será armazenada
var blobClient = new BlobClient("YOUR_CONNECTION_STRING", container, fileName);
// Envia a imagem
using (var stream = new MemoryStream(imageBytes)) {
blobClient.Upload(stream);
}
// Retorna a URL da imagem
return blobClient.Uri.AbsoluteUri;
}
Pronto, temos upload de imagem no Azure Storage com C# em cerca de 20 linhas de código. Agora, basta salvar essa URL no banco de dados, junto com o registro a que a imagem pertence.
Repare na diferença em relação ao cenário inicial. Em vez de guardar megabytes de Base64 em uma coluna, o banco passa a guardar apenas uma string curta com o endereço do arquivo.
Expondo o upload em um endpoint
Com o serviço pronto, falta expor o upload de imagem no Azure Storage para o frontend. Uma forma simples é criar um endpoint que recebe o Base64 no corpo da requisição.
O exemplo abaixo usa Minimal APIs e o contrato IImageUploadService, que veremos na próxima seção:
// Contrato da requisição enviada pelo frontend
public record UploadImageRequest(string Base64Image);
app.MapPost("/images", async (UploadImageRequest request, IImageUploadService service) =>
{
// Envia a imagem para o container "images" e devolve a URL
var url = await service.UploadBase64ImageAsync(request.Base64Image, "images");
return Results.Ok(new { url });
});
Assim, o frontend envia a imagem tratada e recebe de volta apenas a URL. Em seguida, ele pode usar esse endereço direto em uma tag img ou salvá-lo junto com o restante do formulário.
Melhorias no upload de imagem no Azure Storage
O código acima resolve o problema de forma direta. Ainda assim, algumas melhorias deixam o upload de imagem no Azure Storage mais robusto para produção.
Definindo o content type
Por padrão, o blob fica salvo com um tipo genérico de conteúdo. Nesse caso, alguns navegadores podem baixar o arquivo em vez de exibi-lo.
Para evitar isso, informe o ContentType nos cabeçalhos HTTP do blob. Além disso, a versão assíncrona do método libera a thread enquanto o envio acontece:
// Define o tipo de conteúdo para o navegador exibir a imagem
var options = new BlobUploadOptions {
HttpHeaders = new BlobHttpHeaders { ContentType = "image/jpeg" }
};
using var stream = new MemoryStream(imageBytes);
await blobClient.UploadAsync(stream, options);
As classes BlobUploadOptions e BlobHttpHeaders ficam no namespace Azure.Storage.Blobs.Models.
Criando uma interface para o serviço
Lá no início, comentei que poderíamos criar uma interface para esse serviço. Essa abstração facilita testes e a troca de provedor no futuro:
public interface IImageUploadService {
Task<string> UploadBase64ImageAsync(string base64Image, string container);
}
Depois, basta registrar a implementação no contêiner de injeção de dependência do ASP.NET Core. Dessa forma, a API depende apenas do contrato, e não do Azure.
Segurança no upload de imagem no Azure Storage
No upload de imagem no Azure Storage, nunca deixe a connection string fixa no código. Em vez disso, leia o valor da configuração, como mostro no artigo sobre o ConfigurationManager no ASP.NET.
Em produção, a Microsoft recomenda ir além e usar autenticação sem senha, com o Microsoft Entra ID e a classe DefaultAzureCredential. Assim, nenhum segredo da conta de armazenamento fica exposto na aplicação.
Por outro lado, se o seu time trabalha com JavaScript no backend, vale ler também como fazer o upload de imagem com Node.js. E, para colocar tudo no ar, confira o guia de deploy com GitHub Actions e Microsoft Azure.
Conclusão
Guardar imagens no banco de dados ou no disco do servidor parece prático no começo. Contudo, essas abordagens cobram um preço alto em tamanho de banco, consumo de recursos e escalabilidade.
O upload de imagem no Azure Storage separa o armazenamento da aplicação. Com isso, cada arquivo ganha uma URL própria, as máquinas podem nascer e morrer à vontade e o custo continua baixo.
Com o SDK Azure.Storage.Blobs, o upload de imagem no Azure Storage se resume a poucos passos: gerar um nome, limpar o Base64, converter para bytes, enviar e devolver a URL. Por fim, isolar tudo em um serviço deixa a API pronta para mudanças futuras.
FAQ
Por que não salvar imagens no banco de dados?
Porque o banco cresce demais, as imagens ficam sem URL e cada leitura prende a conexão e a thread da API. Um storage resolve esses problemas com custo menor.
O container precisa existir antes do upload?
Sim, com o código deste artigo o container precisa existir, senão a operação retorna erro. Você pode criá-lo pelo portal ou chamar CreateIfNotExists no BlobContainerClient.
Por que gerar nomes aleatórios para as imagens?
Para evitar conflito com arquivos existentes, já que o Upload lança exceção quando o blob já existe. Além disso, nomes novos evitam problemas de cache ao trocar uma imagem.
Preciso remover o prefixo data:image do Base64?
Sim, porque o Convert.FromBase64String não reconhece esse prefixo e lança uma exceção. A expressão regular do artigo faz essa limpeza.
Qual pacote NuGet devo usar?
Use o Azure.Storage.Blobs, que é a biblioteca oficial e atual do Azure para Blob Storage. Ele traz as classes BlobClient e BlobContainerClient.