DEV Community

Douglas SantAnna Figueredo
Douglas SantAnna Figueredo

Posted on

1

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 = "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();
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.

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more