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 /usersPOST 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+Pon 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 apidirectory, and rundotnet run.
- Start the Functions project: open a new terminal window, navigate to the functionsproject, and run the commandfunc start --dotnet-isolated
- Verify: Observe the output in the functionsterminal. You should see the users being logged there, confirming that the function is running correctly and accessing the API.
 

 
    
Top comments (0)