diff --git a/.aspire/settings.json b/.aspire/settings.json
new file mode 100644
index 0000000..231f885
--- /dev/null
+++ b/.aspire/settings.json
@@ -0,0 +1,3 @@
+{
+ "appHostPath": "..\\src\\Werkr.AppHost\\Werkr.AppHost.csproj"
+}
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
index 0aa16a3..a63fa8f 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,395 +1,395 @@
-# top-most EditorConfig file
-root = true
-
-# Don't use tabs for indentation.
-[*]
-indent_style = space
-end_of_line = lf
-# (Please don't specify an indent_size here; that has too many unintended consequences.)
-
-# Code files
-[*.{cs,csx,vb,vbx}]
-indent_size = 4
-insert_final_newline = true
-charset = utf-8
-
-# XML project files
-[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
-indent_size = 4
-charset = utf-8
-
-# XML config files
-[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
-indent_size = 2
-
-# JSON files
-[*.json]
-charset = utf-8
-indent_size = 2
-
-# Powershell files
-[*.ps1]
-charset = utf-8
-indent_size = 4
-
-# Shell script files
-[*.sh]
-charset = utf-8
-indent_size = 2
-
-# Dotnet code style settings:
-[*.{cs,vb}]
-
-# IDE0055: Fix formatting
-dotnet_diagnostic.IDE0055.severity = warning
-
-# Sort using and Import directives with System.* appearing first
-dotnet_sort_system_directives_first = true
-dotnet_separate_import_directive_groups = false
-# Avoid "this." and "Me." if not necessary
-dotnet_style_qualification_for_field = false:silent
-dotnet_style_qualification_for_property = false:silent
-dotnet_style_qualification_for_method = false:silent
-dotnet_style_qualification_for_event = false:silent
-
-# Use language keywords instead of framework type names for type references
-dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
-dotnet_style_predefined_type_for_member_access = true:suggestion
-
-# Suggest more modern language features when available
-dotnet_style_object_initializer = true:suggestion
-dotnet_style_collection_initializer = true:suggestion
-dotnet_style_coalesce_expression = true:suggestion
-dotnet_style_null_propagation = true:suggestion
-dotnet_style_explicit_tuple_names = true:suggestion
-
-# Non-private static fields are PascalCase
-dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion
-dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields
-dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
-
-dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
-dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
-dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
-
-dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
-
-# Non-private readonly fields are PascalCase
-dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion
-dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields
-dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_static_field_style
-
-dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field
-dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
-dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly
-
-dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case
-
-# Constants are PascalCase
-dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion
-dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants
-dotnet_naming_rule.constants_should_be_pascal_case.style = non_private_static_field_style
-
-dotnet_naming_symbols.constants.applicable_kinds = field, local
-dotnet_naming_symbols.constants.required_modifiers = const
-
-dotnet_naming_style.constant_style.capitalization = pascal_case
-
-# Static fields are camelCase and start with s_
-dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion
-dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields
-dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style
-
-dotnet_naming_symbols.static_fields.applicable_kinds = field
-dotnet_naming_symbols.static_fields.required_modifiers = static
-
-dotnet_naming_style.static_field_style.capitalization = camel_case
-dotnet_naming_style.static_field_style.required_prefix = s_
-
-# Instance fields are camelCase and start with _
-dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion
-dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields
-dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style
-
-dotnet_naming_symbols.instance_fields.applicable_kinds = field
-
-dotnet_naming_style.instance_field_style.capitalization = camel_case
-dotnet_naming_style.instance_field_style.required_prefix = _
-
-# Locals and parameters are camelCase
-dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion
-dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters
-dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style
-
-dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local
-
-dotnet_naming_style.camel_case_style.capitalization = camel_case
-
-# Local functions are PascalCase
-dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion
-dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions
-dotnet_naming_rule.local_functions_should_be_pascal_case.style = non_private_static_field_style
-
-dotnet_naming_symbols.local_functions.applicable_kinds = local_function
-
-dotnet_naming_style.local_function_style.capitalization = pascal_case
-
-# By default, name items with PascalCase
-dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion
-dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members
-dotnet_naming_rule.members_should_be_pascal_case.style = non_private_static_field_style
-
-dotnet_naming_symbols.all_members.applicable_kinds = *
-
-dotnet_naming_style.pascal_case_style.capitalization = pascal_case
-
-# error RS2008: Enable analyzer release tracking for the analyzer project containing rule '{0}'
-dotnet_diagnostic.RS2008.severity = none
-
-# IDE0035: Remove unreachable code
-dotnet_diagnostic.IDE0035.severity = warning
-
-# IDE0036: Order modifiers
-dotnet_diagnostic.IDE0036.severity = warning
-
-# IDE0043: Format string contains invalid placeholder
-dotnet_diagnostic.IDE0043.severity = warning
-
-# IDE0044: Make field readonly
-dotnet_diagnostic.IDE0044.severity = warning
-
-# RS0016: Only enable if API files are present
-dotnet_public_api_analyzer.require_api_files = true
-dotnet_style_readonly_field= true:silent
-dotnet_style_operator_placement_when_wrapping = beginning_of_line
-tab_width = 4
-end_of_line = lf
-dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
-dotnet_style_prefer_auto_properties = true:silent
-dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
-dotnet_style_prefer_conditional_expression_over_assignment = true:silent
-dotnet_style_prefer_conditional_expression_over_return = true:silent
-dotnet_style_prefer_inferred_tuple_names = true:suggestion
-dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
-dotnet_style_prefer_compound_assignment = true:suggestion
-dotnet_style_prefer_simplified_interpolation = true:suggestion
-dotnet_style_namespace_match_folder = true:suggestion
-dotnet_diagnostic.CA1838.severity = suggestion
-dotnet_diagnostic.CA1848.severity = suggestion
-dotnet_diagnostic.CA1873.severity = suggestion
-dotnet_diagnostic.CA5350.severity = error
-dotnet_diagnostic.CA5351.severity = error
-dotnet_diagnostic.CA5359.severity = warning
-dotnet_diagnostic.CA5360.severity = warning
-dotnet_diagnostic.CA5364.severity = error
-dotnet_diagnostic.CA5365.severity = suggestion
-dotnet_diagnostic.CA5384.severity = warning
-dotnet_diagnostic.CA5385.severity = warning
-dotnet_diagnostic.CA5397.severity = error
-dotnet_diagnostic.CA2201.severity = warning
-dotnet_diagnostic.CA2251.severity = suggestion
-dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
-dotnet_style_allow_multiple_blank_lines_experimental = true:silent
-dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
-dotnet_code_quality_unused_parameters = all:suggestion
-dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
-dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
-dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
-dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
-
-# CSharp code style settings:
-[*.cs]
-max_line_length = 120
-
-# Newline settings
-csharp_new_line_before_open_brace =false
-csharp_new_line_before_else =false
-csharp_new_line_before_catch =false
-csharp_new_line_before_finally =false
-csharp_new_line_before_members_in_object_initializers = true
-csharp_new_line_before_members_in_anonymous_types = true
-csharp_new_line_between_query_expression_clauses = true
-
-# Indentation preferences
-csharp_indent_block_contents = true
-csharp_indent_braces =false
-csharp_indent_case_contents = true
-csharp_indent_case_contents_when_block = true
-csharp_indent_switch_labels = true
-csharp_indent_labels = flush_left
-
-# Prefer "var" everywhere
-csharp_style_var_for_built_in_types = false:suggestion
-csharp_style_var_when_type_is_apparent = false:suggestion
-csharp_style_var_elsewhere = false:suggestion
-
-# Prefer method-like constructs to have a block body
-csharp_style_expression_bodied_methods = true:none
-csharp_style_expression_bodied_constructors = true:none
-csharp_style_expression_bodied_operators = true:none
-
-# Prefer property-like constructs to have an expression-body
-csharp_style_expression_bodied_properties = true:none
-csharp_style_expression_bodied_indexers = true:none
-csharp_style_expression_bodied_accessors = true:none
-
-# Suggest more modern language features when available
-csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
-csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
-csharp_style_inlined_variable_declaration = true:suggestion
-csharp_style_throw_expression = true:suggestion
-csharp_style_conditional_delegate_call = true:suggestion
-
-# Space preferences
-csharp_space_after_cast = false
-csharp_space_after_colon_in_inheritance_clause = true
-csharp_space_after_comma = true
-csharp_space_after_dot = false
-csharp_space_after_keywords_in_control_flow_statements = true
-csharp_space_after_semicolon_in_for_statement = true
-csharp_space_around_binary_operators = before_and_after
-csharp_space_around_declaration_statements = ignore
-csharp_space_before_colon_in_inheritance_clause = true
-csharp_space_before_comma = false
-csharp_space_before_dot = false
-csharp_space_before_open_square_brackets = false
-csharp_space_before_semicolon_in_for_statement = false
-csharp_space_between_empty_square_brackets = false
-csharp_space_between_method_call_empty_parameter_list_parentheses = true
-csharp_space_between_method_call_name_and_opening_parenthesis = false
-csharp_space_between_method_call_parameter_list_parentheses = true
-csharp_space_between_method_declaration_empty_parameter_list_parentheses = true
-csharp_space_between_method_declaration_name_and_open_parenthesis = false
-csharp_space_between_method_declaration_parameter_list_parentheses = true
-csharp_space_between_parentheses = false
-csharp_space_between_square_brackets = false
-
-# Blocks are allowed
-csharp_prefer_braces = true:silent
-csharp_preserve_single_line_blocks = true
-csharp_preserve_single_line_statements = true
-csharp_style_expression_bodied_lambdas= true:silent
-csharp_using_directive_placement = outside_namespace:silent
-csharp_prefer_simple_using_statement = true:suggestion
-csharp_style_namespace_declarations = file_scoped:warning
-csharp_style_expression_bodied_local_functions = true:silent
-csharp_style_prefer_null_check_over_type_check = true:suggestion
-csharp_prefer_simple_default_expression = true:suggestion
-csharp_style_prefer_local_over_anonymous_function = true:suggestion
-csharp_style_prefer_index_operator = true:suggestion
-csharp_style_prefer_range_operator = true:suggestion
-csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
-csharp_style_prefer_tuple_swap = true:suggestion
-csharp_style_deconstructed_variable_declaration = true:suggestion
-csharp_style_unused_value_assignment_preference = discard_variable:suggestion
-csharp_style_unused_value_expression_statement_preference = discard_variable:silent
-dotnet_diagnostic.CA1805.severity = warning
-dotnet_diagnostic.CA1869.severity = warning
-dotnet_diagnostic.CA1873.severity = warning
-dotnet_diagnostic.CA2016.severity = warning
-dotnet_diagnostic.IDE0004.severity = suggestion
-dotnet_diagnostic.IDE0005.severity = suggestion
-dotnet_diagnostic.IDE0008.severity = suggestion
-dotnet_diagnostic.IDE0016.severity = suggestion
-dotnet_diagnostic.IDE0017.severity = suggestion
-dotnet_diagnostic.IDE0020.severity = suggestion
-dotnet_diagnostic.IDE0019.severity = suggestion
-dotnet_diagnostic.IDE0018.severity = suggestion
-dotnet_diagnostic.IDE0022.severity = suggestion
-dotnet_diagnostic.IDE0023.severity = suggestion
-dotnet_diagnostic.IDE0024.severity = suggestion
-dotnet_diagnostic.IDE0025.severity = suggestion
-dotnet_diagnostic.IDE0026.severity = suggestion
-dotnet_diagnostic.IDE0027.severity = suggestion
-dotnet_diagnostic.IDE0028.severity = suggestion
-dotnet_diagnostic.IDE0029.severity = suggestion
-dotnet_diagnostic.IDE0030.severity = suggestion
-dotnet_diagnostic.IDE0031.severity = suggestion
-dotnet_diagnostic.IDE0032.severity = suggestion
-dotnet_diagnostic.IDE0034.severity = suggestion
-dotnet_diagnostic.IDE0040.severity = suggestion
-dotnet_diagnostic.IDE0041.severity = warning
-dotnet_diagnostic.IDE0045.severity = suggestion
-dotnet_diagnostic.IDE0046.severity = suggestion
-dotnet_diagnostic.IDE0048.severity = suggestion
-dotnet_diagnostic.IDE0054.severity = suggestion
-dotnet_diagnostic.IDE0057.severity = suggestion
-dotnet_diagnostic.IDE0056.severity = suggestion
-dotnet_diagnostic.IDE0058.severity = warning
-dotnet_diagnostic.IDE0059.severity = suggestion
-dotnet_diagnostic.IDE0060.severity = warning
-dotnet_diagnostic.IDE0063.severity = suggestion
-dotnet_diagnostic.IDE0066.severity = suggestion
-dotnet_diagnostic.IDE0071.severity = suggestion
-dotnet_diagnostic.IDE0072.severity = suggestion
-dotnet_diagnostic.IDE0075.severity = suggestion
-dotnet_diagnostic.IDE0074.severity = suggestion
-dotnet_diagnostic.IDE0090.severity = warning
-dotnet_diagnostic.IDE0082.severity = suggestion
-dotnet_diagnostic.IDE0083.severity = suggestion
-dotnet_diagnostic.IDE0120.severity = suggestion
-dotnet_diagnostic.IDE0270.severity = warning
-dotnet_diagnostic.IDE0305.severity = warning
-dotnet_diagnostic.IDE0330.severity = warning
-dotnet_diagnostic.IDE2004.severity = suggestion
-dotnet_diagnostic.IDE2002.severity = suggestion
-dotnet_diagnostic.IDE2001.severity = suggestion
-dotnet_diagnostic.IDE1006.severity = warning
-dotnet_diagnostic.IDE0180.severity = suggestion
-dotnet_diagnostic.MSTEST0037.severity = warning
-dotnet_diagnostic.MSTEST0049.severity = warning
-dotnet_diagnostic.MSTEST0058.severity = warning
-dotnet_diagnostic.SYSLIB1045.severity = warning
-csharp_prefer_static_local_function = true:suggestion
-csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
-csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent
-csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
-csharp_style_prefer_switch_expression = true:suggestion
-csharp_style_prefer_pattern_matching = true:silent
-csharp_style_prefer_not_pattern = true:suggestion
-csharp_style_prefer_extended_property_pattern = true:suggestion
-csharp_style_prefer_method_group_conversion = true:silent
-csharp_style_prefer_top_level_statements = false:silent
-csharp_style_prefer_utf8_string_literals = true:suggestion
-
-[src/CodeStyle/**.{cs,vb}]
-# warning RS0005: Do not use generic CodeAction.Create to create CodeAction
-dotnet_diagnostic.RS0005.severity = none
-
-[src/{Analyzers,CodeStyle,Features,Workspaces,EditorFeatures, VisualStudio}/**/*.{cs,vb}]
-
-# IDE0011: Add braces
-csharp_prefer_braces = when_multiline:warning
-# NOTE: We need the below severity entry for Add Braces due to https://github.com/dotnet/roslyn/issues/44201
-dotnet_diagnostic.IDE0011.severity = warning
-
-# IDE0040: Add accessibility modifiers
-dotnet_diagnostic.IDE0040.severity = warning
-
-# CONSIDER: Are IDE0051 and IDE0052 too noisy to be warnings for IDE editing scenarios? Should they be made build-only warnings?
-# IDE0051: Remove unused private member
-dotnet_diagnostic.IDE0051.severity = warning
-
-# IDE0052: Remove unread private member
-dotnet_diagnostic.IDE0052.severity = warning
-
-# IDE0059: Unnecessary assignment to a value
-dotnet_diagnostic.IDE0059.severity = warning
-
-# IDE0060: Remove unused parameter
-dotnet_diagnostic.IDE0060.severity = warning
-
-# CA1822: Make member static
-dotnet_diagnostic.CA1822.severity = warning
-
-# Prefer "var" everywhere
-dotnet_diagnostic.IDE0007.severity = warning
-csharp_style_var_for_built_in_types = true:warning
-csharp_style_var_when_type_is_apparent = true:warning
-csharp_style_var_elsewhere = true:warning
-
-[src/{VisualStudio}/**/*.{cs,vb}]
-# CA1822: Make member static
-# Not enforced as a build 'warning' for 'VisualStudio' layer due to large number of false positives from https://github.com/dotnet/roslyn-analyzers/issues/3857 and https://github.com/dotnet/roslyn-analyzers/issues/3858
-# Additionally, there is a risk of accidentally breaking an internal API that partners rely on though IVT.
-dotnet_diagnostic.CA1822.severity = suggestion
+# top-most EditorConfig file
+root = true
+
+# Don't use tabs for indentation.
+[*]
+indent_style = space
+end_of_line = lf
+# (Please don't specify an indent_size here; that has too many unintended consequences.)
+
+# Code files
+[*.{cs,csx,vb,vbx}]
+indent_size = 4
+insert_final_newline = true
+charset = utf-8
+
+# XML project files
+[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
+indent_size = 4
+charset = utf-8
+
+# XML config files
+[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
+indent_size = 2
+
+# JSON files
+[*.json]
+charset = utf-8
+indent_size = 2
+
+# Powershell files
+[*.ps1]
+charset = utf-8
+indent_size = 4
+
+# Shell script files
+[*.sh]
+charset = utf-8
+indent_size = 2
+
+# Dotnet code style settings:
+[*.{cs,vb}]
+
+# IDE0055: Fix formatting
+dotnet_diagnostic.IDE0055.severity = warning
+
+# Sort using and Import directives with System.* appearing first
+dotnet_sort_system_directives_first = true
+dotnet_separate_import_directive_groups = false
+# Avoid "this." and "Me." if not necessary
+dotnet_style_qualification_for_field = false:silent
+dotnet_style_qualification_for_property = false:silent
+dotnet_style_qualification_for_method = false:silent
+dotnet_style_qualification_for_event = false:silent
+
+# Use language keywords instead of framework type names for type references
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
+
+# Suggest more modern language features when available
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+
+# Non-private static fields are PascalCase
+dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields
+dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
+
+dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
+dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
+dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
+
+dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
+
+# Non-private readonly fields are PascalCase
+dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields
+dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_static_field_style
+
+dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field
+dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
+dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly
+
+dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case
+
+# Constants are PascalCase
+dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants
+dotnet_naming_rule.constants_should_be_pascal_case.style = non_private_static_field_style
+
+dotnet_naming_symbols.constants.applicable_kinds = field, local
+dotnet_naming_symbols.constants.required_modifiers = const
+
+dotnet_naming_style.constant_style.capitalization = pascal_case
+
+# Static fields are camelCase and start with s_
+dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion
+dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields
+dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style
+
+dotnet_naming_symbols.static_fields.applicable_kinds = field
+dotnet_naming_symbols.static_fields.required_modifiers = static
+
+dotnet_naming_style.static_field_style.capitalization = camel_case
+dotnet_naming_style.static_field_style.required_prefix = s_
+
+# Instance fields are camelCase and start with _
+dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion
+dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields
+dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style
+
+dotnet_naming_symbols.instance_fields.applicable_kinds = field
+
+dotnet_naming_style.instance_field_style.capitalization = camel_case
+dotnet_naming_style.instance_field_style.required_prefix = _
+
+# Locals and parameters are camelCase
+dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion
+dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters
+dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style
+
+dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local
+
+dotnet_naming_style.camel_case_style.capitalization = camel_case
+
+# Local functions are PascalCase
+dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions
+dotnet_naming_rule.local_functions_should_be_pascal_case.style = non_private_static_field_style
+
+dotnet_naming_symbols.local_functions.applicable_kinds = local_function
+
+dotnet_naming_style.local_function_style.capitalization = pascal_case
+
+# By default, name items with PascalCase
+dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members
+dotnet_naming_rule.members_should_be_pascal_case.style = non_private_static_field_style
+
+dotnet_naming_symbols.all_members.applicable_kinds = *
+
+dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+
+# error RS2008: Enable analyzer release tracking for the analyzer project containing rule '{0}'
+dotnet_diagnostic.RS2008.severity = none
+
+# IDE0035: Remove unreachable code
+dotnet_diagnostic.IDE0035.severity = warning
+
+# IDE0036: Order modifiers
+dotnet_diagnostic.IDE0036.severity = warning
+
+# IDE0043: Format string contains invalid placeholder
+dotnet_diagnostic.IDE0043.severity = warning
+
+# IDE0044: Make field readonly
+dotnet_diagnostic.IDE0044.severity = warning
+
+# RS0016: Only enable if API files are present
+dotnet_public_api_analyzer.require_api_files = true
+dotnet_style_readonly_field= true:silent
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+tab_width = 4
+end_of_line = lf
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+dotnet_style_prefer_auto_properties = true:silent
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_compound_assignment = true:suggestion
+dotnet_style_prefer_simplified_interpolation = true:suggestion
+dotnet_style_namespace_match_folder = true:suggestion
+dotnet_diagnostic.CA1838.severity = suggestion
+dotnet_diagnostic.CA1848.severity = suggestion
+dotnet_diagnostic.CA1873.severity = suggestion
+dotnet_diagnostic.CA5350.severity = error
+dotnet_diagnostic.CA5351.severity = error
+dotnet_diagnostic.CA5359.severity = warning
+dotnet_diagnostic.CA5360.severity = warning
+dotnet_diagnostic.CA5364.severity = error
+dotnet_diagnostic.CA5365.severity = suggestion
+dotnet_diagnostic.CA5384.severity = warning
+dotnet_diagnostic.CA5385.severity = warning
+dotnet_diagnostic.CA5397.severity = error
+dotnet_diagnostic.CA2201.severity = warning
+dotnet_diagnostic.CA2251.severity = suggestion
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+dotnet_style_allow_multiple_blank_lines_experimental = true:silent
+dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
+dotnet_code_quality_unused_parameters = all:suggestion
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
+
+# CSharp code style settings:
+[*.cs]
+max_line_length = 120
+
+# Newline settings
+csharp_new_line_before_open_brace =false
+csharp_new_line_before_else =false
+csharp_new_line_before_catch =false
+csharp_new_line_before_finally =false
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces =false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_switch_labels = true
+csharp_indent_labels = flush_left
+
+# Prefer "var" everywhere
+csharp_style_var_for_built_in_types = false:suggestion
+csharp_style_var_when_type_is_apparent = false:suggestion
+csharp_style_var_elsewhere = false:suggestion
+
+# Prefer method-like constructs to have a block body
+csharp_style_expression_bodied_methods = true:none
+csharp_style_expression_bodied_constructors = true:none
+csharp_style_expression_bodied_operators = true:none
+
+# Prefer property-like constructs to have an expression-body
+csharp_style_expression_bodied_properties = true:none
+csharp_style_expression_bodied_indexers = true:none
+csharp_style_expression_bodied_accessors = true:none
+
+# Suggest more modern language features when available
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_conditional_delegate_call = true:suggestion
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = ignore
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = true
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = true
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = true
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = true
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Blocks are allowed
+csharp_prefer_braces = true:silent
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+csharp_style_expression_bodied_lambdas= true:silent
+csharp_using_directive_placement = outside_namespace:silent
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_style_namespace_declarations = file_scoped:warning
+csharp_style_expression_bodied_local_functions = true:silent
+csharp_style_prefer_null_check_over_type_check = true:suggestion
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_prefer_local_over_anonymous_function = true:suggestion
+csharp_style_prefer_index_operator = true:suggestion
+csharp_style_prefer_range_operator = true:suggestion
+csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
+csharp_style_prefer_tuple_swap = true:suggestion
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+dotnet_diagnostic.CA1805.severity = warning
+dotnet_diagnostic.CA1869.severity = warning
+dotnet_diagnostic.CA1873.severity = warning
+dotnet_diagnostic.CA2016.severity = warning
+dotnet_diagnostic.IDE0004.severity = suggestion
+dotnet_diagnostic.IDE0005.severity = suggestion
+dotnet_diagnostic.IDE0008.severity = suggestion
+dotnet_diagnostic.IDE0016.severity = suggestion
+dotnet_diagnostic.IDE0017.severity = suggestion
+dotnet_diagnostic.IDE0020.severity = suggestion
+dotnet_diagnostic.IDE0019.severity = suggestion
+dotnet_diagnostic.IDE0018.severity = suggestion
+dotnet_diagnostic.IDE0022.severity = suggestion
+dotnet_diagnostic.IDE0023.severity = suggestion
+dotnet_diagnostic.IDE0024.severity = suggestion
+dotnet_diagnostic.IDE0025.severity = suggestion
+dotnet_diagnostic.IDE0026.severity = suggestion
+dotnet_diagnostic.IDE0027.severity = suggestion
+dotnet_diagnostic.IDE0028.severity = suggestion
+dotnet_diagnostic.IDE0029.severity = suggestion
+dotnet_diagnostic.IDE0030.severity = suggestion
+dotnet_diagnostic.IDE0031.severity = suggestion
+dotnet_diagnostic.IDE0032.severity = suggestion
+dotnet_diagnostic.IDE0034.severity = suggestion
+dotnet_diagnostic.IDE0040.severity = suggestion
+dotnet_diagnostic.IDE0041.severity = warning
+dotnet_diagnostic.IDE0045.severity = suggestion
+dotnet_diagnostic.IDE0046.severity = suggestion
+dotnet_diagnostic.IDE0048.severity = suggestion
+dotnet_diagnostic.IDE0054.severity = suggestion
+dotnet_diagnostic.IDE0057.severity = suggestion
+dotnet_diagnostic.IDE0056.severity = suggestion
+dotnet_diagnostic.IDE0058.severity = warning
+dotnet_diagnostic.IDE0059.severity = suggestion
+dotnet_diagnostic.IDE0060.severity = warning
+dotnet_diagnostic.IDE0063.severity = suggestion
+dotnet_diagnostic.IDE0066.severity = suggestion
+dotnet_diagnostic.IDE0071.severity = suggestion
+dotnet_diagnostic.IDE0072.severity = suggestion
+dotnet_diagnostic.IDE0075.severity = suggestion
+dotnet_diagnostic.IDE0074.severity = suggestion
+dotnet_diagnostic.IDE0090.severity = warning
+dotnet_diagnostic.IDE0082.severity = suggestion
+dotnet_diagnostic.IDE0083.severity = suggestion
+dotnet_diagnostic.IDE0120.severity = suggestion
+dotnet_diagnostic.IDE0270.severity = warning
+dotnet_diagnostic.IDE0305.severity = warning
+dotnet_diagnostic.IDE0330.severity = warning
+dotnet_diagnostic.IDE2004.severity = suggestion
+dotnet_diagnostic.IDE2002.severity = suggestion
+dotnet_diagnostic.IDE2001.severity = suggestion
+dotnet_diagnostic.IDE1006.severity = warning
+dotnet_diagnostic.IDE0180.severity = suggestion
+dotnet_diagnostic.MSTEST0037.severity = warning
+dotnet_diagnostic.MSTEST0049.severity = warning
+dotnet_diagnostic.MSTEST0058.severity = warning
+dotnet_diagnostic.SYSLIB1045.severity = warning
+csharp_prefer_static_local_function = true:suggestion
+csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
+csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent
+csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
+csharp_style_prefer_switch_expression = true:suggestion
+csharp_style_prefer_pattern_matching = true:silent
+csharp_style_prefer_not_pattern = true:suggestion
+csharp_style_prefer_extended_property_pattern = true:suggestion
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_top_level_statements = false:silent
+csharp_style_prefer_utf8_string_literals = true:suggestion
+
+[src/CodeStyle/**.{cs,vb}]
+# warning RS0005: Do not use generic CodeAction.Create to create CodeAction
+dotnet_diagnostic.RS0005.severity = none
+
+[src/{Analyzers,CodeStyle,Features,Workspaces,EditorFeatures, VisualStudio}/**/*.{cs,vb}]
+
+# IDE0011: Add braces
+csharp_prefer_braces = when_multiline:warning
+# NOTE: We need the below severity entry for Add Braces due to https://github.com/dotnet/roslyn/issues/44201
+dotnet_diagnostic.IDE0011.severity = warning
+
+# IDE0040: Add accessibility modifiers
+dotnet_diagnostic.IDE0040.severity = warning
+
+# CONSIDER: Are IDE0051 and IDE0052 too noisy to be warnings for IDE editing scenarios? Should they be made build-only warnings?
+# IDE0051: Remove unused private member
+dotnet_diagnostic.IDE0051.severity = warning
+
+# IDE0052: Remove unread private member
+dotnet_diagnostic.IDE0052.severity = warning
+
+# IDE0059: Unnecessary assignment to a value
+dotnet_diagnostic.IDE0059.severity = warning
+
+# IDE0060: Remove unused parameter
+dotnet_diagnostic.IDE0060.severity = warning
+
+# CA1822: Make member static
+dotnet_diagnostic.CA1822.severity = warning
+
+# Prefer "var" everywhere
+dotnet_diagnostic.IDE0007.severity = warning
+csharp_style_var_for_built_in_types = true:warning
+csharp_style_var_when_type_is_apparent = true:warning
+csharp_style_var_elsewhere = true:warning
+
+[src/{VisualStudio}/**/*.{cs,vb}]
+# CA1822: Make member static
+# Not enforced as a build 'warning' for 'VisualStudio' layer due to large number of false positives from https://github.com/dotnet/roslyn-analyzers/issues/3857 and https://github.com/dotnet/roslyn-analyzers/issues/3858
+# Additionally, there is a risk of accidentally breaking an internal API that partners rely on though IVT.
+dotnet_diagnostic.CA1822.severity = suggestion
diff --git a/.github/workflows/DocFX_gh-pages.yml b/.github/workflows/DocFX_gh-pages.yml
index 0c3744a..c61978b 100644
--- a/.github/workflows/DocFX_gh-pages.yml
+++ b/.github/workflows/DocFX_gh-pages.yml
@@ -3,116 +3,57 @@ on:
push:
branches:
- main
+ paths:
+ - 'docs/**'
+ - 'src/**/*.csproj'
+ - '.github/workflows/DocFX_gh-pages.yml'
+
+permissions:
+ contents: write
+
jobs:
- document:
- runs-on: windows-latest
- env:
- DOTNET_NOLOGO: true
- DOCFX_SOURCE_BRANCH_NAME: ${{ github.ref }}
- strategy:
- matrix:
- dotnet-version: [ '7.0.x' ]
- steps:
- - name: Check out repository code
- uses: actions/checkout@v2
- - name: Check out Werkr.Common
- uses: actions/checkout@v2
- with:
- repository: DarkgreyDevelopment/Werkr.Common
- path: src/Werkr.Common
- token: ${{ secrets.CI_TOKEN }}
- - name: Check out Werkr.Common.Configuration
- uses: actions/checkout@v2
- with:
- repository: DarkgreyDevelopment/Werkr.Common.Configuration
- path: src/Werkr.Common.Configuration
- token: ${{ secrets.CI_TOKEN }}
- - name: Check out Werkr.Installers
- uses: actions/checkout@v2
- with:
- repository: DarkgreyDevelopment/Werkr.Installers
- path: src/Werkr.Installers
- token: ${{ secrets.CI_TOKEN }}
- - name: Check out Werkr.Server
- uses: actions/checkout@v2
- with:
- repository: DarkgreyDevelopment/Werkr.Server
- path: src/Werkr.Server
- token: ${{ secrets.CI_TOKEN }}
- - name: Check out Werkr.Agent
- uses: actions/checkout@v2
- with:
- repository: DarkgreyDevelopment/Werkr.Agent
- path: src/Werkr.Agent
- token: ${{ secrets.CI_TOKEN }}
- - name: Get DocFX
- shell: pwsh
- run: |
- $IWRParams = @{
- Uri = "https://github.com/dotnet/docfx/releases/download/v2.59.4/docfx.zip"
- OutFile = '${{ github.workspace }}/docfx.zip'
- Method = 'Get'
- }
- Invoke-WebRequest @IWRParams
- Expand-Archive -Path '${{ github.workspace }}/docfx.zip' -DestinationPath '${{ github.workspace }}/docfx'
- - name: Custom File processing.
- shell: pwsh
- run: |
- $DocsPath = '${{ github.workspace }}/docs'
- $CopyParams = @{
- Verbose = $true
- Force = $true
- }
- copy-item -Path '${{ github.workspace }}/LICENSE' -Destination "$DocsPath/LICENSE.md" @CopyParams
- copy-item -Path '${{ github.workspace }}/README.md' -Destination "$DocsPath/index.md" @CopyParams
- copy-Item -Path '${{ github.workspace }}/docs/docfx/*' -Destination $DocsPath -Exclude README.md -Verbose -Recurse
- - name: Generate Documentation and build site.
- shell: pwsh
- run: |
- Write-Host "`nGenerating API documentation:"
- & '${{ github.workspace }}/docfx/docfx.exe' metadata '${{ github.workspace }}/docs/docfx.json'
- Write-Host "`nCreating docfx site:"
- & '${{ github.workspace }}/docfx/docfx.exe' '${{ github.workspace }}/docs/docfx.json'
- - name: Compress Site for upload as Artifact.
- shell: pwsh
- run: |
- $CopyToSiteParams = @{
- Destination = '${{ github.workspace }}/docs/_site'
- Verbose = $true
- }
- copy-item -Path '${{ github.workspace }}/docs/CNAME' @CopyToSiteParams
- copy-item -Path '${{ github.workspace }}/docs/_config.yml' @CopyToSiteParams
- Write-Host "`nCompressing Site for Artifact Upload"
- Compress-Archive -Path '${{ github.workspace }}/docs/_site' -DestinationPath '${{ github.workspace }}/docs/_site.zip'
- - name: Upload Artifacts
- uses: actions/upload-artifact@v1
- with:
- name: site
- path: ${{ github.workspace }}/docs/_site.zip
- publish:
- needs: document
+ build-and-publish:
runs-on: ubuntu-latest
env:
DOTNET_NOLOGO: true
- DOCFX_SOURCE_BRANCH_NAME: ${{ github.ref }}
- strategy:
- matrix:
- dotnet-version: [ '7.0.x' ]
+ DOTNET_CLI_TELEMETRY_OPTOUT: true
steps:
- - name: Checkout
- uses: actions/checkout@v3
- - name: Download Artifacts
- uses: actions/download-artifact@v1
- with:
- name: site
- path: ${{ github.workspace }}/download
- - name: Verify WorkSpace Contents
- shell: pwsh
+ - name: Check out repository
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ global-json-file: global.json
+
+ - name: Install DocFX
+ run: dotnet tool install -g docfx --version 2.78.3
+
+ - name: Prepare documentation sources
+ run: |
+ cp LICENSE docs/LICENSE.md
+ cp README.md docs/index.md
+ cp -r docs/docfx/* docs/ 2>/dev/null || true
+
+ - name: Generate API metadata
+ run: docfx metadata docs/docfx.json
+
+ - name: Build documentation site
+ run: docfx docs/docfx.json
+
+ - name: Copy CNAME and config
run: |
- Write-Host "`Extracting Site."
- Expand-Archive -Path '${{ github.workspace }}/download/_site.zip' -DestinationPath '${{ github.workspace }}'
- - name: Publish Site Content
+ cp docs/CNAME docs/_site/ 2>/dev/null || true
+ cp docs/_config.yml docs/_site/ 2>/dev/null || true
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: docfx-site
+ path: docs/_site
+
+ - name: Deploy to GitHub Pages
uses: JamesIves/github-pages-deploy-action@v4
with:
- BRANCH: gh-pages
- FOLDER: ${{ github.workspace }}/_site
+ branch: gh-pages
+ folder: docs/_site
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c9e6789..b56ebb8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,79 +1,98 @@
-name: CI
-
-on:
- pull_request:
- branches: [main, develop]
- push:
- branches: [main, develop]
-
-concurrency:
- group: ci-${{ github.ref }}
- cancel-in-progress: true
-
-jobs:
- build-and-test:
- name: Build & Test
- runs-on: ubuntu-latest
- env:
- DOTNET_NOLOGO: true
- DOTNET_CLI_TELEMETRY_OPTOUT: true
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Setup .NET 10
- uses: actions/setup-dotnet@v4
- with:
- dotnet-version: '10.0.x'
-
- - name: Restore tools
- run: dotnet tool restore
-
- - name: Determine version
- id: version
- run: |
- VERSION_JSON=$(dotnet gitversion /output json)
- echo "semVer=$(echo $VERSION_JSON | jq -r '.SemVer')" >> $GITHUB_OUTPUT
- echo "assemblySemVer=$(echo $VERSION_JSON | jq -r '.AssemblySemVer')" >> $GITHUB_OUTPUT
- echo "assemblySemFileVer=$(echo $VERSION_JSON | jq -r '.AssemblySemFileVer')" >> $GITHUB_OUTPUT
- echo "informationalVersion=$(echo $VERSION_JSON | jq -r '.InformationalVersion')" >> $GITHUB_OUTPUT
-
- - name: Restore dependencies
- run: |
- # Backup lock files before restore
- find . -name "packages.lock.json" -exec cp {} {}.backup \;
-
- # Restore with force-evaluate for linux-x64
- dotnet restore Werkr.slnx --force-evaluate
-
- # Validate lock file changes (skip Windows-only Installer projects)
- for lockfile in $(find . -name "packages.lock.json" ! -name "*.backup" ! -path "*/Installer/*"); do
- backup="${lockfile}.backup"
- if [ -f "$backup" ]; then
- pwsh scripts/Test-LockFileChanges.ps1 -BackupPath "$backup" -CurrentPath "$lockfile" -ToPlatform "linux-x64"
- rm -f "$backup"
- fi
- done
-
- - name: Build
- run: >
- dotnet build Werkr.slnx -c Release --no-restore
- /p:Version=${{ steps.version.outputs.semVer }}
- /p:AssemblyVersion=${{ steps.version.outputs.assemblySemVer }}
- /p:FileVersion=${{ steps.version.outputs.assemblySemFileVer }}
- /p:InformationalVersion="${{ steps.version.outputs.informationalVersion }}"
-
- - name: Test
- run: >
- dotnet test --solution Werkr.slnx -c Release --no-build
- --logger "trx;LogFileName=results.trx"
- --results-directory TestResults
-
- - name: Upload test results
- if: always()
- uses: actions/upload-artifact@v4
- with:
- name: test-results
- path: TestResults/**/*.trx
+name: CI
+
+on:
+ pull_request:
+ branches: [main, develop]
+ push:
+ branches: [main, develop]
+
+concurrency:
+ group: ci-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build-and-test:
+ name: Build & Test
+ runs-on: ubuntu-latest
+ env:
+ DOTNET_NOLOGO: true
+ DOTNET_CLI_TELEMETRY_OPTOUT: true
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup .NET 10
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.x'
+
+ - name: Restore tools
+ run: dotnet tool restore
+
+ - name: Determine version
+ id: version
+ run: |
+ VERSION_JSON=$(dotnet gitversion /output json)
+ echo "semVer=$(echo $VERSION_JSON | jq -r '.SemVer')" >> $GITHUB_OUTPUT
+ echo "assemblySemVer=$(echo $VERSION_JSON | jq -r '.AssemblySemVer')" >> $GITHUB_OUTPUT
+ echo "assemblySemFileVer=$(echo $VERSION_JSON | jq -r '.AssemblySemFileVer')" >> $GITHUB_OUTPUT
+ echo "informationalVersion=$(echo $VERSION_JSON | jq -r '.InformationalVersion')" >> $GITHUB_OUTPUT
+
+ - name: Setup Node.js 22
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: npm
+ cache-dependency-path: src/Werkr.Server/graph-ui/package-lock.json
+
+ - name: Install graph-ui dependencies
+ run: npm ci --prefix src/Werkr.Server/graph-ui
+
+ - name: Run JS tests
+ run: npm test --prefix src/Werkr.Server/graph-ui
+
+ - name: Build JS bundles (production)
+ run: npm run build:prod --prefix src/Werkr.Server/graph-ui
+
+ - name: Check bundle sizes
+ run: node src/Werkr.Server/graph-ui/scripts/check-bundle-size.mjs
+
+ - name: Restore dependencies
+ run: |
+ # Backup lock files before restore
+ find . -name "packages.lock.json" -exec cp {} {}.backup \;
+
+ # Restore with force-evaluate for linux-x64
+ dotnet restore Werkr.slnx --force-evaluate
+
+ # Validate lock file changes (skip Windows-only Installer projects)
+ for lockfile in $(find . -name "packages.lock.json" ! -name "*.backup" ! -path "*/Installer/*"); do
+ backup="${lockfile}.backup"
+ if [ -f "$backup" ]; then
+ pwsh scripts/Test-LockFileChanges.ps1 -BackupPath "$backup" -CurrentPath "$lockfile" -ToPlatform "linux-x64"
+ rm -f "$backup"
+ fi
+ done
+
+ - name: Build
+ run: >
+ dotnet build Werkr.slnx -c Release --no-restore
+ /p:Version=${{ steps.version.outputs.semVer }}
+ /p:AssemblyVersion=${{ steps.version.outputs.assemblySemVer }}
+ /p:FileVersion=${{ steps.version.outputs.assemblySemFileVer }}
+ /p:InformationalVersion="${{ steps.version.outputs.informationalVersion }}"
+
+ - name: Test
+ run: >
+ dotnet test --solution Werkr.slnx -c Release --no-build
+ --logger "trx;LogFileName=results.trx"
+ --results-directory TestResults
+
+ - name: Upload test results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: test-results
+ path: TestResults/**/*.trx
diff --git a/.gitignore b/.gitignore
index a986f85..eb4a183 100644
--- a/.gitignore
+++ b/.gitignore
@@ -66,6 +66,7 @@ docs/projects/*
# Visual Studio Code Configuration
.vscode/*
+!.vscode/tasks.json
# Visual Studio Configuration
.vs/*
@@ -78,6 +79,10 @@ docs/projects/*
# SQLite Database Files
*.db
+
+# Node / TypeScript build artifacts
+node_modules/
+**/wwwroot/js/dist/
*.db-shm
*.db-wal
@@ -94,6 +99,9 @@ docker-compose.override.yml
# Aspire publish output
aspire-output/
+# User-specific project files
+*.user
+
# Secrets directory
secrets/
@@ -132,4 +140,4 @@ logs/
*.dll
*.exe
-*.pdb
\ No newline at end of file
+*.pdb
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..4b7af21
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,447 @@
+{
+ "version": "2.0.0",
+ "inputs": [
+ {
+ "id": "migrationName",
+ "type": "promptString",
+ "description": "Name for the EF Core migration",
+ "default": "migration"
+ }
+ ],
+ "tasks": [
+ {
+ "label": "verify:restore",
+ "type": "process",
+ "command": "pwsh",
+ "args": [
+ "--noprofile",
+ "-c",
+ "dotnet",
+ "restore",
+ "Werkr.slnx"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile",
+ "presentation": {
+ "reveal": "silent",
+ "revealProblems": "onProblem",
+ "showReuseMessage": false
+ }
+ },
+ {
+ "label": "verify:build",
+ "type": "process",
+ "command": "pwsh",
+ "args": [
+ "--noprofile",
+ "-c",
+ "dotnet",
+ "build",
+ "Werkr.slnx"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile",
+ "presentation": {
+ "reveal": "silent",
+ "revealProblems": "onProblem",
+ "showReuseMessage": false
+ }
+ },
+ {
+ "label": "verify:format",
+ "type": "process",
+ "command": "pwsh",
+ "args": [
+ "--noprofile",
+ "-c",
+ "dotnet",
+ "format",
+ "Werkr.slnx"
+ ],
+ "problemMatcher": [],
+ "presentation": {
+ "reveal": "silent",
+ "revealProblems": "onProblem",
+ "showReuseMessage": false
+ }
+ },
+ {
+ "label": "verify:test-unit",
+ "type": "process",
+ "command": "pwsh",
+ "args": [
+ "--noprofile",
+ "-c",
+ "dotnet",
+ "test",
+ "--project",
+ "src/Test/Werkr.Tests.Data/Werkr.Tests.Data.csproj"
+ ],
+ "group": "test",
+ "problemMatcher": "$msCompile",
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "presentation": {
+ "reveal": "always",
+ "showReuseMessage": false
+ }
+ },
+ {
+ "label": "verify:test-integration",
+ "type": "process",
+ "command": "pwsh",
+ "args": [
+ "--noprofile",
+ "-c",
+ "dotnet",
+ "test",
+ "--project",
+ "src/Test/Werkr.Tests.Server/Werkr.Tests.Server.csproj"
+ ],
+ "group": "test",
+ "problemMatcher": "$msCompile",
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "presentation": {
+ "reveal": "always",
+ "showReuseMessage": false
+ }
+ },
+ {
+ "label": "verify:test-e2e",
+ "type": "process",
+ "command": "pwsh",
+ "args": [
+ "--noprofile",
+ "-c",
+ "dotnet",
+ "test",
+ "--project",
+ "src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj"
+ ],
+ "group": "test",
+ "problemMatcher": "$msCompile",
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "presentation": {
+ "reveal": "always",
+ "showReuseMessage": false
+ }
+ },
+ {
+ "label": "verify:start-apphost",
+ "type": "process",
+ "command": "pwsh",
+ "args": [
+ "--noprofile",
+ "-c",
+ "dotnet",
+ "run",
+ "--project",
+ "src/Werkr.AppHost/Werkr.AppHost.csproj"
+ ],
+ "group": "test",
+ "problemMatcher": "$msCompile",
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "presentation": {
+ "reveal": "always",
+ "showReuseMessage": false
+ }
+ },
+ {
+ "label": "verify:docker-check",
+ "type": "process",
+ "command": "pwsh",
+ "args": [
+ "--noprofile",
+ "-c",
+ "docker",
+ "compose",
+ "config"
+ ],
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "problemMatcher": [],
+ "presentation": {
+ "reveal": "always",
+ "showReuseMessage": false
+ }
+ },
+ {
+ "label": "verify:docker-build",
+ "type": "process",
+ "command": "pwsh",
+ "args": [
+ "--noprofile",
+ "-File",
+ "${workspaceFolder}/scripts/docker-build.ps1"
+ ],
+ "group": "build",
+ "problemMatcher": [],
+ "presentation": {
+ "reveal": "always",
+ "showReuseMessage": false
+ }
+ },
+ {
+ "label": "docker:start",
+ "type": "process",
+ "command": "pwsh",
+ "args": [
+ "--noprofile",
+ "-c",
+ "docker",
+ "compose",
+ "up",
+ "-d"
+ ],
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "problemMatcher": [],
+ "presentation": {
+ "reveal": "always",
+ "showReuseMessage": false
+ }
+ },
+ {
+ "label": "docker:stop",
+ "type": "process",
+ "command": "pwsh",
+ "args": [
+ "--noprofile",
+ "-c",
+ "docker",
+ "compose",
+ "down"
+ ],
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "problemMatcher": [],
+ "presentation": {
+ "reveal": "always",
+ "showReuseMessage": false
+ }
+ },
+ {
+ "label": "docker:restart",
+ "dependsOn": [
+ "docker:stop",
+ "docker:start"
+ ],
+ "dependsOrder": "sequence",
+ "problemMatcher": [],
+ "presentation": {
+ "reveal": "always",
+ "showReuseMessage": false
+ }
+ },
+ {
+ "label": "ef:app:postgres",
+ "type": "shell",
+ "command": "pwsh",
+ "args": [
+ "--noprofile",
+ "-c",
+ "dotnet",
+ "ef",
+ "migrations",
+ "add",
+ "${input:migrationName}",
+ "--project",
+ "src/Werkr.Data",
+ "--startup-project",
+ "src/Werkr.Api",
+ "--context",
+ "PostgresWerkrDbContext",
+ "--output-dir",
+ "Migrations/Postgres"
+ ],
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "isBackground": false,
+ "problemMatcher": [],
+ "group": "build",
+ "presentation": {
+ "reveal": "always",
+ "showReuseMessage": false
+ }
+ },
+ {
+ "label": "ef:app:sqlite",
+ "type": "shell",
+ "command": "pwsh",
+ "args": [
+ "--noprofile",
+ "-c",
+ "dotnet",
+ "ef",
+ "migrations",
+ "add",
+ "${input:migrationName}",
+ "--project",
+ "src/Werkr.Data",
+ "--startup-project",
+ "src/Werkr.Api",
+ "--context",
+ "SqliteWerkrDbContext",
+ "--output-dir",
+ "Migrations/Sqlite"
+ ],
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "isBackground": false,
+ "problemMatcher": [],
+ "group": "build",
+ "presentation": {
+ "reveal": "always",
+ "showReuseMessage": false
+ }
+ },
+ {
+ "label": "ef:app:both",
+ "dependsOn": [
+ "ef:app:postgres",
+ "ef:app:sqlite"
+ ],
+ "dependsOrder": "sequence",
+ "problemMatcher": [],
+ "group": "build",
+ "presentation": {
+ "reveal": "always",
+ "showReuseMessage": false
+ }
+ },
+ {
+ "label": "ef:identity:postgres",
+ "type": "shell",
+ "command": "pwsh",
+ "args": [
+ "--noprofile",
+ "-c",
+ "dotnet",
+ "ef",
+ "migrations",
+ "add",
+ "${input:migrationName}",
+ "--project",
+ "src/Werkr.Data.Identity",
+ "--startup-project",
+ "src/Werkr.Server",
+ "--context",
+ "PostgresWerkrIdentityDbContext",
+ "--output-dir",
+ "Migrations/Postgres"
+ ],
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "isBackground": false,
+ "problemMatcher": [],
+ "group": "build",
+ "presentation": {
+ "reveal": "always",
+ "showReuseMessage": false
+ }
+ },
+ {
+ "label": "ef:identity:sqlite",
+ "type": "shell",
+ "command": "pwsh",
+ "args": [
+ "--noprofile",
+ "-c",
+ "dotnet",
+ "ef",
+ "migrations",
+ "add",
+ "${input:migrationName}",
+ "--project",
+ "src/Werkr.Data.Identity",
+ "--startup-project",
+ "src/Werkr.Server",
+ "--context",
+ "SqliteWerkrIdentityDbContext",
+ "--output-dir",
+ "Migrations/Sqlite"
+ ],
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "isBackground": false,
+ "problemMatcher": [],
+ "group": "build",
+ "presentation": {
+ "reveal": "always",
+ "showReuseMessage": false
+ }
+ },
+ {
+ "label": "ef:identity:both",
+ "dependsOn": [
+ "ef:identity:postgres",
+ "ef:identity:sqlite"
+ ],
+ "dependsOrder": "sequence",
+ "problemMatcher": [],
+ "group": "build",
+ "presentation": {
+ "reveal": "always",
+ "showReuseMessage": false
+ }
+ },
+ {
+ "label": "ef:all",
+ "dependsOn": [
+ "ef:app:both",
+ "ef:identity:both"
+ ],
+ "dependsOrder": "sequence",
+ "problemMatcher": [],
+ "group": "build",
+ "presentation": {
+ "reveal": "always",
+ "showReuseMessage": false
+ }
+ },
+ {
+ "label": "verify:build-server",
+ "type": "shell",
+ "command": "pwsh --noprofile -c 'dotnet build src/Werkr.Server/Werkr.Server.csproj'"
+ },
+ {
+ "label": "verify:test-e2e-verbose",
+ "type": "shell",
+ "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj --verbosity normal 2>&1 | tail -50'"
+ },
+ {
+ "label": "verify:test-e2e-failures",
+ "type": "shell",
+ "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj -- --report-trx 2>&1 | grep -i -E \"failed|error|FAIL\" | head -20'"
+ },
+ {
+ "label": "verify:e2e-fail-detail",
+ "type": "shell",
+ "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj --no-build 2>&1 | grep -i -E \"failed|FAIL\" | head -20'"
+ },
+ {
+ "label": "verify:e2e-tail",
+ "type": "shell",
+ "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj'"
+ },
+ {
+ "label": "verify:test-server",
+ "type": "shell",
+ "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Server/Werkr.Tests.Server.csproj'"
+ }
+ ]
+}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index b6542c9..a26b719 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,6 +1,7 @@
true
+ true
@@ -14,35 +15,35 @@
-
-
-
-
+
+
+
+
-
+
-
+
-
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
+
+
@@ -53,7 +54,7 @@
-
+
@@ -67,8 +68,8 @@
-
-
-
+
+
+
-
\ No newline at end of file
+
diff --git a/scripts/Test-LockFileChanges.ps1 b/scripts/Test-LockFileChanges.ps1
index 82197e3..25c94a8 100644
--- a/scripts/Test-LockFileChanges.ps1
+++ b/scripts/Test-LockFileChanges.ps1
@@ -1,304 +1,304 @@
-#!/usr/bin/env pwsh
-<#
-.SYNOPSIS
- Verifies that only platform-specific packages changed in a NuGet lock file.
-
-.DESCRIPTION
- Compares a backup lock file against the current lock file and validates that
- only expected platform-specific Aspire packages differ between them.
-
- Expected platform-specific packages:
- - Aspire.Dashboard.Sdk.
- - Aspire.Hosting.Orchestration.
-
-.PARAMETER BackupPath
- Path to the backup lock file (before regeneration).
-
-.PARAMETER CurrentPath
- Path to the current lock file (after regeneration).
-
-.PARAMETER FromPlatform
- The source platform RID (e.g., linux-x64, linux-arm64). Defaults to linux-x64.
-
-.PARAMETER ToPlatform
- The target platform RID (e.g., linux-x64, linux-arm64). Defaults to linux-arm64.
-
-.EXAMPLE
- ./Test-LockFileChanges.ps1 -BackupPath packages.lock.json.backup -CurrentPath packages.lock.json
-
-.EXAMPLE
- ./Test-LockFileChanges.ps1 -BackupPath packages.lock.json.backup -CurrentPath packages.lock.json -FromPlatform linux-arm64 -ToPlatform linux-x64
-#>
-[CmdletBinding()]
-param(
- [Parameter(Mandatory)]
- [string]$BackupPath,
-
- [Parameter(Mandatory)]
- [string]$CurrentPath,
-
- [Parameter()]
- [ValidatePattern('^((linux|win)-(x64|arm64)|osx-arm64)$')]
- [string]$FromPlatform = 'linux-x64',
-
- [Parameter()]
- [ValidatePattern('^((linux|win)-(x64|arm64)|osx-arm64)$')]
- [string]$ToPlatform = 'linux-arm64'
-)
-
-$ErrorActionPreference = 'Stop'
-
-# Validate platform format
-Write-Host "Platform transition: $FromPlatform -> $ToPlatform"
-
-# Platform-specific package patterns that are allowed to differ
-# These packages have platform-specific variants that will change when switching RIDs
-# We allow any valid platform suffix since we support multiple target platforms
-# Microsoft.NET.ILLink.Tasks is implicitly added by .NET 10 SDK when PublishSingleFile+SelfContained are enabled
-$AllowedPackagePatterns = @(
- '^Aspire\.Dashboard\.Sdk\.(linux|win|osx)-(x64|arm64)$',
- '^Aspire\.Hosting\.Orchestration\.(linux|win|osx)-(x64|arm64)$',
- '^Microsoft\.NET\.ILLink\.Tasks$'
-)
-
-function Test-AllowedPackage {
- param([string]$PackageName)
- foreach ($pattern in $AllowedPackagePatterns) {
- if ($PackageName -match $pattern) {
- return $true
- }
- }
- return $false
-}
-
-function Test-ProjectDependencyChangesAllowed {
- param(
- [hashtable]$BackupEntry,
- [hashtable]$CurrentEntry
- )
-
- if (($null -eq $BackupEntry) -or ($null -eq $CurrentEntry)) {
- return $false
- }
-
- if (($BackupEntry.type -ne 'Project') -or ($CurrentEntry.type -ne 'Project')) {
- return $false
- }
-
- $backupDeps = @{}
- $currentDeps = @{}
-
- if ($BackupEntry.ContainsKey('dependencies') -and ($null -ne $BackupEntry.dependencies)) {
- $backupDeps = $BackupEntry.dependencies
- }
- if ($CurrentEntry.ContainsKey('dependencies') -and ($null -ne $CurrentEntry.dependencies)) {
- $currentDeps = $CurrentEntry.dependencies
- }
-
- # True means: the only differences between the two Project dependency maps are
- # allowed platform-specific packages, and there is at least one such difference.
- $allDependencyNames = @($backupDeps.Keys; $currentDeps.Keys) | Select-Object -Unique
- $foundAllowedDifference = $false
-
- foreach ($dependencyName in $allDependencyNames) {
- $inBackup = $backupDeps.ContainsKey($dependencyName)
- $inCurrent = $currentDeps.ContainsKey($dependencyName)
-
- if (($inBackup -and $inCurrent) -and ($backupDeps[$dependencyName] -eq $currentDeps[$dependencyName])) {
- continue
- }
-
- if (-not (Test-AllowedPackage $dependencyName)) {
- return $false
- }
-
- $foundAllowedDifference = $true
- }
-
- return $foundAllowedDifference
-}
-
-function Test-LockEntryDifferent {
- param(
- [hashtable]$BackupEntry,
- [hashtable]$CurrentEntry
- )
-
- if (($null -eq $BackupEntry) -or ($null -eq $CurrentEntry)) {
- return $true
- }
-
- if (($BackupEntry.type -eq 'Transitive') -and ($CurrentEntry.type -eq 'Transitive')) {
- return $BackupEntry.contentHash -ne $CurrentEntry.contentHash
- }
-
- if (($BackupEntry.type -eq 'Project') -and ($CurrentEntry.type -eq 'Project')) {
- $backupDeps = @{}
- $currentDeps = @{}
-
- if ($BackupEntry.ContainsKey('dependencies') -and ($null -ne $BackupEntry.dependencies)) {
- $backupDeps = $BackupEntry.dependencies
- }
- if ($CurrentEntry.ContainsKey('dependencies') -and ($null -ne $CurrentEntry.dependencies)) {
- $currentDeps = $CurrentEntry.dependencies
- }
-
- $allDependencyNames = @($backupDeps.Keys; $currentDeps.Keys) | Select-Object -Unique
- foreach ($dependencyName in $allDependencyNames) {
- $inBackup = $backupDeps.ContainsKey($dependencyName)
- $inCurrent = $currentDeps.ContainsKey($dependencyName)
-
- if (
- ($inBackup -and $inCurrent) -and
- ($backupDeps[$dependencyName] -eq $currentDeps[$dependencyName])
- ) {
- continue
- }
-
- return $true
- }
-
- return $false
- }
-
- $backupJson = $BackupEntry | ConvertTo-Json -Compress
- $currentJson = $CurrentEntry | ConvertTo-Json -Compress
- return $backupJson -ne $currentJson
-}
-
-# Read and parse both lock files
-$backup = Get-Content $BackupPath -Raw | ConvertFrom-Json -AsHashtable
-$current = Get-Content $CurrentPath -Raw | ConvertFrom-Json -AsHashtable
-
-$unexpectedChanges = @()
-$expectedChanges = @()
-
-function Test-PlatformFramework {
- param([string]$Framework)
- # Framework entries like "net10.0/linux-x64" or "net10.0/win-arm64" are platform-specific
- return $Framework -match '^net\d+\.\d+/(linux|win|osx)-(x64|x86|arm64|arm)$'
-}
-
-# Compare each target framework
-foreach ($framework in $current.dependencies.Keys) {
- $backupDeps = $backup.dependencies[$framework]
- $currentDeps = $current.dependencies[$framework]
-
- if ($null -eq $backupDeps) {
- if (-not (Test-PlatformFramework $framework)) {
- $unexpectedChanges += "New framework added: $framework"
- }
- continue
- }
-
- # Find all unique package names across both
- $allPackages = @($backupDeps.Keys) + @($currentDeps.Keys) | Select-Object -Unique
-
- foreach ($package in $allPackages) {
- $inBackup = $backupDeps.ContainsKey($package)
- $inCurrent = $currentDeps.ContainsKey($package)
-
- if ($inBackup -and $inCurrent) {
- # Package exists in both - check if it changed
- $backupEntry = $backupDeps[$package]
- $currentEntry = $currentDeps[$package]
-
- if (Test-LockEntryDifferent -BackupEntry $backupEntry -CurrentEntry $currentEntry) {
- if (Test-AllowedPackage $package) {
- $expectedChanges += [PSCustomObject]@{
- Package = $package
- Type = 'Modified'
- Framework = $framework
- }
- } elseif (Test-ProjectDependencyChangesAllowed -BackupEntry $backupEntry -CurrentEntry $currentEntry) {
- $expectedChanges += [PSCustomObject]@{
- Package = $package
- Type = 'Modified'
- Framework = $framework
- }
- } else {
- $unexpectedChanges += "Package modified: $package in $framework"
- }
- }
- } elseif ($inBackup -and -not $inCurrent) {
- # Package removed
- if (Test-AllowedPackage $package) {
- $expectedChanges += [PSCustomObject]@{
- Package = $package
- Type = 'Removed'
- Framework = $framework
- }
- } else {
- $unexpectedChanges += "Package removed: $package from $framework"
- }
- } elseif (-not $inBackup -and $inCurrent) {
- # Package added
- if (Test-AllowedPackage $package) {
- $expectedChanges += [PSCustomObject]@{
- Package = $package
- Type = 'Added'
- Framework = $framework
- }
- } else {
- $unexpectedChanges += "Package added: $package to $framework"
- }
- }
- }
-}
-
-# Check for removed frameworks
-foreach ($framework in $backup.dependencies.Keys) {
- if (-not $current.dependencies.ContainsKey($framework)) {
- if (-not (Test-PlatformFramework $framework)) {
- $unexpectedChanges += "Framework removed: $framework"
- }
- }
-}
-
-# Report results
-if ($expectedChanges.Count -gt 0) {
- Write-Host 'Platform-specific package changes:'
- foreach ($change in $expectedChanges) {
- $versionInfo = [string]::Empty
- if ($change.Type -eq 'Modified') {
- $backupEntry = $backup.dependencies[$change.Framework][$change.Package]
- $currentEntry = $current.dependencies[$change.Framework][$change.Package]
- if ($null -ne $backupEntry -and $null -ne $currentEntry -and $backupEntry.ContainsKey('resolved') -and $currentEntry.ContainsKey('resolved')) {
- $backupVersion = $backupEntry.resolved
- $currentVersion = $currentEntry.resolved
- $versionInfo = " (version: $backupVersion -> $currentVersion)"
- }
- } elseif ($change.Type -eq 'Removed') {
- $backupEntry = $backup.dependencies[$change.Framework][$change.Package]
- if ($null -ne $backupEntry -and $backupEntry.ContainsKey('resolved')) {
- $backupVersion = $backupEntry.resolved
- $versionInfo = " (version: $backupVersion)"
- }
- } elseif ($change.Type -eq 'Added') {
- $currentEntry = $current.dependencies[$change.Framework][$change.Package]
- if ($null -ne $currentEntry -and $currentEntry.ContainsKey('resolved')) {
- $currentVersion = $currentEntry.resolved
- $versionInfo = " (version: $currentVersion)"
- }
- }
- Write-Host " [$($change.Type)] $($change.Package)$versionInfo"
- }
- Write-Host [string]::Empty
-}
-
-if ($unexpectedChanges.Count -gt 0) {
- Write-Error (
- 'ERROR: Non-platform-specific packages changed in lock file! ' +
- 'Only platform-specific Aspire packages (and the project entries that reference them) should differ between platforms.' +
- "`nUnexpected changes detected:" + ($unexpectedChanges | ForEach-Object { "`n - $_" })
- )
- exit 1
-}
-
-if ($expectedChanges.Count -eq 0) {
- Write-Host 'No changes detected in lock file'
-} else {
- Write-Host 'Lock file updated successfully - only platform-specific packages changed'
-}
-
-exit 0
+#!/usr/bin/env pwsh
+<#
+.SYNOPSIS
+ Verifies that only platform-specific packages changed in a NuGet lock file.
+
+.DESCRIPTION
+ Compares a backup lock file against the current lock file and validates that
+ only expected platform-specific Aspire packages differ between them.
+
+ Expected platform-specific packages:
+ - Aspire.Dashboard.Sdk.
+ - Aspire.Hosting.Orchestration.
+
+.PARAMETER BackupPath
+ Path to the backup lock file (before regeneration).
+
+.PARAMETER CurrentPath
+ Path to the current lock file (after regeneration).
+
+.PARAMETER FromPlatform
+ The source platform RID (e.g., linux-x64, linux-arm64). Defaults to linux-x64.
+
+.PARAMETER ToPlatform
+ The target platform RID (e.g., linux-x64, linux-arm64). Defaults to linux-arm64.
+
+.EXAMPLE
+ ./Test-LockFileChanges.ps1 -BackupPath packages.lock.json.backup -CurrentPath packages.lock.json
+
+.EXAMPLE
+ ./Test-LockFileChanges.ps1 -BackupPath packages.lock.json.backup -CurrentPath packages.lock.json -FromPlatform linux-arm64 -ToPlatform linux-x64
+#>
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory)]
+ [string]$BackupPath,
+
+ [Parameter(Mandatory)]
+ [string]$CurrentPath,
+
+ [Parameter()]
+ [ValidatePattern('^((linux|win)-(x64|arm64)|osx-arm64)$')]
+ [string]$FromPlatform = 'linux-x64',
+
+ [Parameter()]
+ [ValidatePattern('^((linux|win)-(x64|arm64)|osx-arm64)$')]
+ [string]$ToPlatform = 'linux-arm64'
+)
+
+$ErrorActionPreference = 'Stop'
+
+# Validate platform format
+Write-Host "Platform transition: $FromPlatform -> $ToPlatform"
+
+# Platform-specific package patterns that are allowed to differ
+# These packages have platform-specific variants that will change when switching RIDs
+# We allow any valid platform suffix since we support multiple target platforms
+# Microsoft.NET.ILLink.Tasks is implicitly added by .NET 10 SDK when PublishSingleFile+SelfContained are enabled
+$AllowedPackagePatterns = @(
+ '^Aspire\.Dashboard\.Sdk\.(linux|win|osx)-(x64|arm64)$',
+ '^Aspire\.Hosting\.Orchestration\.(linux|win|osx)-(x64|arm64)$',
+ '^Microsoft\.NET\.ILLink\.Tasks$'
+)
+
+function Test-AllowedPackage {
+ param([string]$PackageName)
+ foreach ($pattern in $AllowedPackagePatterns) {
+ if ($PackageName -match $pattern) {
+ return $true
+ }
+ }
+ return $false
+}
+
+function Test-ProjectDependencyChangesAllowed {
+ param(
+ [hashtable]$BackupEntry,
+ [hashtable]$CurrentEntry
+ )
+
+ if (($null -eq $BackupEntry) -or ($null -eq $CurrentEntry)) {
+ return $false
+ }
+
+ if (($BackupEntry.type -ne 'Project') -or ($CurrentEntry.type -ne 'Project')) {
+ return $false
+ }
+
+ $backupDeps = @{}
+ $currentDeps = @{}
+
+ if ($BackupEntry.ContainsKey('dependencies') -and ($null -ne $BackupEntry.dependencies)) {
+ $backupDeps = $BackupEntry.dependencies
+ }
+ if ($CurrentEntry.ContainsKey('dependencies') -and ($null -ne $CurrentEntry.dependencies)) {
+ $currentDeps = $CurrentEntry.dependencies
+ }
+
+ # True means: the only differences between the two Project dependency maps are
+ # allowed platform-specific packages, and there is at least one such difference.
+ $allDependencyNames = @($backupDeps.Keys; $currentDeps.Keys) | Select-Object -Unique
+ $foundAllowedDifference = $false
+
+ foreach ($dependencyName in $allDependencyNames) {
+ $inBackup = $backupDeps.ContainsKey($dependencyName)
+ $inCurrent = $currentDeps.ContainsKey($dependencyName)
+
+ if (($inBackup -and $inCurrent) -and ($backupDeps[$dependencyName] -eq $currentDeps[$dependencyName])) {
+ continue
+ }
+
+ if (-not (Test-AllowedPackage $dependencyName)) {
+ return $false
+ }
+
+ $foundAllowedDifference = $true
+ }
+
+ return $foundAllowedDifference
+}
+
+function Test-LockEntryDifferent {
+ param(
+ [hashtable]$BackupEntry,
+ [hashtable]$CurrentEntry
+ )
+
+ if (($null -eq $BackupEntry) -or ($null -eq $CurrentEntry)) {
+ return $true
+ }
+
+ if (($BackupEntry.type -eq 'Transitive') -and ($CurrentEntry.type -eq 'Transitive')) {
+ return $BackupEntry.contentHash -ne $CurrentEntry.contentHash
+ }
+
+ if (($BackupEntry.type -eq 'Project') -and ($CurrentEntry.type -eq 'Project')) {
+ $backupDeps = @{}
+ $currentDeps = @{}
+
+ if ($BackupEntry.ContainsKey('dependencies') -and ($null -ne $BackupEntry.dependencies)) {
+ $backupDeps = $BackupEntry.dependencies
+ }
+ if ($CurrentEntry.ContainsKey('dependencies') -and ($null -ne $CurrentEntry.dependencies)) {
+ $currentDeps = $CurrentEntry.dependencies
+ }
+
+ $allDependencyNames = @($backupDeps.Keys; $currentDeps.Keys) | Select-Object -Unique
+ foreach ($dependencyName in $allDependencyNames) {
+ $inBackup = $backupDeps.ContainsKey($dependencyName)
+ $inCurrent = $currentDeps.ContainsKey($dependencyName)
+
+ if (
+ ($inBackup -and $inCurrent) -and
+ ($backupDeps[$dependencyName] -eq $currentDeps[$dependencyName])
+ ) {
+ continue
+ }
+
+ return $true
+ }
+
+ return $false
+ }
+
+ $backupJson = $BackupEntry | ConvertTo-Json -Compress
+ $currentJson = $CurrentEntry | ConvertTo-Json -Compress
+ return $backupJson -ne $currentJson
+}
+
+# Read and parse both lock files
+$backup = Get-Content $BackupPath -Raw | ConvertFrom-Json -AsHashtable
+$current = Get-Content $CurrentPath -Raw | ConvertFrom-Json -AsHashtable
+
+$unexpectedChanges = @()
+$expectedChanges = @()
+
+function Test-PlatformFramework {
+ param([string]$Framework)
+ # Framework entries like "net10.0/linux-x64" or "net10.0/win-arm64" are platform-specific
+ return $Framework -match '^net\d+\.\d+/(linux|win|osx)-(x64|x86|arm64|arm)$'
+}
+
+# Compare each target framework
+foreach ($framework in $current.dependencies.Keys) {
+ $backupDeps = $backup.dependencies[$framework]
+ $currentDeps = $current.dependencies[$framework]
+
+ if ($null -eq $backupDeps) {
+ if (-not (Test-PlatformFramework $framework)) {
+ $unexpectedChanges += "New framework added: $framework"
+ }
+ continue
+ }
+
+ # Find all unique package names across both
+ $allPackages = @($backupDeps.Keys) + @($currentDeps.Keys) | Select-Object -Unique
+
+ foreach ($package in $allPackages) {
+ $inBackup = $backupDeps.ContainsKey($package)
+ $inCurrent = $currentDeps.ContainsKey($package)
+
+ if ($inBackup -and $inCurrent) {
+ # Package exists in both - check if it changed
+ $backupEntry = $backupDeps[$package]
+ $currentEntry = $currentDeps[$package]
+
+ if (Test-LockEntryDifferent -BackupEntry $backupEntry -CurrentEntry $currentEntry) {
+ if (Test-AllowedPackage $package) {
+ $expectedChanges += [PSCustomObject]@{
+ Package = $package
+ Type = 'Modified'
+ Framework = $framework
+ }
+ } elseif (Test-ProjectDependencyChangesAllowed -BackupEntry $backupEntry -CurrentEntry $currentEntry) {
+ $expectedChanges += [PSCustomObject]@{
+ Package = $package
+ Type = 'Modified'
+ Framework = $framework
+ }
+ } else {
+ $unexpectedChanges += "Package modified: $package in $framework"
+ }
+ }
+ } elseif ($inBackup -and -not $inCurrent) {
+ # Package removed
+ if (Test-AllowedPackage $package) {
+ $expectedChanges += [PSCustomObject]@{
+ Package = $package
+ Type = 'Removed'
+ Framework = $framework
+ }
+ } else {
+ $unexpectedChanges += "Package removed: $package from $framework"
+ }
+ } elseif (-not $inBackup -and $inCurrent) {
+ # Package added
+ if (Test-AllowedPackage $package) {
+ $expectedChanges += [PSCustomObject]@{
+ Package = $package
+ Type = 'Added'
+ Framework = $framework
+ }
+ } else {
+ $unexpectedChanges += "Package added: $package to $framework"
+ }
+ }
+ }
+}
+
+# Check for removed frameworks
+foreach ($framework in $backup.dependencies.Keys) {
+ if (-not $current.dependencies.ContainsKey($framework)) {
+ if (-not (Test-PlatformFramework $framework)) {
+ $unexpectedChanges += "Framework removed: $framework"
+ }
+ }
+}
+
+# Report results
+if ($expectedChanges.Count -gt 0) {
+ Write-Host 'Platform-specific package changes:'
+ foreach ($change in $expectedChanges) {
+ $versionInfo = [string]::Empty
+ if ($change.Type -eq 'Modified') {
+ $backupEntry = $backup.dependencies[$change.Framework][$change.Package]
+ $currentEntry = $current.dependencies[$change.Framework][$change.Package]
+ if ($null -ne $backupEntry -and $null -ne $currentEntry -and $backupEntry.ContainsKey('resolved') -and $currentEntry.ContainsKey('resolved')) {
+ $backupVersion = $backupEntry.resolved
+ $currentVersion = $currentEntry.resolved
+ $versionInfo = " (version: $backupVersion -> $currentVersion)"
+ }
+ } elseif ($change.Type -eq 'Removed') {
+ $backupEntry = $backup.dependencies[$change.Framework][$change.Package]
+ if ($null -ne $backupEntry -and $backupEntry.ContainsKey('resolved')) {
+ $backupVersion = $backupEntry.resolved
+ $versionInfo = " (version: $backupVersion)"
+ }
+ } elseif ($change.Type -eq 'Added') {
+ $currentEntry = $current.dependencies[$change.Framework][$change.Package]
+ if ($null -ne $currentEntry -and $currentEntry.ContainsKey('resolved')) {
+ $currentVersion = $currentEntry.resolved
+ $versionInfo = " (version: $currentVersion)"
+ }
+ }
+ Write-Host " [$($change.Type)] $($change.Package)$versionInfo"
+ }
+ Write-Host [string]::Empty
+}
+
+if ($unexpectedChanges.Count -gt 0) {
+ Write-Error (
+ 'ERROR: Non-platform-specific packages changed in lock file! ' +
+ 'Only platform-specific Aspire packages (and the project entries that reference them) should differ between platforms.' +
+ "`nUnexpected changes detected:" + ($unexpectedChanges | ForEach-Object { "`n - $_" })
+ )
+ exit 1
+}
+
+if ($expectedChanges.Count -eq 0) {
+ Write-Host 'No changes detected in lock file'
+} else {
+ Write-Host 'Lock file updated successfully - only platform-specific packages changed'
+}
+
+exit 0
diff --git a/src/Installer/Msi/CustomActions/Werkr.Installer.Msi.CustomActions.csproj b/src/Installer/Msi/CustomActions/Werkr.Installer.Msi.CustomActions.csproj
index 14c2500..ae52c1e 100644
--- a/src/Installer/Msi/CustomActions/Werkr.Installer.Msi.CustomActions.csproj
+++ b/src/Installer/Msi/CustomActions/Werkr.Installer.Msi.CustomActions.csproj
@@ -30,7 +30,7 @@
-
+
diff --git a/src/Installer/Msi/CustomActions/packages.lock.json b/src/Installer/Msi/CustomActions/packages.lock.json
index 52e6310..9d0c1f5 100644
--- a/src/Installer/Msi/CustomActions/packages.lock.json
+++ b/src/Installer/Msi/CustomActions/packages.lock.json
@@ -1,107 +1,107 @@
-{
- "version": 1,
- "dependencies": {
- ".NETFramework,Version=v4.8.1": {
- "System.Text.Json": {
- "type": "Direct",
- "requested": "[10.0.4, )",
- "resolved": "10.0.4",
- "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==",
- "dependencies": {
- "Microsoft.Bcl.AsyncInterfaces": "10.0.4",
- "System.Buffers": "4.6.1",
- "System.IO.Pipelines": "10.0.4",
- "System.Memory": "4.6.3",
- "System.Runtime.CompilerServices.Unsafe": "6.1.2",
- "System.Text.Encodings.Web": "10.0.4",
- "System.Threading.Tasks.Extensions": "4.6.3",
- "System.ValueTuple": "4.6.1"
- }
- },
- "WixToolset.Dtf.CustomAction": {
- "type": "Direct",
- "requested": "[6.0.2, )",
- "resolved": "6.0.2",
- "contentHash": "VJRjIOzIkfolXw+kUWenyx2YQT/gagpr6Cs0XnvLPz7xl6pp/v+lQdwWh+wBu2P6apkg4yH1XI1WmXrTCjlD9g==",
- "dependencies": {
- "WixToolset.Dtf.WindowsInstaller": "6.0.2"
- }
- },
- "WixToolset.Dtf.WindowsInstaller": {
- "type": "Direct",
- "requested": "[6.0.2, )",
- "resolved": "6.0.2",
- "contentHash": "Tnc1EIjE5A7nvEcnUQLEgf1F1sXWzd3L4/n1/PXMnlt7dzaTEvR2OTiFgHD6Y+Tq9qZzAQZelcoS0Ps9bCLufw=="
- },
- "Microsoft.Bcl.AsyncInterfaces": {
- "type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "2JmoSZ1wDf1/TUyTtLTXeicXCnWxXkeStGnzRRmAw+5CBIGhg6q9ieJXu4FjeLzawSGd5PMhcropNa3lPJDaKA==",
- "dependencies": {
- "System.Threading.Tasks.Extensions": "4.6.3"
- }
- },
- "System.Buffers": {
- "type": "Transitive",
- "resolved": "4.6.1",
- "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw=="
- },
- "System.IO.Pipelines": {
- "type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==",
- "dependencies": {
- "System.Buffers": "4.6.1",
- "System.Memory": "4.6.3",
- "System.Threading.Tasks.Extensions": "4.6.3"
- }
- },
- "System.Memory": {
- "type": "Transitive",
- "resolved": "4.6.3",
- "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==",
- "dependencies": {
- "System.Buffers": "4.6.1",
- "System.Numerics.Vectors": "4.6.1",
- "System.Runtime.CompilerServices.Unsafe": "6.1.2"
- }
- },
- "System.Numerics.Vectors": {
- "type": "Transitive",
- "resolved": "4.6.1",
- "contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q=="
- },
- "System.Runtime.CompilerServices.Unsafe": {
- "type": "Transitive",
- "resolved": "6.1.2",
- "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw=="
- },
- "System.Text.Encodings.Web": {
- "type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==",
- "dependencies": {
- "System.Buffers": "4.6.1",
- "System.Memory": "4.6.3",
- "System.Runtime.CompilerServices.Unsafe": "6.1.2"
- }
- },
- "System.Threading.Tasks.Extensions": {
- "type": "Transitive",
- "resolved": "4.6.3",
- "contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==",
- "dependencies": {
- "System.Runtime.CompilerServices.Unsafe": "6.1.2"
- }
- },
- "System.ValueTuple": {
- "type": "Transitive",
- "resolved": "4.6.1",
- "contentHash": "+RJT4qaekpZ7DDLhf+LTjq+E48jieKiY9ulJ+BoxKmZblIJfIJT8Ufcaa/clQqnYvWs8jugfGSMu8ylS0caG0w=="
- },
- "werkr.common.configuration": {
- "type": "Project"
- }
- }
- }
+{
+ "version": 1,
+ "dependencies": {
+ ".NETFramework,Version=v4.8.1": {
+ "System.Text.Json": {
+ "type": "Direct",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==",
+ "dependencies": {
+ "Microsoft.Bcl.AsyncInterfaces": "10.0.5",
+ "System.Buffers": "4.6.1",
+ "System.IO.Pipelines": "10.0.5",
+ "System.Memory": "4.6.3",
+ "System.Runtime.CompilerServices.Unsafe": "6.1.2",
+ "System.Text.Encodings.Web": "10.0.5",
+ "System.Threading.Tasks.Extensions": "4.6.3",
+ "System.ValueTuple": "4.6.1"
+ }
+ },
+ "WixToolset.Dtf.CustomAction": {
+ "type": "Direct",
+ "requested": "[6.0.2, )",
+ "resolved": "6.0.2",
+ "contentHash": "VJRjIOzIkfolXw+kUWenyx2YQT/gagpr6Cs0XnvLPz7xl6pp/v+lQdwWh+wBu2P6apkg4yH1XI1WmXrTCjlD9g==",
+ "dependencies": {
+ "WixToolset.Dtf.WindowsInstaller": "6.0.2"
+ }
+ },
+ "WixToolset.Dtf.WindowsInstaller": {
+ "type": "Direct",
+ "requested": "[6.0.2, )",
+ "resolved": "6.0.2",
+ "contentHash": "Tnc1EIjE5A7nvEcnUQLEgf1F1sXWzd3L4/n1/PXMnlt7dzaTEvR2OTiFgHD6Y+Tq9qZzAQZelcoS0Ps9bCLufw=="
+ },
+ "Microsoft.Bcl.AsyncInterfaces": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "hQB3Hq1LlF0NkGVNyZIvwIQIY3LM7Cw1oYjNiTvdNqmzzipVAWEK1c5sj2H5aFX0udnjgPLxSYKq2fupueS8ow==",
+ "dependencies": {
+ "System.Threading.Tasks.Extensions": "4.6.3"
+ }
+ },
+ "System.Buffers": {
+ "type": "Transitive",
+ "resolved": "4.6.1",
+ "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw=="
+ },
+ "System.IO.Pipelines": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==",
+ "dependencies": {
+ "System.Buffers": "4.6.1",
+ "System.Memory": "4.6.3",
+ "System.Threading.Tasks.Extensions": "4.6.3"
+ }
+ },
+ "System.Memory": {
+ "type": "Transitive",
+ "resolved": "4.6.3",
+ "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==",
+ "dependencies": {
+ "System.Buffers": "4.6.1",
+ "System.Numerics.Vectors": "4.6.1",
+ "System.Runtime.CompilerServices.Unsafe": "6.1.2"
+ }
+ },
+ "System.Numerics.Vectors": {
+ "type": "Transitive",
+ "resolved": "4.6.1",
+ "contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q=="
+ },
+ "System.Runtime.CompilerServices.Unsafe": {
+ "type": "Transitive",
+ "resolved": "6.1.2",
+ "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw=="
+ },
+ "System.Text.Encodings.Web": {
+ "type": "Transitive",
+ "resolved": "10.0.5",
+ "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==",
+ "dependencies": {
+ "System.Buffers": "4.6.1",
+ "System.Memory": "4.6.3",
+ "System.Runtime.CompilerServices.Unsafe": "6.1.2"
+ }
+ },
+ "System.Threading.Tasks.Extensions": {
+ "type": "Transitive",
+ "resolved": "4.6.3",
+ "contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==",
+ "dependencies": {
+ "System.Runtime.CompilerServices.Unsafe": "6.1.2"
+ }
+ },
+ "System.ValueTuple": {
+ "type": "Transitive",
+ "resolved": "4.6.1",
+ "contentHash": "+RJT4qaekpZ7DDLhf+LTjq+E48jieKiY9ulJ+BoxKmZblIJfIJT8Ufcaa/clQqnYvWs8jugfGSMu8ylS0caG0w=="
+ },
+ "werkr.common.configuration": {
+ "type": "Project"
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/src/Test/Werkr.Tests.Agent/Security/UrlValidatorTests.cs b/src/Test/Werkr.Tests.Agent/Security/UrlValidatorTests.cs
index 9c7af21..d7b3816 100644
--- a/src/Test/Werkr.Tests.Agent/Security/UrlValidatorTests.cs
+++ b/src/Test/Werkr.Tests.Agent/Security/UrlValidatorTests.cs
@@ -97,7 +97,10 @@ public void RelativeUrl_Rejected( ) {
UnauthorizedAccessException ex = Assert.ThrowsExactly(
( ) => validator.ValidateUrl( "/api/data" ) );
- Assert.Contains( "absolute", ex.Message );
+ // On Unix, Uri.TryCreate parses "/api/data" as file:///api/data (hitting scheme check).
+ // On Windows, it fails to parse (hitting the absolute-URI check).
+ bool matchesAbsoluteOrScheme = ex.Message.Contains( "absolute" ) || ex.Message.Contains( "scheme" );
+ Assert.IsTrue( matchesAbsoluteOrScheme, $"Expected 'absolute' or 'scheme' in message: {ex.Message}" );
}
// ══════════════════════════════════════════════════════════════════════════════
diff --git a/src/Test/Werkr.Tests.Agent/packages.lock.json b/src/Test/Werkr.Tests.Agent/packages.lock.json
index 5be9d1d..ce52fe3 100644
--- a/src/Test/Werkr.Tests.Agent/packages.lock.json
+++ b/src/Test/Werkr.Tests.Agent/packages.lock.json
@@ -146,8 +146,8 @@
},
"Microsoft.AspNetCore.Metadata": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "levTTo69d5gYARtSzRV4wMvD1YUtkWvI/fOiTEG+k4v1WEEFBxrHLdUVEvxCfiuCb1Y1XuPAbbBsKQi9wNtU3w=="
+ "resolved": "10.0.5",
+ "contentHash": "nXVB1K4RzyhDHKYWLiq3+aJopJZKO5ojFqHV9PZ74fe4VWM/8itoouqsd2KIqSooIwQ13UDNlPQfN2rWr7hc2A=="
},
"Microsoft.Bcl.AsyncInterfaces": {
"type": "Transitive",
@@ -188,36 +188,36 @@
},
"Microsoft.EntityFrameworkCore.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "qDcJqCfN1XYyX0ID/Hd9/kQTRvlia8S+Yuwyl9uFhBIKnOCbl9WMdGQCzbZUKbkpkfvf3P9CDdXsnxHyE3O0Aw=="
+ "resolved": "10.0.5",
+ "contentHash": "32c58Rnm47Qvhimawf67KO9PytgPz3QoWye7Abapt0Yocw/JnzMiSNj/pRoIKyn8Jxypkv86zxKD4Q/zNTc0Ag=="
},
"Microsoft.EntityFrameworkCore.Analyzers": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "pQeMHCyD3yTtCEGnHV4VsgKUvrESo3MR5mnh8sgQ1hWYmI1YFsUutDowBIxkobeWRtaRmBqQAtF7XQFW6FWuNA=="
+ "resolved": "10.0.5",
+ "contentHash": "ipC4u1VojgEfoIZhtbS2Sx5IluJTP/Jf1hz3yGsxGBgSukYY/CquI6rAjxn5H58CZgVn36qcuPPtNMwZ0AUzMg=="
},
"Microsoft.EntityFrameworkCore.Relational": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "DOTjTHy93W3TwpMLM4SCm0n57Sc0Jj3+m2S6LSTstKyBB34eT1UouaMS19mpWwvtj42+sRiEjA3+rOTNoNzXFQ==",
+ "resolved": "10.0.5",
+ "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==",
"dependencies": {
- "Microsoft.EntityFrameworkCore": "10.0.4",
- "Microsoft.Extensions.Caching.Memory": "10.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.Logging": "10.0.4"
+ "Microsoft.EntityFrameworkCore": "10.0.5",
+ "Microsoft.Extensions.Caching.Memory": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5"
}
},
"Microsoft.EntityFrameworkCore.Sqlite.Core": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "8D3Kk7assWpi93DuicgucDqGoOsgEgLlZy8io0FUlSGG2b4wkRWkjXn4xFBX+BzxExjfcYvHhtcBpqkXhe2p0A==",
+ "resolved": "10.0.5",
+ "contentHash": "rVH43bcUyZiMn0SnCpVnvFpl4PFxT4GwmuVVLcT4JL0NtzuHY9ymKV+Llb5cjuJ+6+gEl4eixy2rE8nxOPcBSA==",
"dependencies": {
- "Microsoft.Data.Sqlite.Core": "10.0.4",
- "Microsoft.EntityFrameworkCore.Relational": "10.0.4",
- "Microsoft.Extensions.Caching.Memory": "10.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.DependencyModel": "10.0.4",
- "Microsoft.Extensions.Logging": "10.0.4",
+ "Microsoft.Data.Sqlite.Core": "10.0.5",
+ "Microsoft.EntityFrameworkCore.Relational": "10.0.5",
+ "Microsoft.Extensions.Caching.Memory": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.DependencyModel": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5",
"SQLitePCLRaw.core": "2.1.11"
}
},
@@ -233,22 +233,22 @@
},
"Microsoft.Extensions.Caching.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "uDRooaV6N3WZ0kdlNPMB68/MdGn/in1Fs7Db7DnIm85RBTPy4P321WO+daAImiYpH5dekjNggDqy1N44WaIlMA==",
+ "resolved": "10.0.5",
+ "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==",
"dependencies": {
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.Caching.Memory": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "CLLussNUMdSbyJOu4VBF7sqskHGB/5N1EcFzrqG/HsPATN8fCRUcfp0qns1VwkxKHwxrtYCh5FKe+kM81Q1PHA==",
+ "resolved": "10.0.5",
+ "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==",
"dependencies": {
- "Microsoft.Extensions.Caching.Abstractions": "10.0.4",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "10.0.4",
- "Microsoft.Extensions.Options": "10.0.4",
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.Caching.Abstractions": "10.0.5",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.Compliance.Abstractions": {
@@ -262,46 +262,46 @@
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "601B3ha6XvOsOcu9GVd2dVd1KEDuqr49r46GUWhNJkeZDhZ/NI9EYTyoeQjZQEi8ZUvnrv++FbTfGmC8F0vgtg==",
+ "resolved": "10.0.5",
+ "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==",
"dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "ilnL/kQn62Gx3OZCVT7SJrBNi0CRIhS8VEunmE6i/a9lp9l/eos+hpxMvCW4iX2aVc/NWeDhxuQusZL7zvmKIg==",
+ "resolved": "10.0.5",
+ "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==",
"dependencies": {
- "Microsoft.Extensions.Configuration": "10.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4"
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5"
}
},
"Microsoft.Extensions.Configuration.FileExtensions": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "LD4T4s2uW2kZUkwGc4A9KK5o3wfkgySHKEiYqV0NXeNdeLN563NgNqDpi3DNXAdrt2TwU0rK7QMPdWLLIaMipA==",
+ "resolved": "10.0.5",
+ "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==",
"dependencies": {
- "Microsoft.Extensions.Configuration": "10.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4",
- "Microsoft.Extensions.FileProviders.Physical": "10.0.4",
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5",
+ "Microsoft.Extensions.FileProviders.Physical": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "NkvJ8aSr3AG30yabjv7ZWwTG/wq5OElNTlNq39Ok2HSEF3TIwAc1f1xnTJlR/GuoJmEgkfT7WBO9YbSXRk41+g==",
+ "resolved": "10.0.5",
+ "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "SIe9zlVQJecnk/DTmevIcl6+aEDYhoVLc2eG2AKwVeNEC8CSyxHAbh4lf0xtHq9JUum0vVTEByGNTK+b6oihTQ=="
+ "resolved": "10.0.5",
+ "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA=="
},
"Microsoft.Extensions.DependencyInjection.AutoActivation": {
"type": "Transitive",
@@ -313,26 +313,26 @@
},
"Microsoft.Extensions.DependencyModel": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "LiJXylfk8pk+2zsUsITkou3QTFMJ8RNJ0oKKY0Oyjt6HJctGJwPw//ZgoNO4J29zKaT+dR4/PI2jW/znRcspLg=="
+ "resolved": "10.0.5",
+ "contentHash": "xA4kkL+QS6KCAOKz/O0oquHs44Ob8J7zpBCNt3wjkBWDg5aCqfwG8rWWLsg5V86AM0sB849g9JjPjIdksTCIKg=="
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "R9W7AttMedwOwJ7wRqTGBoVbX2JmlyWA+LJQUhizmS7Be9f6EJUn/+lvaIYDrOYtA1UzAfrwU871hpvZSPyIkg==",
+ "resolved": "10.0.5",
+ "contentHash": "vAJHd4yOpmKoK+jBuYV7a3y+Ab9U4ARCc29b6qvMy276RgJFw9LFs0DdsPqOL3ahwzyrX7tM+i4cCxU/RX0qAg==",
"dependencies": {
- "Microsoft.Extensions.Configuration": "10.0.4",
- "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.4",
- "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4"
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "JH2RyevIwJ1E9mBsZRXR+12TnUauptKgzCdOghhk3sE+dqcxB16GoE7x+0IuqTbaixM1ESXTNoqEw/IBnhM7LQ==",
+ "resolved": "10.0.5",
+ "contentHash": "/nYGrpa9/0BZofrVpBbbj+Ns8ZesiPE0V/KxsuHgDgHQopIzN54nRaQGSuvPw16/kI9sW1Zox5yyAPqvf0Jz6A==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4",
- "Microsoft.Extensions.Options": "10.0.4"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
}
},
"Microsoft.Extensions.Diagnostics.ExceptionSummarization": {
@@ -350,26 +350,26 @@
},
"Microsoft.Extensions.FileProviders.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "3hLXFZ1E/Kj3obIcb9iMCC95MpW2e8EkWxpXKgUfgGBfm+yn507pHAjPaHoi2U3GlSHIm/21DPCDLumwlMowjw==",
+ "resolved": "10.0.5",
+ "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==",
"dependencies": {
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.FileProviders.Physical": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "gVVHdOFwlnXmTtx41e2aGfcFXX+8+9DPkOzEqQuHN8rOv+6RQWs/wfeQLaosOt3CQLKNoCaFmHopTtGB9PB5fg==",
+ "resolved": "10.0.5",
+ "contentHash": "dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==",
"dependencies": {
- "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4",
- "Microsoft.Extensions.FileSystemGlobbing": "10.0.4",
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5",
+ "Microsoft.Extensions.FileSystemGlobbing": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.FileSystemGlobbing": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "eCEFVuuZL++SqMcdB5i4KA16GvcxCzdKKK+clapXYyGMkhd4BxwZi2/vGzo8s7a8Vi0BA78p5u/NScgOP1pzTg=="
+ "resolved": "10.0.5",
+ "contentHash": "mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw=="
},
"Microsoft.Extensions.Http": {
"type": "Transitive",
@@ -395,12 +395,12 @@
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "S8+6fCuMOhJZGk8sGFtOy3VsF9mk9x4UOL59GM91REiA/fmCDjunKKIw4RmStG87qyXPfxelDJf2pXIbTuaBdw==",
+ "resolved": "10.0.5",
+ "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection": "10.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "10.0.4",
- "Microsoft.Extensions.Options": "10.0.4"
+ "Microsoft.Extensions.DependencyInjection": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
}
},
"Microsoft.Extensions.Logging.Configuration": {
@@ -425,29 +425,29 @@
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "kRxa2Zjzhg/ohh7EklpqQpBIcyQnC3meWxCcpZBn+0QWy/fY1DmDd45JiW8Vyrpj2J1RDtau5yRHiLZS/AoxUw==",
+ "resolved": "10.0.5",
+ "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4",
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "amQUITwSnkbMPxh/ngneNykz4UtytEOXo0M/pbwdBiQU57EAVvBV5PFI8r/dRastUj0yxHVwrH64N9ACR5SdGQ==",
+ "resolved": "10.0.5",
+ "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==",
"dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.Configuration.Binder": "10.0.4",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4",
- "Microsoft.Extensions.Options": "10.0.4",
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Configuration.Binder": "10.0.5",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "lABYqiRH9HgYJsjzO3W7+cucUwWXhEkiyrRylANdIubnzcESlkIsLowXpQ4E+sc7kjMLbk1hk5oxw4qTKowTEg=="
+ "resolved": "10.0.5",
+ "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g=="
},
"Microsoft.Extensions.Resilience": {
"type": "Transitive",
@@ -799,8 +799,8 @@
},
"Npgsql": {
"type": "Transitive",
- "resolved": "10.0.0",
- "contentHash": "xZAYhPOU2rUIFpV48xsqhCx9vXs6Y+0jX2LCoSEfDFYMw9jtAOUk3iQsCnDLrFIv9NT3JGMihn7nnuZsPKqJmA==",
+ "resolved": "10.0.2",
+ "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "10.0.0"
}
@@ -1321,8 +1321,8 @@
"type": "Project",
"dependencies": {
"Google.Protobuf": "[3.34.0, )",
- "Microsoft.AspNetCore.Authorization": "[10.0.4, )",
- "Microsoft.Extensions.Configuration.Json": "[10.0.4, )",
+ "Microsoft.AspNetCore.Authorization": "[10.0.5, )",
+ "Microsoft.Extensions.Configuration.Json": "[10.0.5, )",
"Microsoft.IdentityModel.Tokens": "[8.16.0, )",
"Werkr.Common.Configuration": "[1.0.0, )"
}
@@ -1334,8 +1334,8 @@
"type": "Project",
"dependencies": {
"Grpc.Net.Client": "[2.76.0, )",
- "Microsoft.Extensions.Hosting.Abstractions": "[10.0.4, )",
- "System.Security.Cryptography.ProtectedData": "[10.0.4, )",
+ "Microsoft.Extensions.Hosting.Abstractions": "[10.0.5, )",
+ "System.Security.Cryptography.ProtectedData": "[10.0.5, )",
"Werkr.Common": "[1.0.0, )",
"Werkr.Data": "[1.0.0, )"
}
@@ -1344,9 +1344,9 @@
"type": "Project",
"dependencies": {
"EFCore.NamingConventions": "[10.0.1, )",
- "Microsoft.EntityFrameworkCore": "[10.0.4, )",
- "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.4, )",
- "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.0, )",
+ "Microsoft.EntityFrameworkCore": "[10.0.5, )",
+ "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.5, )",
+ "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )",
"Werkr.Common": "[1.0.0, )"
}
},
@@ -1407,6 +1407,12 @@
"Microsoft.Extensions.Http": "8.0.0"
}
},
+ "Grpc.Tools": {
+ "type": "CentralTransitive",
+ "requested": "[2.78.0, )",
+ "resolved": "2.78.0",
+ "contentHash": "6jPG2gHon+w2PczW8jjrCRnW/g9eEfCdd7aK6mDooptWtuPsV3ZxAwKKEx7LGEDVoT4c2SViRl8Yu3L1XiWIIg=="
+ },
"MailKit": {
"type": "CentralTransitive",
"requested": "[4.15.1, )",
@@ -1418,84 +1424,84 @@
},
"Microsoft.AspNetCore.Authorization": {
"type": "CentralTransitive",
- "requested": "[10.0.4, )",
- "resolved": "10.0.4",
- "contentHash": "uCg18hZTfzMEft8uxgPTM4s0sXsETfTnAJ00yR0LD6/ABz6NeEq1RMPIOpkTqbipatw+eNWKqDOV4Gus5OGjAQ==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "NbFi4wN6fUvZK4AKmixpfx0IvqtVimKEn8ZX28LkzZBVo09YnLbyRrJ1001IVQDLbV+aYpS/cLhVJu5JD0rY5A==",
"dependencies": {
- "Microsoft.AspNetCore.Metadata": "10.0.4",
- "Microsoft.Extensions.Diagnostics": "10.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "10.0.4",
- "Microsoft.Extensions.Options": "10.0.4"
+ "Microsoft.AspNetCore.Metadata": "10.0.5",
+ "Microsoft.Extensions.Diagnostics": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
}
},
"Microsoft.Data.Sqlite.Core": {
"type": "CentralTransitive",
- "requested": "[10.0.3, )",
- "resolved": "10.0.4",
- "contentHash": "UkmpN2pDkrtVLh+ypRDCbBij9mhPqOPzvHI625rf+VeT3FHnBwBjAY7XgjK8rGDI74fDx7C1SSIf2OAaAX4g2A==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "jFYXnh7s0RShCw6Vkf+ReGCw+mVi7ISg1YaEzYCJcXnUifmbW+aqvCsRJuSRj2ZuQ+oqetpjxlZtbpMmk5FKqQ==",
"dependencies": {
"SQLitePCLRaw.core": "2.1.11"
}
},
"Microsoft.EntityFrameworkCore": {
"type": "CentralTransitive",
- "requested": "[10.0.4, )",
- "resolved": "10.0.4",
- "contentHash": "kzTsfFK2GCytp6DDTfQOmxPU4gbGdrIlP7PxrxF3ESNLtfXrC8BoUVZENBN2WORlZPAD7CVX6AYIglgkpXQooA==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "9tNBmK3EpYVGRQLiqP+bqK2m+TD0Gv//4vCzR7ZOgl4FWzCFyOpYdIVka13M4kcBdPdSJcs3wbHr3rmzOqbIMA==",
"dependencies": {
- "Microsoft.EntityFrameworkCore.Abstractions": "10.0.4",
- "Microsoft.EntityFrameworkCore.Analyzers": "10.0.4",
- "Microsoft.Extensions.Caching.Memory": "10.0.4",
- "Microsoft.Extensions.Logging": "10.0.4"
+ "Microsoft.EntityFrameworkCore.Abstractions": "10.0.5",
+ "Microsoft.EntityFrameworkCore.Analyzers": "10.0.5",
+ "Microsoft.Extensions.Caching.Memory": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5"
}
},
"Microsoft.EntityFrameworkCore.Sqlite": {
"type": "CentralTransitive",
- "requested": "[10.0.4, )",
- "resolved": "10.0.4",
- "contentHash": "cbc/Ave31CbPQ9E29dfaA4QjcsBoc8KokNlLC6Noj0uToHDQ9PPllD+k6HluVbaFpflsU8XGwrQxOoyvXlU97g==",
- "dependencies": {
- "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.4",
- "Microsoft.Extensions.Caching.Memory": "10.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.DependencyModel": "10.0.4",
- "Microsoft.Extensions.Logging": "10.0.4",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "lxeRviglTkkmzYJVJ600yb6gJjnf5za9v7uH+0byuSXTGv7U8cT6hz7qRTmiGSOfLcl86QFdy2BBKaUFd6NQug==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.5",
+ "Microsoft.Extensions.Caching.Memory": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.DependencyModel": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5",
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.11",
"SQLitePCLRaw.core": "2.1.11"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "CentralTransitive",
- "requested": "[10.0.3, )",
- "resolved": "10.0.4",
- "contentHash": "3x9X9SMAMdAoEwWxHfsT2a9dTBqEtfYfbEOFw+UPtBshEH2gHWJeazxrZ1FK1O18MoCbe1NxINg5qciB01pEcg==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==",
"dependencies": {
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.Configuration.Json": {
"type": "CentralTransitive",
- "requested": "[10.0.4, )",
- "resolved": "10.0.4",
- "contentHash": "gn2Rf0dvIa6Sz/WJ5cNHhG/oUOT1yrHXd7Q0vCpXDlLsMuRqv9G5NBXFJbSh/ZRzSbvbOQWMV0amQS/3N0Fzzg==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==",
"dependencies": {
- "Microsoft.Extensions.Configuration": "10.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.Configuration.FileExtensions": "10.0.4",
- "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4"
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5"
}
},
"Microsoft.Extensions.Hosting.Abstractions": {
"type": "CentralTransitive",
- "requested": "[10.0.4, )",
- "resolved": "10.0.4",
- "contentHash": "+5mQrqlBhqNUaPyDmFSNM/qiWStvE9LMxZW1MRF0NhEbO781xNeKryXNR9gGDJ0PmYFDAVoMT9ffX+I15LPFTw==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "+Wb7KAMVZTomwJkQrjuPTe5KBzGod7N8XeG+ScxRlkPOB4sZLG4ccVwjV4Phk5BCJt7uIMnGHVoN6ZMVploX+g==",
"dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4",
- "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.4",
- "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "10.0.4"
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5"
}
},
"Microsoft.Extensions.Http.Resilience": {
@@ -1511,11 +1517,11 @@
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "CentralTransitive",
- "requested": "[10.0.3, )",
- "resolved": "10.0.4",
- "contentHash": "PDMMt7fvBatv6hcxxyJtXIzSwn7Dy00W6I2vDAOTYrQqNM2dF5A2L9n0uMzdPz2IPoNZWkAmYjoOCEdDLq0i4w==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5"
}
},
"Microsoft.Extensions.ServiceDiscovery": {
@@ -1540,13 +1546,13 @@
},
"Npgsql.EntityFrameworkCore.PostgreSQL": {
"type": "CentralTransitive",
- "requested": "[10.0.0, )",
- "resolved": "10.0.0",
- "contentHash": "E2+uSWxSB8LdsUVwPaqRWOcGOP92biry2JEwc0KJMdLJF+aZdczeIdEXVwEyv4nSVMQJH0o8tLhyAMiR6VF0lw==",
+ "requested": "[10.0.1, )",
+ "resolved": "10.0.1",
+ "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==",
"dependencies": {
- "Microsoft.EntityFrameworkCore": "[10.0.0, 11.0.0)",
- "Microsoft.EntityFrameworkCore.Relational": "[10.0.0, 11.0.0)",
- "Npgsql": "10.0.0"
+ "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)",
+ "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)",
+ "Npgsql": "10.0.2"
}
},
"OpenTelemetry.Exporter.OpenTelemetryProtocol": {
@@ -1614,9 +1620,9 @@
},
"System.Security.Cryptography.ProtectedData": {
"type": "CentralTransitive",
- "requested": "[10.0.4, )",
- "resolved": "10.0.4",
- "contentHash": "LmDnaYkcWkZSlZ07L7YcB6bH8sCiBZ7j28kPbYiXdF6f0iUiN7rxsRORlZGdj5saN/wZIqvF7lDBn/cpjU+e2g=="
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "kxR4O/8o32eNN3m4qbLe3UifYqeyEpallCyVAsLvL5ZFJVyT3JCb+9du/WHfC09VyJh1Q+p/Gd4+AwM7Rz4acg=="
}
}
}
diff --git a/src/Test/Werkr.Tests.Data/Unit/Communication/WorkflowEventBroadcasterTests.cs b/src/Test/Werkr.Tests.Data/Unit/Communication/WorkflowEventBroadcasterTests.cs
new file mode 100644
index 0000000..ba42093
--- /dev/null
+++ b/src/Test/Werkr.Tests.Data/Unit/Communication/WorkflowEventBroadcasterTests.cs
@@ -0,0 +1,153 @@
+using Microsoft.Extensions.Logging.Abstractions;
+using Werkr.Core.Communication;
+
+namespace Werkr.Tests.Data.Unit.Communication;
+
+///
+/// Unit tests for the class, validating subscription management, event
+/// fan-out delivery, and thread-safe subscriber count tracking.
+///
+[TestClass]
+public class WorkflowEventBroadcasterTests {
+ ///
+ /// The instance under test.
+ ///
+ private WorkflowEventBroadcaster _broadcaster = null!;
+
+ ///
+ /// Gets or sets the MSTest providing per-test cancellation tokens and metadata.
+ ///
+ public TestContext TestContext { get; set; } = null!;
+
+ ///
+ /// Creates a fresh for each test.
+ ///
+ [TestInitialize]
+ public void TestInit( ) {
+ _broadcaster = new WorkflowEventBroadcaster(
+ NullLogger.Instance
+ );
+ }
+
+ ///
+ /// Verifies that returns a non-null subscription
+ /// with a readable channel.
+ ///
+ [TestMethod]
+ public void Subscribe_ReturnsSubscriptionWithReader( ) {
+ using WorkflowEventSubscription sub = _broadcaster.Subscribe( );
+
+ Assert.IsNotNull( sub );
+ Assert.IsNotNull( sub.Reader );
+ }
+
+ ///
+ /// Verifies that published events are delivered to a single subscriber.
+ ///
+ [TestMethod]
+ public async Task Publish_DeliversEventToSubscriber( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+ using WorkflowEventSubscription sub = _broadcaster.Subscribe( );
+
+ Guid runId = Guid.NewGuid( );
+ StepStartedEvent evt = new( runId, 1, "Step1", 10, DateTime.UtcNow );
+ _broadcaster.Publish( evt );
+
+ bool available = await sub.Reader.WaitToReadAsync( ct );
+
+ Assert.IsTrue( available );
+ Assert.IsTrue( sub.Reader.TryRead( out WorkflowEvent? received ) );
+ Assert.AreEqual( runId, received!.WorkflowRunId );
+ }
+
+ ///
+ /// Verifies that published events fan out to all active subscribers.
+ ///
+ [TestMethod]
+ public async Task Publish_FansOutToMultipleSubscribers( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+ using WorkflowEventSubscription sub1 = _broadcaster.Subscribe( );
+ using WorkflowEventSubscription sub2 = _broadcaster.Subscribe( );
+
+ Guid runId = Guid.NewGuid( );
+ StepStartedEvent evt = new( runId, 1, "Step1", 10, DateTime.UtcNow );
+ _broadcaster.Publish( evt );
+
+ Assert.IsTrue( await sub1.Reader.WaitToReadAsync( ct ) );
+ Assert.IsTrue( sub1.Reader.TryRead( out WorkflowEvent? received1 ) );
+ Assert.AreEqual( runId, received1!.WorkflowRunId );
+
+ Assert.IsTrue( await sub2.Reader.WaitToReadAsync( ct ) );
+ Assert.IsTrue( sub2.Reader.TryRead( out WorkflowEvent? received2 ) );
+ Assert.AreEqual( runId, received2!.WorkflowRunId );
+ }
+
+ ///
+ /// Verifies that disposing a subscription removes it from the broadcaster so
+ /// subsequent publishes are not delivered to the disposed subscriber.
+ ///
+ [TestMethod]
+ public void Dispose_RemovesSubscriber( ) {
+ WorkflowEventSubscription sub = _broadcaster.Subscribe( );
+ sub.Dispose( );
+
+ Guid runId = Guid.NewGuid( );
+ StepStartedEvent evt = new( runId, 1, "Step1", 10, DateTime.UtcNow );
+ _broadcaster.Publish( evt );
+
+ // Channel was completed on unsubscribe; TryRead should return false.
+ Assert.IsFalse( sub.Reader.TryRead( out _ ) );
+ }
+
+ ///
+ /// Verifies that concurrent subscribe and unsubscribe operations do not corrupt
+ /// the subscriber list.
+ ///
+ [TestMethod]
+ public async Task ConcurrentSubscribeUnsubscribe_DoesNotCorruptState( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+ const int Iterations = 100;
+ List tasks = [];
+
+ for (int i = 0; i < Iterations; i++) {
+ tasks.Add( Task.Run( ( ) => {
+ WorkflowEventSubscription sub = _broadcaster.Subscribe( );
+ sub.Dispose( );
+ }, ct ) );
+ }
+
+ await Task.WhenAll( tasks );
+
+ // After all subscribe/unsubscribe pairs complete, a new publish should succeed
+ // without throwing (no corrupted list).
+ using WorkflowEventSubscription final = _broadcaster.Subscribe( );
+ Guid runId = Guid.NewGuid( );
+ StepStartedEvent evt = new( runId, 1, "Step1", 10, DateTime.UtcNow );
+ _broadcaster.Publish( evt );
+
+ Assert.IsTrue( await final.Reader.WaitToReadAsync( ct ) );
+ Assert.IsTrue( final.Reader.TryRead( out WorkflowEvent? received ) );
+ Assert.AreEqual( runId, received!.WorkflowRunId );
+ }
+
+ ///
+ /// Verifies that publishing with no subscribers does not throw.
+ ///
+ [TestMethod]
+ public void Publish_WithNoSubscribers_DoesNotThrow( ) {
+ Guid runId = Guid.NewGuid( );
+ StepStartedEvent evt = new( runId, 1, "Step1", 10, DateTime.UtcNow );
+
+ _broadcaster.Publish( evt );
+ }
+
+ ///
+ /// Verifies that disposing a subscription twice does not throw.
+ ///
+ [TestMethod]
+ public void Dispose_CalledTwice_DoesNotThrow( ) {
+ WorkflowEventSubscription sub = _broadcaster.Subscribe( );
+ sub.Dispose( );
+ sub.Dispose( );
+ }
+}
diff --git a/src/Test/Werkr.Tests.Data/Unit/Endpoints/FilterEndpointTests.cs b/src/Test/Werkr.Tests.Data/Unit/Endpoints/FilterEndpointTests.cs
new file mode 100644
index 0000000..e7ffc0c
--- /dev/null
+++ b/src/Test/Werkr.Tests.Data/Unit/Endpoints/FilterEndpointTests.cs
@@ -0,0 +1,254 @@
+using System.Reflection;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Werkr.Data;
+using Werkr.Data.Entities.Settings;
+
+namespace Werkr.Tests.Data.Unit.Endpoints;
+
+///
+/// Unit tests for the FilterEndpoints class, validating page key allowlist completeness
+/// and that filter CRUD operations enforce ownership and produce correct persistence results.
+///
+[TestClass]
+public class FilterEndpointTests {
+ ///
+ /// The in-memory SQLite connection used for database operations.
+ ///
+ private SqliteConnection _connection = null!;
+ ///
+ /// The SQLite-backed used for test data persistence.
+ ///
+ private SqliteWerkrDbContext _dbContext = null!;
+
+ ///
+ /// Gets or sets the MSTest providing per-test cancellation tokens and metadata.
+ ///
+ public TestContext TestContext { get; set; } = null!;
+
+ ///
+ /// The complete set of valid page keys that the filter endpoints must accept.
+ ///
+ private static readonly HashSet s_expectedPageKeys = [
+ "runs", "workflows", "jobs", "agents", "schedules", "tasks",
+ "all-workflow-runs", "workflow-dashboard"
+ ];
+
+ ///
+ /// Creates an in-memory SQLite database and the schema for each test.
+ ///
+ [TestInitialize]
+ public void TestInit( ) {
+ _connection = new SqliteConnection( "DataSource=:memory:" );
+ _connection.Open( );
+
+ DbContextOptions options = new DbContextOptionsBuilder( )
+ .UseSqlite( _connection )
+ .Options;
+
+ _dbContext = new SqliteWerkrDbContext( options );
+ _ = _dbContext.Database.EnsureCreated( );
+ }
+
+ ///
+ /// Disposes the database context and SQLite connection after each test.
+ ///
+ [TestCleanup]
+ public void TestCleanup( ) {
+ _dbContext?.Dispose( );
+ _connection?.Dispose( );
+ }
+
+ ///
+ /// Verifies that the s_validPageKeys field on FilterEndpoints contains exactly the
+ /// expected set of page keys including all-workflow-runs and workflow-dashboard.
+ ///
+ [TestMethod]
+ public void ValidPageKeys_ContainsAllExpectedKeys( ) {
+ Assembly apiAssembly = Assembly.Load( "Werkr.Api" );
+ Type? endpointsType = apiAssembly.GetType( "Werkr.Api.Endpoints.FilterEndpoints" );
+ Assert.IsNotNull( endpointsType, "FilterEndpoints type not found in Werkr.Api assembly" );
+
+ FieldInfo? field = endpointsType.GetField(
+ "s_validPageKeys",
+ BindingFlags.NonPublic | BindingFlags.Static
+ );
+
+ Assert.IsNotNull( field, "s_validPageKeys field not found on FilterEndpoints" );
+
+ object? value = field.GetValue( null );
+ _ = Assert.IsInstanceOfType>( value );
+
+ HashSet actualKeys = (HashSet)value;
+
+ foreach (string expected in s_expectedPageKeys) {
+ Assert.Contains(
+ expected, actualKeys,
+ $"Missing page key: '{expected}'"
+ );
+ }
+
+ Assert.HasCount(
+ s_expectedPageKeys.Count,
+ actualKeys,
+ $"Page key count mismatch. Expected: {s_expectedPageKeys.Count}, Actual: {actualKeys.Count}"
+ );
+ }
+
+ ///
+ /// Verifies that creating a filter persists the entity with the correct owner and page key.
+ ///
+ [TestMethod]
+ public async Task CreateFilter_PersistsWithCorrectOwnerAndPageKey( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+ string userId = "user-1";
+
+ SavedFilter entity = new( ) {
+ OwnerId = userId,
+ PageKey = "runs",
+ Name = "My Filter",
+ CriteriaJson = "{\"status\":\"Running\"}",
+ IsShared = false,
+ Created = DateTime.UtcNow,
+ LastUpdated = DateTime.UtcNow,
+ Version = 1,
+ };
+
+ _ = _dbContext.SavedFilters.Add( entity );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ SavedFilter? loaded = await _dbContext.SavedFilters
+ .FirstOrDefaultAsync( f => f.OwnerId == userId && f.PageKey == "runs", ct );
+
+ Assert.IsNotNull( loaded );
+ Assert.AreEqual( "My Filter", loaded.Name );
+ Assert.AreEqual( "{\"status\":\"Running\"}", loaded.CriteriaJson );
+ Assert.IsFalse( loaded.IsShared );
+ }
+
+ ///
+ /// Verifies that filters can be queried by page key and include both owned and shared filters.
+ ///
+ [TestMethod]
+ public async Task QueryFilters_ReturnsBothOwnedAndShared( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+ string userId = "user-1";
+
+ SavedFilter owned = new( ) {
+ OwnerId = userId,
+ PageKey = "runs",
+ Name = "My Filter",
+ CriteriaJson = "{}",
+ Created = DateTime.UtcNow,
+ LastUpdated = DateTime.UtcNow,
+ Version = 1,
+ };
+
+ SavedFilter shared = new( ) {
+ OwnerId = "other-user",
+ PageKey = "runs",
+ Name = "Shared Filter",
+ CriteriaJson = "{}",
+ IsShared = true,
+ Created = DateTime.UtcNow,
+ LastUpdated = DateTime.UtcNow,
+ Version = 1,
+ };
+
+ SavedFilter differentPage = new( ) {
+ OwnerId = userId,
+ PageKey = "jobs",
+ Name = "Jobs Filter",
+ CriteriaJson = "{}",
+ Created = DateTime.UtcNow,
+ LastUpdated = DateTime.UtcNow,
+ Version = 1,
+ };
+
+ _dbContext.SavedFilters.AddRange( owned, shared, differentPage );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ List results = await _dbContext.SavedFilters
+ .Where( f => f.PageKey == "runs" && (f.OwnerId == userId || f.IsShared) )
+ .OrderBy( f => f.Name )
+ .ToListAsync( ct );
+
+ Assert.HasCount( 2, results );
+ Assert.AreEqual( "My Filter", results[0].Name );
+ Assert.AreEqual( "Shared Filter", results[1].Name );
+ }
+
+ ///
+ /// Verifies that deleting a filter only removes the targeted entity.
+ ///
+ [TestMethod]
+ public async Task DeleteFilter_RemovesOnlyTargetEntity( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+
+ SavedFilter filter1 = new( ) {
+ OwnerId = "user-1",
+ PageKey = "runs",
+ Name = "Filter 1",
+ CriteriaJson = "{}",
+ Created = DateTime.UtcNow,
+ LastUpdated = DateTime.UtcNow,
+ Version = 1,
+ };
+
+ SavedFilter filter2 = new( ) {
+ OwnerId = "user-1",
+ PageKey = "runs",
+ Name = "Filter 2",
+ CriteriaJson = "{}",
+ Created = DateTime.UtcNow,
+ LastUpdated = DateTime.UtcNow,
+ Version = 1,
+ };
+
+ _dbContext.SavedFilters.AddRange( filter1, filter2 );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ _ = _dbContext.SavedFilters.Remove( filter1 );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ List remaining = await _dbContext.SavedFilters.ToListAsync( ct );
+
+ Assert.HasCount( 1, remaining );
+ Assert.AreEqual( "Filter 2", remaining[0].Name );
+ }
+
+ ///
+ /// Verifies that updating a filter increments the version and persists the new values.
+ ///
+ [TestMethod]
+ public async Task UpdateFilter_IncrementsVersionAndPersists( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+
+ SavedFilter entity = new( ) {
+ OwnerId = "user-1",
+ PageKey = "runs",
+ Name = "Original",
+ CriteriaJson = "{}",
+ Created = DateTime.UtcNow,
+ LastUpdated = DateTime.UtcNow,
+ Version = 1,
+ };
+
+ _ = _dbContext.SavedFilters.Add( entity );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ entity.Name = "Updated";
+ entity.CriteriaJson = "{\"status\":\"Failed\"}";
+ entity.Version++;
+ entity.LastUpdated = DateTime.UtcNow;
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ SavedFilter? loaded = await _dbContext.SavedFilters
+ .AsNoTracking( )
+ .FirstOrDefaultAsync( f => f.Id == entity.Id, ct );
+
+ Assert.IsNotNull( loaded );
+ Assert.AreEqual( "Updated", loaded.Name );
+ Assert.AreEqual( "{\"status\":\"Failed\"}", loaded.CriteriaJson );
+ }
+}
diff --git a/src/Test/Werkr.Tests.Data/Unit/Scheduling/RetryFromFailedServiceTests.cs b/src/Test/Werkr.Tests.Data/Unit/Scheduling/RetryFromFailedServiceTests.cs
new file mode 100644
index 0000000..c0817f7
--- /dev/null
+++ b/src/Test/Werkr.Tests.Data/Unit/Scheduling/RetryFromFailedServiceTests.cs
@@ -0,0 +1,516 @@
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging.Abstractions;
+using Werkr.Common.Models;
+using Werkr.Core.Scheduling;
+using Werkr.Data;
+using Werkr.Data.Entities.Tasks;
+using Werkr.Data.Entities.Workflows;
+
+namespace Werkr.Tests.Data.Unit.Scheduling;
+
+///
+/// Unit tests for , validating retry-from-failed orchestration
+/// including variable override batching, failed-step validation, and schedule creation.
+///
+[TestClass]
+public class RetryFromFailedServiceTests {
+ ///
+ /// The in-memory SQLite connection used for database operations.
+ ///
+ private SqliteConnection _connection = null!;
+ ///
+ /// The SQLite-backed used for test data persistence.
+ ///
+ private SqliteWerkrDbContext _dbContext = null!;
+ ///
+ /// The instance under test.
+ ///
+ private RetryFromFailedService _service = null!;
+
+ ///
+ /// Gets or sets the MSTest providing per-test cancellation tokens and metadata.
+ ///
+ public TestContext TestContext { get; set; } = null!;
+
+ ///
+ /// Creates an in-memory SQLite database, the schema, and the service under test.
+ ///
+ [TestInitialize]
+ public void TestInit( ) {
+ _connection = new SqliteConnection( "DataSource=:memory:" );
+ _connection.Open( );
+
+ DbContextOptions options = new DbContextOptionsBuilder( )
+ .UseSqlite( _connection )
+ .Options;
+
+ _dbContext = new SqliteWerkrDbContext( options );
+ _ = _dbContext.Database.EnsureCreated( );
+
+ _service = new RetryFromFailedService(
+ _dbContext,
+ NullLogger.Instance
+ );
+ }
+
+ ///
+ /// Disposes the database context and SQLite connection after each test.
+ ///
+ [TestCleanup]
+ public void TestCleanup( ) {
+ _dbContext?.Dispose( );
+ _connection?.Dispose( );
+ }
+
+ #region Helpers
+
+ ///
+ /// Seeds a minimal workflow with a single task, step, run, and failed step execution.
+ /// Returns a tuple of (workflowId, runId, stepId).
+ ///
+ private async Task<(long WorkflowId, Guid RunId, long StepId)> SeedFailedRunAsync(
+ CancellationToken ct ) {
+
+ Workflow workflow = new( ) { Name = "Test Workflow", Description = "Test" };
+ _ = _dbContext.Set( ).Add( workflow );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ WerkrTask task = new( ) {
+ Name = "Test Task",
+ WorkflowId = workflow.Id,
+ ActionType = TaskActionType.PowerShellCommand,
+ Content = "Write-Output 'test'",
+ TargetTags = ["default"],
+ };
+ _ = _dbContext.Set( ).Add( task );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ WorkflowStep step = new( ) {
+ WorkflowId = workflow.Id,
+ TaskId = task.Id,
+ Order = 1,
+ };
+ _ = _dbContext.WorkflowSteps.Add( step );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ Guid runId = Guid.NewGuid( );
+ WorkflowRun run = new( ) {
+ Id = runId,
+ WorkflowId = workflow.Id,
+ StartTime = DateTime.UtcNow.AddMinutes( -5 ),
+ EndTime = DateTime.UtcNow.AddMinutes( -1 ),
+ Status = WorkflowRunStatus.Failed,
+ };
+ _ = _dbContext.WorkflowRuns.Add( run );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ WorkflowStepExecution failedExecution = new( ) {
+ WorkflowRunId = runId,
+ StepId = step.Id,
+ Attempt = 1,
+ Status = StepExecutionStatus.Failed,
+ };
+ _ = _dbContext.WorkflowStepExecutions.Add( failedExecution );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ return (workflow.Id, runId, step.Id);
+ }
+
+ #endregion
+
+ ///
+ /// Verifies that retry succeeds for a valid failed run and creates a new schedule.
+ ///
+ [TestMethod]
+ public async Task RetryAsync_ValidFailedRun_ReturnsResult( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+ (long workflowId, Guid runId, long stepId) = await SeedFailedRunAsync( ct );
+
+ RetryFromFailedService.RetryResult result = await _service.RetryAsync(
+ workflowId, runId, stepId, null, ct
+ );
+
+ Assert.AreEqual( runId, result.RunId );
+ Assert.AreEqual( stepId, result.RetryFromStepId );
+ Assert.AreEqual( 1, result.ResetStepCount );
+ }
+
+ ///
+ /// Verifies that retry transitions the run from Failed to Running.
+ ///
+ [TestMethod]
+ public async Task RetryAsync_TransitionsRunToRunning( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+ (long workflowId, Guid runId, long stepId) = await SeedFailedRunAsync( ct );
+
+ _ = await _service.RetryAsync( workflowId, runId, stepId, null, ct );
+
+ WorkflowRun run = await _dbContext.WorkflowRuns.AsNoTracking( ).FirstAsync( r => r.Id == runId, ct );
+
+ Assert.AreEqual( WorkflowRunStatus.Running, run.Status );
+ Assert.IsNull( run.EndTime );
+ }
+
+ ///
+ /// Verifies that retry creates a new Pending step execution with an incremented attempt number.
+ ///
+ [TestMethod]
+ public async Task RetryAsync_CreatesNewPendingExecution( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+ (long workflowId, Guid runId, long stepId) = await SeedFailedRunAsync( ct );
+
+ _ = await _service.RetryAsync( workflowId, runId, stepId, null, ct );
+
+ List executions = await _dbContext.WorkflowStepExecutions
+ .Where( e => e.WorkflowRunId == runId && e.StepId == stepId )
+ .OrderBy( e => e.Attempt )
+ .ToListAsync( ct );
+
+ Assert.HasCount( 2, executions );
+ Assert.AreEqual( StepExecutionStatus.Failed, executions[0].Status );
+ Assert.AreEqual( 1, executions[0].Attempt );
+ Assert.AreEqual( StepExecutionStatus.Pending, executions[1].Status );
+ Assert.AreEqual( 2, executions[1].Attempt );
+ }
+
+ ///
+ /// Verifies that variable overrides are persisted with incremented versions using
+ /// the batch query (not N+1).
+ ///
+ [TestMethod]
+ public async Task RetryAsync_WithVariableOverrides_PersistsNewVersions( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+ (long workflowId, Guid runId, long stepId) = await SeedFailedRunAsync( ct );
+
+ // Seed existing variables (version 1)
+ WorkflowRunVariable existingVar1 = new( ) {
+ WorkflowRunId = runId,
+ VariableName = "Env",
+ Value = "staging",
+ Version = 1,
+ Source = VariableSource.Default,
+ Created = DateTime.UtcNow,
+ };
+ WorkflowRunVariable existingVar2 = new( ) {
+ WorkflowRunId = runId,
+ VariableName = "Retries",
+ Value = "3",
+ Version = 1,
+ Source = VariableSource.Default,
+ Created = DateTime.UtcNow,
+ };
+ _dbContext.Set( ).AddRange( existingVar1, existingVar2 );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ Dictionary overrides = new( ) {
+ ["Env"] = "production",
+ ["Retries"] = "5",
+ };
+
+ _ = await _service.RetryAsync( workflowId, runId, stepId, overrides, ct );
+
+ List envVars = await _dbContext.Set( )
+ .Where( v => v.WorkflowRunId == runId && v.VariableName == "Env" )
+ .OrderBy( v => v.Version )
+ .ToListAsync( ct );
+
+ Assert.HasCount( 2, envVars );
+ Assert.AreEqual( 1, envVars[0].Version );
+ Assert.AreEqual( "staging", envVars[0].Value );
+ Assert.AreEqual( 2, envVars[1].Version );
+ Assert.AreEqual( "production", envVars[1].Value );
+ Assert.AreEqual( VariableSource.ReExecutionEdit, envVars[1].Source );
+
+ List retriesVars = await _dbContext.Set( )
+ .Where( v => v.WorkflowRunId == runId && v.VariableName == "Retries" )
+ .OrderBy( v => v.Version )
+ .ToListAsync( ct );
+
+ Assert.HasCount( 2, retriesVars );
+ Assert.AreEqual( 2, retriesVars[1].Version );
+ Assert.AreEqual( "5", retriesVars[1].Value );
+ }
+
+ ///
+ /// Verifies that overrides for new variables (no prior version) start at version 1.
+ ///
+ [TestMethod]
+ public async Task RetryAsync_WithNewVariable_StartsAtVersionOne( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+ (long workflowId, Guid runId, long stepId) = await SeedFailedRunAsync( ct );
+
+ Dictionary overrides = new( ) {
+ ["NewVar"] = "hello",
+ };
+
+ _ = await _service.RetryAsync( workflowId, runId, stepId, overrides, ct );
+
+ WorkflowRunVariable? newVar = await _dbContext.Set( )
+ .FirstOrDefaultAsync( v => v.WorkflowRunId == runId && v.VariableName == "NewVar", ct );
+
+ Assert.IsNotNull( newVar );
+ Assert.AreEqual( 1, newVar.Version );
+ Assert.AreEqual( "hello", newVar.Value );
+ Assert.AreEqual( VariableSource.ReExecutionEdit, newVar.Source );
+ }
+
+ ///
+ /// Verifies that retrying a run not in Failed status throws .
+ ///
+ [TestMethod]
+ public async Task RetryAsync_RunNotFailed_Throws( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+ (long workflowId, Guid runId, long stepId) = await SeedFailedRunAsync( ct );
+
+ // Transition the run to Running first
+ WorkflowRun run = await _dbContext.WorkflowRuns.FirstAsync( r => r.Id == runId, ct );
+ run.Status = WorkflowRunStatus.Running;
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ _ = await Assert.ThrowsExactlyAsync( ( ) =>
+ _service.RetryAsync( workflowId, runId, stepId, null, ct )
+ );
+ }
+
+ ///
+ /// Verifies that retrying with a step that doesn't belong to the workflow throws
+ /// .
+ ///
+ [TestMethod]
+ public async Task RetryAsync_StepNotInWorkflow_Throws( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+ (long workflowId, Guid runId, _) = await SeedFailedRunAsync( ct );
+
+ _ = await Assert.ThrowsExactlyAsync( ( ) =>
+ _service.RetryAsync( workflowId, runId, 99999, null, ct )
+ );
+ }
+
+ ///
+ /// Verifies that retrying with a mismatched workflowId (run belongs to a different workflow)
+ /// throws without modifying the run status.
+ ///
+ [TestMethod]
+ public async Task RetryAsync_WorkflowIdMismatch_Throws( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+ (_, Guid runId, long stepId) = await SeedFailedRunAsync( ct );
+
+ long wrongWorkflowId = 99999;
+ _ = await Assert.ThrowsExactlyAsync( ( ) =>
+ _service.RetryAsync( wrongWorkflowId, runId, stepId, null, ct )
+ );
+
+ // Verify the run status was NOT changed.
+ WorkflowRun run = await _dbContext.WorkflowRuns.FirstAsync(r => r.Id == runId, ct);
+ Assert.AreEqual( WorkflowRunStatus.Failed, run.Status );
+ }
+
+ ///
+ /// Verifies that retrying from a step without a Failed execution throws
+ /// .
+ ///
+ [TestMethod]
+ public async Task RetryAsync_StepNotFailed_Throws( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+ (long workflowId, Guid runId, long stepId) = await SeedFailedRunAsync( ct );
+
+ // Change the execution status to Completed
+ WorkflowStepExecution exec = await _dbContext.WorkflowStepExecutions
+ .FirstAsync( e => e.WorkflowRunId == runId && e.StepId == stepId, ct );
+ exec.Status = StepExecutionStatus.Completed;
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ _ = await Assert.ThrowsExactlyAsync( ( ) =>
+ _service.RetryAsync( workflowId, runId, stepId, null, ct )
+ );
+ }
+
+ ///
+ /// Verifies that retry creates a one-time schedule linked to the workflow.
+ ///
+ [TestMethod]
+ public async Task RetryAsync_CreatesOneTimeSchedule( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+ (long workflowId, Guid runId, long stepId) = await SeedFailedRunAsync( ct );
+
+ RetryFromFailedService.RetryResult result = await _service.RetryAsync(
+ workflowId, runId, stepId, null, ct
+ );
+
+ WorkflowSchedule? link = await _dbContext.WorkflowSchedules
+ .FirstOrDefaultAsync( ws => ws.ScheduleId == result.ScheduleId, ct );
+
+ Assert.IsNotNull( link );
+ Assert.AreEqual( workflowId, link.WorkflowId );
+ Assert.IsTrue( link.IsOneTime );
+ Assert.AreEqual( runId, link.WorkflowRunId );
+ }
+
+ #region DAG and Repeated-Retry Tests
+
+ ///
+ /// Seeds a 3-step DAG: A → B → C with B in Failed status and A/C in Completed/Pending.
+ /// Returns (workflowId, runId, stepAId, stepBId, stepCId).
+ ///
+ private async Task<(long WorkflowId, Guid RunId, long StepAId, long StepBId, long StepCId)> SeedDagFailedRunAsync(
+ CancellationToken ct ) {
+
+ Workflow workflow = new() { Name = "DAG Workflow", Description = "A→B→C" };
+ _ = _dbContext.Set( ).Add( workflow );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ WerkrTask taskA = new()
+ {
+ Name = "Task A",
+ WorkflowId = workflow.Id,
+ ActionType = TaskActionType.PowerShellCommand,
+ Content = "echo A",
+ TargetTags = ["default"],
+ };
+ WerkrTask taskB = new()
+ {
+ Name = "Task B",
+ WorkflowId = workflow.Id,
+ ActionType = TaskActionType.PowerShellCommand,
+ Content = "echo B",
+ TargetTags = ["default"],
+ };
+ WerkrTask taskC = new()
+ {
+ Name = "Task C",
+ WorkflowId = workflow.Id,
+ ActionType = TaskActionType.PowerShellCommand,
+ Content = "echo C",
+ TargetTags = ["default"],
+ };
+ _dbContext.Set( ).AddRange( taskA, taskB, taskC );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ WorkflowStep stepA = new() { WorkflowId = workflow.Id, TaskId = taskA.Id, Order = 1 };
+ WorkflowStep stepB = new() { WorkflowId = workflow.Id, TaskId = taskB.Id, Order = 2 };
+ WorkflowStep stepC = new() { WorkflowId = workflow.Id, TaskId = taskC.Id, Order = 3 };
+ _dbContext.WorkflowSteps.AddRange( stepA, stepB, stepC );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ // Dependencies: B depends on A, C depends on B
+ _dbContext.WorkflowStepDependencies.AddRange(
+ new WorkflowStepDependency { StepId = stepB.Id, DependsOnStepId = stepA.Id },
+ new WorkflowStepDependency { StepId = stepC.Id, DependsOnStepId = stepB.Id }
+ );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ Guid runId = Guid.NewGuid();
+ WorkflowRun run = new()
+ {
+ Id = runId,
+ WorkflowId = workflow.Id,
+ StartTime = DateTime.UtcNow.AddMinutes(-5),
+ EndTime = DateTime.UtcNow.AddMinutes(-1),
+ Status = WorkflowRunStatus.Failed,
+ };
+ _ = _dbContext.WorkflowRuns.Add( run );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ // A = Completed (attempt 1), B = Failed (attempt 1), C = Pending (attempt 1)
+ _dbContext.WorkflowStepExecutions.AddRange(
+ new WorkflowStepExecution { WorkflowRunId = runId, StepId = stepA.Id, Attempt = 1, Status = StepExecutionStatus.Completed },
+ new WorkflowStepExecution { WorkflowRunId = runId, StepId = stepB.Id, Attempt = 1, Status = StepExecutionStatus.Failed },
+ new WorkflowStepExecution { WorkflowRunId = runId, StepId = stepC.Id, Attempt = 1, Status = StepExecutionStatus.Pending }
+ );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ return (workflow.Id, runId, stepA.Id, stepB.Id, stepC.Id);
+ }
+
+ ///
+ /// Verifies that retrying from step B in a DAG (A→B→C) resets B and C but not A.
+ ///
+ [TestMethod]
+ public async Task RetryAsync_DagDownstreamReset_ResetsOnlyTargetAndDownstream( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+ (long workflowId, Guid runId, long stepAId, long stepBId, long stepCId) =
+ await SeedDagFailedRunAsync( ct );
+
+ RetryFromFailedService.RetryResult result = await _service.RetryAsync(
+ workflowId, runId, stepBId, null, ct
+ );
+
+ // B and C should be reset (2 steps)
+ Assert.AreEqual( 2, result.ResetStepCount );
+
+ // Step A should NOT have a new execution — still just its original Completed one
+ List execA = await _dbContext.WorkflowStepExecutions
+ .Where(e => e.WorkflowRunId == runId && e.StepId == stepAId)
+ .ToListAsync(ct);
+ Assert.HasCount( 1, execA );
+ Assert.AreEqual( StepExecutionStatus.Completed, execA[0].Status );
+
+ // Step B should have attempt 1 (Failed) + attempt 2 (Pending)
+ List execB = await _dbContext.WorkflowStepExecutions
+ .Where(e => e.WorkflowRunId == runId && e.StepId == stepBId)
+ .OrderBy(e => e.Attempt)
+ .ToListAsync(ct);
+ Assert.HasCount( 2, execB );
+ Assert.AreEqual( StepExecutionStatus.Failed, execB[0].Status );
+ Assert.AreEqual( 1, execB[0].Attempt );
+ Assert.AreEqual( StepExecutionStatus.Pending, execB[1].Status );
+ Assert.AreEqual( 2, execB[1].Attempt );
+
+ // Step C should have attempt 1 (Pending, original) + attempt 2 (Pending, retry)
+ List execC = await _dbContext.WorkflowStepExecutions
+ .Where(e => e.WorkflowRunId == runId && e.StepId == stepCId)
+ .OrderBy(e => e.Attempt)
+ .ToListAsync(ct);
+ Assert.HasCount( 2, execC );
+ Assert.AreEqual( 1, execC[0].Attempt );
+ Assert.AreEqual( StepExecutionStatus.Pending, execC[1].Status );
+ Assert.AreEqual( 2, execC[1].Attempt );
+ }
+
+ ///
+ /// Verifies that retrying twice increments attempt numbers correctly (1→2→3).
+ ///
+ [TestMethod]
+ public async Task RetryAsync_RepeatedRetry_IncrementsAttempts( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+ (long workflowId, Guid runId, long stepId) = await SeedFailedRunAsync( ct );
+
+ // First retry: attempt 1 (Failed) → creates attempt 2 (Pending)
+ _ = await _service.RetryAsync( workflowId, runId, stepId, null, ct );
+
+ // Simulate the retried execution failing again using ExecuteUpdateAsync
+ // (bypasses the change tracker, same as the service's CAS pattern).
+ _dbContext.ChangeTracker.Clear( );
+
+ _ = await _dbContext.WorkflowStepExecutions
+ .Where( e => e.WorkflowRunId == runId && e.StepId == stepId && e.Attempt == 2 )
+ .ExecuteUpdateAsync( s => s.SetProperty( e => e.Status, StepExecutionStatus.Failed ), ct );
+
+ _ = await _dbContext.WorkflowRuns
+ .Where( r => r.Id == runId )
+ .ExecuteUpdateAsync( s => s
+ .SetProperty( r => r.Status, WorkflowRunStatus.Failed )
+ .SetProperty( r => r.EndTime, DateTime.UtcNow ), ct );
+
+ // Second retry: should create attempt 3
+ _ = await _service.RetryAsync( workflowId, runId, stepId, null, ct );
+
+ _dbContext.ChangeTracker.Clear( );
+
+ List allExecs = await _dbContext.WorkflowStepExecutions
+ .Where(e => e.WorkflowRunId == runId && e.StepId == stepId)
+ .OrderBy(e => e.Attempt)
+ .ToListAsync(ct);
+
+ Assert.HasCount( 3, allExecs );
+ Assert.AreEqual( 1, allExecs[0].Attempt );
+ Assert.AreEqual( StepExecutionStatus.Failed, allExecs[0].Status );
+ Assert.AreEqual( 2, allExecs[1].Attempt );
+ Assert.AreEqual( StepExecutionStatus.Failed, allExecs[1].Status );
+ Assert.AreEqual( 3, allExecs[2].Attempt );
+ Assert.AreEqual( StepExecutionStatus.Pending, allExecs[2].Status );
+ }
+
+ #endregion
+}
diff --git a/src/Test/Werkr.Tests.Data/Unit/Workflows/ControlStatementConverterTests.cs b/src/Test/Werkr.Tests.Data/Unit/Workflows/ControlStatementConverterTests.cs
new file mode 100644
index 0000000..58f29ef
--- /dev/null
+++ b/src/Test/Werkr.Tests.Data/Unit/Workflows/ControlStatementConverterTests.cs
@@ -0,0 +1,233 @@
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Werkr.Data;
+using Werkr.Data.Entities.Tasks;
+using Werkr.Data.Entities.Workflows;
+
+namespace Werkr.Tests.Data.Unit.Workflows;
+
+///
+/// Tests that the ControlStatementStringConverter in
+/// correctly round-trips enum values through the database
+/// as strings, including backward-compatible reading of the legacy "Sequential" value.
+///
+[TestClass]
+public class ControlStatementConverterTests {
+ private SqliteConnection _connection = null!;
+ private SqliteWerkrDbContext _dbContext = null!;
+
+ public TestContext TestContext { get; set; } = null!;
+
+ [TestInitialize]
+ public void TestInit( ) {
+ _connection = new SqliteConnection( "DataSource=:memory:" );
+ _connection.Open( );
+
+ DbContextOptions options = new DbContextOptionsBuilder( )
+ .UseSqlite( _connection )
+ .UseSnakeCaseNamingConvention( )
+ .Options;
+
+ _dbContext = new SqliteWerkrDbContext( options );
+ _ = _dbContext.Database.EnsureCreated( );
+ }
+
+ [TestCleanup]
+ public void TestCleanup( ) {
+ _dbContext?.Dispose( );
+ _connection?.Dispose( );
+ }
+
+ ///
+ /// Creates a workflow step with , saves, reloads,
+ /// and verifies it round-trips correctly and is stored as the string "Default".
+ ///
+ [TestMethod]
+ public async Task Default_RoundTrips_AsDefaultString( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+
+ // Arrange — create a workflow and task to host the step
+ Workflow workflow = new( ) { Name = "RoundTrip_WF", Description = "test" };
+ _ = _dbContext.Workflows.Add( workflow );
+ WerkrTask task = new( ) { Name = "RoundTrip_Task", ActionType = TaskActionType.ShellCommand, Content = "echo test", TargetTags = ["test"] };
+ _ = _dbContext.Tasks.Add( task );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ WorkflowStep step = new( ) {
+ WorkflowId = workflow.Id,
+ TaskId = task.Id,
+ Order = 0,
+ ControlStatement = ControlStatement.Default,
+ };
+ _ = _dbContext.WorkflowSteps.Add( step );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ // Detach so the next query hits the database
+ _dbContext.ChangeTracker.Clear( );
+
+ // Act — reload from DB
+ WorkflowStep loaded = await _dbContext.WorkflowSteps.SingleAsync( s => s.Id == step.Id, ct );
+
+ // Assert — enum value round-trips
+ Assert.AreEqual( ControlStatement.Default, loaded.ControlStatement );
+
+ // Assert — raw string in the database is "Default"
+ long stepId = step.Id;
+ string? raw = await _dbContext.Database
+ .SqlQuery( $"SELECT control_statement AS Value FROM workflow_steps WHERE id = {stepId}" )
+ .SingleAsync( ct );
+ Assert.AreEqual( "Default", raw );
+ }
+
+ ///
+ /// Verifies that every non-Default enum member round-trips through the database correctly.
+ ///
+ [TestMethod]
+ [DataRow( ControlStatement.If, "If" )]
+ [DataRow( ControlStatement.Else, "Else" )]
+ [DataRow( ControlStatement.ElseIf, "ElseIf" )]
+ [DataRow( ControlStatement.While, "While" )]
+ [DataRow( ControlStatement.Do, "Do" )]
+ public async Task AllEnumValues_RoundTrip_Correctly( ControlStatement value, string expectedString ) {
+ CancellationToken ct = TestContext.CancellationToken;
+
+ Workflow workflow = new( ) { Name = $"RoundTrip_{value}", Description = "test" };
+ _ = _dbContext.Workflows.Add( workflow );
+ WerkrTask task = new( ) { Name = $"Task_{value}", ActionType = TaskActionType.ShellCommand, Content = "echo test", TargetTags = ["test"] };
+ _ = _dbContext.Tasks.Add( task );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ WorkflowStep step = new( ) {
+ WorkflowId = workflow.Id,
+ TaskId = task.Id,
+ Order = 0,
+ ControlStatement = value,
+ ConditionExpression = value is ControlStatement.If or ControlStatement.ElseIf or ControlStatement.While or ControlStatement.Do
+ ? "$? -eq $true" : null,
+ };
+ _ = _dbContext.WorkflowSteps.Add( step );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ _dbContext.ChangeTracker.Clear( );
+
+ WorkflowStep loaded = await _dbContext.WorkflowSteps.SingleAsync( s => s.Id == step.Id, ct );
+ Assert.AreEqual( value, loaded.ControlStatement );
+
+ long stepId = step.Id;
+ string? raw = await _dbContext.Database
+ .SqlQuery( $"SELECT control_statement AS Value FROM workflow_steps WHERE id = {stepId}" )
+ .SingleAsync( ct );
+ Assert.AreEqual( expectedString, raw );
+ }
+
+ ///
+ /// Verifies that the legacy "Sequential" string value in the database is correctly
+ /// read as by the converter.
+ ///
+ [TestMethod]
+ public async Task LegacySequential_ReadsAs_Default( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+
+ Workflow workflow = new( ) { Name = "Legacy_WF", Description = "test" };
+ _ = _dbContext.Workflows.Add( workflow );
+ WerkrTask task = new( ) { Name = "Legacy_Task", ActionType = TaskActionType.ShellCommand, Content = "echo test", TargetTags = ["test"] };
+ _ = _dbContext.Tasks.Add( task );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ // Insert a step with "Default" first (to get a valid row)
+ WorkflowStep step = new( ) {
+ WorkflowId = workflow.Id,
+ TaskId = task.Id,
+ Order = 0,
+ ControlStatement = ControlStatement.Default,
+ };
+ _ = _dbContext.WorkflowSteps.Add( step );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ // Manually overwrite the stored string to the legacy "Sequential" value
+ long stepId = step.Id;
+ _ = await _dbContext.Database.ExecuteSqlAsync(
+ $"UPDATE workflow_steps SET control_statement = 'Sequential' WHERE id = {stepId}", ct );
+
+ _dbContext.ChangeTracker.Clear( );
+
+ // Act — reload via EF
+ WorkflowStep loaded = await _dbContext.WorkflowSteps.SingleAsync( s => s.Id == step.Id, ct );
+
+ // Assert — converter maps "Sequential" → Default
+ Assert.AreEqual( ControlStatement.Default, loaded.ControlStatement );
+ }
+
+ ///
+ /// Verifies that all legacy database string values from the old ControlStatement enum
+ /// are correctly mapped to the current enum members by the converter.
+ ///
+ [TestMethod]
+ [DataRow( "Parallel", ControlStatement.Default )]
+ [DataRow( "ConditionalIf", ControlStatement.If )]
+ [DataRow( "ConditionalElseIf", ControlStatement.ElseIf )]
+ [DataRow( "ConditionalWhile", ControlStatement.While )]
+ [DataRow( "ConditionalDo", ControlStatement.Do )]
+ [DataRow( "ConditionalElse", ControlStatement.Else )]
+ public async Task LegacyValues_ReadAs_CorrectEnum( string legacyString, ControlStatement expected ) {
+ CancellationToken ct = TestContext.CancellationToken;
+
+ Workflow workflow = new() { Name = $"Legacy_{legacyString}", Description = "test" };
+ _ = _dbContext.Workflows.Add( workflow );
+ WerkrTask task = new() { Name = $"Task_{legacyString}", ActionType = TaskActionType.ShellCommand, Content = "echo test", TargetTags = ["test"] };
+ _ = _dbContext.Tasks.Add( task );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ WorkflowStep step = new()
+ {
+ WorkflowId = workflow.Id,
+ TaskId = task.Id,
+ Order = 0,
+ ControlStatement = ControlStatement.Default,
+ };
+ _ = _dbContext.WorkflowSteps.Add( step );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ long stepId = step.Id;
+ _ = await _dbContext.Database.ExecuteSqlAsync(
+ $"UPDATE workflow_steps SET control_statement = {legacyString} WHERE id = {stepId}", ct );
+
+ _dbContext.ChangeTracker.Clear( );
+
+ WorkflowStep loaded = await _dbContext.WorkflowSteps.SingleAsync(s => s.Id == step.Id, ct);
+ Assert.AreEqual( expected, loaded.ControlStatement );
+ }
+
+ ///
+ /// Verifies that an unrecognized string in the database falls back to .
+ ///
+ [TestMethod]
+ public async Task UnknownString_FallsBackTo_Default( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+
+ Workflow workflow = new() { Name = "Unknown_WF", Description = "test" };
+ _ = _dbContext.Workflows.Add( workflow );
+ WerkrTask task = new() { Name = "Unknown_Task", ActionType = TaskActionType.ShellCommand, Content = "echo test", TargetTags = ["test"] };
+ _ = _dbContext.Tasks.Add( task );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ WorkflowStep step = new()
+ {
+ WorkflowId = workflow.Id,
+ TaskId = task.Id,
+ Order = 0,
+ ControlStatement = ControlStatement.Default,
+ };
+ _ = _dbContext.WorkflowSteps.Add( step );
+ _ = await _dbContext.SaveChangesAsync( ct );
+
+ long stepId = step.Id;
+ _ = await _dbContext.Database.ExecuteSqlAsync(
+ $"UPDATE workflow_steps SET control_statement = 'Bogus' WHERE id = {stepId}", ct );
+
+ _dbContext.ChangeTracker.Clear( );
+
+ WorkflowStep loaded = await _dbContext.WorkflowSteps.SingleAsync(s => s.Id == step.Id, ct);
+ Assert.AreEqual( ControlStatement.Default, loaded.ControlStatement );
+ }
+}
diff --git a/src/Test/Werkr.Tests.Data/Unit/Workflows/WorkflowServiceTests.cs b/src/Test/Werkr.Tests.Data/Unit/Workflows/WorkflowServiceTests.cs
index 51e9fae..1b9b200 100644
--- a/src/Test/Werkr.Tests.Data/Unit/Workflows/WorkflowServiceTests.cs
+++ b/src/Test/Werkr.Tests.Data/Unit/Workflows/WorkflowServiceTests.cs
@@ -625,6 +625,96 @@ await _service.GetTopologicalLevelsAsync(
); // Level 1: B, C
}
+ ///
+ /// Verifies that three independent steps (no dependencies) are all grouped at level 0.
+ ///
+ [TestMethod]
+ public async Task GetTopologicalLevelsAsync_ThreeIndependentSteps_AllAtLevelZero( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+ Workflow workflow = new( ) { Name = "ThreeIndependent", Description = string.Empty };
+ _ = await _service.CreateAsync( workflow, ct );
+ await SeedTaskAsync( ct );
+
+ _ = await _service.AddStepAsync( workflow.Id, new WorkflowStep { TaskId = 1, Order = 0 }, ct );
+ _ = await _service.AddStepAsync( workflow.Id, new WorkflowStep { TaskId = 1, Order = 1 }, ct );
+ _ = await _service.AddStepAsync( workflow.Id, new WorkflowStep { TaskId = 1, Order = 2 }, ct );
+
+ IReadOnlyList> levels =
+ await _service.GetTopologicalLevelsAsync( workflow.Id, ct );
+
+ Assert.HasCount( 1, levels );
+ Assert.HasCount( 3, levels[0] );
+ }
+
+ ///
+ /// Verifies a diamond-shaped DAG: A → B, A → C, B → D, C → D produces
+ /// three levels: [A], [B, C], [D]. B and C are parallelizable at level 1.
+ ///
+ [TestMethod]
+ public async Task GetTopologicalLevelsAsync_DiamondDag_GroupsCorrectly( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+ Workflow workflow = new( ) { Name = "Diamond", Description = string.Empty };
+ _ = await _service.CreateAsync( workflow, ct );
+ await SeedTaskAsync( ct );
+
+ WorkflowStep a = await _service.AddStepAsync( workflow.Id, new WorkflowStep { TaskId = 1, Order = 0 }, ct );
+ WorkflowStep b = await _service.AddStepAsync( workflow.Id, new WorkflowStep { TaskId = 1, Order = 1 }, ct );
+ WorkflowStep c = await _service.AddStepAsync( workflow.Id, new WorkflowStep { TaskId = 1, Order = 2 }, ct );
+ WorkflowStep d = await _service.AddStepAsync( workflow.Id, new WorkflowStep { TaskId = 1, Order = 3 }, ct );
+
+ await _service.AddStepDependencyAsync( b.Id, a.Id, ct );
+ await _service.AddStepDependencyAsync( c.Id, a.Id, ct );
+ await _service.AddStepDependencyAsync( d.Id, b.Id, ct );
+ await _service.AddStepDependencyAsync( d.Id, c.Id, ct );
+
+ IReadOnlyList> levels =
+ await _service.GetTopologicalLevelsAsync( workflow.Id, ct );
+
+ Assert.HasCount( 3, levels );
+ Assert.HasCount( 1, levels[0] ); // Level 0: A
+ Assert.HasCount( 2, levels[1] ); // Level 1: B, C (parallel)
+ Assert.HasCount( 1, levels[2] ); // Level 2: D
+ }
+
+ ///
+ /// Verifies that If/ElseIf/Else steps at the same level are separated from Default steps
+ /// for independent chain-bound processing. Both sets share the same topological level
+ /// but the execution engine will partition them for sequential vs. parallel execution.
+ ///
+ [TestMethod]
+ public async Task GetTopologicalLevelsAsync_MixedControlStatements_SameLevelGrouped( ) {
+ CancellationToken ct = TestContext.CancellationToken;
+ Workflow workflow = new( ) { Name = "MixedControl", Description = string.Empty };
+ _ = await _service.CreateAsync( workflow, ct );
+ await SeedTaskAsync( ct );
+
+ // Root step
+ WorkflowStep root = await _service.AddStepAsync(
+ workflow.Id, new WorkflowStep { TaskId = 1, Order = 0 }, ct );
+
+ // Default step depends on root
+ WorkflowStep defaultStep = await _service.AddStepAsync(
+ workflow.Id, new WorkflowStep { TaskId = 1, Order = 1, ControlStatement = ControlStatement.Default }, ct );
+ await _service.AddStepDependencyAsync( defaultStep.Id, root.Id, ct );
+
+ // If step depends on root
+ WorkflowStep ifStep = await _service.AddStepAsync(
+ workflow.Id, new WorkflowStep { TaskId = 1, Order = 2, ControlStatement = ControlStatement.If, ConditionExpression = "$? -eq $true" }, ct );
+ await _service.AddStepDependencyAsync( ifStep.Id, root.Id, ct );
+
+ IReadOnlyList> levels =
+ await _service.GetTopologicalLevelsAsync( workflow.Id, ct );
+
+ Assert.HasCount( 2, levels );
+ Assert.HasCount( 1, levels[0] ); // Level 0: root
+ Assert.HasCount( 2, levels[1] ); // Level 1: defaultStep + ifStep (execution engine partitions them)
+
+ // Verify both steps are at level 1 with their correct control statements
+ HashSet controlStatements = [.. levels[1].Select( s => s.ControlStatement )];
+ Assert.Contains( ControlStatement.Default, controlStatements );
+ Assert.Contains( ControlStatement.If, controlStatements );
+ }
+
// ── Control Flow Validation ──
///
diff --git a/src/Test/Werkr.Tests.Data/packages.lock.json b/src/Test/Werkr.Tests.Data/packages.lock.json
index 233f97e..4744dce 100644
--- a/src/Test/Werkr.Tests.Data/packages.lock.json
+++ b/src/Test/Werkr.Tests.Data/packages.lock.json
@@ -52,8 +52,8 @@
},
"Microsoft.AspNetCore.Metadata": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "levTTo69d5gYARtSzRV4wMvD1YUtkWvI/fOiTEG+k4v1WEEFBxrHLdUVEvxCfiuCb1Y1XuPAbbBsKQi9wNtU3w=="
+ "resolved": "10.0.5",
+ "contentHash": "nXVB1K4RzyhDHKYWLiq3+aJopJZKO5ojFqHV9PZ74fe4VWM/8itoouqsd2KIqSooIwQ13UDNlPQfN2rWr7hc2A=="
},
"Microsoft.CodeCoverage": {
"type": "Transitive",
@@ -67,36 +67,36 @@
},
"Microsoft.EntityFrameworkCore.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "qDcJqCfN1XYyX0ID/Hd9/kQTRvlia8S+Yuwyl9uFhBIKnOCbl9WMdGQCzbZUKbkpkfvf3P9CDdXsnxHyE3O0Aw=="
+ "resolved": "10.0.5",
+ "contentHash": "32c58Rnm47Qvhimawf67KO9PytgPz3QoWye7Abapt0Yocw/JnzMiSNj/pRoIKyn8Jxypkv86zxKD4Q/zNTc0Ag=="
},
"Microsoft.EntityFrameworkCore.Analyzers": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "pQeMHCyD3yTtCEGnHV4VsgKUvrESo3MR5mnh8sgQ1hWYmI1YFsUutDowBIxkobeWRtaRmBqQAtF7XQFW6FWuNA=="
+ "resolved": "10.0.5",
+ "contentHash": "ipC4u1VojgEfoIZhtbS2Sx5IluJTP/Jf1hz3yGsxGBgSukYY/CquI6rAjxn5H58CZgVn36qcuPPtNMwZ0AUzMg=="
},
"Microsoft.EntityFrameworkCore.Relational": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "DOTjTHy93W3TwpMLM4SCm0n57Sc0Jj3+m2S6LSTstKyBB34eT1UouaMS19mpWwvtj42+sRiEjA3+rOTNoNzXFQ==",
+ "resolved": "10.0.5",
+ "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==",
"dependencies": {
- "Microsoft.EntityFrameworkCore": "10.0.4",
- "Microsoft.Extensions.Caching.Memory": "10.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.Logging": "10.0.4"
+ "Microsoft.EntityFrameworkCore": "10.0.5",
+ "Microsoft.Extensions.Caching.Memory": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5"
}
},
"Microsoft.EntityFrameworkCore.Sqlite.Core": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "8D3Kk7assWpi93DuicgucDqGoOsgEgLlZy8io0FUlSGG2b4wkRWkjXn4xFBX+BzxExjfcYvHhtcBpqkXhe2p0A==",
+ "resolved": "10.0.5",
+ "contentHash": "rVH43bcUyZiMn0SnCpVnvFpl4PFxT4GwmuVVLcT4JL0NtzuHY9ymKV+Llb5cjuJ+6+gEl4eixy2rE8nxOPcBSA==",
"dependencies": {
- "Microsoft.Data.Sqlite.Core": "10.0.4",
- "Microsoft.EntityFrameworkCore.Relational": "10.0.4",
- "Microsoft.Extensions.Caching.Memory": "10.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.DependencyModel": "10.0.4",
- "Microsoft.Extensions.Logging": "10.0.4",
+ "Microsoft.Data.Sqlite.Core": "10.0.5",
+ "Microsoft.EntityFrameworkCore.Relational": "10.0.5",
+ "Microsoft.Extensions.Caching.Memory": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.DependencyModel": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5",
"SQLitePCLRaw.core": "2.1.11"
}
},
@@ -112,22 +112,22 @@
},
"Microsoft.Extensions.Caching.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "uDRooaV6N3WZ0kdlNPMB68/MdGn/in1Fs7Db7DnIm85RBTPy4P321WO+daAImiYpH5dekjNggDqy1N44WaIlMA==",
+ "resolved": "10.0.5",
+ "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==",
"dependencies": {
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.Caching.Memory": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "CLLussNUMdSbyJOu4VBF7sqskHGB/5N1EcFzrqG/HsPATN8fCRUcfp0qns1VwkxKHwxrtYCh5FKe+kM81Q1PHA==",
+ "resolved": "10.0.5",
+ "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==",
"dependencies": {
- "Microsoft.Extensions.Caching.Abstractions": "10.0.4",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "10.0.4",
- "Microsoft.Extensions.Options": "10.0.4",
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.Caching.Abstractions": "10.0.5",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.Compliance.Abstractions": {
@@ -141,46 +141,46 @@
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "601B3ha6XvOsOcu9GVd2dVd1KEDuqr49r46GUWhNJkeZDhZ/NI9EYTyoeQjZQEi8ZUvnrv++FbTfGmC8F0vgtg==",
+ "resolved": "10.0.5",
+ "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==",
"dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "ilnL/kQn62Gx3OZCVT7SJrBNi0CRIhS8VEunmE6i/a9lp9l/eos+hpxMvCW4iX2aVc/NWeDhxuQusZL7zvmKIg==",
+ "resolved": "10.0.5",
+ "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==",
"dependencies": {
- "Microsoft.Extensions.Configuration": "10.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4"
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5"
}
},
"Microsoft.Extensions.Configuration.FileExtensions": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "LD4T4s2uW2kZUkwGc4A9KK5o3wfkgySHKEiYqV0NXeNdeLN563NgNqDpi3DNXAdrt2TwU0rK7QMPdWLLIaMipA==",
+ "resolved": "10.0.5",
+ "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==",
"dependencies": {
- "Microsoft.Extensions.Configuration": "10.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4",
- "Microsoft.Extensions.FileProviders.Physical": "10.0.4",
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5",
+ "Microsoft.Extensions.FileProviders.Physical": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "NkvJ8aSr3AG30yabjv7ZWwTG/wq5OElNTlNq39Ok2HSEF3TIwAc1f1xnTJlR/GuoJmEgkfT7WBO9YbSXRk41+g==",
+ "resolved": "10.0.5",
+ "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "SIe9zlVQJecnk/DTmevIcl6+aEDYhoVLc2eG2AKwVeNEC8CSyxHAbh4lf0xtHq9JUum0vVTEByGNTK+b6oihTQ=="
+ "resolved": "10.0.5",
+ "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA=="
},
"Microsoft.Extensions.DependencyInjection.AutoActivation": {
"type": "Transitive",
@@ -192,26 +192,26 @@
},
"Microsoft.Extensions.DependencyModel": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "LiJXylfk8pk+2zsUsITkou3QTFMJ8RNJ0oKKY0Oyjt6HJctGJwPw//ZgoNO4J29zKaT+dR4/PI2jW/znRcspLg=="
+ "resolved": "10.0.5",
+ "contentHash": "xA4kkL+QS6KCAOKz/O0oquHs44Ob8J7zpBCNt3wjkBWDg5aCqfwG8rWWLsg5V86AM0sB849g9JjPjIdksTCIKg=="
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "R9W7AttMedwOwJ7wRqTGBoVbX2JmlyWA+LJQUhizmS7Be9f6EJUn/+lvaIYDrOYtA1UzAfrwU871hpvZSPyIkg==",
+ "resolved": "10.0.5",
+ "contentHash": "vAJHd4yOpmKoK+jBuYV7a3y+Ab9U4ARCc29b6qvMy276RgJFw9LFs0DdsPqOL3ahwzyrX7tM+i4cCxU/RX0qAg==",
"dependencies": {
- "Microsoft.Extensions.Configuration": "10.0.4",
- "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.4",
- "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4"
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "JH2RyevIwJ1E9mBsZRXR+12TnUauptKgzCdOghhk3sE+dqcxB16GoE7x+0IuqTbaixM1ESXTNoqEw/IBnhM7LQ==",
+ "resolved": "10.0.5",
+ "contentHash": "/nYGrpa9/0BZofrVpBbbj+Ns8ZesiPE0V/KxsuHgDgHQopIzN54nRaQGSuvPw16/kI9sW1Zox5yyAPqvf0Jz6A==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4",
- "Microsoft.Extensions.Options": "10.0.4"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
}
},
"Microsoft.Extensions.Diagnostics.ExceptionSummarization": {
@@ -229,26 +229,26 @@
},
"Microsoft.Extensions.FileProviders.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "3hLXFZ1E/Kj3obIcb9iMCC95MpW2e8EkWxpXKgUfgGBfm+yn507pHAjPaHoi2U3GlSHIm/21DPCDLumwlMowjw==",
+ "resolved": "10.0.5",
+ "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==",
"dependencies": {
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.FileProviders.Physical": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "gVVHdOFwlnXmTtx41e2aGfcFXX+8+9DPkOzEqQuHN8rOv+6RQWs/wfeQLaosOt3CQLKNoCaFmHopTtGB9PB5fg==",
+ "resolved": "10.0.5",
+ "contentHash": "dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==",
"dependencies": {
- "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4",
- "Microsoft.Extensions.FileSystemGlobbing": "10.0.4",
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5",
+ "Microsoft.Extensions.FileSystemGlobbing": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.FileSystemGlobbing": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "eCEFVuuZL++SqMcdB5i4KA16GvcxCzdKKK+clapXYyGMkhd4BxwZi2/vGzo8s7a8Vi0BA78p5u/NScgOP1pzTg=="
+ "resolved": "10.0.5",
+ "contentHash": "mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw=="
},
"Microsoft.Extensions.Http": {
"type": "Transitive",
@@ -274,12 +274,12 @@
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "S8+6fCuMOhJZGk8sGFtOy3VsF9mk9x4UOL59GM91REiA/fmCDjunKKIw4RmStG87qyXPfxelDJf2pXIbTuaBdw==",
+ "resolved": "10.0.5",
+ "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection": "10.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "10.0.4",
- "Microsoft.Extensions.Options": "10.0.4"
+ "Microsoft.Extensions.DependencyInjection": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
}
},
"Microsoft.Extensions.Logging.Configuration": {
@@ -304,29 +304,29 @@
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "kRxa2Zjzhg/ohh7EklpqQpBIcyQnC3meWxCcpZBn+0QWy/fY1DmDd45JiW8Vyrpj2J1RDtau5yRHiLZS/AoxUw==",
+ "resolved": "10.0.5",
+ "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4",
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "amQUITwSnkbMPxh/ngneNykz4UtytEOXo0M/pbwdBiQU57EAVvBV5PFI8r/dRastUj0yxHVwrH64N9ACR5SdGQ==",
+ "resolved": "10.0.5",
+ "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==",
"dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.Configuration.Binder": "10.0.4",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4",
- "Microsoft.Extensions.Options": "10.0.4",
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Configuration.Binder": "10.0.5",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "lABYqiRH9HgYJsjzO3W7+cucUwWXhEkiyrRylANdIubnzcESlkIsLowXpQ4E+sc7kjMLbk1hk5oxw4qTKowTEg=="
+ "resolved": "10.0.5",
+ "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g=="
},
"Microsoft.Extensions.Resilience": {
"type": "Transitive",
@@ -526,8 +526,8 @@
},
"Npgsql": {
"type": "Transitive",
- "resolved": "10.0.0",
- "contentHash": "xZAYhPOU2rUIFpV48xsqhCx9vXs6Y+0jX2LCoSEfDFYMw9jtAOUk3iQsCnDLrFIv9NT3JGMihn7nnuZsPKqJmA==",
+ "resolved": "10.0.2",
+ "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "10.0.0"
}
@@ -668,8 +668,8 @@
"type": "Project",
"dependencies": {
"Grpc.AspNetCore": "[2.76.0, )",
- "Microsoft.AspNetCore.Authentication.JwtBearer": "[10.0.4, )",
- "Microsoft.AspNetCore.OpenApi": "[10.0.4, )",
+ "Microsoft.AspNetCore.Authentication.JwtBearer": "[10.0.5, )",
+ "Microsoft.AspNetCore.OpenApi": "[10.0.5, )",
"Microsoft.IdentityModel.JsonWebTokens": "[8.16.0, )",
"Serilog.AspNetCore": "[10.0.0, )",
"Serilog.Sinks.Console": "[6.1.1, )",
@@ -685,8 +685,8 @@
"type": "Project",
"dependencies": {
"Google.Protobuf": "[3.34.0, )",
- "Microsoft.AspNetCore.Authorization": "[10.0.4, )",
- "Microsoft.Extensions.Configuration.Json": "[10.0.4, )",
+ "Microsoft.AspNetCore.Authorization": "[10.0.5, )",
+ "Microsoft.Extensions.Configuration.Json": "[10.0.5, )",
"Microsoft.IdentityModel.Tokens": "[8.16.0, )",
"Werkr.Common.Configuration": "[1.0.0, )"
}
@@ -698,8 +698,8 @@
"type": "Project",
"dependencies": {
"Grpc.Net.Client": "[2.76.0, )",
- "Microsoft.Extensions.Hosting.Abstractions": "[10.0.4, )",
- "System.Security.Cryptography.ProtectedData": "[10.0.4, )",
+ "Microsoft.Extensions.Hosting.Abstractions": "[10.0.5, )",
+ "System.Security.Cryptography.ProtectedData": "[10.0.5, )",
"Werkr.Common": "[1.0.0, )",
"Werkr.Data": "[1.0.0, )"
}
@@ -708,9 +708,9 @@
"type": "Project",
"dependencies": {
"EFCore.NamingConventions": "[10.0.1, )",
- "Microsoft.EntityFrameworkCore": "[10.0.4, )",
- "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.4, )",
- "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.0, )",
+ "Microsoft.EntityFrameworkCore": "[10.0.5, )",
+ "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.5, )",
+ "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )",
"Werkr.Common": "[1.0.0, )"
}
},
@@ -771,104 +771,110 @@
"Microsoft.Extensions.Http": "8.0.0"
}
},
+ "Grpc.Tools": {
+ "type": "CentralTransitive",
+ "requested": "[2.78.0, )",
+ "resolved": "2.78.0",
+ "contentHash": "6jPG2gHon+w2PczW8jjrCRnW/g9eEfCdd7aK6mDooptWtuPsV3ZxAwKKEx7LGEDVoT4c2SViRl8Yu3L1XiWIIg=="
+ },
"Microsoft.AspNetCore.Authentication.JwtBearer": {
"type": "CentralTransitive",
- "requested": "[10.0.4, )",
- "resolved": "10.0.4",
- "contentHash": "1mUwGyeGrGETdhlU/ZzNZeN2J6ugqf9EztZyG+WQIZwkaH5+lFNza15+cNCXhXNA0MKo1iohr5vj60eqR2J44Q==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "fZzXogChrwQ/SfifQJgeW7AtR8hUv5+LH9oLWjm5OqfnVt3N8MwcMHHMdawvqqdjP79lIZgetnSpj77BLsSI1g==",
"dependencies": {
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1"
}
},
"Microsoft.AspNetCore.Authorization": {
"type": "CentralTransitive",
- "requested": "[10.0.4, )",
- "resolved": "10.0.4",
- "contentHash": "uCg18hZTfzMEft8uxgPTM4s0sXsETfTnAJ00yR0LD6/ABz6NeEq1RMPIOpkTqbipatw+eNWKqDOV4Gus5OGjAQ==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "NbFi4wN6fUvZK4AKmixpfx0IvqtVimKEn8ZX28LkzZBVo09YnLbyRrJ1001IVQDLbV+aYpS/cLhVJu5JD0rY5A==",
"dependencies": {
- "Microsoft.AspNetCore.Metadata": "10.0.4",
- "Microsoft.Extensions.Diagnostics": "10.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "10.0.4",
- "Microsoft.Extensions.Options": "10.0.4"
+ "Microsoft.AspNetCore.Metadata": "10.0.5",
+ "Microsoft.Extensions.Diagnostics": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
}
},
"Microsoft.AspNetCore.OpenApi": {
"type": "CentralTransitive",
- "requested": "[10.0.4, )",
- "resolved": "10.0.4",
- "contentHash": "OsEhbmT4Xenukau5YCR867gr/HmuAJ9DqMBPQGTcmdNU/TqBqdcnB+yLNwD/mTdkHzLBB+XG7cI4H1L5B1jx+Q==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "vTcxIfOPyfFbYk1g8YcXJfkMnlEWVkSnnjxcZLy60zgwiHMRf2SnZR+9E4HlpwKxgE3yfKMOti8J6WfKuKsw6w==",
"dependencies": {
"Microsoft.OpenApi": "2.0.0"
}
},
"Microsoft.Data.Sqlite.Core": {
"type": "CentralTransitive",
- "requested": "[10.0.3, )",
- "resolved": "10.0.4",
- "contentHash": "UkmpN2pDkrtVLh+ypRDCbBij9mhPqOPzvHI625rf+VeT3FHnBwBjAY7XgjK8rGDI74fDx7C1SSIf2OAaAX4g2A==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "jFYXnh7s0RShCw6Vkf+ReGCw+mVi7ISg1YaEzYCJcXnUifmbW+aqvCsRJuSRj2ZuQ+oqetpjxlZtbpMmk5FKqQ==",
"dependencies": {
"SQLitePCLRaw.core": "2.1.11"
}
},
"Microsoft.EntityFrameworkCore": {
"type": "CentralTransitive",
- "requested": "[10.0.4, )",
- "resolved": "10.0.4",
- "contentHash": "kzTsfFK2GCytp6DDTfQOmxPU4gbGdrIlP7PxrxF3ESNLtfXrC8BoUVZENBN2WORlZPAD7CVX6AYIglgkpXQooA==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "9tNBmK3EpYVGRQLiqP+bqK2m+TD0Gv//4vCzR7ZOgl4FWzCFyOpYdIVka13M4kcBdPdSJcs3wbHr3rmzOqbIMA==",
"dependencies": {
- "Microsoft.EntityFrameworkCore.Abstractions": "10.0.4",
- "Microsoft.EntityFrameworkCore.Analyzers": "10.0.4",
- "Microsoft.Extensions.Caching.Memory": "10.0.4",
- "Microsoft.Extensions.Logging": "10.0.4"
+ "Microsoft.EntityFrameworkCore.Abstractions": "10.0.5",
+ "Microsoft.EntityFrameworkCore.Analyzers": "10.0.5",
+ "Microsoft.Extensions.Caching.Memory": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5"
}
},
"Microsoft.EntityFrameworkCore.Sqlite": {
"type": "CentralTransitive",
- "requested": "[10.0.4, )",
- "resolved": "10.0.4",
- "contentHash": "cbc/Ave31CbPQ9E29dfaA4QjcsBoc8KokNlLC6Noj0uToHDQ9PPllD+k6HluVbaFpflsU8XGwrQxOoyvXlU97g==",
- "dependencies": {
- "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.4",
- "Microsoft.Extensions.Caching.Memory": "10.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.DependencyModel": "10.0.4",
- "Microsoft.Extensions.Logging": "10.0.4",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "lxeRviglTkkmzYJVJ600yb6gJjnf5za9v7uH+0byuSXTGv7U8cT6hz7qRTmiGSOfLcl86QFdy2BBKaUFd6NQug==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.5",
+ "Microsoft.Extensions.Caching.Memory": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.DependencyModel": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5",
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.11",
"SQLitePCLRaw.core": "2.1.11"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "CentralTransitive",
- "requested": "[10.0.3, )",
- "resolved": "10.0.4",
- "contentHash": "3x9X9SMAMdAoEwWxHfsT2a9dTBqEtfYfbEOFw+UPtBshEH2gHWJeazxrZ1FK1O18MoCbe1NxINg5qciB01pEcg==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==",
"dependencies": {
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.Configuration.Json": {
"type": "CentralTransitive",
- "requested": "[10.0.4, )",
- "resolved": "10.0.4",
- "contentHash": "gn2Rf0dvIa6Sz/WJ5cNHhG/oUOT1yrHXd7Q0vCpXDlLsMuRqv9G5NBXFJbSh/ZRzSbvbOQWMV0amQS/3N0Fzzg==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==",
"dependencies": {
- "Microsoft.Extensions.Configuration": "10.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.Configuration.FileExtensions": "10.0.4",
- "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4"
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5"
}
},
"Microsoft.Extensions.Hosting.Abstractions": {
"type": "CentralTransitive",
- "requested": "[10.0.4, )",
- "resolved": "10.0.4",
- "contentHash": "+5mQrqlBhqNUaPyDmFSNM/qiWStvE9LMxZW1MRF0NhEbO781xNeKryXNR9gGDJ0PmYFDAVoMT9ffX+I15LPFTw==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "+Wb7KAMVZTomwJkQrjuPTe5KBzGod7N8XeG+ScxRlkPOB4sZLG4ccVwjV4Phk5BCJt7uIMnGHVoN6ZMVploX+g==",
"dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4",
- "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.4",
- "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "10.0.4"
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5"
}
},
"Microsoft.Extensions.Http.Resilience": {
@@ -884,11 +890,11 @@
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "CentralTransitive",
- "requested": "[10.0.3, )",
- "resolved": "10.0.4",
- "contentHash": "PDMMt7fvBatv6hcxxyJtXIzSwn7Dy00W6I2vDAOTYrQqNM2dF5A2L9n0uMzdPz2IPoNZWkAmYjoOCEdDLq0i4w==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5"
}
},
"Microsoft.Extensions.ServiceDiscovery": {
@@ -922,13 +928,13 @@
},
"Npgsql.EntityFrameworkCore.PostgreSQL": {
"type": "CentralTransitive",
- "requested": "[10.0.0, )",
- "resolved": "10.0.0",
- "contentHash": "E2+uSWxSB8LdsUVwPaqRWOcGOP92biry2JEwc0KJMdLJF+aZdczeIdEXVwEyv4nSVMQJH0o8tLhyAMiR6VF0lw==",
+ "requested": "[10.0.1, )",
+ "resolved": "10.0.1",
+ "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==",
"dependencies": {
- "Microsoft.EntityFrameworkCore": "[10.0.0, 11.0.0)",
- "Microsoft.EntityFrameworkCore.Relational": "[10.0.0, 11.0.0)",
- "Npgsql": "10.0.0"
+ "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)",
+ "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)",
+ "Npgsql": "10.0.2"
}
},
"OpenTelemetry.Exporter.OpenTelemetryProtocol": {
@@ -997,18 +1003,18 @@
"System.IdentityModel.Tokens.Jwt": {
"type": "CentralTransitive",
"requested": "[8.16.0, )",
- "resolved": "8.0.1",
- "contentHash": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==",
+ "resolved": "8.16.0",
+ "contentHash": "rrs2u7DRMXQG2yh0oVyF/vLwosfRv20Ld2iEpYcKwQWXHjfV+gFXNQsQ9p008kR9Ou4pxBs68Q6/9zC8Gi1wjg==",
"dependencies": {
- "Microsoft.IdentityModel.JsonWebTokens": "8.0.1",
- "Microsoft.IdentityModel.Tokens": "8.0.1"
+ "Microsoft.IdentityModel.JsonWebTokens": "8.16.0",
+ "Microsoft.IdentityModel.Tokens": "8.16.0"
}
},
"System.Security.Cryptography.ProtectedData": {
"type": "CentralTransitive",
- "requested": "[10.0.4, )",
- "resolved": "10.0.4",
- "contentHash": "LmDnaYkcWkZSlZ07L7YcB6bH8sCiBZ7j28kPbYiXdF6f0iUiN7rxsRORlZGdj5saN/wZIqvF7lDBn/cpjU+e2g=="
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "kxR4O/8o32eNN3m4qbLe3UifYqeyEpallCyVAsLvL5ZFJVyT3JCb+9du/WHfC09VyJh1Q+p/Gd4+AwM7Rz4acg=="
}
}
}
diff --git a/src/Test/Werkr.Tests.Server/Authorization/PageAuthorizationTests.cs b/src/Test/Werkr.Tests.Server/Authorization/PageAuthorizationTests.cs
index c648a1a..7422096 100644
--- a/src/Test/Werkr.Tests.Server/Authorization/PageAuthorizationTests.cs
+++ b/src/Test/Werkr.Tests.Server/Authorization/PageAuthorizationTests.cs
@@ -45,7 +45,11 @@ public class PageAuthorizationTests {
["/tasks/create"] = string.Empty,
["/tasks/{Id:long}"] = string.Empty,
["/workflows"] = string.Empty,
- ["/workflows/create"] = string.Empty,
+ ["/workflows/create"] = "Admin,Operator",
+ ["/workflows/new/dag-editor"] = "Admin,Operator",
+ ["/workflows/new/edit"] = "Admin,Operator",
+ ["/workflows/{Id:long}/dag-editor"] = "Admin,Operator",
+ ["/workflows/{Id:long}/edit"] = "Admin,Operator",
["/workflows/{Id:long}"] = string.Empty,
["/workflows/{WorkflowId:long}/runs"] = string.Empty,
["/workflows/runs/{RunId:guid}"] = string.Empty,
diff --git a/src/Test/Werkr.Tests.Server/Components/ActionParameterEditorTests.cs b/src/Test/Werkr.Tests.Server/Components/ActionParameterEditorTests.cs
index 01d9745..4bfff68 100644
--- a/src/Test/Werkr.Tests.Server/Components/ActionParameterEditorTests.cs
+++ b/src/Test/Werkr.Tests.Server/Components/ActionParameterEditorTests.cs
@@ -37,15 +37,15 @@ public void Renders_Optgroups_For_Each_Category( ) {
}
///
- /// Verifies that all 27 action options appear in the dropdown.
+ /// Verifies that all 31 action options appear in the dropdown.
///
[TestMethod]
- public void Renders_All_TwentySeven_Actions_In_Dropdown( ) {
+ public void Renders_All_ThirtyOne_Actions_In_Dropdown( ) {
IRenderedComponent cut = Render( );
// All
public async Task> GetAllAsync( CancellationToken ct = default ) {
List dbSchedules = await _db.Schedules.ToListAsync( ct );
- List result = new( dbSchedules.Count );
- foreach (DbSchedule dbSchedule in dbSchedules) {
- result.Add( await BuildComposite(
- dbSchedule,
- ct
- ) );
+ return await BuildCompositeBatch( dbSchedules, ct );
+ }
+
+ ///
+ /// Loads multiple composites by their IDs in batch queries
+ /// (one query per entity type instead of per schedule).
+ ///
+ public async Task> GetByIdsAsync(
+ IEnumerable scheduleIds,
+ CancellationToken ct = default
+ ) {
+ List ids = [.. scheduleIds];
+ if (ids.Count == 0) {
+ return [];
}
- return result;
+
+ List dbSchedules = await _db.Schedules
+ .Where(s => ids.Contains(s.Id))
+ .ToListAsync(ct);
+
+ return await BuildCompositeBatch( dbSchedules, ct );
}
///
@@ -378,6 +391,81 @@ public async Task PreviewOccurrencesAsync(
schedule, windowEnd, holidayDates, schedule.HolidayCalendarMode );
}
+ ///
+ /// Batch-assembles composites from multiple entities,
+ /// loading all sub-entities with one query per entity type.
+ ///
+ private async Task> BuildCompositeBatch(
+ List dbSchedules,
+ CancellationToken ct
+ ) {
+ if (dbSchedules.Count == 0) {
+ return [];
+ }
+
+ List ids = [.. dbSchedules.Select(s => s.Id)];
+
+ Dictionary starts = await _db.StartDateTimeInfos
+ .Where(e => ids.Contains(e.ScheduleId))
+ .ToDictionaryAsync(e => e.ScheduleId, ct);
+
+ Dictionary expirations = await _db.ExpirationDateTimeInfos
+ .Where(e => ids.Contains(e.ScheduleId))
+ .ToDictionaryAsync(e => e.ScheduleId, ct);
+
+ Dictionary repeatOpts = await _db.ScheduleRepeatOptions
+ .Where(e => ids.Contains(e.ScheduleId))
+ .ToDictionaryAsync(e => e.ScheduleId, ct);
+
+ Dictionary dailies = await _db.DailyRecurrences
+ .Where(e => ids.Contains(e.ScheduleId))
+ .ToDictionaryAsync(e => e.ScheduleId, ct);
+
+ Dictionary weeklies = await _db.WeeklyRecurrences
+ .Where(e => ids.Contains(e.ScheduleId))
+ .ToDictionaryAsync(e => e.ScheduleId, ct);
+
+ Dictionary monthlies = await _db.MonthlyRecurrences
+ .Where(e => ids.Contains(e.ScheduleId))
+ .ToDictionaryAsync(e => e.ScheduleId, ct);
+
+ Dictionary holidayLinks = await _db.ScheduleHolidayCalendars
+ .Where(shc => ids.Contains(shc.ScheduleId))
+ .ToDictionaryAsync(shc => shc.ScheduleId, ct);
+
+ // Load any referenced holiday calendars
+ List calendarIds = [.. holidayLinks.Values.Select(l => l.HolidayCalendarId).Distinct()];
+ Dictionary calendars = calendarIds.Count > 0
+ ? await _db.HolidayCalendars
+ .Where(hc => calendarIds.Contains(hc.Id))
+ .ToDictionaryAsync(hc => hc.Id, ct)
+ : [];
+
+ List result = new(dbSchedules.Count);
+ foreach (DbSchedule db in dbSchedules) {
+ Guid id = db.Id;
+ Schedule schedule = new()
+ {
+ DbSchedule = db,
+ StartDateTime = starts.GetValueOrDefault(id),
+ Expiration = expirations.GetValueOrDefault(id),
+ RepeatOptions = repeatOpts.GetValueOrDefault(id),
+ DailyRecurrence = dailies.GetValueOrDefault(id),
+ WeeklyRecurrence = weeklies.GetValueOrDefault(id),
+ MonthlyRecurrence = monthlies.GetValueOrDefault(id),
+ };
+
+ if (holidayLinks.TryGetValue( id, out ScheduleHolidayCalendar? link )) {
+ schedule.HolidayCalendarMode = link.Mode;
+ _ = calendars.TryGetValue( link.HolidayCalendarId, out HolidayCalendar? cal );
+ schedule.HolidayCalendar = cal;
+ }
+
+ result.Add( schedule );
+ }
+ return result;
+ }
+
///
/// Assembles a composite from a and its sub-entities.
/// Follows the reference code's pattern of loading each sub-entity by FK.
diff --git a/src/Werkr.Core/Workflows/WorkflowService.cs b/src/Werkr.Core/Workflows/WorkflowService.cs
index a83866b..f0bc41f 100644
--- a/src/Werkr.Core/Workflows/WorkflowService.cs
+++ b/src/Werkr.Core/Workflows/WorkflowService.cs
@@ -1,5 +1,7 @@
using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Logging;
+using Werkr.Common.Models;
using Werkr.Data;
using Werkr.Data.Entities.Workflows;
@@ -75,6 +77,7 @@ public async Task UpdateAsync(
existing.Name = workflow.Name;
existing.Description = workflow.Description;
existing.Enabled = workflow.Enabled;
+ existing.TargetTags = workflow.TargetTags;
_ = await dbContext.SaveChangesAsync( ct );
@@ -227,6 +230,8 @@ public async Task UpdateStepAsync(
existing.MaxIterations = step.MaxIterations;
existing.AgentConnectionIdOverride = step.AgentConnectionIdOverride;
existing.DependencyMode = step.DependencyMode;
+ existing.InputVariableName = step.InputVariableName;
+ existing.OutputVariableName = step.OutputVariableName;
_ = await dbContext.SaveChangesAsync( ct );
return existing;
@@ -466,6 +471,202 @@ public async Task>> GetTopologicalLeve
: (IReadOnlyList>)levels;
}
+ ///
+ /// Atomically applies a batch of step add/update/delete operations and dependency changes
+ /// within a single database transaction. Validates the resulting DAG before committing.
+ ///
+ /// The workflow to apply changes to.
+ /// The batch request containing all operations.
+ /// Cancellation token.
+ /// Batch response with temp-to-real ID mappings and validation results.
+ public async Task BatchUpdateStepsAsync(
+ long workflowId,
+ WorkflowStepBatchRequest request,
+ CancellationToken ct = default
+ ) {
+ if (request.Operations.Count == 0) {
+ return new WorkflowStepBatchResponse( true, [], [] );
+ }
+
+ // Verify workflow exists
+ bool workflowExists = await dbContext.Workflows.AnyAsync(
+ w => w.Id == workflowId,
+ ct
+ );
+ if (!workflowExists) {
+ return new WorkflowStepBatchResponse( false, [], [$"Workflow with Id={workflowId} was not found."] );
+ }
+
+ // Verify all positive StepIds belong to this workflow
+ List positiveStepIds = [.. request.Operations
+ .Where( o => o.StepId > 0 )
+ .Select( o => o.StepId )
+ .Distinct( )];
+
+ if (positiveStepIds.Count > 0) {
+ List existingIds = await dbContext.WorkflowSteps
+ .Where( s => s.WorkflowId == workflowId && positiveStepIds.Contains( s.Id ) )
+ .Select( s => s.Id )
+ .ToListAsync( ct );
+
+ List invalid = [.. positiveStepIds.Except( existingIds )];
+ if (invalid.Count > 0) {
+ return new WorkflowStepBatchResponse( false, [],
+ [$"Step IDs do not belong to workflow {workflowId}: {string.Join( ", ", invalid )}"] );
+ }
+ }
+
+ await using IDbContextTransaction tx = await dbContext.Database.BeginTransactionAsync( ct );
+ try {
+ Dictionary tempToReal = [];
+ List mappings = [];
+
+ // ── Phase 1: Process "Add" operations ──
+ List adds = [.. request.Operations.Where( o => string.Equals( o.OperationType, "Add", StringComparison.OrdinalIgnoreCase ) )];
+
+ foreach (StepBatchOperation add in adds) {
+ if (add.TaskId is null) {
+ await tx.RollbackAsync( ct );
+ return new WorkflowStepBatchResponse( false, [],
+ [$"Add operation for temp step {add.StepId} is missing TaskId."] );
+ }
+
+ WorkflowStep step = new( ) {
+ WorkflowId = workflowId,
+ TaskId = add.TaskId.Value,
+ Order = add.Order,
+ ControlStatement = Enum.Parse( add.ControlStatement, ignoreCase: true ),
+ ConditionExpression = add.ConditionExpression,
+ MaxIterations = add.MaxIterations,
+ AgentConnectionIdOverride = add.AgentConnectionIdOverride,
+ DependencyMode = Enum.Parse( add.DependencyMode, ignoreCase: true ),
+ InputVariableName = add.InputVariableName,
+ OutputVariableName = add.OutputVariableName,
+ };
+
+ _ = dbContext.WorkflowSteps.Add( step );
+ _ = await dbContext.SaveChangesAsync( ct );
+
+ tempToReal[add.StepId] = step.Id;
+ mappings.Add( new StepIdMapping( add.StepId, step.Id ) );
+
+ if (logger.IsEnabled( LogLevel.Debug )) {
+ logger.LogDebug( "Batch: created step {RealId} (temp {TempId}) in workflow {WorkflowId}.",
+ step.Id.ToString( ),
+ add.StepId.ToString( ),
+ workflowId.ToString( )
+ );
+ }
+ }
+
+ // Helper to resolve temp IDs to real IDs
+ long ResolveId( long id ) => id < 0 && tempToReal.TryGetValue( id, out long real ) ? real : id;
+
+ // ── Phase 2: Process "Update" operations ──
+ List updates = [.. request.Operations.Where( o => string.Equals( o.OperationType, "Update", StringComparison.OrdinalIgnoreCase ) )];
+
+ foreach (StepBatchOperation update in updates) {
+ long realId = ResolveId( update.StepId );
+ WorkflowStep existing = await dbContext.WorkflowSteps.FirstOrDefaultAsync(
+ s => s.Id == realId,
+ ct
+ ) ?? throw new KeyNotFoundException( $"Step {realId} not found during batch update." );
+
+ if (update.TaskId is not null) {
+ existing.TaskId = update.TaskId.Value;
+ }
+ existing.Order = update.Order;
+ existing.ControlStatement = Enum.Parse( update.ControlStatement, ignoreCase: true );
+ existing.ConditionExpression = update.ConditionExpression;
+ existing.MaxIterations = update.MaxIterations;
+ existing.AgentConnectionIdOverride = update.AgentConnectionIdOverride;
+ existing.DependencyMode = Enum.Parse( update.DependencyMode, ignoreCase: true );
+ existing.InputVariableName = update.InputVariableName;
+ existing.OutputVariableName = update.OutputVariableName;
+ }
+
+ // ── Phase 3: Process dependency changes ──
+ foreach (StepBatchOperation op in request.Operations) {
+ if (op.DependencyChanges is null) {
+ continue;
+ }
+
+ long realStepId = ResolveId( op.StepId );
+
+ foreach (DependencyBatchItem depChange in op.DependencyChanges) {
+ long realDepId = ResolveId( depChange.DependsOnStepId );
+
+ if (string.Equals( depChange.OperationType, "Add", StringComparison.OrdinalIgnoreCase )) {
+ if (realStepId == realDepId) {
+ continue;
+ }
+
+ bool alreadyExists = await dbContext.WorkflowStepDependencies
+ .AnyAsync( d => d.StepId == realStepId && d.DependsOnStepId == realDepId, ct );
+ if (alreadyExists) {
+ continue;
+ }
+
+ _ = dbContext.WorkflowStepDependencies.Add( new WorkflowStepDependency {
+ StepId = realStepId,
+ DependsOnStepId = realDepId,
+ } );
+ } else if (string.Equals( depChange.OperationType, "Delete", StringComparison.OrdinalIgnoreCase )) {
+ WorkflowStepDependency? dep = await dbContext.WorkflowStepDependencies
+ .FirstOrDefaultAsync( d => d.StepId == realStepId && d.DependsOnStepId == realDepId, ct );
+ if (dep is not null) {
+ _ = dbContext.WorkflowStepDependencies.Remove( dep );
+ }
+ }
+ }
+ }
+
+ // ── Phase 4: Process "Delete" operations (after deps cleaned up) ──
+ List deletes = [.. request.Operations.Where( o => string.Equals( o.OperationType, "Delete", StringComparison.OrdinalIgnoreCase ) )];
+
+ foreach (StepBatchOperation delete in deletes) {
+ long realId = ResolveId( delete.StepId );
+ WorkflowStep? step = await dbContext.WorkflowSteps
+ .FirstOrDefaultAsync( s => s.Id == realId, ct );
+ if (step is not null) {
+ // Remove dependencies first
+ List deps = await dbContext.WorkflowStepDependencies
+ .Where( d => d.StepId == realId || d.DependsOnStepId == realId )
+ .ToListAsync( ct );
+ dbContext.WorkflowStepDependencies.RemoveRange( deps );
+ _ = dbContext.WorkflowSteps.Remove( step );
+ }
+ }
+
+ _ = await dbContext.SaveChangesAsync( ct );
+
+ // ── Phase 5: Validate resulting DAG ──
+ try {
+ _ = await ValidateDagAsync( workflowId, ct );
+ } catch (InvalidOperationException ex) {
+ await tx.RollbackAsync( ct );
+ return new WorkflowStepBatchResponse( false, [], [ex.Message] );
+ }
+
+ await tx.CommitAsync( ct );
+
+ if (logger.IsEnabled( LogLevel.Information )) {
+ logger.LogInformation(
+ "Batch completed for workflow {WorkflowId}: {AddCount} adds, {UpdateCount} updates, {DeleteCount} deletes.",
+ workflowId.ToString( ),
+ adds.Count.ToString( ),
+ updates.Count.ToString( ),
+ deletes.Count.ToString( )
+ );
+ }
+
+ return new WorkflowStepBatchResponse( true, mappings, [] );
+ } catch (Exception ex) when (ex is not InvalidOperationException) {
+ await tx.RollbackAsync( ct );
+ return new WorkflowStepBatchResponse( false, [], [ex.Message] );
+ }
+ }
+
/// Validates control flow constraints on a topologically sorted step list.
private static void ValidateControlFlow( List sorted ) {
HashSet processedIds = [];
diff --git a/src/Werkr.Core/packages.lock.json b/src/Werkr.Core/packages.lock.json
index 5dd923f..f613895 100644
--- a/src/Werkr.Core/packages.lock.json
+++ b/src/Werkr.Core/packages.lock.json
@@ -20,22 +20,22 @@
},
"Microsoft.Extensions.Hosting.Abstractions": {
"type": "Direct",
- "requested": "[10.0.4, )",
- "resolved": "10.0.4",
- "contentHash": "+5mQrqlBhqNUaPyDmFSNM/qiWStvE9LMxZW1MRF0NhEbO781xNeKryXNR9gGDJ0PmYFDAVoMT9ffX+I15LPFTw==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "+Wb7KAMVZTomwJkQrjuPTe5KBzGod7N8XeG+ScxRlkPOB4sZLG4ccVwjV4Phk5BCJt7uIMnGHVoN6ZMVploX+g==",
"dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4",
- "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.4",
- "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "10.0.4"
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5"
}
},
"System.Security.Cryptography.ProtectedData": {
"type": "Direct",
- "requested": "[10.0.4, )",
- "resolved": "10.0.4",
- "contentHash": "LmDnaYkcWkZSlZ07L7YcB6bH8sCiBZ7j28kPbYiXdF6f0iUiN7rxsRORlZGdj5saN/wZIqvF7lDBn/cpjU+e2g=="
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "kxR4O/8o32eNN3m4qbLe3UifYqeyEpallCyVAsLvL5ZFJVyT3JCb+9du/WHfC09VyJh1Q+p/Gd4+AwM7Rz4acg=="
},
"Grpc.Core.Api": {
"type": "Transitive",
@@ -52,189 +52,189 @@
},
"Microsoft.AspNetCore.Metadata": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "levTTo69d5gYARtSzRV4wMvD1YUtkWvI/fOiTEG+k4v1WEEFBxrHLdUVEvxCfiuCb1Y1XuPAbbBsKQi9wNtU3w=="
+ "resolved": "10.0.5",
+ "contentHash": "nXVB1K4RzyhDHKYWLiq3+aJopJZKO5ojFqHV9PZ74fe4VWM/8itoouqsd2KIqSooIwQ13UDNlPQfN2rWr7hc2A=="
},
"Microsoft.EntityFrameworkCore.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "qDcJqCfN1XYyX0ID/Hd9/kQTRvlia8S+Yuwyl9uFhBIKnOCbl9WMdGQCzbZUKbkpkfvf3P9CDdXsnxHyE3O0Aw=="
+ "resolved": "10.0.5",
+ "contentHash": "32c58Rnm47Qvhimawf67KO9PytgPz3QoWye7Abapt0Yocw/JnzMiSNj/pRoIKyn8Jxypkv86zxKD4Q/zNTc0Ag=="
},
"Microsoft.EntityFrameworkCore.Analyzers": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "pQeMHCyD3yTtCEGnHV4VsgKUvrESo3MR5mnh8sgQ1hWYmI1YFsUutDowBIxkobeWRtaRmBqQAtF7XQFW6FWuNA=="
+ "resolved": "10.0.5",
+ "contentHash": "ipC4u1VojgEfoIZhtbS2Sx5IluJTP/Jf1hz3yGsxGBgSukYY/CquI6rAjxn5H58CZgVn36qcuPPtNMwZ0AUzMg=="
},
"Microsoft.EntityFrameworkCore.Relational": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "DOTjTHy93W3TwpMLM4SCm0n57Sc0Jj3+m2S6LSTstKyBB34eT1UouaMS19mpWwvtj42+sRiEjA3+rOTNoNzXFQ==",
+ "resolved": "10.0.5",
+ "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==",
"dependencies": {
- "Microsoft.EntityFrameworkCore": "10.0.4",
- "Microsoft.Extensions.Caching.Memory": "10.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.Logging": "10.0.4"
+ "Microsoft.EntityFrameworkCore": "10.0.5",
+ "Microsoft.Extensions.Caching.Memory": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5"
}
},
"Microsoft.EntityFrameworkCore.Sqlite.Core": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "8D3Kk7assWpi93DuicgucDqGoOsgEgLlZy8io0FUlSGG2b4wkRWkjXn4xFBX+BzxExjfcYvHhtcBpqkXhe2p0A==",
- "dependencies": {
- "Microsoft.Data.Sqlite.Core": "10.0.4",
- "Microsoft.EntityFrameworkCore.Relational": "10.0.4",
- "Microsoft.Extensions.Caching.Memory": "10.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.DependencyModel": "10.0.4",
- "Microsoft.Extensions.Logging": "10.0.4",
+ "resolved": "10.0.5",
+ "contentHash": "rVH43bcUyZiMn0SnCpVnvFpl4PFxT4GwmuVVLcT4JL0NtzuHY9ymKV+Llb5cjuJ+6+gEl4eixy2rE8nxOPcBSA==",
+ "dependencies": {
+ "Microsoft.Data.Sqlite.Core": "10.0.5",
+ "Microsoft.EntityFrameworkCore.Relational": "10.0.5",
+ "Microsoft.Extensions.Caching.Memory": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.DependencyModel": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5",
"SQLitePCLRaw.core": "2.1.11"
}
},
"Microsoft.Extensions.Caching.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "uDRooaV6N3WZ0kdlNPMB68/MdGn/in1Fs7Db7DnIm85RBTPy4P321WO+daAImiYpH5dekjNggDqy1N44WaIlMA==",
+ "resolved": "10.0.5",
+ "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==",
"dependencies": {
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.Caching.Memory": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "CLLussNUMdSbyJOu4VBF7sqskHGB/5N1EcFzrqG/HsPATN8fCRUcfp0qns1VwkxKHwxrtYCh5FKe+kM81Q1PHA==",
+ "resolved": "10.0.5",
+ "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==",
"dependencies": {
- "Microsoft.Extensions.Caching.Abstractions": "10.0.4",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "10.0.4",
- "Microsoft.Extensions.Options": "10.0.4",
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.Caching.Abstractions": "10.0.5",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "601B3ha6XvOsOcu9GVd2dVd1KEDuqr49r46GUWhNJkeZDhZ/NI9EYTyoeQjZQEi8ZUvnrv++FbTfGmC8F0vgtg==",
+ "resolved": "10.0.5",
+ "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==",
"dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "ilnL/kQn62Gx3OZCVT7SJrBNi0CRIhS8VEunmE6i/a9lp9l/eos+hpxMvCW4iX2aVc/NWeDhxuQusZL7zvmKIg==",
+ "resolved": "10.0.5",
+ "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==",
"dependencies": {
- "Microsoft.Extensions.Configuration": "10.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4"
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5"
}
},
"Microsoft.Extensions.Configuration.FileExtensions": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "LD4T4s2uW2kZUkwGc4A9KK5o3wfkgySHKEiYqV0NXeNdeLN563NgNqDpi3DNXAdrt2TwU0rK7QMPdWLLIaMipA==",
+ "resolved": "10.0.5",
+ "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==",
"dependencies": {
- "Microsoft.Extensions.Configuration": "10.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4",
- "Microsoft.Extensions.FileProviders.Physical": "10.0.4",
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5",
+ "Microsoft.Extensions.FileProviders.Physical": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "NkvJ8aSr3AG30yabjv7ZWwTG/wq5OElNTlNq39Ok2HSEF3TIwAc1f1xnTJlR/GuoJmEgkfT7WBO9YbSXRk41+g==",
+ "resolved": "10.0.5",
+ "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "SIe9zlVQJecnk/DTmevIcl6+aEDYhoVLc2eG2AKwVeNEC8CSyxHAbh4lf0xtHq9JUum0vVTEByGNTK+b6oihTQ=="
+ "resolved": "10.0.5",
+ "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA=="
},
"Microsoft.Extensions.DependencyModel": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "LiJXylfk8pk+2zsUsITkou3QTFMJ8RNJ0oKKY0Oyjt6HJctGJwPw//ZgoNO4J29zKaT+dR4/PI2jW/znRcspLg=="
+ "resolved": "10.0.5",
+ "contentHash": "xA4kkL+QS6KCAOKz/O0oquHs44Ob8J7zpBCNt3wjkBWDg5aCqfwG8rWWLsg5V86AM0sB849g9JjPjIdksTCIKg=="
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "R9W7AttMedwOwJ7wRqTGBoVbX2JmlyWA+LJQUhizmS7Be9f6EJUn/+lvaIYDrOYtA1UzAfrwU871hpvZSPyIkg==",
+ "resolved": "10.0.5",
+ "contentHash": "vAJHd4yOpmKoK+jBuYV7a3y+Ab9U4ARCc29b6qvMy276RgJFw9LFs0DdsPqOL3ahwzyrX7tM+i4cCxU/RX0qAg==",
"dependencies": {
- "Microsoft.Extensions.Configuration": "10.0.4",
- "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.4",
- "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4"
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "JH2RyevIwJ1E9mBsZRXR+12TnUauptKgzCdOghhk3sE+dqcxB16GoE7x+0IuqTbaixM1ESXTNoqEw/IBnhM7LQ==",
+ "resolved": "10.0.5",
+ "contentHash": "/nYGrpa9/0BZofrVpBbbj+Ns8ZesiPE0V/KxsuHgDgHQopIzN54nRaQGSuvPw16/kI9sW1Zox5yyAPqvf0Jz6A==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4",
- "Microsoft.Extensions.Options": "10.0.4"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
}
},
"Microsoft.Extensions.FileProviders.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "3hLXFZ1E/Kj3obIcb9iMCC95MpW2e8EkWxpXKgUfgGBfm+yn507pHAjPaHoi2U3GlSHIm/21DPCDLumwlMowjw==",
+ "resolved": "10.0.5",
+ "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==",
"dependencies": {
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.FileProviders.Physical": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "gVVHdOFwlnXmTtx41e2aGfcFXX+8+9DPkOzEqQuHN8rOv+6RQWs/wfeQLaosOt3CQLKNoCaFmHopTtGB9PB5fg==",
+ "resolved": "10.0.5",
+ "contentHash": "dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==",
"dependencies": {
- "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4",
- "Microsoft.Extensions.FileSystemGlobbing": "10.0.4",
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5",
+ "Microsoft.Extensions.FileSystemGlobbing": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.FileSystemGlobbing": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "eCEFVuuZL++SqMcdB5i4KA16GvcxCzdKKK+clapXYyGMkhd4BxwZi2/vGzo8s7a8Vi0BA78p5u/NScgOP1pzTg=="
+ "resolved": "10.0.5",
+ "contentHash": "mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw=="
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "S8+6fCuMOhJZGk8sGFtOy3VsF9mk9x4UOL59GM91REiA/fmCDjunKKIw4RmStG87qyXPfxelDJf2pXIbTuaBdw==",
+ "resolved": "10.0.5",
+ "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection": "10.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "10.0.4",
- "Microsoft.Extensions.Options": "10.0.4"
+ "Microsoft.Extensions.DependencyInjection": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
}
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "kRxa2Zjzhg/ohh7EklpqQpBIcyQnC3meWxCcpZBn+0QWy/fY1DmDd45JiW8Vyrpj2J1RDtau5yRHiLZS/AoxUw==",
+ "resolved": "10.0.5",
+ "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4",
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "amQUITwSnkbMPxh/ngneNykz4UtytEOXo0M/pbwdBiQU57EAVvBV5PFI8r/dRastUj0yxHVwrH64N9ACR5SdGQ==",
+ "resolved": "10.0.5",
+ "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==",
"dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.Configuration.Binder": "10.0.4",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4",
- "Microsoft.Extensions.Options": "10.0.4",
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Configuration.Binder": "10.0.5",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5",
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
- "resolved": "10.0.4",
- "contentHash": "lABYqiRH9HgYJsjzO3W7+cucUwWXhEkiyrRylANdIubnzcESlkIsLowXpQ4E+sc7kjMLbk1hk5oxw4qTKowTEg=="
+ "resolved": "10.0.5",
+ "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g=="
},
"Microsoft.IdentityModel.Abstractions": {
"type": "Transitive",
@@ -251,8 +251,8 @@
},
"Npgsql": {
"type": "Transitive",
- "resolved": "10.0.0",
- "contentHash": "xZAYhPOU2rUIFpV48xsqhCx9vXs6Y+0jX2LCoSEfDFYMw9jtAOUk3iQsCnDLrFIv9NT3JGMihn7nnuZsPKqJmA==",
+ "resolved": "10.0.2",
+ "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "10.0.0"
}
@@ -288,8 +288,8 @@
"type": "Project",
"dependencies": {
"Google.Protobuf": "[3.34.0, )",
- "Microsoft.AspNetCore.Authorization": "[10.0.4, )",
- "Microsoft.Extensions.Configuration.Json": "[10.0.4, )",
+ "Microsoft.AspNetCore.Authorization": "[10.0.5, )",
+ "Microsoft.Extensions.Configuration.Json": "[10.0.5, )",
"Microsoft.IdentityModel.Tokens": "[8.16.0, )",
"Werkr.Common.Configuration": "[1.0.0, )"
}
@@ -301,9 +301,9 @@
"type": "Project",
"dependencies": {
"EFCore.NamingConventions": "[10.0.1, )",
- "Microsoft.EntityFrameworkCore": "[10.0.4, )",
- "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.4, )",
- "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.0, )",
+ "Microsoft.EntityFrameworkCore": "[10.0.5, )",
+ "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.5, )",
+ "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )",
"Werkr.Common": "[1.0.0, )"
}
},
@@ -326,80 +326,80 @@
},
"Microsoft.AspNetCore.Authorization": {
"type": "CentralTransitive",
- "requested": "[10.0.4, )",
- "resolved": "10.0.4",
- "contentHash": "uCg18hZTfzMEft8uxgPTM4s0sXsETfTnAJ00yR0LD6/ABz6NeEq1RMPIOpkTqbipatw+eNWKqDOV4Gus5OGjAQ==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "NbFi4wN6fUvZK4AKmixpfx0IvqtVimKEn8ZX28LkzZBVo09YnLbyRrJ1001IVQDLbV+aYpS/cLhVJu5JD0rY5A==",
"dependencies": {
- "Microsoft.AspNetCore.Metadata": "10.0.4",
- "Microsoft.Extensions.Diagnostics": "10.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "10.0.4",
- "Microsoft.Extensions.Options": "10.0.4"
+ "Microsoft.AspNetCore.Metadata": "10.0.5",
+ "Microsoft.Extensions.Diagnostics": "10.0.5",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Options": "10.0.5"
}
},
"Microsoft.Data.Sqlite.Core": {
"type": "CentralTransitive",
- "requested": "[10.0.3, )",
- "resolved": "10.0.4",
- "contentHash": "UkmpN2pDkrtVLh+ypRDCbBij9mhPqOPzvHI625rf+VeT3FHnBwBjAY7XgjK8rGDI74fDx7C1SSIf2OAaAX4g2A==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "jFYXnh7s0RShCw6Vkf+ReGCw+mVi7ISg1YaEzYCJcXnUifmbW+aqvCsRJuSRj2ZuQ+oqetpjxlZtbpMmk5FKqQ==",
"dependencies": {
"SQLitePCLRaw.core": "2.1.11"
}
},
"Microsoft.EntityFrameworkCore": {
"type": "CentralTransitive",
- "requested": "[10.0.4, )",
- "resolved": "10.0.4",
- "contentHash": "kzTsfFK2GCytp6DDTfQOmxPU4gbGdrIlP7PxrxF3ESNLtfXrC8BoUVZENBN2WORlZPAD7CVX6AYIglgkpXQooA==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "9tNBmK3EpYVGRQLiqP+bqK2m+TD0Gv//4vCzR7ZOgl4FWzCFyOpYdIVka13M4kcBdPdSJcs3wbHr3rmzOqbIMA==",
"dependencies": {
- "Microsoft.EntityFrameworkCore.Abstractions": "10.0.4",
- "Microsoft.EntityFrameworkCore.Analyzers": "10.0.4",
- "Microsoft.Extensions.Caching.Memory": "10.0.4",
- "Microsoft.Extensions.Logging": "10.0.4"
+ "Microsoft.EntityFrameworkCore.Abstractions": "10.0.5",
+ "Microsoft.EntityFrameworkCore.Analyzers": "10.0.5",
+ "Microsoft.Extensions.Caching.Memory": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5"
}
},
"Microsoft.EntityFrameworkCore.Sqlite": {
"type": "CentralTransitive",
- "requested": "[10.0.4, )",
- "resolved": "10.0.4",
- "contentHash": "cbc/Ave31CbPQ9E29dfaA4QjcsBoc8KokNlLC6Noj0uToHDQ9PPllD+k6HluVbaFpflsU8XGwrQxOoyvXlU97g==",
- "dependencies": {
- "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.4",
- "Microsoft.Extensions.Caching.Memory": "10.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.DependencyModel": "10.0.4",
- "Microsoft.Extensions.Logging": "10.0.4",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "lxeRviglTkkmzYJVJ600yb6gJjnf5za9v7uH+0byuSXTGv7U8cT6hz7qRTmiGSOfLcl86QFdy2BBKaUFd6NQug==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.5",
+ "Microsoft.Extensions.Caching.Memory": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.DependencyModel": "10.0.5",
+ "Microsoft.Extensions.Logging": "10.0.5",
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.11",
"SQLitePCLRaw.core": "2.1.11"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "CentralTransitive",
- "requested": "[10.0.3, )",
- "resolved": "10.0.4",
- "contentHash": "3x9X9SMAMdAoEwWxHfsT2a9dTBqEtfYfbEOFw+UPtBshEH2gHWJeazxrZ1FK1O18MoCbe1NxINg5qciB01pEcg==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==",
"dependencies": {
- "Microsoft.Extensions.Primitives": "10.0.4"
+ "Microsoft.Extensions.Primitives": "10.0.5"
}
},
"Microsoft.Extensions.Configuration.Json": {
"type": "CentralTransitive",
- "requested": "[10.0.4, )",
- "resolved": "10.0.4",
- "contentHash": "gn2Rf0dvIa6Sz/WJ5cNHhG/oUOT1yrHXd7Q0vCpXDlLsMuRqv9G5NBXFJbSh/ZRzSbvbOQWMV0amQS/3N0Fzzg==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==",
"dependencies": {
- "Microsoft.Extensions.Configuration": "10.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "10.0.4",
- "Microsoft.Extensions.Configuration.FileExtensions": "10.0.4",
- "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4"
+ "Microsoft.Extensions.Configuration": "10.0.5",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.5",
+ "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "CentralTransitive",
- "requested": "[10.0.3, )",
- "resolved": "10.0.4",
- "contentHash": "PDMMt7fvBatv6hcxxyJtXIzSwn7Dy00W6I2vDAOTYrQqNM2dF5A2L9n0uMzdPz2IPoNZWkAmYjoOCEdDLq0i4w==",
+ "requested": "[10.0.5, )",
+ "resolved": "10.0.5",
+ "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5"
}
},
"Microsoft.IdentityModel.Tokens": {
@@ -414,13 +414,13 @@
},
"Npgsql.EntityFrameworkCore.PostgreSQL": {
"type": "CentralTransitive",
- "requested": "[10.0.0, )",
- "resolved": "10.0.0",
- "contentHash": "E2+uSWxSB8LdsUVwPaqRWOcGOP92biry2JEwc0KJMdLJF+aZdczeIdEXVwEyv4nSVMQJH0o8tLhyAMiR6VF0lw==",
+ "requested": "[10.0.1, )",
+ "resolved": "10.0.1",
+ "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==",
"dependencies": {
- "Microsoft.EntityFrameworkCore": "[10.0.0, 11.0.0)",
- "Microsoft.EntityFrameworkCore.Relational": "[10.0.0, 11.0.0)",
- "Npgsql": "10.0.0"
+ "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)",
+ "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)",
+ "Npgsql": "10.0.2"
}
}
}
diff --git a/src/Werkr.Data.Identity/Migrations/Postgres/20260310023641_InitialCreate.Designer.cs b/src/Werkr.Data.Identity/Migrations/Postgres/20260312050754_InitialCreate.Designer.cs
similarity index 97%
rename from src/Werkr.Data.Identity/Migrations/Postgres/20260310023641_InitialCreate.Designer.cs
rename to src/Werkr.Data.Identity/Migrations/Postgres/20260312050754_InitialCreate.Designer.cs
index feb3534..fa3df1b 100644
--- a/src/Werkr.Data.Identity/Migrations/Postgres/20260310023641_InitialCreate.Designer.cs
+++ b/src/Werkr.Data.Identity/Migrations/Postgres/20260312050754_InitialCreate.Designer.cs
@@ -1,528 +1,528 @@
-//
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
-using Werkr.Data.Identity;
-
-#nullable disable
-
-namespace Werkr.Data.Identity.Migrations.Postgres
-{
- [DbContext(typeof(PostgresWerkrIdentityDbContext))]
- [Migration("20260310023641_InitialCreate")]
- partial class InitialCreate
- {
- ///
- protected override void BuildTargetModel(ModelBuilder modelBuilder)
- {
-#pragma warning disable 612, 618
- modelBuilder
- .HasDefaultSchema("werkr_identity")
- .HasAnnotation("ProductVersion", "10.0.3")
- .HasAnnotation("Relational:MaxIdentifierLength", 63);
-
- NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
-
- modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
- {
- b.Property("Id")
- .HasColumnType("text")
- .HasColumnName("id");
-
- b.Property("ConcurrencyStamp")
- .IsConcurrencyToken()
- .HasColumnType("text")
- .HasColumnName("concurrency_stamp");
-
- b.Property("Name")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)")
- .HasColumnName("name");
-
- b.Property("NormalizedName")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)")
- .HasColumnName("normalized_name");
-
- b.HasKey("Id")
- .HasName("pk_roles");
-
- b.HasIndex("NormalizedName")
- .IsUnique()
- .HasDatabaseName("RoleNameIndex");
-
- b.ToTable("roles", "werkr_identity");
- });
-
- modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
- {
- b.Property("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("integer")
- .HasColumnName("id");
-
- NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
-
- b.Property("ClaimType")
- .HasColumnType("text")
- .HasColumnName("claim_type");
-
- b.Property("ClaimValue")
- .HasColumnType("text")
- .HasColumnName("claim_value");
-
- b.Property("RoleId")
- .IsRequired()
- .HasColumnType("text")
- .HasColumnName("role_id");
-
- b.HasKey("Id")
- .HasName("pk_role_claims");
-
- b.HasIndex("RoleId")
- .HasDatabaseName("ix_role_claims_role_id");
-
- b.ToTable("role_claims", "werkr_identity");
- });
-
- modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
- {
- b.Property("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("integer")
- .HasColumnName("id");
-
- NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
-
- b.Property("ClaimType")
- .HasColumnType("text")
- .HasColumnName("claim_type");
-
- b.Property("ClaimValue")
- .HasColumnType("text")
- .HasColumnName("claim_value");
-
- b.Property("UserId")
- .IsRequired()
- .HasColumnType("text")
- .HasColumnName("user_id");
-
- b.HasKey("Id")
- .HasName("pk_user_claims");
-
- b.HasIndex("UserId")
- .HasDatabaseName("ix_user_claims_user_id");
-
- b.ToTable("user_claims", "werkr_identity");
- });
-
- modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
- {
- b.Property("LoginProvider")
- .HasMaxLength(128)
- .HasColumnType("character varying(128)")
- .HasColumnName("login_provider");
-
- b.Property("ProviderKey")
- .HasMaxLength(128)
- .HasColumnType("character varying(128)")
- .HasColumnName("provider_key");
-
- b.Property("ProviderDisplayName")
- .HasColumnType("text")
- .HasColumnName("provider_display_name");
-
- b.Property("UserId")
- .IsRequired()
- .HasColumnType("text")
- .HasColumnName("user_id");
-
- b.HasKey("LoginProvider", "ProviderKey")
- .HasName("pk_user_logins");
-
- b.HasIndex("UserId")
- .HasDatabaseName("ix_user_logins_user_id");
-
- b.ToTable("user_logins", "werkr_identity");
- });
-
- modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
- {
- b.Property("UserId")
- .HasColumnType("text")
- .HasColumnName("user_id");
-
- b.Property("RoleId")
- .HasColumnType("text")
- .HasColumnName("role_id");
-
- b.HasKey("UserId", "RoleId")
- .HasName("pk_user_roles");
-
- b.HasIndex("RoleId")
- .HasDatabaseName("ix_user_roles_role_id");
-
- b.ToTable("user_roles", "werkr_identity");
- });
-
- modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
- {
- b.Property("UserId")
- .HasColumnType("text")
- .HasColumnName("user_id");
-
- b.Property("LoginProvider")
- .HasMaxLength(128)
- .HasColumnType("character varying(128)")
- .HasColumnName("login_provider");
-
- b.Property("Name")
- .HasMaxLength(128)
- .HasColumnType("character varying(128)")
- .HasColumnName("name");
-
- b.Property("Value")
- .HasColumnType("text")
- .HasColumnName("value");
-
- b.HasKey("UserId", "LoginProvider", "Name")
- .HasName("pk_user_tokens");
-
- b.ToTable("user_tokens", "werkr_identity");
- });
-
- modelBuilder.Entity("Werkr.Data.Identity.Entities.ApiKey", b =>
- {
- b.Property("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("uuid")
- .HasColumnName("id");
-
- b.Property("CreatedByUserId")
- .IsRequired()
- .HasColumnType("text")
- .HasColumnName("created_by_user_id");
-
- b.Property("CreatedUtc")
- .HasColumnType("timestamp with time zone")
- .HasColumnName("created_utc");
-
- b.Property("ExpiresUtc")
- .HasColumnType("timestamp with time zone")
- .HasColumnName("expires_utc");
-
- b.Property("IsRevoked")
- .HasColumnType("boolean")
- .HasColumnName("is_revoked");
-
- b.Property("KeyHash")
- .IsRequired()
- .HasMaxLength(128)
- .HasColumnType("character varying(128)")
- .HasColumnName("key_hash");
-
- b.Property("KeyPrefix")
- .IsRequired()
- .HasMaxLength(16)
- .HasColumnType("character varying(16)")
- .HasColumnName("key_prefix");
-
- b.Property("LastUsedUtc")
- .HasColumnType("timestamp with time zone")
- .HasColumnName("last_used_utc");
-
- b.Property("Name")
- .IsRequired()
- .HasMaxLength(200)
- .HasColumnType("character varying(200)")
- .HasColumnName("name");
-
- b.Property("Role")
- .IsRequired()
- .HasMaxLength(64)
- .HasColumnType("character varying(64)")
- .HasColumnName("role");
-
- b.HasKey("Id")
- .HasName("pk_api_keys");
-
- b.HasIndex("CreatedByUserId")
- .HasDatabaseName("ix_api_keys_created_by_user_id");
-
- b.HasIndex("KeyHash")
- .IsUnique()
- .HasDatabaseName("ix_api_keys_key_hash");
-
- b.HasIndex("KeyPrefix")
- .HasDatabaseName("ix_api_keys_key_prefix");
-
- b.ToTable("api_keys", "werkr_identity");
- });
-
- modelBuilder.Entity("Werkr.Data.Identity.Entities.ConfigurationSettings", b =>
- {
- b.Property("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("uuid")
- .HasColumnName("id");
-
- b.Property("AllowRegistration")
- .HasColumnType("boolean")
- .HasColumnName("allow_registration");
-
- b.Property("Created")
- .HasColumnType("timestamp with time zone")
- .HasColumnName("created");
-
- b.Property("DefaultKeySize")
- .HasColumnType("integer")
- .HasColumnName("default_key_size");
-
- b.Property("LastUpdated")
- .HasColumnType("timestamp with time zone")
- .HasColumnName("last_updated");
-
- b.Property("PollingIntervalSeconds")
- .HasColumnType("integer")
- .HasColumnName("polling_interval_seconds");
-
- b.Property("RunDetailPollingIntervalSeconds")
- .HasColumnType("integer")
- .HasColumnName("run_detail_polling_interval_seconds");
-
- b.Property("ServerName")
- .IsRequired()
- .HasMaxLength(200)
- .HasColumnType("character varying(200)")
- .HasColumnName("server_name");
-
- b.Property("Version")
- .IsConcurrencyToken()
- .HasColumnType("integer")
- .HasColumnName("version");
-
- b.HasKey("Id")
- .HasName("pk_config_settings");
-
- b.ToTable("config_settings", "werkr_identity");
- });
-
- modelBuilder.Entity("Werkr.Data.Identity.Entities.RolePermission", b =>
- {
- b.Property("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("bigint")
- .HasColumnName("id");
-
- NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
-
- b.Property("Permission")
- .IsRequired()
- .HasMaxLength(64)
- .HasColumnType("character varying(64)")
- .HasColumnName("permission");
-
- b.Property("RoleId")
- .IsRequired()
- .HasColumnType("text")
- .HasColumnName("role_id");
-
- b.HasKey("Id")
- .HasName("pk_role_permissions");
-
- b.HasIndex("RoleId", "Permission")
- .IsUnique()
- .HasDatabaseName("ix_role_permissions_role_id_permission");
-
- b.ToTable("role_permissions", "werkr_identity");
- });
-
- modelBuilder.Entity("Werkr.Data.Identity.Entities.WerkrUser", b =>
- {
- b.Property("Id")
- .HasColumnType("text")
- .HasColumnName("id");
-
- b.Property("AccessFailedCount")
- .HasColumnType("integer")
- .HasColumnName("access_failed_count");
-
- b.Property("ChangePassword")
- .HasColumnType("boolean")
- .HasColumnName("change_password");
-
- b.Property("ConcurrencyStamp")
- .IsConcurrencyToken()
- .HasColumnType("text")
- .HasColumnName("concurrency_stamp");
-
- b.Property("Email")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)")
- .HasColumnName("email");
-
- b.Property("EmailConfirmed")
- .HasColumnType("boolean")
- .HasColumnName("email_confirmed");
-
- b.Property("Enabled")
- .HasColumnType("boolean")
- .HasColumnName("enabled");
-
- b.Property("LastLoginUtc")
- .HasColumnType("timestamp with time zone")
- .HasColumnName("last_login_utc");
-
- b.Property("LockoutEnabled")
- .HasColumnType("boolean")
- .HasColumnName("lockout_enabled");
-
- b.Property("LockoutEnd")
- .HasColumnType("timestamp with time zone")
- .HasColumnName("lockout_end");
-
- b.Property("Name")
- .IsRequired()
- .HasMaxLength(256)
- .HasColumnType("character varying(256)")
- .HasColumnName("name");
-
- b.Property("NormalizedEmail")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)")
- .HasColumnName("normalized_email");
-
- b.Property("NormalizedUserName")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)")
- .HasColumnName("normalized_user_name");
-
- b.Property("PasswordHash")
- .HasColumnType("text")
- .HasColumnName("password_hash");
-
- b.Property("PhoneNumber")
- .HasColumnType("text")
- .HasColumnName("phone_number");
-
- b.Property("PhoneNumberConfirmed")
- .HasColumnType("boolean")
- .HasColumnName("phone_number_confirmed");
-
- b.Property("Requires2FA")
- .HasColumnType("boolean")
- .HasColumnName("requires2fa");
-
- b.Property("SecurityStamp")
- .HasColumnType("text")
- .HasColumnName("security_stamp");
-
- b.Property("TwoFactorEnabled")
- .HasColumnType("boolean")
- .HasColumnName("two_factor_enabled");
-
- b.Property("UserName")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)")
- .HasColumnName("user_name");
-
- b.HasKey("Id")
- .HasName("pk_users");
-
- b.HasIndex("NormalizedEmail")
- .HasDatabaseName("EmailIndex");
-
- b.HasIndex("NormalizedUserName")
- .IsUnique()
- .HasDatabaseName("UserNameIndex");
-
- b.ToTable("users", "werkr_identity");
- });
-
- modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
- {
- b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
- .WithMany()
- .HasForeignKey("RoleId")
- .OnDelete(DeleteBehavior.Cascade)
- .IsRequired()
- .HasConstraintName("fk_role_claims_roles_role_id");
- });
-
- modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
- {
- b.HasOne("Werkr.Data.Identity.Entities.WerkrUser", null)
- .WithMany()
- .HasForeignKey("UserId")
- .OnDelete(DeleteBehavior.Cascade)
- .IsRequired()
- .HasConstraintName("fk_user_claims_users_user_id");
- });
-
- modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
- {
- b.HasOne("Werkr.Data.Identity.Entities.WerkrUser", null)
- .WithMany()
- .HasForeignKey("UserId")
- .OnDelete(DeleteBehavior.Cascade)
- .IsRequired()
- .HasConstraintName("fk_user_logins_users_user_id");
- });
-
- modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
- {
- b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
- .WithMany()
- .HasForeignKey("RoleId")
- .OnDelete(DeleteBehavior.Cascade)
- .IsRequired()
- .HasConstraintName("fk_user_roles_roles_role_id");
-
- b.HasOne("Werkr.Data.Identity.Entities.WerkrUser", null)
- .WithMany()
- .HasForeignKey("UserId")
- .OnDelete(DeleteBehavior.Cascade)
- .IsRequired()
- .HasConstraintName("fk_user_roles_users_user_id");
- });
-
- modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
- {
- b.HasOne("Werkr.Data.Identity.Entities.WerkrUser", null)
- .WithMany()
- .HasForeignKey("UserId")
- .OnDelete(DeleteBehavior.Cascade)
- .IsRequired()
- .HasConstraintName("fk_user_tokens_users_user_id");
- });
-
- modelBuilder.Entity("Werkr.Data.Identity.Entities.ApiKey", b =>
- {
- b.HasOne("Werkr.Data.Identity.Entities.WerkrUser", "CreatedByUser")
- .WithMany()
- .HasForeignKey("CreatedByUserId")
- .OnDelete(DeleteBehavior.Cascade)
- .IsRequired()
- .HasConstraintName("fk_api_keys_users_created_by_user_id");
-
- b.Navigation("CreatedByUser");
- });
-
- modelBuilder.Entity("Werkr.Data.Identity.Entities.RolePermission", b =>
- {
- b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", "Role")
- .WithMany()
- .HasForeignKey("RoleId")
- .OnDelete(DeleteBehavior.Cascade)
- .IsRequired()
- .HasConstraintName("fk_role_permissions_roles_role_id");
-
- b.Navigation("Role");
- });
-#pragma warning restore 612, 618
- }
- }
-}
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using Werkr.Data.Identity;
+
+#nullable disable
+
+namespace Werkr.Data.Identity.Migrations.Postgres
+{
+ [DbContext(typeof(PostgresWerkrIdentityDbContext))]
+ [Migration("20260312050754_InitialCreate")]
+ partial class InitialCreate
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("werkr_identity")
+ .HasAnnotation("ProductVersion", "10.0.4")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text")
+ .HasColumnName("id");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("text")
+ .HasColumnName("concurrency_stamp");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)")
+ .HasColumnName("name");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)")
+ .HasColumnName("normalized_name");
+
+ b.HasKey("Id")
+ .HasName("pk_roles");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex");
+
+ b.ToTable("roles", "werkr_identity");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text")
+ .HasColumnName("claim_type");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text")
+ .HasColumnName("claim_value");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("role_id");
+
+ b.HasKey("Id")
+ .HasName("pk_role_claims");
+
+ b.HasIndex("RoleId")
+ .HasDatabaseName("ix_role_claims_role_id");
+
+ b.ToTable("role_claims", "werkr_identity");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text")
+ .HasColumnName("claim_type");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text")
+ .HasColumnName("claim_value");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("pk_user_claims");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_user_claims_user_id");
+
+ b.ToTable("user_claims", "werkr_identity");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)")
+ .HasColumnName("login_provider");
+
+ b.Property("ProviderKey")
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)")
+ .HasColumnName("provider_key");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("text")
+ .HasColumnName("provider_display_name");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("user_id");
+
+ b.HasKey("LoginProvider", "ProviderKey")
+ .HasName("pk_user_logins");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("ix_user_logins_user_id");
+
+ b.ToTable("user_logins", "werkr_identity");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text")
+ .HasColumnName("user_id");
+
+ b.Property("RoleId")
+ .HasColumnType("text")
+ .HasColumnName("role_id");
+
+ b.HasKey("UserId", "RoleId")
+ .HasName("pk_user_roles");
+
+ b.HasIndex("RoleId")
+ .HasDatabaseName("ix_user_roles_role_id");
+
+ b.ToTable("user_roles", "werkr_identity");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text")
+ .HasColumnName("user_id");
+
+ b.Property("LoginProvider")
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)")
+ .HasColumnName("login_provider");
+
+ b.Property("Name")
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)")
+ .HasColumnName("name");
+
+ b.Property("Value")
+ .HasColumnType("text")
+ .HasColumnName("value");
+
+ b.HasKey("UserId", "LoginProvider", "Name")
+ .HasName("pk_user_tokens");
+
+ b.ToTable("user_tokens", "werkr_identity");
+ });
+
+ modelBuilder.Entity("Werkr.Data.Identity.Entities.ApiKey", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("CreatedByUserId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("created_by_user_id");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_utc");
+
+ b.Property("ExpiresUtc")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expires_utc");
+
+ b.Property("IsRevoked")
+ .HasColumnType("boolean")
+ .HasColumnName("is_revoked");
+
+ b.Property("KeyHash")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)")
+ .HasColumnName("key_hash");
+
+ b.Property("KeyPrefix")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("character varying(16)")
+ .HasColumnName("key_prefix");
+
+ b.Property("LastUsedUtc")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_used_utc");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("name");
+
+ b.Property("Role")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("role");
+
+ b.HasKey("Id")
+ .HasName("pk_api_keys");
+
+ b.HasIndex("CreatedByUserId")
+ .HasDatabaseName("ix_api_keys_created_by_user_id");
+
+ b.HasIndex("KeyHash")
+ .IsUnique()
+ .HasDatabaseName("ix_api_keys_key_hash");
+
+ b.HasIndex("KeyPrefix")
+ .HasDatabaseName("ix_api_keys_key_prefix");
+
+ b.ToTable("api_keys", "werkr_identity");
+ });
+
+ modelBuilder.Entity("Werkr.Data.Identity.Entities.ConfigurationSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("AllowRegistration")
+ .HasColumnType("boolean")
+ .HasColumnName("allow_registration");
+
+ b.Property("Created")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created");
+
+ b.Property("DefaultKeySize")
+ .HasColumnType("integer")
+ .HasColumnName("default_key_size");
+
+ b.Property("LastUpdated")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_updated");
+
+ b.Property("PollingIntervalSeconds")
+ .HasColumnType("integer")
+ .HasColumnName("polling_interval_seconds");
+
+ b.Property("RunDetailPollingIntervalSeconds")
+ .HasColumnType("integer")
+ .HasColumnName("run_detail_polling_interval_seconds");
+
+ b.Property("ServerName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("server_name");
+
+ b.Property("Version")
+ .IsConcurrencyToken()
+ .HasColumnType("integer")
+ .HasColumnName("version");
+
+ b.HasKey("Id")
+ .HasName("pk_config_settings");
+
+ b.ToTable("config_settings", "werkr_identity");
+ });
+
+ modelBuilder.Entity("Werkr.Data.Identity.Entities.RolePermission", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Permission")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("permission");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("role_id");
+
+ b.HasKey("Id")
+ .HasName("pk_role_permissions");
+
+ b.HasIndex("RoleId", "Permission")
+ .IsUnique()
+ .HasDatabaseName("ix_role_permissions_role_id_permission");
+
+ b.ToTable("role_permissions", "werkr_identity");
+ });
+
+ modelBuilder.Entity("Werkr.Data.Identity.Entities.WerkrUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text")
+ .HasColumnName("id");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("integer")
+ .HasColumnName("access_failed_count");
+
+ b.Property("ChangePassword")
+ .HasColumnType("boolean")
+ .HasColumnName("change_password");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("text")
+ .HasColumnName("concurrency_stamp");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)")
+ .HasColumnName("email");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("boolean")
+ .HasColumnName("email_confirmed");
+
+ b.Property("Enabled")
+ .HasColumnType("boolean")
+ .HasColumnName("enabled");
+
+ b.Property("LastLoginUtc")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_login_utc");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("boolean")
+ .HasColumnName("lockout_enabled");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("lockout_end");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)")
+ .HasColumnName("name");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)")
+ .HasColumnName("normalized_email");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)")
+ .HasColumnName("normalized_user_name");
+
+ b.Property("PasswordHash")
+ .HasColumnType("text")
+ .HasColumnName("password_hash");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("text")
+ .HasColumnName("phone_number");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("boolean")
+ .HasColumnName("phone_number_confirmed");
+
+ b.Property("Requires2FA")
+ .HasColumnType("boolean")
+ .HasColumnName("requires2fa");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("text")
+ .HasColumnName("security_stamp");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("boolean")
+ .HasColumnName("two_factor_enabled");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)")
+ .HasColumnName("user_name");
+
+ b.HasKey("Id")
+ .HasName("pk_users");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex");
+
+ b.ToTable("users", "werkr_identity");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_role_claims_roles_role_id");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.HasOne("Werkr.Data.Identity.Entities.WerkrUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_user_claims_users_user_id");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.HasOne("Werkr.Data.Identity.Entities.WerkrUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_user_logins_users_user_id");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_user_roles_roles_role_id");
+
+ b.HasOne("Werkr.Data.Identity.Entities.WerkrUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_user_roles_users_user_id");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.HasOne("Werkr.Data.Identity.Entities.WerkrUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_user_tokens_users_user_id");
+ });
+
+ modelBuilder.Entity("Werkr.Data.Identity.Entities.ApiKey", b =>
+ {
+ b.HasOne("Werkr.Data.Identity.Entities.WerkrUser", "CreatedByUser")
+ .WithMany()
+ .HasForeignKey("CreatedByUserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_api_keys_users_created_by_user_id");
+
+ b.Navigation("CreatedByUser");
+ });
+
+ modelBuilder.Entity("Werkr.Data.Identity.Entities.RolePermission", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", "Role")
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_role_permissions_roles_role_id");
+
+ b.Navigation("Role");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Werkr.Data.Identity/Migrations/Postgres/20260310023641_InitialCreate.cs b/src/Werkr.Data.Identity/Migrations/Postgres/20260312050754_InitialCreate.cs
similarity index 99%
rename from src/Werkr.Data.Identity/Migrations/Postgres/20260310023641_InitialCreate.cs
rename to src/Werkr.Data.Identity/Migrations/Postgres/20260312050754_InitialCreate.cs
index 27be5aa..2ee2584 100644
--- a/src/Werkr.Data.Identity/Migrations/Postgres/20260310023641_InitialCreate.cs
+++ b/src/Werkr.Data.Identity/Migrations/Postgres/20260312050754_InitialCreate.cs
@@ -1,10 +1,9 @@
-#nullable disable
-
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
-namespace Werkr.Data.Identity.Migrations.Postgres;
+#nullable disable
+namespace Werkr.Data.Identity.Migrations.Postgres;
///
public partial class InitialCreate : Migration {
///
diff --git a/src/Werkr.Data.Identity/Migrations/Postgres/PostgresWerkrIdentityDbContextModelSnapshot.cs b/src/Werkr.Data.Identity/Migrations/Postgres/PostgresWerkrIdentityDbContextModelSnapshot.cs
index 584ddeb..90ed642 100644
--- a/src/Werkr.Data.Identity/Migrations/Postgres/PostgresWerkrIdentityDbContextModelSnapshot.cs
+++ b/src/Werkr.Data.Identity/Migrations/Postgres/PostgresWerkrIdentityDbContextModelSnapshot.cs
@@ -1,525 +1,525 @@
-//
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
-using Werkr.Data.Identity;
-
-#nullable disable
-
-namespace Werkr.Data.Identity.Migrations.Postgres
-{
- [DbContext(typeof(PostgresWerkrIdentityDbContext))]
- partial class PostgresWerkrIdentityDbContextModelSnapshot : ModelSnapshot
- {
- protected override void BuildModel(ModelBuilder modelBuilder)
- {
-#pragma warning disable 612, 618
- modelBuilder
- .HasDefaultSchema("werkr_identity")
- .HasAnnotation("ProductVersion", "10.0.3")
- .HasAnnotation("Relational:MaxIdentifierLength", 63);
-
- NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
-
- modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
- {
- b.Property("Id")
- .HasColumnType("text")
- .HasColumnName("id");
-
- b.Property("ConcurrencyStamp")
- .IsConcurrencyToken()
- .HasColumnType("text")
- .HasColumnName("concurrency_stamp");
-
- b.Property("Name")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)")
- .HasColumnName("name");
-
- b.Property("NormalizedName")
- .HasMaxLength(256)
- .HasColumnType("character varying(256)")
- .HasColumnName("normalized_name");
-
- b.HasKey("Id")
- .HasName("pk_roles");
-
- b.HasIndex("NormalizedName")
- .IsUnique()
- .HasDatabaseName("RoleNameIndex");
-
- b.ToTable("roles", "werkr_identity");
- });
-
- modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
- {
- b.Property("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("integer")
- .HasColumnName("id");
-
- NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
-
- b.Property("ClaimType")
- .HasColumnType("text")
- .HasColumnName("claim_type");
-
- b.Property("ClaimValue")
- .HasColumnType("text")
- .HasColumnName("claim_value");
-
- b.Property("RoleId")
- .IsRequired()
- .HasColumnType("text")
- .HasColumnName("role_id");
-
- b.HasKey("Id")
- .HasName("pk_role_claims");
-
- b.HasIndex("RoleId")
- .HasDatabaseName("ix_role_claims_role_id");
-
- b.ToTable("role_claims", "werkr_identity");
- });
-
- modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim