Walkthrough Generated Code Insert

BasicCRUD Insert Query with Bridge

Let us walk through the generated code for the insert operation.

The first thing that we will look at is the input API model. We have an AddCustomerInputAPIModel generated.

using Sumeru.Flex;
using System;
using System.ComponentModel.DataAnnotations;
namespace ECommerceDemo.CustomerModule
{
/// <summary>
///
/// </summary>
public class AddCustomerInputAPIModel : IFlexInputAPIModel
{
// Write your input attributes/properties here
}
}

This is where the developer will write the model and structure the model depending on the input they want from the users. The InputAPIModel starting point is the controller action. This is the input from the applications which are consuming the APIs.

public partial class CustomerController : FlexControllerBridge
{
/// <summary>
///
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
[HttpPost]
[Route("AddCustomer")]
[ProducesResponseType(typeof(BadRequestResult), 400)]
[ProducesResponseType(typeof(string), 201)]
public async Task<IActionResult> AddCustomer([FromBody]AddCustomerInputAPIModel value)
{
if (value == null)
{
ModelState.AddModelError("requesterror", $"{nameof(AddCustomerInputAPIModel)} can not be empty");
_logger.LogDebug($"{nameof(AddCustomerInputAPIModel)} can not be empty" + DateTime.Now.ToString());
return ControllerContext.ReturnFlexBadRequestError();
}
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
CommandResult cmdResult = null;
//Set the values for the parameter
AddCustomerParams addParams = new AddCustomerParams();
FlexHostContextInfoBridge hostContextInfo = new FlexHostContextInfoBridge();
hostContextInfo.Populate<IFlexHostHttpContextAccesorBridge>(_appHttpContextAccessor);
addParams.HostContextInfo = hostContextInfo;
addParams.Model = value;
cmdResult = await ProcessCustomerService.AddCustomer(addParams);
if (cmdResult.Status != Status.Success)
{
ModelState.ComposeFlexBadRequestError(cmdResult.Errors());
return ControllerContext.ReturnFlexBadRequestError();
}
return StatusCode(201, cmdResult.result);
}
}

Here one thing to notice is that we have some AddCustomerParams. Will explain that later.

Here we have something called FlexHostContextInfoBridge. This carries the user information across the application.

Now, lets look at the controller Constructor.

[Route("api/[controller]")]
[ApiController]
public partial class CustomerController : FlexControllerBridge
{
IProcessCustomerService ProcessCustomerService;
ILogger<CustomerController> _logger;
IFlexHostHttpContextAccesorBridge _appHttpContextAccessor;
/// <summary>
///
/// </summary>
/// <param name="_ProcessCustomerService"></param>
public CustomerController(IProcessCustomerService _ProcessCustomerService, ILogger<CustomerController> logger, IFlexHostHttpContextAccesorBridge appHttpContextAccessor)
{
ProcessCustomerService = _ProcessCustomerService;
_logger = logger;
_appHttpContextAccessor = appHttpContextAccessor;
}
}

We have something called IFlexHostHttpContextAccessorBridge. What it does is, if you go to the endpoint you can find the ContextAccessorBridge. This implements FlexHostHttpContextAcessorBridge. This is a class that is used whenever the HTTP context is loaded. With this, you can write your logic to find the UserId, HostId, ContextId..etc and any other information which you need to get through the HTTP context. It can be other implementation in case of some gRPC implementation or any other source other than asp.net core http end point when its used as an input, so that's why it is made highly decoupled. In this particular case, we will be using the IHttpContextAcessor in that we can fetch any of the context information available to us.

As you can see, we can have the hostname, user ID, username. It can be extended in the Bridge, to add any more fields that are needed. It can be some other header information or it can be anything that comes from the context. So that details are all fetched from The Constructor automatically when you have configured in your DI and then these are passed to a context info. Similarly, we have the context info also. As you will see, we have given four fields. Any more required fields can be added.

The field names should be the same at both the places so that mapping happens automatically and if there is any difference, you can always configure this mapper for any changes. This is already given by default, and if you only need by different names and fields in the context and info then we will need this.

We will dedicate a full section on context info and context accessor, which we will see later. Here, it is being accessed in the context info and it is passed to the AddParams. As we can see the AddCustomerParams, it is a part of our IProcessCustomerService, where it contains the AddCustomerInputAPIModel. So the model is being accessed, that already contains the context info for you inside the Context Params. If we go to the ProcessService, it defines the ContextInfo for us. The context accessories is mapped to the context info and it is passed to the ProcessCustomerService.AddCustomer.

public partial class ProcessCustomerService : IProcessCustomerService
{
/// <summary>
/// YourRemarksForMethod
/// </summary>
/// <param name="param"></param>
/// <returns></returns>
public async Task<CommandResult> AddCustomer(AddCustomerParams param)
{
AddCustomerDataPacket packet = _flexHost.GetFlexiFlowDataPacket<AddCustomerDataPacket>();
packet.InputParams = param;
try
{
FlexiBusinessRuleSequenceBase<AddCustomerDataPacket> sequence = _flexHost.GetFlexiBusinessRuleSequence<AddCustomerSequence, AddCustomerDataPacket>();
await FlexiBusinessRule.Run(sequence, packet);
}
catch (FlexiFlowAbnormalTerminationException ex)
{
packet.AddError("requesterror", ex);
return new CommandResult(Status.Failed, packet.Errors());
}
if (packet.HasError)
{
return new CommandResult(Status.Failed, packet.Errors());
}
else
{
AddCustomerCommand cmd = new AddCustomerCommand
{
Id = _pkGenerator.GenerateKey(),
model = param.Model,
HostContextInfo=param.HostContextInfo
};
await ProcessCommand(cmd);
CommandResult cmdResult = new CommandResult(Status.Success);
AddCustomerResultModel outputResult = new AddCustomerResultModel();
outputResult.Id = cmd.Id;
cmdResult.result = outputResult;
return cmdResult;
}
}
}
public class AddCustomerResultModel : FlexOutputAPIModelForCreate
{
}

Let's see what the 'Add Customer' does. In the 'AddCustomer', it takes the Param, creates a data packet which fills the data packet with the Param and executes the Flexi flow. It goes through each plugin one by one as defined in the sequence and executes the validation. We will understand more about validation later.

As we saw, it validates all the plugins. If any error is there, it will return the customer with an error. So the controller receives the command result with the error. If all are not successful, then it creates a command and we generate the primary key.

As you can see in the video, the primary key generator is configured in the DI. so usually do that thing like we have that no integer ID from the database.

We don't recommend Integer ID from database as people usually do. Instead, a sequencer ID, which is good as In 64, and that helps microservices and distributed database without compromising on the performance.

That one gives the generation. If you need any ID or some other logic, you can always customize this. We will get back to that in a separate section for customizing the primary key.

We create the ID there itself while giving the command so that the message can be traced throughout the bus and whether it's being processed not processed. It is possible when we generate it as shown. It can be done at a later stage also but is it preferred in this manner. Then the model of the command needs to be filled with the Param's model and the host context info with the Param's context info, This Context Info will be carried across the application, So wherever you need a user ID to be fetched like which is the current logged in user or any other host info, it will be available throughout the application. This is very useful when you are doing a multi-tenant application so you can switch from a single tenant to a multi-tenant and all from a multi-tenant to a single tenant without changing any code with just by configuration changes.

The command result is then given back control of the Controller and that is being passed back to the user or the application that is calling the API.

public partial class ProcessCustomerService : IProcessCustomerService
{
private async Task ProcessCommand(AddCustomerCommand cmd)
{
await _bus.Send(cmd);
}
}

So once the process has happened here at customer command. We are talking about creating the process command. We go back to the process command. We are doing a 'bus.Send'.

If you are doing a non-eventually and you don't want the eventual consistency to happen through the bus, we have another option for non-eventual consistency. It can be chosen in the feature diagram. It will generate the code to persist the data in the database. we will see that code in a while.

Moving on, this message command is passed on the bus. That is being handled by the Command Handler. The Command Handler receives the message command in which all the data is filled by the process services. Also, do remember this 'IMessageHandlerContext', which is specific to the Nservice Bus.

public class AddCustomerCommandHandler : IHandleFlexBusGammaCommand<AddCustomerCommand>
{
ILogger<AddCustomerCommandHandler> _logger;
IFlexHost _flexHost;
/// <summary>
///
/// </summary>
/// <param name="logger"></param>
public AddCustomerCommandHandler(ILogger<AddCustomerCommandHandler> logger, IFlexHost flexHost)
{
_logger = logger;
_flexHost = flexHost;
}
/// <summary>
///
/// </summary>
/// <param name="message"></param>
/// <param name="context"></param>
/// <returns></returns>
public async Task Handle(AddCustomerCommand message, IMessageHandlerContext context)
{
_logger.LogTrace($"Executing AddCustomerCommandHandler: {nameof(AddCustomerCommandHandler)}");
AddCustomerPostBusDataPacket packet = _flexHost.GetFlexiFlowDataPacket<AddCustomerPostBusDataPacket>();
//Fill your data to datapacket here
packet.cmd = message;
FlexiPluginSequenceBase<AddCustomerPostBusDataPacket> sequence = _flexHost.GetFlexiPluginSequence<AddCustomerPostBusSequence, AddCustomerPostBusDataPacket>();
await FlexiFlow.Run(sequence, packet, new BusGammaHandlerContextBridge(context, message.HostContextInfo));
}
}

After that, any processing happens, we need this context so that it is maintained transactional and you can utilize the NService Bus and other diagrams. Well, again here we are creating a packet and running the sequence of the plug-in. Here, there is only one plug-in. The reason we have created a data packet in the plugin principle is that your bus is decoupled. It can be Bus Omega or any other bus also whenever we Implement.

This is a very standard code we can easily replace that with any other bus code and this code is specific to Nservice Bus. There are few parameters we have to pass it to the Nservice Bus. The method signature will change from bus to bus, but your business validations or the business logic do not change of what is written in the plug-in.

What happens is, it calls the plugin and if the bus is changed, the bus specific code will be written and it will call the same plug-in. So whatever you've written for your business won't go away.

public partial class AddCustomerPlugin : FlexiPluginBase, IFlexiPlugin<AddCustomerPostBusDataPacket>
{
public override string Id { get; set; } = "39f682d485cdecf2c3b66da16f866f05";
public override string FriendlyName { get; set; } = "AddCustomerPlugin";
protected string OnRaiseEventCondition = "";
IFlexRepositoryBridge _repo;
readonly ILogger<AddCustomerPlugin> _logger;
Customer _model;
IFlexHost _flexHost;
IWriteDbConnectionProviderBridge _connectionProvider;
/// <summary>
///
/// </summary>
/// <param name="repo"></param>
/// <param name="logger"></param>
public AddCustomerPlugin(IFlexRepositoryBridge repo, ILogger<AddCustomerPlugin> logger, IFlexHost flexHost, IWriteDbConnectionProviderBridge connectionProvider)
{
_repo = repo;
_logger = logger;
_flexHost = flexHost;
_connectionProvider = connectionProvider;
}
/// <summary>
///
/// </summary>
/// <param name="packet"></param>
public async Task Execute(AddCustomerPostBusDataPacket packet)
{
_connectionProvider.ConfigureDbConnectionString(packet.cmd.HostContextInfo);
_repo.InitializeConnection(_connectionProvider);
_model = _flexHost.GetDomainModel<Customer>().AddCustomer(packet.cmd);
_repo.InsertOrUpdate(_model);
await _repo.SaveAsync();
_logger.LogDebug("Customer inserted into Database: " + _model.Id);
//TODO: Specify your condition to raise event here...
//TODO: Set the value of OnRaiseEventCondition according to your business logic
//OnRaiseEventCondition = CONDITION_ONSUCCESS;
RaiseEventCondition raiseEventCondition = new RaiseEventCondition(OnRaiseEventCondition);
await raiseEventCondition.RaiseEvent<AddCustomerPlugin>(this, new object[] { packet.FlexServiceBusContext });
}
}

Now let's look into the plug-in. The command is passed to the plug-in through the data packet. Here we are maintaining a connection provider. We will have a detailed section on connection providers later.

Let's just assume we need to create an instance of a connection provider for the repository to run. The model is fetched from the 'DomainModel.AddCustomer' and the 'repo.InsertOrUpdate' happens for the model and the 'SaveAsync'.

Now when we see 'GetDomainModel', the 'FlexHost.GetDomainModel' is essential because we have some client and the industry implementation.

public partial class Customer : DomainModelBridge
{
#region "Public Methods"
/// <summary>
///
/// </summary>
/// <param name="cmd"></param>
/// <returns></returns>
public virtual Customer AddCustomer(AddCustomerCommand cmd)
{
Guard.AgainstNull("Customer command cannot be empty", cmd);
this.Convert(cmd.model);
this.CreatedBy = cmd.HostContextInfo.UserId;
this.LastModifiedBy = cmd.HostContextInfo.UserId;
//Map any other field not handled by Automapper config
this.SetAdded(cmd.Id);
//Set your appropriate SetAdded for the inner object here
return this;
}

It will automatically get your client implementation for the domain model.

Let us see the 'Add Customer'. The 'AddCustomer' receives the 'AddCustomerCommand', and that command carries the model and that is mapped to The Domain model.

We will discuss the 'AddCustomerDomainModel' when we see that in action and 'created by' and 'last Modified by' is accessed from the context info user ID.

So this is all about the insert operation flow that is happening in the code. In the next video we will look at the query flow.