Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ The libraries integrate with:
- Use internal accessibility on types by default
- Use sealed for classes unless we consider inheritance a valid use-case
- Use Nullable Reference Types
- Don't use null forgiveness operator without justification
- Remember to dispose `IDisposable`/`IAsyncDisposable` instances
- We want to minimize external dependencies
- For HTTP APIs we should have `...Request` and `...Response` DTOs (see `LookupPersonRequest.cs` and the corresponding response as an example)
Expand Down
51 changes: 43 additions & 8 deletions src/Altinn.App.Api/Controllers/InstancesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ public async Task<ActionResult<InstanceResponse>> Post(

Instance instance;
instanceTemplate.Process = null;
ProcessStateChange? change = null;
ProcessStateChange change;

try
{
Expand Down Expand Up @@ -381,9 +381,18 @@ await _instanceClient.DeleteInstance(
Guid.Parse(instance.Id.Split("/")[1])
);

// notify app and store events
_logger.LogInformation("Events sent to process engine: {Events}", change?.Events);
await _processEngine.HandleEventsAndUpdateStorage(instance, null, change?.Events);
ProcessChangeResult startProcessResult = await _processEngine.Start(
instance,
change,
User,
language: language,
ct: HttpContext.RequestAborted
);

if (!startProcessResult.Success)
return Conflict(startProcessResult.ErrorMessage);

instance = startProcessResult.MutatedInstance;
}
catch (Exception exception)
{
Expand Down Expand Up @@ -565,6 +574,9 @@ public async Task<ActionResult<InstanceResponse>> PostSimplified(

processResult = await _processEngine.GenerateProcessStartEvents(request);

if (!processResult.Success)
return Conflict(processResult.ErrorMessage);

Instance? source = null;

if (isCopyRequest)
Expand Down Expand Up @@ -598,11 +610,20 @@ public async Task<ActionResult<InstanceResponse>> PostSimplified(
}

instance = await _instanceClient.GetInstance(instance);
await _processEngine.HandleEventsAndUpdateStorage(

var startProcessResult = await _processEngine.Start(
instance,
instansiationInstance.Prefill,
processResult.ProcessStateChange?.Events
processResult.ProcessStateChange,
User,
prefill: instansiationInstance.Prefill,
language: language,
ct: HttpContext.RequestAborted
);

if (!startProcessResult.Success)
return Conflict(startProcessResult.ErrorMessage);

instance = startProcessResult.MutatedInstance;
}
catch (Exception exception)
{
Expand Down Expand Up @@ -724,7 +745,21 @@ public async Task<ActionResult> CopyInstance(

targetInstance = await _instanceClient.GetInstance(targetInstance);

await _processEngine.HandleEventsAndUpdateStorage(targetInstance, null, startResult.ProcessStateChange?.Events);
if (!startResult.Success)
return Conflict(startResult.ErrorMessage);

var startProcessResult = await _processEngine.Start(
targetInstance,
startResult.ProcessStateChange,
User,
language: language,
ct: HttpContext.RequestAborted
);

if (!startProcessResult.Success)
return Conflict(startProcessResult.ErrorMessage);

targetInstance = startProcessResult.MutatedInstance;

await RegisterEvent("app.instance.created", targetInstance);

Expand Down
16 changes: 12 additions & 4 deletions src/Altinn.App.Api/Controllers/ProcessController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,20 @@ public async Task<ActionResult<AppProcessState>> StartProcess(
return Conflict(result.ErrorMessage);
}

await _processEngine.HandleEventsAndUpdateStorage(instance, null, result.ProcessStateChange?.Events);

AppProcessState appProcessState = await ConvertAndAuthorizeActions(
var startProcessResult = await _processEngine.Start(
instance,
result.ProcessStateChange?.NewProcessState
result.ProcessStateChange,
User,
ct: HttpContext.RequestAborted
);
if (!startProcessResult.Success)
{
return GetResultForError(startProcessResult);
}

instance = startProcessResult.MutatedInstance;

AppProcessState appProcessState = await ConvertAndAuthorizeActions(instance, instance.Process);
return Ok(appProcessState);
}
catch (PlatformHttpException e)
Expand Down
7 changes: 7 additions & 0 deletions src/Altinn.App.Core/Features/Telemetry/Telemetry.Processes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ internal void ProcessEnded(ProcessStateChange processChange)
return activity;
}

internal Activity? StartProcessStartExecutionActivity(Instance instance)
{
var activity = ActivitySource.StartActivity($"{Prefix}.StartExecution");
activity?.SetInstanceId(instance);
return activity;
}

internal Activity? StartProcessNextActivity(Instance instance, string? action)
{
var activity = ActivitySource.StartActivity($"{Prefix}.Next");
Expand Down
14 changes: 14 additions & 0 deletions src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Security.Claims;
using Altinn.App.Core.Internal.Process.ProcessTasks.ServiceTasks;
using Altinn.App.Core.Models.Process;
using Altinn.Platform.Storage.Interface.Models;
Expand Down Expand Up @@ -35,4 +36,17 @@ Task<Instance> HandleEventsAndUpdateStorage(
Dictionary<string, string>? prefill,
List<InstanceEvent>? events
);

/// <summary>
/// Dispatches process start events to storage and auto-runs any initial service tasks.
/// Call after instance creation and data storage, with the result from <see cref="GenerateProcessStartEvents"/>.
/// </summary>
Task<ProcessChangeResult> Start(
Instance instance,
ProcessStateChange processStateChange,
ClaimsPrincipal user,
Dictionary<string, string>? prefill = null,
string? language = null,
CancellationToken ct = default
);
}
55 changes: 52 additions & 3 deletions src/Altinn.App.Core/Internal/Process/ProcessEngine.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Security.Claims;
using Altinn.App.Core.Extensions;
using Altinn.App.Core.Features;
using Altinn.App.Core.Features.Action;
Expand Down Expand Up @@ -149,6 +150,46 @@ out ProcessError? startEventError
return changeResult;
}

/// <inheritdoc/>
public async Task<ProcessChangeResult> Start(
Instance instance,
ProcessStateChange processStateChange,
ClaimsPrincipal user,
Dictionary<string, string>? prefill = null,
string? language = null,
CancellationToken ct = default
)
{
using var activity = _telemetry?.StartProcessStartExecutionActivity(instance);

// HandleEvents mutates instance in-place; we discard the DispatchToStorage
// return (same as callers did before this method existed).
bool isServiceTask = IsServiceTask(instance);
await HandleEventsAndUpdateStorage(instance, isServiceTask ? null : prefill, processStateChange.Events);

if (!isServiceTask)
{
var result = new ProcessChangeResult(instance) { Success = true, ProcessStateChange = processStateChange };
activity?.SetProcessChangeResult(result);
return result;
}

// Auto-run through initial service tasks, forwarding prefill to the first user task
var nextResult = await Next(
new ProcessNextRequest
{
Instance = instance,
User = user,
Action = null,
Language = language,
Prefill = prefill,
},
ct
);
activity?.SetProcessChangeResult(nextResult);
return nextResult;
}

/// <inheritdoc/>
public async Task<ProcessChangeResult> Next(ProcessNextRequest request, CancellationToken ct = default)
{
Expand Down Expand Up @@ -196,6 +237,7 @@ public async Task<ProcessChangeResult> Next(ProcessNextRequest request, Cancella
Action = firstIteration ? request.Action : null,
ActionOnBehalfOf = firstIteration ? request.ActionOnBehalfOf : null,
Language = request.Language,
Prefill = request.Prefill,
};

result = await ProcessNext(processNextRequest, ct);
Expand Down Expand Up @@ -360,7 +402,7 @@ serviceTaskResult is ServiceTaskFailedResult
}
}

MoveToNextResult moveToNextResult = await HandleMoveToNext(instance, processNextAction);
MoveToNextResult moveToNextResult = await HandleMoveToNext(instance, processNextAction, request.Prefill);

if (moveToNextResult.IsEndEvent)
{
Expand Down Expand Up @@ -747,7 +789,11 @@ private async Task<InstanceEvent> GenerateProcessChangeEvent(string eventType, I
return instanceEvent;
}

private async Task<MoveToNextResult> HandleMoveToNext(Instance instance, string? action)
private async Task<MoveToNextResult> HandleMoveToNext(
Instance instance,
string? action,
Dictionary<string, string>? prefill = null
)
{
ProcessStateChange? processStateChange = await MoveProcessStateToNextAndGenerateEvents(instance, action);

Expand All @@ -756,7 +802,10 @@ private async Task<MoveToNextResult> HandleMoveToNext(Instance instance, string?
return new MoveToNextResult(instance, null);
}

instance = await HandleEventsAndUpdateStorage(instance, null, processStateChange.Events);
// Only apply prefill when the destination is a user task, not a service task.
// Service tasks can have data types too, and we don't want prefill applied to them.
Dictionary<string, string>? prefillForTask = IsServiceTask(instance) ? null : prefill;
instance = await HandleEventsAndUpdateStorage(instance, prefillForTask, processStateChange.Events);
await _processEventDispatcher.RegisterEventWithEventsComponent(instance);

return new MoveToNextResult(instance, processStateChange);
Expand Down
1 change: 1 addition & 0 deletions src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class ProcessChangeResult
/// Gets or sets a value indicating whether the process change was successful
/// </summary>
[MemberNotNullWhen(true, nameof(ProcessStateChange))]
[MemberNotNullWhen(true, nameof(MutatedInstance))]
[MemberNotNullWhen(false, nameof(ErrorMessage), nameof(ErrorType))]
public bool Success { get; init; }

Expand Down
6 changes: 6 additions & 0 deletions src/Altinn.App.Core/Models/Process/ProcessNextRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,10 @@ public sealed record ProcessNextRequest
/// The language the user sent with process/next (not required)
/// </summary>
public required string? Language { get; init; }

/// <summary>
/// Prefill data to apply when the next user task starts.
/// Used during instantiation to forward prefill past initial service tasks.
/// </summary>
public Dictionary<string, string>? Prefill { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ internal static InstancesControllerFixture Create(Authenticated? auth = null)
services.AddSingleton(new Mock<IAppResources>(MockBehavior.Strict).Object);

var httpContextMock = new Mock<HttpContext>(MockBehavior.Strict);
httpContextMock.Setup(hc => hc.RequestAborted).Returns(CancellationToken.None);
services.AddTransient(_ => httpContextMock.Object);

services.AddTransient<InternalPatchService>();
Expand Down
Loading
Loading