Independent technical project
Building the integration—not just diagramming it.
This project demonstrates how I would implement a production-minded ERP/CRM integration using C#, ASP.NET Core, and Microsoft Azure. I focused on the parts that make integrations dependable in real environments: validation, persistence, duplicate protection, secrets, telemetry, automated tests, and repeatable deployment.
Portfolio context: This is an independent technical demonstration created to show my ability to build and explain Azure/C# integration patterns. It is not presented as two years of professional Azure or C# implementation experience.
The engineering problem
How can an ERP safely send transactions to another enterprise system without creating duplicates, exposing credentials, or making failures invisible?
01
Reliability
Design for retries, duplicate messages, validation failures, and downstream issues.
02
Security
Keep credentials out of source code and separate application logic from secret management.
03
Operability
Make every request traceable through logs, correlation IDs, test coverage, and repeatable releases.
1. API implementation
The ASP.NET Core API accepts JSON payloads from an ERP or CRM source, validates them, routes business logic through a service layer, and returns consistent HTTP responses.
C# endpoint
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
private readonly IOrderIntegrationService _service;
public OrdersController(IOrderIntegrationService service)
=> _service = service;
[HttpPost]
public async Task<IActionResult> Create(
[FromBody] OrderRequest request,
CancellationToken ct)
{
var result = await _service.ProcessAsync(request, ct);
return result.IsDuplicate
? Ok(result)
: CreatedAtAction(nameof(GetById),
new { id = result.OrderId }, result);
}
}
Service-layer logic
public async Task<OrderResult> ProcessAsync(
OrderRequest request,
CancellationToken ct)
{
Validate(request);
var existing = await _repo.FindByExternalIdAsync(
request.SourceSystem,
request.ExternalOrderId,
ct);
if (existing is not null)
return OrderResult.Duplicate(existing.Id);
var order = Order.Create(request);
await _repo.AddAsync(order, ct);
await _archive.SavePayloadAsync(request, ct);
_logger.LogInformation(
"Processed {Source}/{ExternalOrderId}",
request.SourceSystem,
request.ExternalOrderId);
return OrderResult.Created(order.Id);
}
2. Database design
The relational model separates customer, order, item, event, and error data so transactions remain queryable while integration history stays traceable.
CustomersCustomerIdExternalCustomerIdName
OrdersOrderIdSourceSystemExternalOrderIdStatus
OrderItemsOrderItemIdOrderIdSKUQuantity
IntegrationEventsCorrelationIdPayloadUriProcessedUtc
IntegrationErrorsCorrelationIdErrorTypeMessage
CREATE UNIQUE INDEX UX_Orders_Source_External
ON Orders(SourceSystem, ExternalOrderId);
CREATE INDEX IX_IntegrationEvents_CorrelationId
ON IntegrationEvents(CorrelationId);
CREATE INDEX IX_Orders_Status_CreatedUtc
ON Orders(Status, CreatedUtc);
3. Handling duplicate requests
A source system can successfully submit an order but fail to receive the response, causing it to retry. The integration therefore treats Source System + External Order ID as an idempotency key. The application checks first, and the database unique index provides a second protection layer.
POST /api/orders
D365 · SO-10482
Lookup existing
Duplicate found
Return existing result
4. Security implementation
Secrets are not hard-coded into application logic. Configuration is designed around Azure Key Vault and identity-based access patterns, allowing App Service to retrieve protected configuration without embedding credentials in source code.
var credential = new DefaultAzureCredential();
builder.Configuration.AddAzureKeyVault(
new Uri(keyVaultUri),
credential);
builder.Services.AddDbContext<IntegrationDbContext>(options =>
options.UseSqlServer(
builder.Configuration["SqlConnectionString"]));
5. Blob Storage archive
Original inbound payloads are archived separately from relational transaction data. This supports troubleshooting, auditability, and replay scenarios without overloading the transactional schema.
var blobName =
$"{request.SourceSystem}/{DateTime.UtcNow:yyyy/MM/dd}/" +
$"{request.ExternalOrderId}-{correlationId}.json";
await _blobContainer.UploadBlobAsync(
blobName,
BinaryData.FromObjectAsJson(request),
cancellationToken);
6. Failure handling & observability
I designed the logging path so failures can be traced from request through processing. Correlation IDs connect API logs, integration events, and error records.
Correlation ID
POST /api/orders
Validation
Exception captured
400 + telemetry
_logger.LogError(
ex,
"Order processing failed. CorrelationId={CorrelationId} Source={Source} ExternalId={ExternalId}",
correlationId,
request.SourceSystem,
request.ExternalOrderId);
7. Automated testing
The goal is not ceremonial coverage. The tests focus on business behaviors and integration edge cases that could break production processing.
✓ Valid order creates transaction
✓ Duplicate order returns existing record
✓ Missing external ID fails validation
✓ Negative amount is rejected
✓ Unsupported currency fails validation
✓ Archive service is called after save
✓ SQL exception is logged
✓ Correlation ID is preserved
✓ Invalid customer returns 400
✓ Repository duplicate race is handled
✓ Blob failure is surfaced and logged
✓ Cancellation token is honored
[Fact]
public async Task ProcessAsync_DuplicateOrder_ReturnsExisting()
{
_repo.Setup(r => r.FindByExternalIdAsync(
"D365", "SO-10482", It.IsAny<CancellationToken>()))
.ReturnsAsync(new Order { Id = 4182 });
var result = await _service.ProcessAsync(
ValidRequest(), CancellationToken.None);
Assert.True(result.IsDuplicate);
Assert.Equal(4182, result.OrderId);
}
8. CI/CD pipeline
The project includes an Azure DevOps pipeline path so build, test, publish, and deployment steps are repeatable rather than manual.
Restore dependencies✓ Passed
Build .NET solution✓ Passed
Run xUnit tests✓ Passed*
Publish artifact✓ Configured
Deploy to App Service✓ Configured
*Portfolio representation of the intended pipeline flow. Final public screenshots should be replaced with real pipeline output once the project is executed in Azure DevOps.
trigger:
- main
steps:
- task: UseDotNet@2
inputs:
packageType: sdk
version: '8.x'
- script: dotnet restore
- script: dotnet build --configuration Release --no-restore
- script: dotnet test --configuration Release --no-build
- task: DotNetCoreCLI@2
inputs:
command: publish
arguments: '--configuration Release --output $(Build.ArtifactStagingDirectory)'
- task: AzureWebApp@1
inputs:
appType: webApp
appName: 'theresa-integration-api'
9. What this project proves
Cloud architectureI can reason about how App Service, SQL, Storage, Key Vault, telemetry, and CI/CD work together.
C# developmentI can structure controllers, services, repositories, DTOs, validation, async processing, and dependency injection.
Integration thinkingI can account for retries, duplicates, failures, traceability, and separation of concerns.
Data designI can model relational transaction data and apply indexes and integrity constraints.
TestingI can write behavior-focused xUnit tests rather than relying on manual verification.
DevOpsI can define a repeatable path from commit to test to publish to deployment.
My systems-thinking lens
My professional work often sits between business requirements, enterprise platforms, users, operations, data, and technical teams. This project applies that same systems-thinking approach to cloud engineering: understand the dependency chain, make failure visible, separate responsibilities, and design the whole lifecycle—not just the interface.