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
intoDDPCustomer
-
Finds matching record using
AccountNumber
andBusinessUnit
-
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
Post a Comment