Application A
Instructions for setting up application A
Application A is responsible for triggering the instantiation of application B and passing data to it. To accomplish this, several steps must be followed.
- Extend the application process with an additional process task.
- Add logic for instantiation of application B to an event trigger.
- Populate/retrieve and pass on relevant data to the newly created instance of application B.
Add task to process
Why the need for multiple tasks
In most cases it is necessary to pass the data the end user has added to the form, further on to application B. This data is preserved in the pdf-element which is stored as a part of the instance object. However, this pdf is only generated at the end of a process task. Hence there is need for an extra task to be able to retrieve the pdf. You will need to have at least two tasks, where the final task is not a data task. The task can be a confirm or feedback task type. We recommend using the confirm task type, and this is what the following guidelines will use.
How to extend the process with multiple tasks
In order to add task types to extend the application process, we need to update process.bpmn
and policy.xml
.
- You will find examples of how to adapt the
process.bpmn
file, where the application process is defined, in the process documentation.
When using the confirm task type we need to allow for going back to a previous task type, which also means that we need to take advantage of exclusive gateways. Read more about exclusive gateways here. - The
policy.xml
file, where the authorization rules are defined, needs updates so read and write operations can be done on the new task.
See XACML policy , policy editor and Guidelines for authorization rules for details. Most apps allow this by default by the current template.
Trigger the instantiation of application B
Why triggering instantiation of application B needs custom adaptations
The essential purpose of a multi-app solution relies on the fact that an instance of an application is created by a given trigger action in another Altinn application. The native way of instantiating an Altinn application is by doing an API POST request to the running Altinn application. However, there is no built-in way in the Altinn context to trigger this behaviour which means that we will need to trigger the API request as custom code in application A.
How to customize application A to trigger instantiation of application B
The general approach for an Altinn application to perform custom operations is by implement code on certain hooks, which are predefined functions in app-backend. Read about how this custom code is added here.
If not already present, create a file for implementing the custom code that runs on the end events,
ProcessTaskEnd.cs
. In the file, implement the code that creates the instance object that will be used as the foundation for the new instance of application B. See the below example for a template. Make sure that the instance creation happens when the task is finished, i.e. use theProcessTaskEnd.End()
function. This is necessary since the user can go back to the data task and do changes on the form.
TheinstanceOwner
-part of the instance object is essential as this is where you specify the instance owner. Defining it byOrganisationNumber
means that the owner is an organization, while defining it byPersonNumber
means that the owner is a private person.
A natural part of the instance object is the prefill section where you will add the desired data that the new instance of application B should be prefilled with. The resulting instance object, will look something like this:var instanceTemplate = new InstansiationInstance { InstanceOwner = new InstanceOwner { //OrganisationNumber = [receiverOrgNr], Or //PersonNumber = [receiverSsnNr], }, Prefill = new() { {"someDataInReceiverDataModel", someValueFromThisTriggerAppliaction}, {"moreDataInReceiverDataModel", someStaticValue}, ... }, };
To actually perform the request to create the instance, we need to add a client. Refer to the consume documentation to see an example of how such a client can be added to the application. A fitting name for the client used in this context might be f.ex.
AppInstantiationClient
. In addition to the instructions in the referenced documentation, our constructor needs additional configuration to the HttpClient. Add the following code the the constructor to add a subscription key to the header of the requests sent by the http client.public AppClient( ... HttpClient httpClient, ... { ... httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey); _client = httpClient; ... }
Instead of creating a function in the client named
GetCountry
, as in the referenced documentation above, implement the following function,CreateNewInstance
:public async Task<Instance> CreateNewInstance(string AppOwnerOrgName, string applicationB, InstansiationInstance instanceTemplate) { string apiUrl = $"{AppOwnerOrgName}/{applicationB}/instances/create"; string envUrl = $"https://{AppOwnerOrgName}.apps.{_settings.HostName}"; _client.BaseAddress = new Uri(envUrl); StringContent content = new StringContent(JsonConvert.SerializeObject(instanceTemplate), Encoding.UTF8, "application/json"); HttpResponseMessage response = await _client.PostAsync(apiUrl, content); if (response.IsSuccessStatusCode) { Instance createdInstance = JsonConvert.DeserializeObject<Instance>(await response.Content.ReadAsStringAsync()); return createdInstance; } throw await PlatformHttpException.CreateAsync(response); }
In the
ProcessTaskEnd.cs
file, add the new AppInstantiationClient to theProcessTaskEnd
class in the same way as the CountryClient is added to theDataProcessingHandler
class in the consume documentation. Further, call the method triggering the request in the appInstantiationClient like this:Instance applicationBInstance = await _appInstantiationClient.CreateNewInstance([AppOwnerOrgName], [applicationB], [instanceTemplate]);
If the client should authenticate itself as the end user, rather than the application owner via maskinporten, please reference the app-lib client implementations on details for how to adapt the API request in the AppInstantiationClient to achieve this.
Delivering Data to Application B
Why manipulating the data for application B in application A
It is natural to utilize the multi-app solution to control the presentation of information in application B dynamically depending on what data is inserted in an instance of application A. This means that all adaptions on application B that is dependent on data retrieved from application A, must be done in application A and in some way delivered or represented in application B.
Retrieving data from application A to pass over to application B
Before some data types can be added, they must be retrieved from Altinn Storage since the application does not have
direct access to this by default.
The probably most attractive data type to pass over from application A to application B is the pdf including all the
information filled into the instance of application A. To retrieve this data from application A, it needs to be
collected from Altinn Storage. The pdf exists on the instance object as a part of the dataTypes
field with the name ref-data-as-pdf
. This can be fetched by getting the instance and fetch the data directly on the
retrieved instance object, or by using the already defined GetBinaryData
method on the dataClient.
See example code below of both below:
// Using the instance object directly with the GetInstance method on the InstanceClient
Instance updatedInstance = await _instanceClient.GetInstance(innsendingsApp, mutliAppOwner, int.Parse(instance.InstanceOwner.PartyId), instanceGuid);
DataElement pdf = updatedInstance.Data.FindLast(d => d.DataType == "ref-data-as-pdf");
// Using the GetBinaryData method on the DataClient
var stream = await _dataClient.GetBinaryData(instance.Org, instance.AppId,int.Parse(instance.InstanceOwner.PartyId), instanceGuid, Guid.Parse(pdf.Id));
NB: In order to use these methods in the ProcessTaskEnd
class, the constructor needs to be configured to use the
InstanceClient and/or the DataClient.
How to control data in application B from application A
There are several ways to control certain data in application B, whereas one or multiple can be utilized:
Alt 1: Add data as values on the datamodel of application B by adding the data model field name and the corresponding value in the
prefill
field of the instance template that you created in the Trigger the instantiation of application B section above.Alt 2: If the intention is to manipulate the texts in Altinn Inbox for the instances of application B, use presentation fields .
Alt 3: Add data as binary data by doing a POST request to the instance of application B.
public async Task<DataElement> InsertBinaryData(string org, string app, string instanceId, string contentType, string filename, Stream stream) { string envUrl = $"https://{org}.apps.{_settings.HostName}"; _client.BaseAddress = new Uri(envUrl); string apiUrl = $"{org}/{app}/instances/{instanceId}/data?dataType=vedlegg"; StreamContent content = new(stream); content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); if (!string.IsNullOrEmpty(filename)) { content.Headers.ContentDisposition = new ContentDispositionHeaderValue(DispositionTypeNames.Attachment) { FileName = filename, FileNameStar = filename }; } HttpResponseMessage response = await _client.PostAsync(apiUrl, content); if (response.IsSuccessStatusCode) { await Task.CompletedTask; } }