Skip to content
Merged
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
25 changes: 18 additions & 7 deletions TaskPipeline.sln
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,20 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskPipeline.Abstractions", "src/TaskPipeline.Abstractions/TaskPipeline.Abstractions.csproj", "{11111111-1111-1111-1111-111111111111}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskPipeline", "src\TaskPipeline\TaskPipeline.csproj", "{22222222-2222-2222-2222-222222222222}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskPipeline", "src/TaskPipeline/TaskPipeline.csproj", "{22222222-2222-2222-2222-222222222222}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskPipeline.Tests", "tests\TaskPipeline.Tests\TaskPipeline.Tests.csproj", "{33333333-3333-3333-3333-333333333333}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskPipeline.Tests", "tests/TaskPipeline.Tests/TaskPipeline.Tests.csproj", "{33333333-3333-3333-3333-333333333333}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClaimFlow.Web", "examples\ClaimFlow\src\ClaimFlow.Web\ClaimFlow.Web.csproj", "{47198912-FB8A-99BD-4642-C2727B26C921}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{11111111-1111-1111-1111-111111111111}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{11111111-1111-1111-1111-111111111111}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11111111-1111-1111-1111-111111111111}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11111111-1111-1111-1111-111111111111}.Release|Any CPU.Build.0 = Release|Any CPU
{22222222-2222-2222-2222-222222222222}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{22222222-2222-2222-2222-222222222222}.Debug|Any CPU.Build.0 = Debug|Any CPU
{22222222-2222-2222-2222-222222222222}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -26,5 +24,18 @@ Global
{33333333-3333-3333-3333-333333333333}.Debug|Any CPU.Build.0 = Debug|Any CPU
{33333333-3333-3333-3333-333333333333}.Release|Any CPU.ActiveCfg = Release|Any CPU
{33333333-3333-3333-3333-333333333333}.Release|Any CPU.Build.0 = Release|Any CPU
{47198912-FB8A-99BD-4642-C2727B26C921}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{47198912-FB8A-99BD-4642-C2727B26C921}.Debug|Any CPU.Build.0 = Debug|Any CPU
{47198912-FB8A-99BD-4642-C2727B26C921}.Release|Any CPU.ActiveCfg = Release|Any CPU
{47198912-FB8A-99BD-4642-C2727B26C921}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{47198912-FB8A-99BD-4642-C2727B26C921} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {38D337CA-80CF-401B-988C-33ADFCD6666B}
EndGlobalSection
EndGlobal
27 changes: 27 additions & 0 deletions examples/ClaimFlow/ClaimFlow.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClaimFlow.Web", "src\ClaimFlow.Web\ClaimFlow.Web.csproj", "{7A1C0EA2-5E40-4D04-87F5-39F8B44F0101}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClaimFlow.Web.Tests", "tests\ClaimFlow.Web.Tests\ClaimFlow.Web.Tests.csproj", "{3DA3871D-3D8E-4FE1-9E50-A85D4CB95E56}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7A1C0EA2-5E40-4D04-87F5-39F8B44F0101}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7A1C0EA2-5E40-4D04-87F5-39F8B44F0101}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7A1C0EA2-5E40-4D04-87F5-39F8B44F0101}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7A1C0EA2-5E40-4D04-87F5-39F8B44F0101}.Release|Any CPU.Build.0 = Release|Any CPU
{3DA3871D-3D8E-4FE1-9E50-A85D4CB95E56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3DA3871D-3D8E-4FE1-9E50-A85D4CB95E56}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3DA3871D-3D8E-4FE1-9E50-A85D4CB95E56}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3DA3871D-3D8E-4FE1-9E50-A85D4CB95E56}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
8 changes: 8 additions & 0 deletions examples/ClaimFlow/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
</PropertyGroup>
</Project>
56 changes: 56 additions & 0 deletions examples/ClaimFlow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# ClaimFlow

ClaimFlow is a compact ASP.NET Core MVC sample that demonstrates how to use the `TaskPipeline` NuGet package in a realistic business workflow.

## Scenario

The app processes insurance claims and shows why a strongly typed orchestration library is useful when a workflow has:

- ordered steps;
- business conditions;
- parallel branches;
- explicit merge logic;
- cancellation support;
- partial failure handling;
- execution auditing.

## Stack

- ASP.NET Core MVC (.NET 8)
- SQLite via EF Core
- TaskPipeline from NuGet
- Bootstrap-based UI
- xUnit tests

## What the sample demonstrates

- sequential pipeline nodes;
- `AddConditional` for true/false business branches;
- `AddFork` with named parallel branches;
- `DelegateMergeStrategy` for aggregation after fork completion;
- `ContinueOnError` failure mode;
- persisted execution snapshots;
- a simple dashboard for claims and execution details.

## Project layout

```text
src/
ClaimFlow.Web/
tests/
ClaimFlow.Web.Tests/
```

## Run

```bash
dotnet restore
dotnet build
dotnet run --project src/ClaimFlow.Web
```

Then open the local URL printed by ASP.NET Core.

## Notes

The sample uses mock integrations for anti-fraud, repair quotes, policy lookup, payment pre-checks, and notifications so the application can run locally with SQLite only.
18 changes: 18 additions & 0 deletions examples/ClaimFlow/src/ClaimFlow.Web/ClaimFlow.Web.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<RootNamespace>ClaimFlow.Web</RootNamespace>
<AssemblyName>ClaimFlow.Web</AssemblyName>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.12">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.12" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\src\TaskPipeline\TaskPipeline.csproj" />
</ItemGroup>
</Project>
122 changes: 122 additions & 0 deletions examples/ClaimFlow/src/ClaimFlow.Web/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
using System.Text.Json;
using ClaimFlow.Web.Data;
using ClaimFlow.Web.Domain;
using ClaimFlow.Web.Models;
using ClaimFlow.Web.Pipeline;
using ClaimFlow.Web.Services;
using ClaimFlow.Web.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace ClaimFlow.Web.Controllers;

public sealed class HomeController(
IClaimRepository claimRepository,
ClaimProcessingService claimProcessingService,
AppDbContext dbContext,
ILogger<HomeController> logger) : Controller
{
[HttpGet]
public async Task<IActionResult> Index(CancellationToken cancellationToken)
{
var claims = await claimRepository.ListAsync(cancellationToken);
var policies = await dbContext.Policies.AsNoTracking().OrderBy(x => x.PolicyNumber).ToListAsync(cancellationToken);

var model = new DashboardViewModel
{
Claims = claims.Select(x => new ClaimListItemViewModel
{
Id = x.Id,
CustomerName = x.CustomerName,
PolicyNumber = x.PolicyNumber,
ClaimType = x.ClaimType,
Status = x.Status,
FinalDecisionType = x.FinalDecisionType,
ApprovedPayoutAmount = x.ApprovedPayoutAmount,
CreatedAtUtc = x.CreatedAtUtc
}).ToList(),
Policies = policies.Select(x => new DemoPolicyViewModel
{
PolicyNumber = x.PolicyNumber,
IsActive = x.IsActive,
CoverageLimit = x.CoverageLimit,
SupportsAutoPayment = x.SupportsAutoPayment
}).ToList()
};

return View(model);
}

[HttpGet]
public IActionResult NewClaim()
{
return View(new ClaimRequest
{
ClaimType = ClaimType.Kasko,
HasPhotos = true
});
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> NewClaim(ClaimRequest request, CancellationToken cancellationToken)
{
if (!ModelState.IsValid)
{
return View(request);
}

try
{
var result = await claimProcessingService.ProcessAsync(request, cancellationToken);
TempData["Flash"] = $"Claim {result.Claim.Id} processed with status {result.Claim.Status}.";
return RedirectToAction(nameof(Details), new { id = result.Claim.Id });
}
catch (OperationCanceledException)
{
TempData["Flash"] = "Claim processing was cancelled.";
return RedirectToAction(nameof(Index));
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to process claim.");
ModelState.AddModelError(string.Empty, exception.Message);
return View(request);
}
}

[HttpGet]
public async Task<IActionResult> Details(Guid id, CancellationToken cancellationToken)
{
var claim = await claimRepository.GetAsync(id, cancellationToken);
if (claim is null)
{
return NotFound();
}

var latestExecution = claim.Executions.OrderByDescending(x => x.CreatedAtUtc).FirstOrDefault();
var summary = latestExecution is null
? null
: JsonSerializer.Deserialize<ExecutionSummary>(latestExecution.SummaryJson);

var model = new ClaimDetailsViewModel
{
Claim = claim,
ExecutionSummary = summary,
MissingDocuments = ParseJsonArray(claim.MissingDocumentsJson),
Warnings = ParseJsonArray(claim.WarningsJson)
};

return View(model);
}


[HttpGet]
public IActionResult Error()
{
return View();
}

private static List<string> ParseJsonArray(string json)
=> JsonSerializer.Deserialize<List<string>>(json) ?? [];
}
56 changes: 56 additions & 0 deletions examples/ClaimFlow/src/ClaimFlow.Web/Data/AppDbContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using ClaimFlow.Web.Domain;
using Microsoft.EntityFrameworkCore;

namespace ClaimFlow.Web.Data;

public sealed class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<ClaimCase> Claims => Set<ClaimCase>();
public DbSet<ClaimExecutionSnapshot> Executions => Set<ClaimExecutionSnapshot>();
public DbSet<PolicyRecord> Policies => Set<PolicyRecord>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ClaimCase>()
.HasMany(x => x.Executions)
.WithOne(x => x.ClaimCase)
.HasForeignKey(x => x.ClaimCaseId)
.OnDelete(DeleteBehavior.Cascade);

modelBuilder.Entity<PolicyRecord>()
.HasIndex(x => x.PolicyNumber)
.IsUnique();

modelBuilder.Entity<PolicyRecord>().HasData(
new PolicyRecord
{
Id = 1,
PolicyNumber = "KSK-1001",
IsActive = true,
CoverageLimit = 7000m,
CoversGlass = true,
CoversTowTruck = true,
SupportsAutoPayment = true
},
new PolicyRecord
{
Id = 2,
PolicyNumber = "OSG-2001",
IsActive = true,
CoverageLimit = 2500m,
CoversGlass = false,
CoversTowTruck = false,
SupportsAutoPayment = true
},
new PolicyRecord
{
Id = 3,
PolicyNumber = "KSK-9000",
IsActive = false,
CoverageLimit = 10000m,
CoversGlass = true,
CoversTowTruck = true,
SupportsAutoPayment = false
});
}
}
46 changes: 46 additions & 0 deletions examples/ClaimFlow/src/ClaimFlow.Web/Domain/ClaimCase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.ComponentModel.DataAnnotations;

namespace ClaimFlow.Web.Domain;

public sealed class ClaimCase
{
public Guid Id { get; set; } = Guid.NewGuid();

[MaxLength(128)]
public string CustomerName { get; set; } = string.Empty;

[MaxLength(128)]
public string CustomerEmail { get; set; } = string.Empty;

[MaxLength(32)]
public string PolicyNumber { get; set; } = string.Empty;

public ClaimType ClaimType { get; set; }
public ClaimStatus Status { get; set; } = ClaimStatus.Draft;
public FinalDecisionType FinalDecisionType { get; set; } = FinalDecisionType.None;

[MaxLength(1024)]
public string Description { get; set; } = string.Empty;

public decimal EstimatedDamageAmount { get; set; }
public decimal ApprovedPayoutAmount { get; set; }
public bool HasPoliceReport { get; set; }
public bool HasPhotos { get; set; }
public bool RequiresTowTruck { get; set; }
public bool InjuryInvolved { get; set; }
public bool IsVipCustomer { get; set; }

[MaxLength(2048)]
public string MissingDocumentsJson { get; set; } = "[]";

[MaxLength(2048)]
public string WarningsJson { get; set; } = "[]";

[MaxLength(4096)]
public string FinalDecisionReason { get; set; } = string.Empty;

public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
public DateTime? ProcessedAtUtc { get; set; }

public ICollection<ClaimExecutionSnapshot> Executions { get; set; } = new List<ClaimExecutionSnapshot>();
}
Loading
Loading