DEV Community

Douglas SantAnna Figueredo
Douglas SantAnna Figueredo

Posted on

Azure Function with .NET Isolated Worker: Sharing Services Between API and Function Apps

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
Enter fullscreen mode Exit fullscreen mode
  • Create the API project:
mkdir api
cd api
dotnet new webapi -f net8.0  # Use webapi template for cleaner API project
Enter fullscreen mode Exit fullscreen mode
  • Create the Function project:
cd ..
mkdir functions
cd functions
func init --worker-runtime dotnet-isolated  # Or func init http for an HTTP triggered function
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • Add a project reference from the Functions project to the API project:
cd functions
dotnet add reference ../api/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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
}
Enter fullscreen mode Exit fullscreen mode
  • 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; }
}
Enter fullscreen mode Exit fullscreen mode
  • 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();
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 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>();
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 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 = "[email protected]" }, 
        new User { Name = "Bob", Email = "[email protected]" }, 
        new User { Name = "Charlie", Email = "[email protected]" }, // 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();
Enter fullscreen mode Exit fullscreen mode
  • 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"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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();
Enter fullscreen mode Exit fullscreen mode
  • Create the Function by running command func new --name users, select function type TimerTrigger
// 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}");
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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 (or Cmd+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 run dotnet run.
  • Start the Functions project: open a new terminal window, navigate to the functions project, and run the command func 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)