Introduction
This documentation will serve as a basis for Integration between various Oryon Projects. In this guide the projects People.Core and People will be used as examples, being Oryon.People.Core.Api the Source Application(SA) and Oryon.People.Api the Target Application(TA). These applications DO NOT communicate directly between each other, instead they communicate through Azure Service Bus.
Why is Integration needed?
Integration is needed to ensure that when something is updated, it will update as well on every target reference throughout the application. For example, in case you need to update a Company name, by using Integration, that Company will be updated everywhere its CompanyId is referenced.
How does it work?
Integration enables an application to publish events to multiple interested subscribers asynchronously, without those applications knowing each other nor directly communicate, using Azure Service Bus as an intermediary.
Below, you can see a flowchart that represents the communication between projects:
The Publish Event set at People.Core will send a message to Azure Service Bus, which in turn will trigger a function to People Subscriber Integration Event who is actively seeking for events and will update accordingly to the data initially sent.
What does it allow?
-
Allows a messaging service triggered by the Publisher Event: The Publisher Event sends a message with only the necessary data provided to Azure Service Bus which in turn will trigger a function, this will ensure that the SA will quickly return to its core processing because the Azure Service Bus handles all the responsibility of delivering these messages to the Subscribers.
-
With the function triggered by the Azure Service Bus, this data is sent to all Subscribers that are interested in consuming it.
Guide
In this guide, a few important points will be referenced in order to properly connect the Source Application (SA) and the Target Application (TA):
- Both the SA and TA must run
Oryon.Constellation.Tenantin localhost. In order to do so, you must go to theappsettings.Development.jsonfile that is located in theOryon.People/src/Server/Api/Oryon.People.Api/appsettings.json/appsettings.Development.jsonand change to your Local URL:
ℹ️ The path to the appsettings.Development.json file is the same in all projects.
"MultiTenantEnvironmentConfiguration": {
"TenantApiRootUri": "https://localhost:00000", // <-- Change to Oryon.Constellation.Tenant Local URL
"DistributedCacheSlidingExpiration": "00:30:10"
}
Most likely, both projects will already be connected to Azure Services. But in case they aren't, you will also need to create that connection on ConnectionStrings and ServiceBusConfiguration, both are inside the appsettings.Development file that is mentioned above.
"ConnectionStrings": {
"PeopleDatabase": "Insert Connection String Here",
"RedisCache": "Insert Connection String Here",
"ActivityPlannerDatabase": "Insert Connection String Here",
"ServiceBus": "Insert Connection String Here",
"Storage": "Insert Connection String Here"
},
"ServiceBusConfiguration": {
"UseIntegration": true,
"InternalBusTopic": "Insert Connection String Here",
"InternalBusDefaultSubscriptions": [ "project-subscriber-local" ], // <-- Make sure to name the right project
},
ℹ️ Ask your Team Lead for the connection string.
You can also need to run Oryon.Constellation.Core in Local URL in case you need to access to this API.
Some projects will also have require you to go to local.settings.json file located at Oryon.People/src/Server/Integration/Oryon.People.Messaging/local.settings.json and change both Constellation.Tenant and Constellation.Core to Local URL in order to run:
"ModuleUris__ConstellationCore": "https://localhost:11111", // <-- Change to Oryon.Constellation.Core Local URL, if needed.
"ModuleUris__ConstellationTenant": "https://localhost:00000" // <- Change to Oryon.Constellation.Tenant Local URL.
ℹ️ Just like the appsettings.Development.json, the local.settings.json file should also have the same path in all projects.
- In the TA case, you will also need to set multiple startup projects, these being (in the given case) the
Oryon.People.ApiandOryon.People.Messagingin order to run both the API and the Messaging Function from Azure.
ℹ️ To do this, you need to right-click on the Solution and choose the option "Configure Startup Projects". This will open a new window with three (3) options and you choose the "Multiple Startup Projects" and select the "Start" action for both the above mentioned projects inside that solution, once it's done, just aplly your changes.
- You also need to run
Oryon.Constellation. Just like before, go to appsettings.Development.json file located atOryon.Constellation/src/Client/WebAssembly/wwwroot/appsettings.json/appsettings.Development.jsonand change Constellation.Tenant, Oryon.People(TA) and Oryon.People.Core(SA) to Local URL:
"ClientConfiguration": {
"ForcedTenantIdentifier": "localdev", //<-- When running projects locally, you need to change from hybriddev to localdev as demonstrated here.
},
"ModuleUris": {
"ConstellationTenant": "https://localhost:00000", // <-- Change to Oryon.Constellation.Tenant Local URL.
"ConstellationCore": "https://localhost:11111", // <-- Only change if needed.
"People": "https://localhost:44444", // <-- Change to Oryon.People Local URL.
"PeopleCore": "https://localhost:33333", // <-- Change to Oryon.People.Core Local URL.
...
}
📝 NOTE: With Constellation.Tenant running locally, you need to change from hybriddev to localdev.
- Finally, if the Event is not already created, you will have to create one for the SA project, for this you will have to open
Oryon.Constellation.Sharedand create an Event called (for this specific case) AllergyTypeUpdated onOryon.Constellation.Shared/src/Model/Oryon.Constellation.Shared.Model/Integration/Events/PeopleCoreEvents. This Event will subscribe with both the SA and the TA in order to allow communication between them using Azure Service Bus.
Implementation
Publish Event
Now, in this case we want to create a publish event for AllergyTypes in order to update everywhere this AllergyType is referenced. In order to do so, you must go to AllergyTypeService and to the Update method, since your purpose is to update this type.
#region Task<OryonContentResponse<AllergyTypeModel>> UpdateAsync(AllergyTypeModel data)
public async Task<OryonContentResponse<AllergyTypeModel>> UpdateAsync(AllergyTypeModel data)
{
Logger.LogTrace($"AllergyTypeService: Validating if Update Allergy Type request is null.");
if (data == null)
{
Logger.LogError($"AllergyTypeService: Update Allergy Type request is null.");
throw new ArgumentNullException(nameof(data));
}
try
{
Logger.LogTrace($"AllergyTypeService: Validating if update Allergy Type request is correct, according to the data annotations.");
if (!TryValidateModel(data))
{
Logger.LogError($"AllergyTypeService: Update Allergy Type request is wrong, according to the data annotations.");
return new OryonContentResponse<AllergyTypeModel>().SetFailed()
.AddValidationErrorToResponseModel(this.ModelState);
}
Logger.LogTrace($"AllergyTypeService: Updating AllergyType {data.Name}");
var UpdateResponse = await AllergyTypeRepository.UpdateAsync(data);
#region Allergy event publish
if (UpdateResponse.Success && UpdateResponse.OperationAcknowledged)
{
var mapReference = UpdateResponse.ModifyGenericType<AllergyTypeModel, GenericLocalizedReference>();
mapReference.Data = new()
{
TenantId = UpdateResponse.Data.TenantId,
TargetId = UpdateResponse.Data.Id,
DisplayName = UpdateResponse.Data.Name,
CompanyId = null
};
MessagingService.Publish(mapReference, nameof(PeopleCoreEvents.AllergyTypeUpdated));
}
#endregion
return UpdateResponse;
}
catch (Exception ex)
{
return new OryonContentResponse<AllergyTypeModel>().SetFailed()
.AddErrorToResponseModel(ex)
.ToLogger(Logger);
}
As you can see, you need to create a new variable called UpdateResponse and set it to equal the response of the Update method at the Repository. After this, you need to create an if statement which will validate if the response is successful and if the operation was acknowledged.
Then, you create a variable called mapReference which will map the TenantId, TargetId, DisplayName and CompanyId (if applicable) to the current UpdateResponse.Data. This will later be compared in the Subscriber, which will compare when the targetId matches the Id of the type you want to update.
After this, you need to input the publish event as stated above which will call the event at PeopleCoreEvents and then return the UpdateResponse.
Events
In this case, the Event that you'll be using is an existent one called AllergyTypeUpdated inside PeopleCoreEvents. But sometimes, with new implementations, you might need to create an event.
To do this, you need to open Oryon.Constellation.Shared and go to src/Model/Oryon.Constellation.Shared.Model/Integration/Events, and open the file relevant to the Source Application (SA), in this case PeopleCoreEvents, and add the new event on to the file.
namespace Oryon.Constellation.Shared.Model.Integration.Events
{
public enum PeopleCoreEvents
{
ContractTermUpdated,
MaritalStatusTypeUpdated,
KnowledgeUpdate,
PersonalQualificationUpdate,
LanguageLevelTypeUpdated,
DegreeTypeUpdated,
RelationshipTypeUpdated,
MedicalAptitudeTypeUpdated,
AllergyTypeUpdated, // <-- This is the event that you need to create in this use case.
ChronicDiseaseTypeUpdated,
VaccineTypeUpdated,
FoodIntoleranceTypeUpdated,
WorkPermitsTypeUpdated,
WorkVisaTypeUpdated,
WorkTypeUpdated,
RecruitmentDecisionsTypeUpdated,
RefusalReasonsTypeUpdated,
TerminationReasonUpdated,
CompentencyTypeUpdated,
CompetencyLevelUpdated,
DisabilityTypeUpdated,
EthnicityTypeUpdated,
GenderTypeUpdated,
PoliticalIdeologyTypeUpdated,
ReligionTypeUpdated,
SexualOrientationTypeUpdated
}
}
NOTE: After this, you have to commit your change in Oryon.Constellation.Shared in order to have this event in all other projects.
Subscriber
Now, the Subscriber will be located in the TA under Oryon.People/src/Server/Api/Integration/Oryon.People.Messaging/Subscribers/Person/PersonalData/Wellbeing/WellbeingSubscriber. Since AllergyType is inside the WellbeingModel, you need to go to the WellbeingSubscriber file and create an AllergyTypeUpdate method:
#region Task<OryonResponse> HandleAllergyTypeUpdate(OryonIntegrationEvent<GenericLocalizedReference> allergyIntegrationEvent)
[OryonIntegrationEvent(nameof(PeopleCoreEvents.AllergyTypeUpdated))]
public async Task<OryonResponse> HandleAllergyTypeUpdate(OryonIntegrationEvent<GenericLocalizedReference> allergyIntegrationEvent)
{
#region Filter
string fieldToFilter = $"{nameof(AllergyModel.Type)}.{nameof(GenericLocalizedReference.TargetId)}";
OryonPagedRequest<PagingSortingAndFiltering> allergyTypeFilter = new OryonPagedRequest<PagingSortingAndFiltering>()
.Add(fieldToFilter, allergyIntegrationEvent.Data.TargetId);
#endregion
OryonContentResponse<List<PersonModelDTO>> personResponse = await PersonService.SearchAsync(new());
if (!personResponse.Success) return personResponse;
foreach (Guid personId in personResponse.Data.Select(p => p.Id))
{
OryonContentResponse<List<AllergyModel>> searchAllergyData = await WellbeingService.SearchAllergiesAsync(personId, allergyTypeFilter);
if (!searchAllergyData.Success) return searchAllergyData;
foreach (AllergyModel allergy in searchAllergyData.Data.Where(x => x.Type?.TargetId == allergyIntegrationEvent.Data.TargetId))
{
allergy!.Type.DisplayName = allergyIntegrationEvent.Data.DisplayName;
var result = await WellbeingService.UpdateAllergyAsync(personId, allergy);
if (!result.Success) return result;
}
}
return new OryonResponse();
}
#endregion
First, you need to call an OryonIntegrationEvent which will communicate with PeopleCoreEvents, specifically the AllergyTypeUpdated event.
Then, you need to create an async method called, in this case, HandleAllergyTypeUpdate which calls for the OryonIntegrationEvent and the type of the AllergyModel which is this case is a GenericLocalizedReference.
After this, you need to filter the TargetId of the AllergyType that you want to update:
string fieldToFilter = $"{nameof(AllergyModel.Type)}.{nameof(GenericLocalizedReference.TargetId)}";
OryonPagedRequest<PagingSortingAndFiltering> allergyTypeFilter = new OryonPagedRequest<PagingSortingAndFiltering>()
.Add(fieldToFilter, allergyIntegrationEvent.Data.TargetId);
This filter will get the Name of the AllergyType that corresponds with the TargetId sent by the Publisher event. Once you have the filter, you'll make a request to the service to search the database for the necessary information.
Since the AllergyModel is referenced inside the WellbeingModel and the WellbeingModel itself is referenced inside the PersonModel, you need to search all the existent Person and then do a foreach to filter every PersonId in order to select the Person or List of Persons that have AllergyTypes.
OryonContentResponse<List<PersonModelDTO>> personResponse = await PersonService.SearchAsync(new());
if (!personResponse.Success) return personResponse;
foreach (Guid personId in personResponse.Data.Select(p => p.Id))
{
OryonContentResponse<List<AllergyModel>> searchAllergyData = await WellbeingService.SearchAllergiesAsync(personId, allergyTypeFilter);
if (!searchAllergyData.Success) return searchAllergyData;
foreach (AllergyModel allergy in searchAllergyData.Data.Where(x => x.Type?.TargetId == allergyIntegrationEvent.Data.TargetId))
{
allergy!.Type.DisplayName = allergyIntegrationEvent.Data.DisplayName;
var result = await WellbeingService.UpdateAllergyAsync(personId, allergy);
if (!result.Success) return result;
}
With this foreach, you will select every PersonId inside PersonModelDTO, then inside you have to make a new request for every AllergyModel inside Wellbeing.
After that, you'll need a second foreach to find every AllergyType where its TargetId matches the TargetId sent by the integration event.
Finally, you equal the Type.TargetId.DisplayName to the DisplayName that comes from allergyIntegrationEvent.Data.TargetId and create a new variable to await the UpdateAllergyAsync.
Repository
In order to ensure that all data is updated, even if the target is deleted, you must add the condition IsIntegration to the if statement in the AllergyType Update method in the WellbeingRepository:
case 1:
{
AllergyModel rowToUpdate = dataResults.Single();
if (!data.IsDeleted || IsIntegration)
{
UpdateDefinition<PersonModel> updateDefinition = Builders<PersonModel>.Update.HandleUpdate(CurrentUserReference)
.Set(x => x.PersonalData.Wellbeing.Allergies.FirstMatchingElement().Type, data.Type)
.Set(x => x.PersonalData.Wellbeing.Allergies.FirstMatchingElement().Description, data.Description)
.Set(x => x.PersonalData.Wellbeing.Allergies.FirstMatchingElement().ModifiedBy, CurrentUserReference)
.Set(x => x.PersonalData.Wellbeing.Allergies.FirstMatchingElement().ModifiedOn, DateTime.Now)
.SetIf(x => x.PersonalData.Wellbeing.Allergies.FirstMatchingElement().IsDeleted, false, condition: rowToUpdate.IsDeleted && !IsIntegration)
.SetIf(x => x.PersonalData.Wellbeing.Allergies.FirstMatchingElement().DeletedBy, null, condition: rowToUpdate.IsDeleted && !IsIntegration)
.SetIf(x => x.PersonalData.Wellbeing.Allergies.FirstMatchingElement().DeletedOn, null, condition: rowToUpdate.IsDeleted && !IsIntegration);
await PersonCollection.UpdateOneAsync(personFilter, updateDefinition);
}
return (await GetAllergyAsync(personId, data.Id)).AcknowledgeOperation();
}
In case that which you want to update is at the top level, you will need to change the MarkAsUndeleted in the Update method in order to ensure that deleted models also are updated.
.MarkAsUndeleted(rowToUpdate.IsDeleted && !IsIntegration);
Also, you need to check if the field that you want to update is being filtered in the Not Equals(Ne) filter inside the Update Method, if not, this field should be added to this filter.
FilterDefinition<AllergyModel> allergyFilter = Builders<AllergyModel>.Filter.Ne(c => c.Type, data.Type) |
Builders<AllergyModel>.Filter.Ne(c => c.IsDeleted, data.IsDeleted) |
Builders<AllergyModel>.Filter.Ne(c => c.Description, data.Description)
.OryonIntegrationComparator(this, c => c.CreatedBy, data.CreatedBy)
.OryonIntegrationComparator(this, c => c.DeletedBy, data.DeletedBy);
📝 NOTE: The OryonIntegrationComparator method works just like the Filter.Ne, but it checks if the field has been updated by Integration instead of being manually updated by a user
Conclusion
This is an instruction to get started on Integration with real code from the Oryon.People and Oryon.People.Core applications. Of course, not all Integrations are equal due to differences between applications and as such be wary of this.