This tutorial demonstrates how to create an Azure Function with a .NET isolated worker process that utilizes services defined in a separate API project. This architecture allows you to share business logic and data access code between your API and functions, promoting code reuse and maintainability.
Prerequisites:
- Visual Studio Code: Installed and configured for .NET development.
- SQL Server Instance: A running SQL Server instance with a database and a "Users" table already created. (We'll focus on the integration, not the full database setup.)
- Azurite: Installed for local Azure Storage emulation (if using triggers that require storage).
- .NET SDK and Runtime (Version 8): .NET 8 SDK and runtime environment installed on your machine.
- Azure Functions Core Tools: Azure Functions Core Tools installed for local function development and deployment.
For this example, we'll use a simplified scenario with fake data and methods. It's assumed you have a database server running and a database with a "Users" table already created. We'll focus on the integration between the API and Function rather than full database setup.
Step-by-Step Guide
Project Setup
- Create a project folder:
mkdir az-func-api
cd az-func-api
- Create the API project:
mkdir api
cd api
dotnet new webapi -f net8.0 # Use webapi template for cleaner API project
- Create the Function project:
cd ..
mkdir functions
cd functions
func init --worker-runtime dotnet-isolated # Or func init http for an HTTP triggered function
Follow the prompts to configure your function. Choose the dotnet (isolated worker model)
and then c#-isolated
.
- Add both projects to the solution:
cd ..
dotnet new sln # Create Solution File
dotnet sln add api
dotnet sln add functions
- Add a project reference from the Functions project to the API project:
cd functions
dotnet add reference ../api/
API Project Setup
- Install NuGet Packages:
cd api
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 8.0.1
dotnet add package Microsoft.EntityFrameworkCore.Tools --version 8.0.1
Configure Database Connection:
Add your database connection string to appsettings.json
in the API project:
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost,1433;Database=MyDatabase;User Id=sa;Password=MyPassword;Encrypt=True;TrustServerCertificate=True"
}
}
- Create Data Model (
User.cs
):
// api/Models/User.cs
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; } // Added Email property for demonstration
}
- Create Data Context (
DataContext.cs
):
// api/Data/DataContext.cs
public class DataContext : DbContext
{
public DataContext(DbContextOptions<DataContext> options) : base(options) { }
public DbSet<User> Users { get; set; }
}
- Create Repository Interface and Implementation (
IUserRepository.cs
,UserRepository.cs
):
// api/Interfaces/IUserRepository.cs
public interface IUserRepository
{
Task<List<User>> GetUsers(CancellationToken cancellationToken);
Task CreateUsers(List<User> users);
}
// api/Repositories/UserRepository.cs
public class UserRepository : IUserRepository
{
private readonly DataContext _context;
public UserRepository(DataContext context)
{
_context = context;
}
public async Task<List<User>> GetUsers(CancellationToken cancellationToken)
{
return await _context.Users.ToListAsync(cancellationToken);
}
public async Task CreateUsers(List<User> users)
{
await _context.Users.AddRangeAsync(users);
await _context.SaveChangesAsync();
}
}
- Register Services (
ServiceExtensions.cs
):
// api/Extensions/ServiceExtensions.cs
public static class ServiceExtensions
{
public static void AddApplicationServices(this IServiceCollection services, string connectionString)
{
services.AddDbContext<DataContext>(opt =>
opt.UseSqlServer(connectionString));
services.AddTransient<IUserRepository, UserRepository>();
}
}
- Configure API Endpoints (
Program.cs
):
// api/Program.cs
var builder = WebApplication.CreateBuilder(args);
// Retrieve the connection string (better practice than hardcoding)
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
if (string.IsNullOrEmpty(connectionString))
{
throw new InvalidOperationException("Connection string 'DefaultConnection' is missing.");
}
builder.Services.AddApplicationServices(connectionString); // Pass connection string here
var app = builder.Build();
app.MapPost("/users", async (IUserRepository userRepository) =>
{
var fakeUsers = new List<User> // List of fake users
{
new User { Name = "Alice", Email = "alice@example.com" },
new User { Name = "Bob", Email = "bob@example.com" },
new User { Name = "Charlie", Email = "charlie@example.com" }, // Add more fake users as needed
};
await userRepository.CreateUsers(fakeUsers);
return Results.Created($"/users", fakeUsers);
});
app.MapGet("/users", async (IUserRepository userRepository, CancellationToken cancellationToken) =>
{
return Results.Ok(await userRepository.GetUsers(cancellationToken));
});
app.Run();
- Test API Endpoints: Use Postman or a similar tool to test the
/users
POST and GET endpoints.
Functions Project Setup
- Configure
local.settings.json
:
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"DefaultConnection": "Server=localhost,1433;Database=MyDatabase;User Id=sa;Password=MyPassword;Encrypt=True;TrustServerCertificate=True"
}
}
- Configure
Program.cs
:
// functions/Program.cs
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using System;
var builder = FunctionsApplication.CreateBuilder(args);
builder.ConfigureFunctionsWebApplication();
builder.Configuration
.AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables();
var connectionString = builder.Configuration.GetValue<string>("Values:DefaultConnection");
if (string.IsNullOrEmpty(connectionString))
throw new InvalidOperationException("Connection string 'DefaultConnection' is missing.");
builder.Services.AddApplicationServices(connectionString); // Use the same extension method
builder.Build().Run();
- Create the Function by running command
func new --name users
, select function typeTimerTrigger
// functions/listUsers.cs
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using api.Interfaces; // Important: Use the interface from the API project
namespace MyFunctionApp
{
public class listUsers
{
private readonly ILogger _logger;
private readonly IUserRepository _userRepository; // Inject the repository
public listUsers(ILoggerFactory loggerFactory, IUserRepository userRepository)
{
_logger = loggerFactory.CreateLogger<listUsers>();
_userRepository = userRepository;
}
[Function("listUsers")]
public async Task Run([TimerTrigger("0/10 * * * * *")] CancellationToken cancellationToken) // Example: Timer trigger
{
var users = await _userRepository.GetUsers(cancellationToken); // Use the repository
foreach (var user in users)
{
_logger.LogInformation($"Username: {user.Name}, Email: {user.Email}");
}
}
}
}
Run the Projects
Start Azurite (if needed): If your Azure Function uses any triggers or bindings that require Azure Storage (like Queue Storage, Blob Storage, or Table Storage), you'll need to start Azurite, the local Azure Storage emulator.
- Using the VS Code Extension: The easiest way is often through the Azurite extension in VS Code. Install the "Azurite" extension. Then, use
Ctrl+Shift+P
(orCmd+Shift+P
on macOS) and type "Azurite: Start." This will start Azurite. The extension usually configures the connection strings automatically. - Start the API project: open a terminal, navigate to the
api
directory, and rundotnet run
. - Start the Functions project: open a new terminal window, navigate to the
functions
project, and run the commandfunc start --dotnet-isolated
- Verify: Observe the output in the
functions
terminal. You should see the users being logged there, confirming that the function is running correctly and accessing the API.
Top comments (0)