Esses dias me deparei com a tarefa de implementar o Entity Framework Core em uma Azure Function. Me deparei com uma situação na qual vale a pena ser compartilhada.
Sobre a function e o ambiente de desenvolvimento, temos a seguintes características:
- A function é do tipo isolated worker process;
- Utilizei o .NET 7;
- Utilizei o Visual Studio 2022 for Mac.
Antes de seguir, caso seja a sua primeira vez lendo sobre Azure Functions, recomendo a leitura da documentação oficial. Além disso, eu já escrevi alguns posts sobre esse recurso de nuvem, você pode conferir aqui e aqui.
Inicialmente vamos criar o projeto de functions diretamente pela IDE.
A function criada será do tipo Timer trigger.
Para implementar o Entity Framework Core, iremos adicionar os seguintes pacotes ao nosso projeto. Vale mencionar que estamos criando um banco de dados do zero (SQL Server), por isso utilizaremos a abordagem code-first.
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.Tools
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Design
No arquivo local.settings.json, vamos adicionar a connectionString do nosso banco de dados. Nesse caso, eu levantei um SQL Server via docker no meu ambiente local.
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"ConnectionString": "YOUR-CONNECTION-STRING"
}
}
É hora de criar um modelo simples de dados. Vamos declarar a classe Book com apenas duas propriedades.
using System;
namespace AzureFunctionEF.Models
{
public class Book
{
public Guid Id { get; set; }
public string Name { get; set; } = default!;
}
}
Devemos também implementar a classe Context. Observe que fizemos o mapeamento de como a classe Book será utilizada como base da tabela Books
using System;
using AzureFunctionEF.Models;
using Microsoft.EntityFrameworkCore;
namespace AzureFunctionEF.Data
{
public class Context : DbContext
{
public Context(DbContextOptions options) : base(options)
{
}
public DbSet<Book> Books { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Book>()
.ToTable("Books");
}
}
}
Com a classe Context implementada, já podemos fazer a configuração do Entity Framework Core na classe Program.cs.
using AzureFunctionEF.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = new HostBuilder()
.ConfigureFunctionsWorkerDefaults();
builder.ConfigureServices((hostContext, services) =>
{
string connectionString = hostContext.Configuration.GetSection("ConnectionString").Value!;
services.AddDbContext<Context>(
options => SqlServerDbContextOptionsExtensions.UseSqlServer(options, connectionString));
});
builder.Build().Run();
Para finalizar, basta fazermos a injeção de dependência do Context na function criada por default no nosso projeto. Essa function irá basicamente criar um novo registro de Book a cada período determinado pelo trigger da function.
using System;
using AzureFunctionEF.Data;
using AzureFunctionEF.Models;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace AzureFunctionEF
{
public class DefaultFunction
{
private readonly ILogger _logger;
private readonly Context _context;
public DefaultFunction(ILoggerFactory loggerFactory, Context context)
{
_logger = loggerFactory.CreateLogger<DefaultFunction>();
_context = context;
}
[Function("DefaultFunction")]
public async Task RunAsync([TimerTrigger("0 */1 * * * *")] MyInfo myTimer)
{
_logger.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
var id = Guid.NewGuid();
await _context.Books.AddAsync(new Book
{
Id = id,
Name = $"Book_{id}"
});
await _context.SaveChangesAsync();
_logger.LogInformation($"Next timer schedule at: {myTimer.ScheduleStatus.Next}");
}
}
public class MyInfo
{
public MyScheduleStatus ScheduleStatus { get; set; }
public bool IsPastDue { get; set; }
}
public class MyScheduleStatus
{
public DateTime Last { get; set; }
public DateTime Next { get; set; }
public DateTime LastUpdated { get; set; }
}
}
Dito isso, já podemos gerar a primeira migration e fazer o update do banco de dados.
dotnet ef migrations add InitialMigration --verbose
Observe que recebemos um erro, dizendo que a classe Context não pode ser criada.
Microsoft.EntityFrameworkCore.Design.OperationException: Unable to create an object of type 'Context'
Para resolver esse problema, devemos criar a classe ContextFactory que implementa IDesignTimeDbContextFactory. Com isso podemos construir classes que herdam de DbContext em design-time. Recomendo a leitura deste conceito!
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace AzureFunctionEF.Data
{
public class ContextFactory : IDesignTimeDbContextFactory<Context>
{
public Context CreateDbContext(string[] args)
{
var index = Array.IndexOf(args, "--connection-string");
if (index == -1)
throw new Exception("Parameter --connection-string did not provide");
string connectionString = args[index + 1];
Console.WriteLine(connectionString);
var optionsBuilder = new DbContextOptionsBuilder<Context>();
optionsBuilder.UseSqlServer(connectionString);
return new Context(optionsBuilder.Options);
}
}
}
Dessa forma, já podemos criar a migration. Precisaremos desta vez passar a connection string via parâmetro.
dotnet ef migrations add InitialMigration --verbose -- --connection-string '<YOUR-CONNECTION-STRING>'
É hora de aplicarmos essa migration ao banco de dados.
dotnet ef database update --verbose -- --connection-string '<YOUR-CONNECTION-STRING>'
Podemos observar que tudo foi criado corretamente.
Ao executarmos a function, depois de alguns ciclos, percebemos ela está salvando novos registros de forma correta. O Entity Framework Core foi implementado corretamente!
Você já pode baixar o projeto por esse link, e não esquece de me seguir no LinkedIn!
Até a próxima, abraços!