N-Layered Architecture With Azure Functions

N-Layered Architecture With Azure Functions

This article doesn’t contain details of N-Layered architecture from a theoretical perspective. You can find a multitude of materials on the internet about N-layered architecture. Instead, I’m planning to show how to implement an N-layered architecture with Azure functions in one of our projects in practice.

The purpose of our project is to provide additional wrappers over old services and consume them in a Cloud environment using Azure. We used N-Layered architecture, where the last layer was developed using Azure functions.

The first layer is the Proxy layer. The purpose of this layer is to generate services and expose them. We generate svc and wsdl links in this layer, and this layer creates special classes that are proxies over original services.

In most cases, there is no need to write any code in the Proxy layer. However, you can make it easier by writing some sort of factory over services, as I did:

public class MortgageServiceFactory {
    public static ServiceOperationScope CreateService(string username, string password, string endpointAddress) {
        var application = HttpSoapClientFactory.CreateSecureClient(endpointAddress);
        var operationScope = new OperationContextScope(application.InnerChannel);
        OperationContext.Current.OutgoingMessageHeaders.AddWssSecurityHeader(username, password);
        ServiceOperationScope _scope = new ServiceOperationScope {
            Application = application,
                OperationContextScope = operationScope
        };
        return _scope;
    }
}

On the other side, we need somehow isolate our application from the models of the original services. Also, there should be a contract between the Client and Service layers. In the Client Layer, we created models and contracts:

public interface IApplicationClient {
    GMApplicationAckDef UpdateApplication(GMApplicationRequest GnwApplicationRequest);
    GMApplicationAckDef CancelApplication(GMApplicationRequest GnwApplicationRequest);
    GMApplicationAckDef AddApplication(GMApplicationRequest GnwApplicationRequest);
    GMApplicationResponse InquireApplicationStatus(GMStatusRequestDef ApplicationStatusRequest);
}
public class GenworthMortgageApplicationClient: IApplicationClient {
    #region injecting dependencies
    private readonly MortgageAppConfiguration _mortgageConfiguration;
    private readonly ILogger _logger;
    private readonly IMapper _mapper;
    public GenworthMortgageApplicationClient(MortgageAppConfiguration mortgageConfiguration, ILogger logger, IMapper mapper) {
        _mortgageConfiguration = mortgageConfiguration;
        _logger = logger;
        _mapper = mapper;
    }
    #endregion
    public GMApplicationAckDef CancelApplication(GMApplicationRequest GnwApplicationRequest) {
        ServiceOperationScope applicationClient = null;
        try {
            _logger.LogInformation("CancelApplication started..");
            applicationClient = MortgageServiceFactory.CreateService(_mortgageConfiguration.Username, _mortgageConfiguration.Password, _mortgageConfiguration.EndPointAddress);
            var applicationRequest = _mapper.Map(GnwApplicationRequest);
            string requestAsXml = new ObjectXmlDecorator(applicationRequest).AsXml();
            _logger.LogInformation($ "CancelApplication Request params are : {requestAsXml}");
            var response = applicationClient.Application.CancelApplication(applicationRequest);
            _logger.LogInformation($ "CancelApplication Response params are : {response}");
            var responseModel = _mapper.Map(response);
            _logger.LogInformation("CancelApplication ended..");
            applicationClient.Dispose();
            return responseModel;
        } catch (System.Exception ex) {
            _logger.LogError(ex.Message);
            throw;
        } finally {
            applicationClient?.Dispose();
        }
    }..............

The purpose of the Client layer is a high-level wrapper over original Proxy functionalities. It receives the model, maps it to the Proxy model, creates a proxy service instance, and calls its functionality.

Above the Client Layer, we have Service Layer. The responsibility of the Service layer is providing contracts to the above layer, adding required business functionalities, and validating information.

public interface IApplicationClientProvider {
    GMApplicationAckDef UpdateApplication(GMApplicationRequest GnwApplicationRequest);
    GMApplicationAckDef CancelApplication(GMApplicationRequest GnwApplicationRequest);
    GMApplicationAckDef AddApplication(GMApplicationRequest GnwApplicationRequest);
    GMApplicationResponse InquireApplicationStatus(GMStatusRequestDef ApplicationStatusRequest);
    Task PushInsuranceAppStatusAsync(string request);
}
public class ApplicationClientProvider: IApplicationClientProvider {
    private readonly IApplicationClient _applicationClient;
    private readonly GenworthServiceBusConfiguration _serviceBusConfiguration;
    private CloudQueueClient _cloudQueueClient;
    public ApplicationClientProvider(IApplicationClient applicationClient, GenworthServiceBusConfiguration serviceBusConfiguration) {
        _applicationClient = applicationClient;
        _serviceBusConfiguration = serviceBusConfiguration;
    }
    public GMApplicationAckDef CancelApplication(GMApplicationRequest GnwApplicationRequest) {
        try {
            var validator = new GMCancelApplicationRequestValidator();
            var result = validator.Validate(GnwApplicationRequest);
            if (!result.IsValid) {
                var exceptionMessage = result.GetServiceExceptionErrors();
                throw new ServiceException(exceptionMessage);
            }
            return _applicationClient.CancelApplication(GnwApplicationRequest);
        } catch (ServiceException) {
            throw;
        } catch (Exception exp) {
            throw new ServiceException(exp.Message);
        }
    }.........
}

The final layer in our application was the Azure functions layer (API layer). This is the only layer with which users can use and communicate. The purpose is to receive user inputs, parse them, and forward them to the Service layer.

public class GnwMortgageFunctions {
    private readonly ILogger _logger;
    private readonly IApplicationClientProvider _applicationClientProvider;
    public GnwMortgageFunctions(ILogger logger, IApplicationClientProvider applicationClientProvider) {
            _logger = logger;
            _applicationClientProvider = applicationClientProvider;
        }
        [FunctionName("CancelApplication")]
    public async Task CancelApplication([HttpTrigger(AuthorizationLevel.Function, "post", Route = "gnw/cancel")] HttpRequest req) {
        try {
            _logger.LogInformation("CancelApplication started..");
            var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            JObject obj = JObject.Parse(requestBody);
            var request = obj.ToObject();
            var response = _applicationClientProvider.CancelApplication(request);
            _logger.LogInformation("CancelApplication finished..");
            return new OkObjectResult(response);
        } catch (ServiceException ex) {
            _logger.LogError(ex, "CancelApplication Failed");
            return new BadResponseErrorResult(ex);
        } catch (Exception exp) {
            _logger.LogError(exp, "CancelApplication Failed");
            var _exp = new ServiceException(exp.Message);
            return new BadResponseErrorResult(_exp);
        }
    }
}

For registering all services and managing them from one point, we used Startup.

public class Startup: FunctionsStartup {
    public IConfiguration Configuration {
        get;
    }
    public override void Configure(IFunctionsHostBuilder builder) {
        builder.Services.AddLogging(logging => {
            logging.AddNLog();
        });
        var config = new ConfigurationBuilder().AddEnvironmentVariables().Build();
        builder.Services.AddAutoMapper(Assembly.GetAssembly(typeof(CertifaxApplicationProfile)), Assembly.GetAssembly(typeof(DocumentCreationProfile)));
        builder.Services.AddCGuarantyService(config, bindKey: "CGuaranty");
        builder.Services.AddGenworthService(config, bindKey: "GnwMortgage");
        builder.Services.AddManulifeService(config, bindKey: "Manulife");
        builder.Services.AddHomeVaultService(config, bindKey: "HomeVault");
        builder.Services.AddGenWorthServiceBusConfiguration(config, bindKey: "Genworth:ServiceBus");
        //setting current directory for nlog , for saving logs, can be change if needed
        var startupDirectory = System.IO.Directory.GetCurrentDirectory();
        NLog.LayoutRenderers.LayoutRenderer.Register("startupdir", (logEvent) => startupDirectory);
    }
}

Want to dive deeper?

Regularly, I share my senior-level expertise on my DecodeBytes YouTube channel, breaking down complex topics like .NET, Microservices, Apache Kafka, Javascript, Software Design, Node.js, and more into easy-to-understand explanations. Join us and level up your skills!

*