Building a Clean and Maintainable Azure Function with Dependency Injection and Upsert Logic



In this blog, we walk through building an event-based Azure Function that saves customer data into a SQL database using a clean architecture pattern with Dependency Injection (DI), AutoMapper, and a reusable Base Service for Upsert operations.

We’ll cover:

  • Setting up the Azure Function

  • Injecting dependencies cleanly

  • Creating a generic upsert method

  • Handling logging and error reporting


1. Overview of the Azure Function

The Azure Function accepts customer data as JSON through an HTTP POST request and either inserts or updates the corresponding database record.

[Function(nameof(SaveCustomer))]
public class SaveCustomerFunction
{
    private readonly SaveCustomer _customerService;
    private readonly Logger _logger;

    public SaveCustomerFunction(SaveCustomer customerService, Logger logger)
    {
        _customerService = customerService;
        _logger = logger;
    }

    [Function("SaveCustomer")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "post", Route = "save-customer")] HttpRequest req)
    {
        _logger.LogInformation("SaveCustomer function triggered.");

        string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
        var customer = JsonSerializer.Deserialize<Customer>(requestBody);

        try
        {
            await _customerService.SaveCustomerService(customer);
            return new OkResult();
        }
        catch (Exception ex)
        {
            _logger.LogException(ex);
            return new ObjectResult("Internal Server Error") { StatusCode = 500 };
        }
    }
}

2. Registering Dependencies

Add services in Startup.cs (in-process) or Program.cs (isolated worker):

builder.Services.AddDbContext<ConstructionDbContext>(...);
builder.Services.AddScoped<SaveCustomer>();
builder.Services.AddSingleton<Logger>();
builder.Services.AddSingleton<AppConfiguration>();
builder.Services.AddAutoMapper(typeof(CustomerProfile));

3. The SaveCustomer Service

This service validates the incoming payload and calls the base method to upsert:

public class SaveCustomer : Base
{
    private readonly ITypeMapper _mapper;

    public SaveCustomer(ConstructionDbContext dbContext, ITypeMapper mapper, AppConfiguration config, Logger logger)
        : base(dbContext, config, logger)
    {
        _mapper = mapper;
    }

    public async Task SaveCustomerService(Customer customer)
    {
        if (customer == null ||
            string.IsNullOrEmpty(customer.CustomerName) ||
            string.IsNullOrEmpty(customer.AccountNumber) ||
            string.IsNullOrEmpty(customer.BusinessUnit))
        {
            throw new ArgumentNullException(nameof(customer));
        }

        await base.UpsertEntityAsync<DDPCustomer, Customer>(
            customer,
            e => e.AccountNumber == customer.AccountNumber && e.BusinessUnit == customer.BusinessUnit,
            _mapper.Map<DDPCustomer>,
            (existing, mapped) =>
            {
                if (existing != null)
                    mapped.CustomerId = existing.CustomerId;
            });
    }
}

4. Base UpsertEntityAsync Method

This generic method simplifies insert/update logic:

protected async Task UpsertEntityAsync<TEntity, TModel>(
    TModel model,
    Expression<Func<TEntity, bool>> predicate,
    Func<TModel, TEntity> mapFunc,
    Action<TEntity?, TEntity>? copyKeyValues = null)
    where TEntity : class
{
    var existing = await dbContext.Set<TEntity>().FirstOrDefaultAsync(predicate);
    var entity = mapFunc(model);
    copyKeyValues?.Invoke(existing, entity);

    if (existing == null)
        dbContext.Set<TEntity>().Add(entity);
    else
        dbContext.Entry(existing).CurrentValues.SetValues(entity);

    await dbContext.SaveChangesAsync();
}

5. Explaining the Core Call

await base.UpsertEntityAsync<DDPCustomer, Customer>(
    customer,
    e => e.AccountNumber == customer.AccountNumber && e.BusinessUnit == customer.BusinessUnit,
    _mapper.Map<DDPCustomer>,
    (existing, mapped) =>
    {
        if (existing != null)
            mapped.CustomerId = existing.CustomerId;
    });
  • Uses AutoMapper to convert Customer into DDPCustomer

  • Finds matching record using AccountNumber and BusinessUnit

  • If a match exists, retains the original CustomerId

  • Then inserts or updates accordingly


Conclusion

By applying DI, AutoMapper, and a base upsert pattern, we:

  • Keep our Azure Function clean and focused

  • Isolate responsibilities into services

  • Reuse base functionality across models

  • Simplify testing and maintenance

You can now easily extend this pattern for other models like Supplier, Quote, etc.


!

Comments

Popular posts from this blog

πŸ€– Copilot vs Microsoft Copilot vs Copilot Studio: What’s the Difference?

Understanding Auto-Numbering in a Multi-Transaction System

Integrating Dynamics 365 CRM with MuleSoft Using a Synchronous C# Plugin