Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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 @@ -598,11 +607,23 @@ public async Task<ActionResult<InstanceResponse>> PostSimplified(
}

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

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

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