[
  {
    "path": ".aspire/settings.json",
    "content": "{\n  \"appHostPath\": \"../src/AppHost/AppHost.csproj\"\n}"
  },
  {
    "path": ".azdo/pipelines/azure-dev.yml",
    "content": "# Run when commits are pushed to mainline branch (main or master)\n# Set this to the mainline branch you are using\ntrigger:\n  - main\n  - master\n\n# Azure Pipelines workflow to deploy to Azure using azd\n# To configure required secrets and service connection for connecting to Azure, simply run `azd pipeline config --provider azdo`\n# Task \"Install azd\" needs to install setup-azd extension for azdo - https://marketplace.visualstudio.com/items?itemName=ms-azuretools.azd\n# See below for alternative task to install azd if you can't install above task in your organization\n\npool:\n  vmImage: ubuntu-latest\n\nsteps:\n  # setup-azd@1 needs to be manually installed in your organization\n  # if you can't install it, you can use the below bash script to install azd\n  # and remove this step\n  - task: setup-azd@1\n    displayName: Install azd\n\n  # If you can't install above task in your organization, you can comment it and uncomment below task to install azd\n  # - task: Bash@3\n  #   displayName: Install azd\n  #   inputs:\n  #     targetType: 'inline'\n  #     script: |\n  #       curl -fsSL https://aka.ms/install-azd.sh | bash\n\n  - task: UseDotNet@2\n    displayName: Setup .NET\n    inputs:\n      useGlobalJson: true\n\n  # azd delegate auth to az to use service connection with AzureCLI@2\n  - pwsh: |\n      azd config set auth.useAzCliAuth \"true\"\n    displayName: Configure AZD to Use AZ CLI Authentication.\n\n  - task: AzureCLI@2\n    displayName: Provision Infrastructure\n    inputs:\n      azureSubscription: azconnection\n      scriptType: bash\n      scriptLocation: inlineScript\n      keepAzSessionActive: true\n      inlineScript: |\n        azd provision --no-prompt\n    env:\n      AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID)\n      AZURE_ENV_NAME: $(AZURE_ENV_NAME)\n      AZURE_LOCATION: $(AZURE_LOCATION)\n      AZD_INITIAL_ENVIRONMENT_CONFIG: $(secrets.AZD_INITIAL_ENVIRONMENT_CONFIG)\n\n  - task: AzureCLI@2\n    displayName: Deploy Application\n    inputs:\n      azureSubscription: azconnection\n      scriptType: bash\n      scriptLocation: inlineScript\n      keepAzSessionActive: true\n      inlineScript: |\n        azd deploy --no-prompt\n    env:\n      AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID)\n      AZURE_ENV_NAME: $(AZURE_ENV_NAME)\n      AZURE_LOCATION: $(AZURE_LOCATION)\n"
  },
  {
    "path": ".devcontainer/README.md",
    "content": "# Dev Container\n\nThis folder contains configuration for running the project inside a **Dev Container** (VS Code Remote Containers or GitHub Codespaces).\n\n## What this folder does\n- Defines the development environment (SDK versions, tools, extensions)\n- Ensures every developer uses the same environment\n- Simplifies onboarding by eliminating local machine setup issues\n- Supports GitHub Codespaces for cloud-based development\n\n## When to use it\nIf using VS Code:\n1. Install the “Dev Containers” extension.\n2. Open the repository.\n3. Select **“Reopen in Container”**.\n\nIf using GitHub Codespaces:\n- Codespaces will automatically use this configuration when the workspace starts.\n\n## Notes\nThis folder has no effect on the runtime application. It is only used to configure the developer environment.\n"
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "{\n    \"name\": \"Azure Developer CLI\",\n    \"image\": \"mcr.microsoft.com/devcontainers/python:3.13-bullseye\",\n    \"features\": {\n        // See https://containers.dev/features for list of features\n        \"ghcr.io/devcontainers/features/docker-in-docker:2\": {\n        },\n        \"ghcr.io/azure/azure-dev/azd:latest\": {}\n    },\n    \"customizations\": {\n        \"vscode\": {\n            \"extensions\": [\n                \"GitHub.vscode-github-actions\",\n                \"ms-azuretools.azure-dev\",\n                \"ms-azuretools.vscode-azurefunctions\",\n                \"ms-azuretools.vscode-bicep\",\n                \"ms-azuretools.vscode-docker\"\n                // Include other VSCode language extensions if needed\n                // Right click on an extension inside VSCode to add directly to devcontainer.json, or copy the extension ID\n            ]\n        }\n    },\n    \"forwardPorts\": [\n        // Forward ports if needed for local development\n    ],\n    \"postCreateCommand\": \"\",\n    \"remoteUser\": \"vscode\",\n    \"hostRequirements\": {\n        \"memory\": \"8gb\"\n    }\n}\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n# All files\n[*]\nindent_style = space\n\n# Xml files\n[*.{xml,csproj,props,targets,ruleset,nuspec,resx}]\nindent_size = 2\n\n# Javascript files\n[*.js]\nindent_size = 2\n\n# Json files\n[*.{json,config}]\nindent_size = 2\n\n# C# files\n[*.cs]\n\n#### Core EditorConfig Options ####\n\n# Indentation and spacing\nindent_size = 4\ntab_width = 4\n\n# New line preferences\nend_of_line = lf\ninsert_final_newline = true\n\n#### .NET Coding Conventions ####\n[*.{cs,vb}]\n\n# Organize usings\ndotnet_separate_import_directive_groups = false\ndotnet_sort_system_directives_first = true\nfile_header_template = unset\n\n# this. and Me. preferences\ndotnet_style_qualification_for_event = false:silent\ndotnet_style_qualification_for_field = false:silent\ndotnet_style_qualification_for_method = false:silent\ndotnet_style_qualification_for_property = false:silent\n\n# Language keywords vs BCL types preferences\ndotnet_style_predefined_type_for_locals_parameters_members = true:silent\ndotnet_style_predefined_type_for_member_access = true:silent\n\n# Parentheses preferences\ndotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent\ndotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent\ndotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent\ndotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent\n\n# Modifier preferences\ndotnet_style_require_accessibility_modifiers = for_non_interface_members:silent\n\n# Expression-level preferences\ndotnet_style_coalesce_expression = true:suggestion\ndotnet_style_collection_initializer = true:suggestion\ndotnet_style_explicit_tuple_names = true:suggestion\ndotnet_style_null_propagation = true:suggestion\ndotnet_style_object_initializer = true:suggestion\ndotnet_style_operator_placement_when_wrapping = beginning_of_line\ndotnet_style_prefer_auto_properties = true:suggestion\ndotnet_style_prefer_compound_assignment = true:suggestion\ndotnet_style_prefer_conditional_expression_over_assignment = true:suggestion\ndotnet_style_prefer_conditional_expression_over_return = true:suggestion\ndotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion\ndotnet_style_prefer_inferred_tuple_names = true:suggestion\ndotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion\ndotnet_style_prefer_simplified_boolean_expressions = true:suggestion\ndotnet_style_prefer_simplified_interpolation = true:suggestion\n\n# Field preferences\ndotnet_style_readonly_field = true:warning\n\n# Parameter preferences\ndotnet_code_quality_unused_parameters = all:suggestion\n\n# Suppression preferences\ndotnet_remove_unnecessary_suppression_exclusions = none\n\n#### C# Coding Conventions ####\n[*.cs]\n\n# var preferences\ncsharp_style_var_elsewhere = false:silent\ncsharp_style_var_for_built_in_types = false:silent\ncsharp_style_var_when_type_is_apparent = false:silent\n\n# Expression-bodied members\ncsharp_style_expression_bodied_accessors = true:silent\ncsharp_style_expression_bodied_constructors = false:silent\ncsharp_style_expression_bodied_indexers = true:silent\ncsharp_style_expression_bodied_lambdas = true:suggestion\ncsharp_style_expression_bodied_local_functions = false:silent\ncsharp_style_expression_bodied_methods = false:silent\ncsharp_style_expression_bodied_operators = false:silent\ncsharp_style_expression_bodied_properties = true:silent\n\n# Pattern matching preferences\ncsharp_style_pattern_matching_over_as_with_null_check = true:suggestion\ncsharp_style_pattern_matching_over_is_with_cast_check = true:suggestion\ncsharp_style_prefer_not_pattern = true:suggestion\ncsharp_style_prefer_pattern_matching = true:silent\ncsharp_style_prefer_switch_expression = true:suggestion\n\n# Null-checking preferences\ncsharp_style_conditional_delegate_call = true:suggestion\n\n# Modifier preferences\ncsharp_prefer_static_local_function = true:warning\ncsharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent\n\n# Code-block preferences\ncsharp_prefer_braces = true:silent\ncsharp_prefer_simple_using_statement = true:suggestion\n\n# Expression-level preferences\ncsharp_prefer_simple_default_expression = true:suggestion\ncsharp_style_deconstructed_variable_declaration = true:suggestion\ncsharp_style_inlined_variable_declaration = true:suggestion\ncsharp_style_pattern_local_over_anonymous_function = true:suggestion\ncsharp_style_prefer_index_operator = true:suggestion\ncsharp_style_prefer_range_operator = true:suggestion\ncsharp_style_throw_expression = true:suggestion\ncsharp_style_unused_value_assignment_preference = discard_variable:suggestion\ncsharp_style_unused_value_expression_statement_preference = discard_variable:silent\n\n# 'using' directive preferences\ncsharp_using_directive_placement = outside_namespace:silent\n\n#### C# Formatting Rules ####\n\n# New line preferences\ncsharp_new_line_before_catch = true\ncsharp_new_line_before_else = true\ncsharp_new_line_before_finally = true\ncsharp_new_line_before_members_in_anonymous_types = true\ncsharp_new_line_before_members_in_object_initializers = true\ncsharp_new_line_before_open_brace = all\ncsharp_new_line_between_query_expression_clauses = true\n\n# Indentation preferences\ncsharp_indent_block_contents = true\ncsharp_indent_braces = false\ncsharp_indent_case_contents = true\ncsharp_indent_case_contents_when_block = true\ncsharp_indent_labels = one_less_than_current\ncsharp_indent_switch_labels = true\n\n# Space preferences\ncsharp_space_after_cast = false\ncsharp_space_after_colon_in_inheritance_clause = true\ncsharp_space_after_comma = true\ncsharp_space_after_dot = false\ncsharp_space_after_keywords_in_control_flow_statements = true\ncsharp_space_after_semicolon_in_for_statement = true\ncsharp_space_around_binary_operators = before_and_after\ncsharp_space_around_declaration_statements = false\ncsharp_space_before_colon_in_inheritance_clause = true\ncsharp_space_before_comma = false\ncsharp_space_before_dot = false\ncsharp_space_before_open_square_brackets = false\ncsharp_space_before_semicolon_in_for_statement = false\ncsharp_space_between_empty_square_brackets = false\ncsharp_space_between_method_call_empty_parameter_list_parentheses = false\ncsharp_space_between_method_call_name_and_opening_parenthesis = false\ncsharp_space_between_method_call_parameter_list_parentheses = false\ncsharp_space_between_method_declaration_empty_parameter_list_parentheses = false\ncsharp_space_between_method_declaration_name_and_open_parenthesis = false\ncsharp_space_between_method_declaration_parameter_list_parentheses = false\ncsharp_space_between_parentheses = false\ncsharp_space_between_square_brackets = false\n\n# Wrapping preferences\ncsharp_preserve_single_line_blocks = true\ncsharp_preserve_single_line_statements = true\ncsharp_style_namespace_declarations = file_scoped:silent\ncsharp_style_prefer_method_group_conversion = true:silent\ncsharp_style_prefer_top_level_statements = true:silent\ncsharp_style_prefer_primary_constructors = true:suggestion\ncsharp_style_prefer_null_check_over_type_check = true:suggestion\ncsharp_style_prefer_local_over_anonymous_function = true:suggestion\ncsharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion\ncsharp_style_prefer_tuple_swap = true:suggestion\ncsharp_style_prefer_utf8_string_literals = true:suggestion\n\n#### Naming styles ####\n[*.{cs,vb}]\n\n# Naming rules\n\ndotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces\ndotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase\n\ndotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion\ndotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces\ndotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase\n\ndotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion\ndotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters\ndotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase\n\ndotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.methods_should_be_pascalcase.symbols = methods\ndotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase\n\ndotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.properties_should_be_pascalcase.symbols = properties\ndotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase\n\ndotnet_naming_rule.events_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.events_should_be_pascalcase.symbols = events\ndotnet_naming_rule.events_should_be_pascalcase.style = pascalcase\n\ndotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion\ndotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables\ndotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase\n\ndotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion\ndotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants\ndotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase\n\ndotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion\ndotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters\ndotnet_naming_rule.parameters_should_be_camelcase.style = camelcase\n\ndotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields\ndotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase\n\ndotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion\ndotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields\ndotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase\n\ndotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion\ndotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields\ndotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase\n\ndotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields\ndotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase\n\ndotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields\ndotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase\n\ndotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields\ndotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase\n\ndotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields\ndotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase\n\ndotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.enums_should_be_pascalcase.symbols = enums\ndotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase\n\ndotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions\ndotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase\n\ndotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion\ndotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members\ndotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase\n\n# Symbol specifications\n\ndotnet_naming_symbols.interfaces.applicable_kinds = interface\ndotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.interfaces.required_modifiers = \n\ndotnet_naming_symbols.enums.applicable_kinds = enum\ndotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.enums.required_modifiers = \n\ndotnet_naming_symbols.events.applicable_kinds = event\ndotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.events.required_modifiers = \n\ndotnet_naming_symbols.methods.applicable_kinds = method\ndotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.methods.required_modifiers = \n\ndotnet_naming_symbols.properties.applicable_kinds = property\ndotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.properties.required_modifiers = \n\ndotnet_naming_symbols.public_fields.applicable_kinds = field\ndotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal\ndotnet_naming_symbols.public_fields.required_modifiers = \n\ndotnet_naming_symbols.private_fields.applicable_kinds = field\ndotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected\ndotnet_naming_symbols.private_fields.required_modifiers = \n\ndotnet_naming_symbols.private_static_fields.applicable_kinds = field\ndotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected\ndotnet_naming_symbols.private_static_fields.required_modifiers = static\n\ndotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum\ndotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.types_and_namespaces.required_modifiers = \n\ndotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method\ndotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected\ndotnet_naming_symbols.non_field_members.required_modifiers = \n\ndotnet_naming_symbols.type_parameters.applicable_kinds = type_parameter\ndotnet_naming_symbols.type_parameters.applicable_accessibilities = *\ndotnet_naming_symbols.type_parameters.required_modifiers = \n\ndotnet_naming_symbols.private_constant_fields.applicable_kinds = field\ndotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected\ndotnet_naming_symbols.private_constant_fields.required_modifiers = const\n\ndotnet_naming_symbols.local_variables.applicable_kinds = local\ndotnet_naming_symbols.local_variables.applicable_accessibilities = local\ndotnet_naming_symbols.local_variables.required_modifiers = \n\ndotnet_naming_symbols.local_constants.applicable_kinds = local\ndotnet_naming_symbols.local_constants.applicable_accessibilities = local\ndotnet_naming_symbols.local_constants.required_modifiers = const\n\ndotnet_naming_symbols.parameters.applicable_kinds = parameter\ndotnet_naming_symbols.parameters.applicable_accessibilities = *\ndotnet_naming_symbols.parameters.required_modifiers = \n\ndotnet_naming_symbols.public_constant_fields.applicable_kinds = field\ndotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal\ndotnet_naming_symbols.public_constant_fields.required_modifiers = const\n\ndotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field\ndotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal\ndotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static\n\ndotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field\ndotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected\ndotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static\n\ndotnet_naming_symbols.local_functions.applicable_kinds = local_function\ndotnet_naming_symbols.local_functions.applicable_accessibilities = *\ndotnet_naming_symbols.local_functions.required_modifiers = \n\n# Naming styles\n\ndotnet_naming_style.pascalcase.required_prefix = \ndotnet_naming_style.pascalcase.required_suffix = \ndotnet_naming_style.pascalcase.word_separator = \ndotnet_naming_style.pascalcase.capitalization = pascal_case\n\ndotnet_naming_style.ipascalcase.required_prefix = I\ndotnet_naming_style.ipascalcase.required_suffix = \ndotnet_naming_style.ipascalcase.word_separator = \ndotnet_naming_style.ipascalcase.capitalization = pascal_case\n\ndotnet_naming_style.tpascalcase.required_prefix = T\ndotnet_naming_style.tpascalcase.required_suffix = \ndotnet_naming_style.tpascalcase.word_separator = \ndotnet_naming_style.tpascalcase.capitalization = pascal_case\n\ndotnet_naming_style._camelcase.required_prefix = _\ndotnet_naming_style._camelcase.required_suffix = \ndotnet_naming_style._camelcase.word_separator = \ndotnet_naming_style._camelcase.capitalization = camel_case\n\ndotnet_naming_style.camelcase.required_prefix = \ndotnet_naming_style.camelcase.required_suffix = \ndotnet_naming_style.camelcase.word_separator = \ndotnet_naming_style.camelcase.capitalization = camel_case\n\ndotnet_naming_style.s_camelcase.required_prefix = s_\ndotnet_naming_style.s_camelcase.required_suffix = \ndotnet_naming_style.s_camelcase.word_separator = \ndotnet_naming_style.s_camelcase.capitalization = camel_case\n\ndotnet_style_namespace_match_folder = true:suggestion\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: JasonTaylorDev # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\n# patreon: # Replace with a single Patreon username\n# open_collective: # Replace with a single Open Collective username\n# ko_fi: # Replace with a single Ko-fi username\n# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\n# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\n# liberapay: # Replace with a single Liberapay username\n# issuehunt: # Replace with a single IssueHunt username\n# otechie: # Replace with a single Otechie username\n# lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\n# custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report 🐛\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\ncontact_links:\n  - name: Ask a question ❓\n    url: https://github.com/jasontaylordev/cleanarchitecture/discussions/new\n    about: Ask a question or request support for using the template"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request ✨\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/workflows/azure-dev.yml",
    "content": "name: Azure deployment\n\non:\n  workflow_dispatch:\n  push:\n    # Run when commits are pushed to mainline branch (main or master)\n    # Set this to the mainline branch you are using\n    branches:\n      - main\n      - master\n\n# GitHub Actions workflow to deploy to Azure using azd\n# To configure required secrets for connecting to Azure, simply run `azd pipeline config`\n\n# Set up permissions for deploying with secretless Azure federated credentials\n# https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication\npermissions:\n  id-token: write\n  contents: read\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    env:\n      AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }}\n      AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }}\n      AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }}\n      AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }}\n      AZURE_LOCATION: ${{ vars.AZURE_LOCATION }}\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n\n      - name: Install azd\n        uses: Azure/setup-azd@v2\n\n      - name: Setup .NET\n        uses: actions/setup-dotnet@v5\n        with:\n          global-json-file: 'global.json'\n\n      - name: Log in with Azure (Federated Credentials)\n        run: |\n          azd auth login `\n            --client-id \"$Env:AZURE_CLIENT_ID\" `\n            --federated-credential-provider \"github\" `\n            --tenant-id \"$Env:AZURE_TENANT_ID\"\n        shell: pwsh\n\n      - name: Provision Infrastructure\n        run: azd provision --no-prompt\n        env:\n          AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ secrets.AZD_INITIAL_ENVIRONMENT_CONFIG }}\n\n      - name: Deploy Application\n        run: azd deploy --no-prompt\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\r\n\r\non:\r\n  pull_request:\r\n    branches: [ main ]\r\n    paths-ignore:\r\n      - '.scripts/**'\r\n      - .gitignore\r\n      - CODE_OF_CONDUCT.md\r\n      - LICENSE\r\n      - README.md\r\n\r\n  workflow_call:\r\n    inputs:\r\n      build-artifacts:\r\n        type: boolean\r\n        required: true\r\n        default: false\r\n\r\npermissions:\r\n  contents: read\r\n\r\njobs:\r\n  build:\r\n\r\n    runs-on: ubuntu-latest\r\n\r\n    steps:\r\n    - uses: actions/checkout@v6\r\n      name: Checkout code\r\n\r\n    - name: Cache NuGet packages\r\n      uses: actions/cache@v5\r\n      with:\r\n        path: ~/.nuget/packages\r\n        key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props', '**/*.csproj') }}\r\n        restore-keys: |\r\n          ${{ runner.os }}-nuget-\r\n\r\n    #if (!UseApiOnly)\r\n    - name: Install Node & cache npm packages\r\n      uses: actions/setup-node@v6\r\n      with:\r\n        node-version: '24.x'\r\n        cache: 'npm'\r\n        cache-dependency-path: |\r\n          src/Web/ClientApp/package-lock.json\r\n          src/Web/ClientApp-React/package-lock.json\r\n    #endif\r\n\r\n    - name: Install .NET\r\n      uses: actions/setup-dotnet@v5\r\n\r\n    - name: Restore solution\r\n      run: dotnet restore\r\n\r\n    - name: Build solution\r\n      run: dotnet build --no-restore --configuration Release\r\n      \r\n    #if (!UseApiOnly)\r\n    - name: Install Playwright browsers\r\n      run: pwsh artifacts/bin/Web.AcceptanceTests/release/playwright.ps1 install --with-deps chromium\r\n    #endif\r\n\r\n    - name: Test solution\r\n      run: dotnet test --no-build --configuration Release\r\n"
  },
  {
    "path": ".github/workflows/codeql.yml",
    "content": "name: CodeQL\n\non:\n  push:\n    branches: [ main ]\n    paths-ignore:\n      - .gitignore\n      - CODE_OF_CONDUCT.md\n      - LICENSE\n      - README.md\n\n  pull_request:\n    branches: [ main ]\n\n  schedule:\n    - cron: '00 0 * * 1'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v6\n\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v4\n\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v4\n      env:\n        SkipNSwag: True\n        npm_config_legacy_peer_deps: true\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v4\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  release:\n    types: [published]\n\npermissions:\n  contents: read\n\njobs:\n  publish:\n    name: Publish to NuGet.org\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v6\n        name: Checkout\n\n      - uses: nuget/setup-nuget@v2\n        name: Set up NuGet\n        with:\n          nuget-version: 'latest'\n\n      - name: Install Mono\n        run: sudo apt-get update && sudo apt-get install -y mono-complete\n\n      - name: Extract version\n        id: version\n        run: echo \"value=${GITHUB_REF_NAME#v}\" >> $GITHUB_OUTPUT\n\n      - name: Update template version\n        run: |\n          jq --arg v \"${{ steps.version.outputs.value }}\" \\\n            '.symbols.caPackageVersion.parameters.value = $v' \\\n            .template.config/template.json > tmp.json && mv tmp.json .template.config/template.json\n\n      - name: Update nuspec release notes\n        env:\n          RELEASE_NOTES: ${{ github.event.release.body }}\n        run: |\n          python3 - <<'EOF'\n          import os, xml.etree.ElementTree as ET\n          ET.register_namespace('', 'http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd')\n          tree = ET.parse('CleanArchitecture.nuspec')\n          root = tree.getroot()\n          ns = {'n': 'http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd'}\n          root.find('.//n:releaseNotes', ns).text = os.environ.get('RELEASE_NOTES', '')\n          tree.write('CleanArchitecture.nuspec', xml_declaration=True, encoding='utf-8')\n          EOF\n\n      - name: Pack\n        run: nuget pack CleanArchitecture.nuspec -NoDefaultExcludes -Version ${{ steps.version.outputs.value }}\n        \n      - name: Upload package artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: nuget-package\n          path: '*.nupkg'\n\n      - name: Publish\n        run: nuget push *.nupkg -Source 'https://api.nuget.org/v3/index.json' -ApiKey ${{secrets.NUGET_API_KEY}} -SkipDuplicate"
  },
  {
    "path": ".github/workflows/test-templates.yml",
    "content": "name: Test Templates\n\non:\n  push:\n    branches: [ main ]\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\njobs:\n  test-template:\n    name: ${{ matrix.client-framework }}-${{ matrix.database }}\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: false\n      matrix:\n        client-framework: [angular, react, none]\n        database: [sqlite, sqlserver, postgresql]\n\n    steps:\n    - uses: actions/checkout@v6\n      name: Checkout code\n\n    - name: Install .NET\n      uses: actions/setup-dotnet@v5\n\n    - name: Install Node & cache npm packages\n      if: matrix.client-framework != 'none'\n      uses: actions/setup-node@v6\n      with:\n        node-version: '24.x'\n        cache: 'npm'\n        cache-dependency-path: |\n          src/Web/ClientApp/package-lock.json\n          src/Web/ClientApp-React/package-lock.json\n\n    - name: Cache NuGet packages\n      uses: actions/cache@v5\n      with:\n        path: ~/.nuget/packages\n        key: ${{ runner.os }}-nuget-${{ matrix.client-framework }}-${{ matrix.database }}-${{ hashFiles('**/Directory.Packages.props', '**/*.csproj') }}\n        restore-keys: |\n          ${{ runner.os }}-nuget-\n\n    - name: Install template\n      run: dotnet new install .\n\n    - name: Generate solution\n      run: |\n        dotnet new ca-sln \\\n          --client-framework ${{ matrix.client-framework }} \\\n          --database ${{ matrix.database }} \\\n          --name CleanArchitecture \\\n          --output generated \\\n          --no-update-check\n\n    - name: Build solution\n      working-directory: generated\n      run: dotnet build --configuration Release\n\n    - name: Build client app\n      if: matrix.client-framework != 'none'\n      working-directory: generated/src/Web/ClientApp\n      run: |\n        npm ci\n        npm run build\n\n    - name: Install Playwright browsers\n      if: matrix.client-framework != 'none'\n      working-directory: generated\n      run: pwsh artifacts/bin/Web.AcceptanceTests/release/playwright.ps1 install --with-deps chromium\n\n    - name: Test solution\n      working-directory: generated\n      run: dotnet test --no-build --configuration Release\n"
  },
  {
    "path": ".gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## Get latest from `dotnet new gitignore`\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Mono auto generated files\nmono_crash.*\n\n# Build results\ntools/\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Ww][Ii][Nn]32/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# Auto-generated OpenAPI specification and TypeScript API clients\nsrc/Web/wwwroot/openapi/v1.json\nsrc/Web/ClientApp/src/app/web-api-client.ts\nsrc/Web/ClientApp-React/src/web-api-client.ts\n\n# Visual Studio 2017 auto generated files\nGenerated\\ Files/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUnit\n*.VisualState.xml\nTestResult.xml\nnunit-*.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# Benchmark Results\nBenchmarkDotNet.Artifacts/\n\n# .NET\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n# Tye\n.tye/\n\n# ASP.NET Scaffolding\nScaffoldingReadMe.txt\n\n# StyleCop\nStyleCopReport.xml\n\n# Files built by Visual Studio\n*_i.c\n*_p.c\n*_h.h\n*.ilk\n*.meta\n*.obj\n*.iobj\n*.pch\n*.pdb\n*.ipdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*_wpftmp.csproj\n*.log\n*.tlog\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# Visual Studio Trace Files\n*.e2e\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# AxoCover is a Code Coverage Tool\n.axoCover/*\n!.axoCover/settings.json\n\n# Coverlet is a free, cross platform Code Coverage Tool\ncoverage*.json\ncoverage*.xml\ncoverage*.info\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# Note: Comment the next line if you want to checkin your web deploy settings,\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# NuGet Symbol Packages\n*.snupkg\n# The packages folder can be ignored because of Package Restore\n**/[Pp]ackages/*\n# except build/, which is used as an MSBuild target.\n!**/[Pp]ackages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/[Pp]ackages/repositories.config\n# NuGet v3's project.json files produces more ignorable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n*.appx\n*.appxbundle\n*.appxupload\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!?*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\norleans.codegen.cs\n\n# Including strong name files can present a security risk\n# (https://github.com/github/gitignore/pull/2483#issue-259490424)\n#*.snk\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\nServiceFabricBackup/\n*.rptproj.bak\n\n# SQL Server files\n*.mdf\n*.ldf\n*.ndf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n*.rptproj.rsuser\n*- [Bb]ackup.rdl\n*- [Bb]ackup ([0-9]).rdl\n*- [Bb]ackup ([0-9][0-9]).rdl\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\nnode_modules/\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\n*.vbw\n\n# Visual Studio 6 auto-generated project file (contains which files were open etc.)\n*.vbp\n\n# Visual Studio 6 workspace and project file (working project files containing files to include in project)\n*.dsw\n*.dsp\n\n# Visual Studio 6 technical files\n*.ncb\n*.aps\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# CodeRush personal settings\n.cr/personal\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\n# tools/**\n# !tools/packages.config\n\n# Tabs Studio\n*.tss\n\n# Telerik's JustMock configuration file\n*.jmconfig\n\n# BizTalk build output\n*.btp.cs\n*.btm.cs\n*.odx.cs\n*.xsd.cs\n\n# OpenCover UI analysis results\nOpenCover/\n\n# Azure Stream Analytics local run output\nASALocalRun/\n\n# MSBuild Binary and Structured Log\n*.binlog\n\n# NVidia Nsight GPU debugger configuration file\n*.nvuser\n\n# MFractors (Xamarin productivity tool) working folder\n.mfractor/\n\n# Local History for Visual Studio\n.localhistory/\n\n# Visual Studio History (VSHistory) files\n.vshistory/\n\n# BeatPulse healthcheck temp database\nhealthchecksdb\n\n# Backup folder for Package Reference Convert tool in Visual Studio 2017\nMigrationBackup/\n\n# Ionide (cross platform F# VS Code tools) working folder\n.ionide/\n\n# Fody - auto-generated XML schema\nFodyWeavers.xsd\n\n# VS Code files for those working on multiple tools\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n*.code-workspace\n\n# Local History for Visual Studio Code\n.history/\n\n# Windows Installer files from build outputs\n*.cab\n*.msi\n*.msix\n*.msm\n*.msp\n\n# JetBrains Rider\n*.sln.iml\n\n##\n## Visual studio for Mac\n##\n\n\n# globs\nMakefile.in\n*.userprefs\n*.usertasks\nconfig.make\nconfig.status\naclocal.m4\ninstall-sh\nautom4te.cache/\n*.tar.gz\ntarballs/\ntest-results/\n\n# Mac bundle stuff\n*.dmg\n*.app\n\n# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore\n# General\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\n\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore\n# Windows thumbnail cache files\nThumbs.db\nehthumbs.db\nehthumbs_vista.db\n\n# Dump file\n*.stackdump\n\n# Folder config file\n[Dd]esktop.ini\n\n# Recycle Bin used on file shares\n$RECYCLE.BIN/\n\n# Windows Installer files\n*.cab\n*.msi\n*.msix\n*.msm\n*.msp\n\n# Windows shortcuts\n*.lnk\n\n# Vim temporary swap files\n*.swp\n.azure\n\n# SQLite database files\n*.db\n*.db-shm\n*.db-wal\n"
  },
  {
    "path": ".template.config/dotnetcli.host.json",
    "content": "{\n  \"symbolInfo\": {\n    \"ClientFramework\": {\n      \"longName\": \"client-framework\",\n      \"shortName\": \"cf\"\n    },\n    \"PipelineProvider\": {\n      \"longName\": \"pipeline-provider\",\n      \"shortName\": \"pp\"\n    },\n    \"Database\": {\n      \"longName\": \"database\",\n      \"shortName\": \"db\"\n    }\n  }\n}"
  },
  {
    "path": ".template.config/ide.host.json",
    "content": "{\n  \"$schema\": \"http://json.schemastore.org/vs-2017.3.host\",\n  \"order\": 0,\n  \"icon\": \"icon.png\",\n  \"symbolInfo\": [\n    {\n      \"id\": \"ClientFramework\",\n      \"name\": {\n        \"text\": \"Client Framework\"\n      },\n      \"description\": {\n        \"text\": \"Select the Client Framework type, or select None for Web API only.\"\n      },\n      \"isVisible\": true\n    },\n    {\n      \"id\": \"PipelineProvider\",\n      \"name\": {\n        \"text\": \"Pipeline Provider\"\n      },\n      \"description\": {\n        \"text\": \"Select the pipeline provider.\"\n      },\n      \"isVisible\": true\n    },\n    {\n      \"id\": \"Database\",\n      \"name\": {\n        \"text\": \"Database\"\n      },\n      \"description\": {\n        \"text\": \"Select the database type.\"\n      },\n      \"isVisible\": true\n    },\n  ]\n}\n"
  },
  {
    "path": ".template.config/template.json",
    "content": "{\n  \"$schema\": \"http://json.schemastore.org/template\",\n  \"author\": \"JasonTaylorDev\",\n  \"classifications\": [\n    \"Angular\",\n    \"API\",\n    \"Aspire\",\n    \"Clean Architecture\",\n    \"Cloud\",\n    \"NUnit\",\n    \"Playwright\",\n    \"React\",\n    \"Test\",\n    \"Web\",\n    \"Web API\"],\n  \"name\": \"Clean Architecture Solution\",\n  \"description\": \"A Clean Architecture Solution Template supporting Angular, React, and Web API-only apps built with ASP.NET Core and Aspire.\",\n  \"identity\": \"Clean.Architecture.Solution.CSharp\",\n  \"groupIdentity\": \"Clean.Architecture.Solution\",\n  \"shortName\": \"ca-sln\",\n  \"tags\": {\n    \"language\": \"C#\",\n    \"type\": \"project\"\n  },\n  \"sourceName\": \"CleanArchitecture\",\n  \"preferNameDirectory\": true,\n  \"symbols\": {\n    \"kestrelHttpPort\": {\n      \"type\": \"parameter\",\n      \"datatype\": \"integer\",\n      \"description\": \"Port number to use for the HTTP endpoint in launchSettings.json.\"\n    },\n    \"kestrelHttpPortGenerated\": {\n      \"type\": \"generated\",\n      \"generator\": \"port\",\n      \"parameters\": {\n        \"low\": 5000,\n        \"high\": 5300\n      }\n    },\n    \"kestrelHttpPortReplacer\": {\n      \"type\": \"generated\",\n      \"generator\": \"coalesce\",\n      \"parameters\": {\n        \"sourceVariableName\": \"kestrelHttpPort\",\n        \"fallbackVariableName\": \"kestrelHttpPortGenerated\"\n      },\n      \"replaces\": \"5000\"\n    },\n    \"kestrelHttpsPort\": {\n      \"type\": \"parameter\",\n      \"datatype\": \"integer\",\n      \"description\": \"Port number to use for the HTTPS endpoint in launchSettings.json.\"\n    },\n    \"kestrelHttpsPortGenerated\": {\n      \"type\": \"generated\",\n      \"generator\": \"port\",\n      \"parameters\": {\n        \"low\": 7000,\n        \"high\": 7300\n      }\n    },\n    \"kestrelHttpsPortReplacer\": {\n      \"type\": \"generated\",\n      \"generator\": \"coalesce\",\n      \"parameters\": {\n        \"sourceVariableName\": \"kestrelHttpsPort\",\n        \"fallbackVariableName\": \"kestrelHttpsPortGenerated\"\n      },\n      \"replaces\": \"5001\"\n    },\n    \"appHostHttpPort\": {\n      \"type\": \"parameter\",\n      \"datatype\": \"integer\",\n      \"description\": \"Port number to use for the HTTP endpoint in launchSettings.json of the AppHost project.\"\n    },\n    \"appHostHttpPortGenerated\": {\n      \"type\": \"generated\",\n      \"generator\": \"port\",\n      \"parameters\": {\n        \"low\": 15000,\n        \"high\": 15300\n      }\n    },\n    \"appHostHttpPortReplacer\": {\n      \"type\": \"generated\",\n      \"generator\": \"coalesce\",\n      \"parameters\": {\n        \"sourceVariableName\": \"appHostHttpPort\",\n        \"fallbackVariableName\": \"appHostHttpPortGenerated\"\n      },\n      \"replaces\": \"15000\"\n    },\n    \"appHostHttpsPort\": {\n      \"type\": \"parameter\",\n      \"datatype\": \"integer\",\n      \"description\": \"Port number to use for the HTTPS endpoint in launchSettings.json of the AppHost project.\"\n    },\n    \"appHostHttpsPortGenerated\": {\n      \"type\": \"generated\",\n      \"generator\": \"port\",\n      \"parameters\": {\n        \"low\": 17000,\n        \"high\": 17300\n      }\n    },\n    \"appHostHttpsPortReplacer\": {\n      \"type\": \"generated\",\n      \"generator\": \"coalesce\",\n      \"parameters\": {\n        \"sourceVariableName\": \"appHostHttpsPort\",\n        \"fallbackVariableName\": \"appHostHttpsPortGenerated\"\n      },\n      \"replaces\": \"17000\"\n    },\n    \"appHostOtlpHttpPort\": {\n      \"type\": \"parameter\",\n      \"datatype\": \"integer\",\n      \"description\": \"Port number to use for the OTLP HTTP endpoint in launchSettings.json of the AppHost project.\"\n    },\n    \"appHostOtlpHttpPortGenerated\": {\n      \"type\": \"generated\",\n      \"generator\": \"port\",\n      \"parameters\": {\n        \"low\": 19000,\n        \"high\": 19300\n      }\n    },\n    \"appHostOtlpHttpPortReplacer\": {\n      \"type\": \"generated\",\n      \"generator\": \"coalesce\",\n      \"parameters\": {\n        \"sourceVariableName\": \"appHostOtlpHttpPort\",\n        \"fallbackVariableName\": \"appHostOtlpHttpPortGenerated\"\n      },\n      \"replaces\": \"19000\"\n    },\n    \"appHostOtlpHttpsPort\": {\n      \"type\": \"parameter\",\n      \"datatype\": \"integer\",\n      \"description\": \"Port number to use for the OTLP HTTPS endpoint in launchSettings.json of the AppHost project.\"\n    },\n    \"appHostOtlpHttpsPortGenerated\": {\n      \"type\": \"generated\",\n      \"generator\": \"port\",\n      \"parameters\": {\n        \"low\": 21000,\n        \"high\": 21300\n      }\n    },\n    \"appHostOtlpHttpsPortReplacer\": {\n      \"type\": \"generated\",\n      \"generator\": \"coalesce\",\n      \"parameters\": {\n        \"sourceVariableName\": \"appHostOtlpHttpsPort\",\n        \"fallbackVariableName\": \"appHostOtlpHttpsPortGenerated\"\n      },\n      \"replaces\": \"21000\"\n    },\n    \"appHostResourceHttpPort\": {\n      \"type\": \"parameter\",\n      \"datatype\": \"integer\",\n      \"description\": \"Port number to use for the resource service HTTP endpoint in launchSettings.json of the AppHost project.\"\n    },\n    \"appHostResourceHttpPortGenerated\": {\n      \"type\": \"generated\",\n      \"generator\": \"port\",\n      \"parameters\": {\n        \"low\": 20000,\n        \"high\": 20300\n      }\n    },\n    \"appHostResourceHttpPortReplacer\": {\n      \"type\": \"generated\",\n      \"generator\": \"coalesce\",\n      \"parameters\": {\n        \"sourceVariableName\": \"appHostResourceHttpPort\",\n        \"fallbackVariableName\": \"appHostResourceHttpPortGenerated\"\n      },\n      \"replaces\": \"20000\"\n    },\n    \"appHostResourceHttpsPort\": {\n      \"type\": \"parameter\",\n      \"datatype\": \"integer\",\n      \"description\": \"Port number to use for the resource service HTTPS endpoint in launchSettings.json of the AppHost project.\"\n    },\n    \"appHostResourceHttpsPortGenerated\": {\n      \"type\": \"generated\",\n      \"generator\": \"port\",\n      \"parameters\": {\n        \"low\": 22000,\n        \"high\": 22300\n      }\n    },\n    \"appHostResourceHttpsPortReplacer\": {\n      \"type\": \"generated\",\n      \"generator\": \"coalesce\",\n      \"parameters\": {\n        \"sourceVariableName\": \"appHostResourceHttpsPort\",\n        \"fallbackVariableName\": \"appHostResourceHttpsPortGenerated\"\n      },\n      \"replaces\": \"22000\"\n    },\n    \"caPackageVersion\": {\n      \"type\": \"generated\",\n      \"generator\": \"constant\",\n      \"replaces\": \"caPackageVersion\",\n      \"parameters\": {\n        \"value\": \"0.0.0\"\n      }\n    },\n    \"caRepositoryUrl\": {\n      \"type\": \"generated\",\n      \"generator\": \"constant\",\n      \"replaces\": \"caRepositoryUrl\",\n      \"parameters\": {\n        \"value\": \"https://github.com/jasontaylordev/CleanArchitecture\"\n      }\n    },\n    \"caDocsUrl\": {\n      \"type\": \"generated\",\n      \"generator\": \"constant\",\n      \"replaces\": \"caDocsUrl\",\n      \"parameters\": {\n        \"value\": \"https://cleanarchitecture.jasontaylor.dev\"\n      }\n    },\n    \"ClientFramework\": {\n      \"type\": \"parameter\",\n      \"datatype\": \"choice\",\n      \"choices\": [\n        {\n          \"choice\": \"Angular\",\n          \"description\": \"Use Angular\"\n        },\n        {\n          \"choice\": \"React\",\n          \"description\": \"Use React\"\n        },\n        {\n          \"choice\": \"None\",\n          \"description\": \"Web API only\"\n        }\n      ],\n      \"defaultValue\": \"Angular\",\n      \"description\": \"The type of client framework to use\"\n    },\n    \"UseAngular\": {\n      \"type\": \"computed\",\n      \"value\": \"(ClientFramework == \\\"Angular\\\")\"\n    },\n    \"UseReact\": {\n      \"type\": \"computed\",\n      \"value\": \"(ClientFramework == \\\"React\\\")\"\n    },\n    \"UseApiOnly\": {\n      \"type\": \"computed\",\n      \"value\": \"(ClientFramework == \\\"None\\\")\"\n    },\n    \"Database\": {\n      \"type\": \"parameter\",\n      \"datatype\": \"choice\",\n      \"choices\": [\n        {\n          \"choice\": \"postgresql\",\n          \"description\": \"PostgreSQL\"\n        },\n        {\n          \"choice\": \"sqlite\",\n          \"description\": \"SQLite\"\n        },\n        {\n          \"choice\": \"sqlserver\",\n          \"description\": \"SQL Server\"\n        }\n      ],\n      \"defaultValue\": \"sqlite\",\n      \"description\": \"The database type to use.\"\n    },\n    \"PipelineProvider\": {\n      \"type\": \"parameter\",\n      \"datatype\": \"choice\",\n      \"choices\": [\n        {\n          \"choice\": \"azdo\",\n          \"description\": \"Azure Pipelines\"\n        },\n        {\n          \"choice\": \"github\",\n          \"description\": \"GitHub Actions\"\n        }\n      ],\n      \"defaultValue\": \"github\",\n      \"description\": \"The pipeline provider to use (github for Github Actions and azdo for Azure Pipelines).\"\n    },\n    \"UseAzurePipelines\": {\n      \"type\": \"computed\",\n      \"value\": \"(PipelineProvider == \\\"azdo\\\")\"\n    },\n    \"UseGithubActions\": {\n      \"type\": \"computed\",\n      \"value\": \"(PipelineProvider == \\\"github\\\")\"\n    },\n    \"UsePostgreSQL\": {\n      \"type\": \"computed\",\n      \"value\": \"(Database == \\\"postgresql\\\")\"\n    },\n    \"UseSqlite\": {\n      \"type\": \"computed\",\n      \"value\": \"(Database == \\\"sqlite\\\")\"\n    },\n    \"UseSqlServer\": {\n      \"type\": \"computed\",\n      \"value\": \"(Database == \\\"sqlserver\\\")\"\n    }\n  },\n  \"sources\": [\n    {\n      \"source\": \"./\",\n      \"target\": \"./\",\n      \"exclude\": [\n        \".azure/**/*\",\n        \".template.config/**/*\",\n        \"templates/**/*\",\n        \"**/*.filelist\",\n        \"**/*.user\",\n        \"**/*.lock.json\",\n        \"*.nuspec\",\n        \"src/Web/appsettings.json\",\n        \"src/Web/appsettings.PostgreSQL.json\",\n        \"src/Web/appsettings.SQLite.json\",\n        \"src/Web/appsettings.SQLServer.json\",\n        \"tests/Application.FunctionalTests/PostgreSQLTestcontainersTestDatabase.cs\",\n        \"tests/Application.FunctionalTests/PostgreSQLTestDatabase.cs\",\n        \"tests/Application.FunctionalTests/SqliteTestDatabase.cs\",\n        \"tests/Application.FunctionalTests/SqlTestcontainersTestDatabase.cs\",\n        \"tests/Application.FunctionalTests/SqlTestDatabase.cs\",\n        \"tests/Application.FunctionalTests/appsettings.json\",\n        \"tests/Application.FunctionalTests/appsettings.PostgreSQL.json\",\n        \"tests/Application.FunctionalTests/appsettings.SQLServer.json\",\n        \".azdo/**/*\",\n        \".github/**/*\"\n      ],\n      \"rename\": {\n        \"README-template.md\": \"README.md\"\n      },\n      \"modifiers\": [\n        {\n          \"condition\": \"(!UseApiOnly)\",\n          \"exclude\": [\n            \"src/Web/Infrastructure/BearerSecuritySchemeTransformer.cs\"\n          ]\n        },\n        {\n          \"condition\": \"(UseAngular)\",\n          \"exclude\": [\n            \"src/Web/ClientApp-React/**\",\n            \"src/Web/Web-webapi.http\"\n          ]\n        },\n        {\n          \"condition\": \"(UseReact)\",\n          \"exclude\": [\n            \"src/Web/ClientApp/**\",\n            \"src/Web/Web-webapi.http\"\n          ],\n          \"rename\": {\n            \"ClientApp-React\": \"ClientApp\"\n          }\n        },\n        {\n          \"condition\": \"(UseApiOnly)\",\n          \"exclude\": [\n            \"src/Web/ClientApp/**\",\n            \"src/Web/ClientApp-React/**\",\n            \"src/Web/Pages/**\",\n            \"src/Web/Web.http\",\n            \"tests/Web.AcceptanceTests/**\"\n          ],\n          \"rename\": {\n            \"Web-webapi.http\": \"Web.http\"\n          }\n        },\n        {\n          \"condition\": \"(UseAzurePipelines)\",\n          \"include\": [\n            \".azdo/**/*\"\n          ]\n        },\n        {\n          \"condition\": \"(UseGithubActions)\",\n          \"include\": [\n            \".github/**/*\"\n          ]\n        },\n        {\n          \"condition\": \"(UsePostgreSQL)\",\n          \"include\": [\n            \"src/Web/appsettings.PostgreSQL.json\",\n            \"tests/Application.FunctionalTests/PostgreSQLTestcontainersTestDatabase.cs\",\n            \"tests/Application.FunctionalTests/PostgreSQLTestDatabase.cs\",\n            \"tests/Application.FunctionalTests/appsettings.PostgreSQL.json\"\n          ],\n          \"rename\": {\n            \"src/Web/appsettings.PostgreSQL.json\": \"src/Web/appsettings.json\",\n            \"tests/Application.FunctionalTests/appsettings.PostgreSQL.json\": \"tests/Application.FunctionalTests/appsettings.json\"\n          }\n        },\n        {\n          \"condition\": \"(UseSqlServer)\",\n          \"include\": [\n            \"src/Web/appsettings.SQLServer.json\",\n            \"tests/Application.FunctionalTests/SqlTestcontainersTestDatabase.cs\",\n            \"tests/Application.FunctionalTests/SqlTestDatabase.cs\",\n            \"tests/Application.FunctionalTests/appsettings.SQLServer.json\"\n          ],\n          \"rename\": {\n            \"src/Web/appsettings.SQLServer.json\": \"src/Web/appsettings.json\",\n            \"tests/Application.FunctionalTests/appsettings.SQLServer.json\": \"tests/Application.FunctionalTests/appsettings.json\"\n          }\n        },\n        {\n          \"condition\": \"(UseSqlite)\",\n          \"include\": [\n            \"src/Web/appsettings.SQLite.json\",\n            \"tests/Application.FunctionalTests/SqliteTestDatabase.cs\"\n          ],\n          \"rename\": {\n            \"src/Web/appsettings.SQLite.json\": \"src/Web/appsettings.json\"\n          }\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to make participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behaviour that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behaviour by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehaviour and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behaviour.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviours that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behaviour may be\nreported by contacting the project team at hello@jasontaylor.dev. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "CleanArchitecture.nuspec",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<package xmlns=\"http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd\">\r\n  <metadata>\r\n\r\n    <id>Clean.Architecture.Solution.Template</id>\r\n    <version>0.0.0</version>\r\n    <title>Clean Architecture Solution Template</title>\r\n    <authors>JasonTaylorDev</authors>\r\n    <description>A Clean Architecture Solution Template supporting Angular, React, and Web API-only apps built with ASP.NET Core and Aspire.</description>\r\n    <summary>\r\n      A Clean Architecture Solution Template supporting Angular, React, and Web API-only apps built with ASP.NET Core and Aspire.\r\n    </summary>\r\n    <releaseNotes></releaseNotes>\r\n\r\n    <projectUrl>https://cleanarchitecture.jasontaylor.dev</projectUrl>\r\n    <repository type=\"git\" url=\"https://github.com/JasonTaylorDev/CleanArchitecture.git\" branch=\"main\" />\r\n\r\n    <license type=\"expression\">MIT</license>\r\n    <requireLicenseAcceptance>false</requireLicenseAcceptance>\r\n    <tags>clean-architecture project template csharp dotnet angular react webapi aspire</tags>\r\n    <icon>icon.png</icon>\r\n    <readme>README.md</readme>\r\n\r\n    <packageTypes>\r\n      <packageType name=\"Template\" />\r\n    </packageTypes>\r\n\r\n  </metadata>\r\n\r\n  <files>\r\n    <file src=\".template.config\\icon.png\" />\r\n    <file src=\"README.md\" />\r\n    <file src=\".\\**\" target=\"content\" exclude=\"**\\node_modules\\**;**\\tools\\**;**\\bin\\**;**\\obj\\**;.\\.vs\\**;.\\.vscode\\**;**\\ClientApp\\dist\\**;**\\ClientApp\\.angular\\**;**\\wwwroot\\dist\\**;content\\Directory.Build.*;.\\.git\\**;.\\.github\\workflows\\jekyll-gh-pages.yml;.\\.github\\workflows\\release.yml;.\\.github\\workflows\\codeql.yml;.\\.github\\workflows\\build.yml;.\\.github\\ISSUE_TEMPLATE\\**;.\\.github\\icon.png;.\\.github\\FUNDING.yml;.\\CODE_OF_CONDUCT.md;.\\LICENSE;.\\README.md;.\\CleanArchitecture.nuspec;.\\src\\Web\\app.db;.\\renovate.json;.\\build\\**;.\\.github\\workflows\\test-templates.yml;.\\artifacts\\**;\" />\r\n  </files>\r\n\r\n</package>\r\n"
  },
  {
    "path": "CleanArchitecture.slnx",
    "content": "<Solution>\n  <Folder Name=\"/Solution Items/\">\n    <File Path=\".editorconfig\" />\n    <File Path=\".gitignore\" />\n    <File Path=\"Directory.Build.props\" />\n    <File Path=\"Directory.Packages.props\" />\n    <File Path=\"global.json\" />\n    <File Path=\"README.md\" />\n  </Folder>\n  <Folder Name=\"/src/\">\n    <Project Path=\"src/AppHost/AppHost.csproj\" DefaultStartup=\"true\" />\n    <Project Path=\"src/ServiceDefaults/ServiceDefaults.csproj\" />\n    <Project Path=\"src/Application/Application.csproj\" />\n    <Project Path=\"src/Domain/Domain.csproj\" />\n    <Project Path=\"src/Infrastructure/Infrastructure.csproj\" />\n    <Project Path=\"src/Shared/Shared.csproj\" />\n    <Project Path=\"src/Web/Web.csproj\" />\n  </Folder>\n  <Folder Name=\"/tests/\">\n    <Project Path=\"tests/Application.FunctionalTests/Application.FunctionalTests.csproj\" />\n    <Project Path=\"tests/TestAppHost/TestAppHost.csproj\" />\n    <Project Path=\"tests/Application.UnitTests/Application.UnitTests.csproj\" />\n    <Project Path=\"tests/Domain.UnitTests/Domain.UnitTests.csproj\" />\n    <Project Path=\"tests/Infrastructure.IntegrationTests/Infrastructure.IntegrationTests.csproj\" />\n    <!--#if (!UseApiOnly)-->\n    <Project Path=\"tests/Web.AcceptanceTests/Web.AcceptanceTests.csproj\" />\n    <!--#endif-->\n  </Folder>\n</Solution>\n"
  },
  {
    "path": "Directory.Build.props",
    "content": "﻿<!-- See https://aka.ms/dotnet/msbuild/customize for more details on customizing your build -->\n<Project>\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n    <!--#if (UsePostgreSQL)-->\n    <!-- Suppress NU1608 until Npgsql.EntityFrameworkCore.PostgreSQL 10.0.0 is released -->\n    <WarningsNotAsErrors>NU1608</WarningsNotAsErrors>\n    <!--#endif-->\n    <ArtifactsPath>$(MSBuildThisFileDirectory)artifacts</ArtifactsPath>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n</Project>"
  },
  {
    "path": "Directory.Packages.props",
    "content": "<!-- For more info on central package management go to https://devblogs.microsoft.com/nuget/introducing-central-package-management/ -->\r\n<Project>\r\n  <PropertyGroup>\r\n    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>\r\n  </PropertyGroup>\r\n  <ItemGroup>\r\n    <PackageVersion Include=\"Ardalis.GuardClauses\" Version=\"5.0.0\" />\r\n    <PackageVersion Include=\"AutoMapper\" Version=\"16.1.1\" />\r\n    <PackageVersion Include=\"Azure.Extensions.AspNetCore.Configuration.Secrets\" Version=\"1.5.0\" />\r\n    <PackageVersion Include=\"Azure.Identity\" Version=\"1.19.0\" />\r\n    <PackageVersion Include=\"coverlet.collector\" Version=\"8.0.1\" />\r\n    <PackageVersion Include=\"FluentValidation.DependencyInjectionExtensions\" Version=\"12.1.1\" />\r\n    <PackageVersion Include=\"MediatR\" Version=\"14.1.0\" />\r\n    <PackageVersion Include=\"MediatR.Contracts\" Version=\"2.0.1\" />\r\n    <PackageVersion Include=\"Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore\" Version=\"10.0.5\" />\r\n    <PackageVersion Include=\"Microsoft.AspNetCore.Identity.EntityFrameworkCore\" Version=\"10.0.5\" />\r\n    <PackageVersion Include=\"Microsoft.AspNetCore.Mvc.Testing\" Version=\"10.0.5\" />\r\n    <PackageVersion Include=\"Microsoft.AspNetCore.OpenApi\" Version=\"10.0.5\" />\r\n    <PackageVersion Include=\"Microsoft.Build.Tasks.Core\" Version=\"18.4.0\" />\r\n    <PackageVersion Include=\"Microsoft.Build.Utilities.Core\" Version=\"18.4.0\" />\r\n    <PackageVersion Include=\"Microsoft.EntityFrameworkCore\" Version=\"10.0.5\" />\r\n    <PackageVersion Include=\"Microsoft.EntityFrameworkCore.Design\" Version=\"10.0.5\" />\r\n    <PackageVersion Include=\"Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore\" Version=\"10.0.5\" />\r\n    <PackageVersion Include=\"Microsoft.Extensions.Hosting\" Version=\"10.0.5\" />\r\n    <PackageVersion Include=\"Microsoft.NET.Test.Sdk\" Version=\"18.3.0\" />\r\n    <PackageVersion Include=\"Microsoft.Extensions.ApiDescription.Server\" Version=\"10.0.5\" />\r\n    <PackageVersion Include=\"Moq\" Version=\"4.20.72\" />\r\n    <PackageVersion Include=\"nunit\" Version=\"4.5.1\" />\r\n    <PackageVersion Include=\"NUnit.Analyzers\" Version=\"4.12.0\" />\r\n    <PackageVersion Include=\"NUnit3TestAdapter\" Version=\"6.1.0\" />\r\n    <PackageVersion Include=\"Respawn\" Version=\"7.0.0\" />\r\n    <PackageVersion Include=\"Scalar.AspNetCore\" Version=\"2.13.11\" />\r\n    <PackageVersion Include=\"Shouldly\" Version=\"4.3.0\" />\r\n    <PackageVersion Include=\"System.IdentityModel.Tokens.Jwt\" Version=\"8.16.0\" />\r\n    <!--#if(!UseApiOnly)-->\r\n    <PackageVersion Include=\"Microsoft.Playwright\" Version=\"1.58.0\" />\r\n    <PackageVersion Include=\"Reqnroll.NUnit\" Version=\"3.3.3\" />\r\n    <!--#endif-->\r\n    <!--#if (UsePostgreSQL)-->\r\n    <PackageVersion Include=\"Aspire.Npgsql.EntityFrameworkCore.PostgreSQL\" Version=\"13.1.3\" />\r\n    <PackageVersion Include=\"Aspire.Hosting.PostgreSQL\" Version=\"13.1.3\" />\r\n    <PackageVersion Include=\"Npgsql.EntityFrameworkCore.PostgreSQL\" Version=\"10.0.1\" />\r\n    <!--#endif-->\r\n    <!--#if (UseSqlServer)-->\r\n    <PackageVersion Include=\"Aspire.Microsoft.EntityFrameworkCore.SqlServer\" Version=\"13.1.3\" />\r\n    <PackageVersion Include=\"Aspire.Hosting.SqlServer\" Version=\"13.1.3\" />\r\n    <!--#endif-->\r\n    <!--#if (UseSqlite)-->\r\n    <PackageVersion Include=\"Microsoft.EntityFrameworkCore.Sqlite\" Version=\"10.0.5\" />\r\n    <PackageVersion Include=\"CommunityToolkit.Aspire.Hosting.SQLite\" Version=\"13.1.1\" />\r\n    <!--#endif-->\r\n    <PackageVersion Include=\"Aspire.Hosting.AppHost\" Version=\"13.1.3\" />\r\n    <PackageVersion Include=\"Aspire.Hosting.Testing\" Version=\"13.1.3\" />\r\n    <PackageVersion Include=\"Aspire.Hosting.JavaScript\" Version=\"13.1.3\" />\r\n    <PackageVersion Include=\"Microsoft.Extensions.Http.Resilience\" Version=\"10.4.0\" />\r\n    <PackageVersion Include=\"Microsoft.Extensions.ServiceDiscovery\" Version=\"10.4.0\" />\r\n    <PackageVersion Include=\"OpenTelemetry.Exporter.OpenTelemetryProtocol\" Version=\"1.15.0\" />\r\n    <PackageVersion Include=\"OpenTelemetry.Extensions.Hosting\" Version=\"1.15.0\" />\r\n    <PackageVersion Include=\"OpenTelemetry.Instrumentation.AspNetCore\" Version=\"1.15.1\" />\r\n    <PackageVersion Include=\"OpenTelemetry.Instrumentation.Http\" Version=\"1.15.0\" />\r\n    <PackageVersion Include=\"OpenTelemetry.Instrumentation.Runtime\" Version=\"1.15.0\" />\r\n  </ItemGroup>\r\n</Project>"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 JasonTaylorDev\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README-template.md",
    "content": "﻿# CleanArchitecture\r\n\r\nThe project was generated using the [Clean.Architecture.Solution.Template](caRepositoryUrl) version caPackageVersion.\r\n\r\n## Build\r\n\r\nRun `dotnet build` to build the solution.\r\n\r\n## Run\r\n\r\nTo run the application:\r\n\r\n```bash\r\ndotnet run --project .\\src\\AppHost\r\n```\r\n\r\nThe Aspire dashboard will open automatically, showing the application URLs and logs.\r\n\r\n## Code Styles & Formatting\r\n\r\nThe template includes [EditorConfig](https://editorconfig.org/) support to help maintain consistent coding styles for multiple developers working on the same project across various editors and IDEs. The **.editorconfig** file defines the coding styles applicable to this solution.\r\n\r\n## Code Scaffolding\r\n\r\nThe template includes support to scaffold new commands and queries.\r\n\r\nStart in the `.\\src\\Application\\` folder.\r\n\r\nCreate a new command:\r\n\r\n```\r\ndotnet new ca-usecase --name CreateTodoList --feature-name TodoLists --usecase-type command --return-type int\r\n```\r\n\r\nCreate a new query:\r\n\r\n```\r\ndotnet new ca-usecase -n GetTodos -fn TodoLists -ut query -rt TodosVm\r\n```\r\n\r\nIf you encounter the error *\"No templates or subcommands found matching: 'ca-usecase'.\"*, install the template and try again:\r\n\r\n```bash\r\ndotnet new install Clean.Architecture.Solution.Template::caPackageVersion\r\n```\r\n\r\n## Test\r\n\r\nThe solution contains unit, integration, and functional tests.\r\n\r\nTo run the tests:\r\n```bash\r\ndotnet test\r\n```\r\n\r\n## Help\r\nTo learn more about the template go to the [project website](caDocsUrl). Here you can find additional guidance, request new features, report a bug, and discuss the template with other users."
  },
  {
    "path": "README.md",
    "content": "# Clean Architecture Solution Template\r\n\r\n[![Build](https://github.com/jasontaylordev/CleanArchitecture/actions/workflows/build.yml/badge.svg)](https://github.com/jasontaylordev/CleanArchitecture/actions/workflows/build.yml)\r\n[![CodeQL](https://github.com/jasontaylordev/CleanArchitecture/actions/workflows/codeql.yml/badge.svg)](https://github.com/jasontaylordev/CleanArchitecture/actions/workflows/codeql.yml)\r\n[![Nuget](https://img.shields.io/nuget/v/Clean.Architecture.Solution.Template?label=NuGet)](https://www.nuget.org/packages/Clean.Architecture.Solution.Template)\r\n[![Nuget](https://img.shields.io/nuget/dt/Clean.Architecture.Solution.Template?label=Downloads)](https://www.nuget.org/packages/Clean.Architecture.Solution.Template)\r\n![Twitter Follow](https://img.shields.io/twitter/follow/jasontaylordev?label=Follow&style=social)\r\n\r\nThe goal of this template is to provide a straightforward and efficient approach to enterprise application development, leveraging the power of Clean Architecture and ASP.NET Core. Using this template, you can effortlessly create a new app with Angular, React, or Web API only, powered by ASP.NET Core and Aspire. Getting started is easy - simply install the **.NET template** (see below for full details).\r\n\r\nFor full documentation, visit **[cleanarchitecture.jasontaylor.dev](https://cleanarchitecture.jasontaylor.dev)**.\r\n\r\nIf you find this project useful, please give it a star. Thanks! ⭐\r\n\r\n## Getting Started\r\n\r\nThe following prerequisites are required to build and run the solution:\r\n\r\n- [.NET 10.0 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) (latest version)\r\n- [Node.js](https://nodejs.org/) (latest LTS, only required if you are using Angular or React)\r\n\r\nThe easiest way to get started is to install the [.NET template](https://www.nuget.org/packages/Clean.Architecture.Solution.Template):\r\n```\r\ndotnet new install Clean.Architecture.Solution.Template\r\n```\r\n\r\nOnce installed, create a new solution using the template. You can choose to use Angular, React, or create a Web API-only solution. Specify the client framework using the `-cf` or `--client-framework` option, and provide the output directory where your project will be created. Here are some examples:\r\n\r\nTo create a Single-Page Application (SPA) with Angular and ASP.NET Core:\r\n```bash\r\ndotnet new ca-sln --client-framework Angular --output YourProjectName\r\n```\r\n\r\nTo create a SPA with React and ASP.NET Core:\r\n```bash\r\ndotnet new ca-sln -cf React -o YourProjectName\r\n```\r\n\r\nTo create a ASP.NET Core Web API-only solution:\r\n```bash\r\ndotnet new ca-sln -cf None -o YourProjectName\r\n```\r\n\r\nLaunch the app:\r\n```bash\r\ncd src/AppHost\r\ndotnet run\r\n```\r\n\r\nTo learn more, run the following command:\r\n```bash\r\ndotnet new ca-sln --help\r\n```\r\n\r\nYou can create use cases (commands or queries) by navigating to `./src/Application` and running `dotnet new ca-usecase`. Here are some examples:\r\n\r\nTo create a new command:\r\n```bash\r\ndotnet new ca-usecase --name CreateTodoList --feature-name TodoLists --usecase-type command --return-type int\r\n```\r\n\r\nTo create a query:\r\n```bash\r\ndotnet new ca-usecase -n GetTodos -fn TodoLists -ut query -rt TodosVm\r\n```\r\n\r\nTo learn more, run the following command:\r\n```bash\r\ndotnet new ca-usecase --help\r\n```\r\n\r\n## Database\r\n\r\nThe template supports [PostgreSQL](https://www.postgresql.org), [SQLite](https://www.sqlite.org/) (default), and [SQL Server](https://learn.microsoft.com/en-us/sql/sql-server/what-is-sql-server). Specify the database to use with the `--database` option:\r\n\r\n```bash\r\ndotnet new ca-sln --database [postgresql|sqlite|sqlserver]\r\n```\r\n\r\nOn application startup, the database is automatically **deleted**, **recreated**, and **seeded** using `ApplicationDbContextInitialiser`. This is a practical strategy for early development, avoiding the overhead of maintaining migrations while keeping the schema and sample data in sync with the domain model.\r\n\r\nThis process includes:\r\n\r\n- Deleting the existing database  \r\n- Recreating the schema from the current model  \r\n- Seeding default roles, users, and data  \r\n\r\nFor production environments, consider using EF Core migrations or migration bundles during deployment.  \r\nFor more information, see [Database Initialisation Strategies for EF Core](https://jasontaylor.dev/ef-core-database-initialisation-strategies).\r\n\r\n## Deploy\r\n\r\nThis template is structured to follow the Azure Developer CLI (azd). You can learn more about `azd` in the [official documentation](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli). To get started:\r\n\r\n```bash\r\n# Log in to Azure\r\nazd auth login\r\n\r\n# Provision and deploy to Azure\r\nazd up\r\n```\r\n\r\nTo set up a CI/CD pipeline (GitHub Actions or Azure DevOps):\r\n\r\n```bash\r\nazd pipeline config\r\n```\r\n\r\n## API Documentation\r\n\r\nThis template includes built-in API documentation using [ASP.NET Core OpenAPI](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/overview) and [Scalar](https://scalar.com/). Once the application is running, navigate to `/scalar` to explore the API using the Scalar UI.\r\n\r\nThe OpenAPI specification is generated at build time and written to `wwwroot/openapi/v1.json`.\r\n\r\n## Technologies\r\n\r\n* [ASP.NET Core 10](https://docs.microsoft.com/en-us/aspnet/core/introduction-to-aspnet-core)\r\n* [Aspire](https://aspire.dev)\r\n* [Entity Framework Core 10](https://docs.microsoft.com/en-us/ef/core/)\r\n* [Angular 21](https://angular.dev/) or [React 19](https://react.dev/)\r\n* [MediatR](https://github.com/jbogard/MediatR)\r\n* [AutoMapper](https://automapper.org/)\r\n* [FluentValidation](https://fluentvalidation.net/)\r\n* [NUnit](https://nunit.org/), [Shouldly](https://docs.shouldly.org/), [Moq](https://github.com/devlooped/moq) & [Respawn](https://github.com/jbogard/Respawn)\r\n* [Scalar](https://scalar.com/)\r\n\r\n## Versions\r\nThe main branch is now on .NET 10.0. The following previous versions are available:\r\n\r\n* [9.0](https://github.com/jasontaylordev/CleanArchitecture/tree/net9.0)\r\n* [8.0](https://github.com/jasontaylordev/CleanArchitecture/tree/net8.0)\r\n* [7.0](https://github.com/jasontaylordev/CleanArchitecture/tree/net7.0)\r\n* [6.0](https://github.com/jasontaylordev/CleanArchitecture/tree/net6.0)\r\n* [5.0](https://github.com/jasontaylordev/CleanArchitecture/tree/net5.0)\r\n* [3.1](https://github.com/jasontaylordev/CleanArchitecture/tree/netcore3.1)\r\n\r\n## Learn More\r\n\r\n* [Clean Architecture Solution Template Documentation](https://cleanarchitecture.jasontaylor.dev)\r\n* [Clean Architecture with ASP.NET Core 3.0 (GOTO 2019)](https://youtu.be/dK4Yb6-LxAk)\r\n\r\n## Support\r\n\r\nIf you are having problems, please let me know by [raising a new issue](https://github.com/jasontaylordev/CleanArchitecture/issues/new/choose).\r\n\r\n## License\r\n\r\nThis project is licensed with the [MIT license](LICENSE).\r\n\r\n"
  },
  {
    "path": "azure.yaml",
    "content": "# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json\n\n# Name of the application.\nname: clean-architecture-azd\nservices:\n  web:\n    language: csharp\n    project: ./src/Web\n    host: appservice\n"
  },
  {
    "path": "build/build.ps1",
    "content": "param (\n    [string]$Target = \"Default\",\n    [string]$Configuration = \"Release\"\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$solution = \"CleanArchitecture.slnx\"\n$clientApps = @(\"./src/Web/ClientApp\", \"./src/Web/ClientApp-React\")\n\nWrite-Host \"Building solution...\"\ndotnet build $solution --configuration $Configuration\nif ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }\n\nforeach ($clientApp in $clientApps) {\n    if (Test-Path $clientApp) {\n        Write-Host \"Installing client dependencies ($clientApp)...\"\n        Push-Location $clientApp\n        try {\n            npm ci\n            if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }\n            npm run build\n            if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }\n        } finally {\n            Pop-Location\n        }\n    }\n}\n\nWrite-Host \"Testing solution...\"\nif ($Target -eq \"Basic\") {\n    $testProjects = Get-ChildItem -Path \"tests\" -Filter \"*.csproj\" -Recurse |\n        Where-Object { $_.Name -notlike \"*AcceptanceTests*\" }\n    foreach ($project in $testProjects) {\n        dotnet test $project.FullName --no-build --configuration $Configuration\n        if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }\n    }\n} else {\n    dotnet test $solution --no-build --configuration $Configuration\n    exit $LASTEXITCODE\n}\n"
  },
  {
    "path": "build/repack.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\n$root = Split-Path $PSScriptRoot -Parent\n$name = \"Clean.Architecture.Solution.Template.0.0.0.nupkg\"\n$pkg  = Join-Path $root \"artifacts\\$name\"\n\nSet-Location $root\n\ndotnet new uninstall Clean.Architecture.Solution.Template 2>$null\nnuget pack \"CleanArchitecture.nuspec\" -NoDefaultExcludes -OutputDirectory \"artifacts\"\ndotnet new install $pkg --force\nRemove-Item $pkg -Force 2>$null\n"
  },
  {
    "path": "build/test.ps1",
    "content": "param (\n    [string[]]$ClientFramework = @(\"angular\", \"react\", \"none\"),\n    [string[]]$Database = @(\"sqlite\", \"sqlserver\", \"postgresql\")\n)\n\n$outputPath = Join-Path (Split-Path $PSScriptRoot -Parent) \"artifacts\\template-tests\"\n$results = @()\n\nfunction CreateAndTestProject {\n    param (\n        [string]$clientFramework,\n        [string]$database\n    )\n\n    $name = \"$clientFramework-$database\"\n    $projectPath = Join-Path $outputPath $name\n\n    try {\n        if (Test-Path $projectPath) {\n            Write-Host \"Removing existing directory: $name\"\n            Remove-Item -Recurse -Force $projectPath\n        }\n\n        Write-Host \"Creating project: $name\"\n        $startTime = Get-Date\n\n        dotnet new ca-sln --client-framework $clientFramework --database $database --name CleanArchitecture --output $projectPath --no-update-check\n        if ($LASTEXITCODE -ne 0) { throw \"dotnet new ca-sln failed for $name\" }\n\n        $exitCode = 0\n        Push-Location $projectPath\n        try {\n            Write-Host \"Building: $name\"\n            dotnet build --configuration Release\n            if ($LASTEXITCODE -ne 0) { throw \"Build failed for $name\" }\n\n            if ($clientFramework -ne \"none\") {\n                Write-Host \"Building client app: $name\"\n                Push-Location \"./src/Web/ClientApp\"\n                try {\n                    npm ci\n                    if ($LASTEXITCODE -ne 0) { throw \"npm ci failed for $name\" }\n                    npm run build\n                    if ($LASTEXITCODE -ne 0) { throw \"npm build failed for $name\" }\n                } finally {\n                    Pop-Location\n                }\n            }\n\n            if ($clientFramework -ne \"none\") {\n                Write-Host \"Installing Playwright browsers: $name\"\n                pwsh artifacts/bin/Web.AcceptanceTests/release/playwright.ps1 install --with-deps chromium\n                if ($LASTEXITCODE -ne 0) { throw \"Playwright install failed for $name\" }\n            }\n\n            Write-Host \"Testing: $name\"\n            dotnet test --no-build --configuration Release\n            if ($LASTEXITCODE -ne 0) { $exitCode = $LASTEXITCODE }\n        } finally {\n            Pop-Location\n        }\n\n        $endTime = Get-Date\n        $duration = $endTime - $startTime\n\n        $script:results += [PSCustomObject]@{\n            ClientFramework = $clientFramework\n            Database        = $database\n            ExitCode        = $exitCode\n            Status          = if ($exitCode -eq 0) { \"Success\" } else { \"Failure\" }\n            Duration        = $duration.ToString(\"c\")\n        }\n    } catch {\n        Write-Host \"An error occurred while processing: $name\"\n        Write-Host $_.Exception.Message\n        $script:results += [PSCustomObject]@{\n            ClientFramework = $clientFramework\n            Database        = $database\n            ExitCode        = -1\n            Status          = \"Error\"\n            Duration        = \"00:00:00.0000000\"\n        }\n    }\n}\n\nif (-not (Test-Path $outputPath)) {\n    New-Item -ItemType Directory -Path $outputPath | Out-Null\n}\n\nforeach ($cf in $ClientFramework) {\n    foreach ($db in $Database) {\n        CreateAndTestProject -clientFramework $cf -database $db\n    }\n}\n\n$results | Format-Table -Property ClientFramework, Database, Status, Duration -AutoSize\n\nif ($results | Where-Object { $_.Status -ne \"Success\" }) {\n    exit 1\n}\n"
  },
  {
    "path": "global.json",
    "content": "{\r\n  \"sdk\": {\r\n    \"version\": \"10.0.201\",\r\n    \"rollForward\": \"latestFeature\"\r\n  }\r\n}"
  },
  {
    "path": "infra/README.md",
    "content": "# Infrastructure (infra)\n\nThis folder contains infrastructure-as-code and deployment templates used with the **Azure Developer CLI (azd)**.\n\n## What this folder includes\n- Bicep files for provisioning Azure resources  \n- Configuration for App Service, hosting, Key Vault, and databases  \n- Deployment workflows used by `azd up`  \n- Environment configuration for cloud deployment  \n\n## How to use it\n\n### Prerequisites\n- Azure account\n- Azure Developer CLI installed (`azd`)\n\n### Deploying with azd\n```bash\nazd auth login\nazd up\n```\n"
  },
  {
    "path": "infra/abbreviations.json",
    "content": "{\n    \"analysisServicesServers\": \"as\",\n    \"apiManagementService\": \"apim-\",\n    \"appConfigurationStores\": \"appcs-\",\n    \"appManagedEnvironments\": \"cae-\",\n    \"appContainerApps\": \"ca-\",\n    \"authorizationPolicyDefinitions\": \"policy-\",\n    \"automationAutomationAccounts\": \"aa-\",\n    \"blueprintBlueprints\": \"bp-\",\n    \"blueprintBlueprintsArtifacts\": \"bpa-\",\n    \"cacheRedis\": \"redis-\",\n    \"cdnProfiles\": \"cdnp-\",\n    \"cdnProfilesEndpoints\": \"cdne-\",\n    \"cognitiveServicesAccounts\": \"cog-\",\n    \"cognitiveServicesFormRecognizer\": \"cog-fr-\",\n    \"cognitiveServicesTextAnalytics\": \"cog-ta-\",\n    \"cognitiveServicesSpeech\": \"cog-sp-\",\n    \"computeAvailabilitySets\": \"avail-\",\n    \"computeCloudServices\": \"cld-\",\n    \"computeDiskEncryptionSets\": \"des\",\n    \"computeDisks\": \"disk\",\n    \"computeDisksOs\": \"osdisk\",\n    \"computeGalleries\": \"gal\",\n    \"computeSnapshots\": \"snap-\",\n    \"computeVirtualMachines\": \"vm\",\n    \"computeVirtualMachineScaleSets\": \"vmss-\",\n    \"containerInstanceContainerGroups\": \"ci\",\n    \"containerRegistryRegistries\": \"cr\",\n    \"containerServiceManagedClusters\": \"aks-\",\n    \"databricksWorkspaces\": \"dbw-\",\n    \"dataFactoryFactories\": \"adf-\",\n    \"dataLakeAnalyticsAccounts\": \"dla\",\n    \"dataLakeStoreAccounts\": \"dls\",\n    \"dataMigrationServices\": \"dms-\",\n    \"dBforMySQLServers\": \"mysql-\",\n    \"devicesIotHubs\": \"iot-\",\n    \"devicesProvisioningServices\": \"provs-\",\n    \"devicesProvisioningServicesCertificates\": \"pcert-\",\n    \"documentDBDatabaseAccounts\": \"cosmos-\",\n    \"eventGridDomains\": \"evgd-\",\n    \"eventGridDomainsTopics\": \"evgt-\",\n    \"eventGridEventSubscriptions\": \"evgs-\",\n    \"eventHubNamespaces\": \"evhns-\",\n    \"eventHubNamespacesEventHubs\": \"evh-\",\n    \"hdInsightClustersHadoop\": \"hadoop-\",\n    \"hdInsightClustersHbase\": \"hbase-\",\n    \"hdInsightClustersKafka\": \"kafka-\",\n    \"hdInsightClustersMl\": \"mls-\",\n    \"hdInsightClustersSpark\": \"spark-\",\n    \"hdInsightClustersStorm\": \"storm-\",\n    \"hybridComputeMachines\": \"arcs-\",\n    \"insightsActionGroups\": \"ag-\",\n    \"insightsComponents\": \"appi-\",\n    \"keyVaultVaults\": \"kv-\",\n    \"kubernetesConnectedClusters\": \"arck\",\n    \"kustoClusters\": \"dec\",\n    \"kustoClustersDatabases\": \"dedb\",\n    \"loadTesting\": \"lt-\",\n    \"logicIntegrationAccounts\": \"ia-\",\n    \"logicWorkflows\": \"logic-\",\n    \"machineLearningServicesWorkspaces\": \"mlw-\",\n    \"managedIdentityUserAssignedIdentities\": \"id-\",\n    \"managementManagementGroups\": \"mg-\",\n    \"migrateAssessmentProjects\": \"migr-\",\n    \"networkApplicationGateways\": \"agw-\",\n    \"networkApplicationSecurityGroups\": \"asg-\",\n    \"networkAzureFirewalls\": \"afw-\",\n    \"networkBastionHosts\": \"bas-\",\n    \"networkConnections\": \"con-\",\n    \"networkDnsZones\": \"dnsz-\",\n    \"networkExpressRouteCircuits\": \"erc-\",\n    \"networkFirewallPolicies\": \"afwp-\",\n    \"networkFirewallPoliciesWebApplication\": \"waf\",\n    \"networkFirewallPoliciesRuleGroups\": \"wafrg\",\n    \"networkFrontDoors\": \"fd-\",\n    \"networkFrontdoorWebApplicationFirewallPolicies\": \"fdfp-\",\n    \"networkLoadBalancersExternal\": \"lbe-\",\n    \"networkLoadBalancersInternal\": \"lbi-\",\n    \"networkLoadBalancersInboundNatRules\": \"rule-\",\n    \"networkLocalNetworkGateways\": \"lgw-\",\n    \"networkNatGateways\": \"ng-\",\n    \"networkNetworkInterfaces\": \"nic-\",\n    \"networkNetworkSecurityGroups\": \"nsg-\",\n    \"networkNetworkSecurityGroupsSecurityRules\": \"nsgsr-\",\n    \"networkNetworkWatchers\": \"nw-\",\n    \"networkPrivateDnsZones\": \"pdnsz-\",\n    \"networkPrivateLinkServices\": \"pl-\",\n    \"networkPublicIPAddresses\": \"pip-\",\n    \"networkPublicIPPrefixes\": \"ippre-\",\n    \"networkRouteFilters\": \"rf-\",\n    \"networkRouteTables\": \"rt-\",\n    \"networkRouteTablesRoutes\": \"udr-\",\n    \"networkTrafficManagerProfiles\": \"traf-\",\n    \"networkVirtualNetworkGateways\": \"vgw-\",\n    \"networkVirtualNetworks\": \"vnet-\",\n    \"networkVirtualNetworksSubnets\": \"snet-\",\n    \"networkVirtualNetworksVirtualNetworkPeerings\": \"peer-\",\n    \"networkVirtualWans\": \"vwan-\",\n    \"networkVpnGateways\": \"vpng-\",\n    \"networkVpnGatewaysVpnConnections\": \"vcn-\",\n    \"networkVpnGatewaysVpnSites\": \"vst-\",\n    \"notificationHubsNamespaces\": \"ntfns-\",\n    \"notificationHubsNamespacesNotificationHubs\": \"ntf-\",\n    \"operationalInsightsWorkspaces\": \"log-\",\n    \"portalDashboards\": \"dash-\",\n    \"postgreSQLServers\": \"psql-\",\n    \"postgreSQLServersDatabases\": \"psqldb-\",\n    \"powerBIDedicatedCapacities\": \"pbi-\",\n    \"purviewAccounts\": \"pview-\",\n    \"recoveryServicesVaults\": \"rsv-\",\n    \"resourcesResourceGroups\": \"rg-\",\n    \"searchSearchServices\": \"srch-\",\n    \"serviceBusNamespaces\": \"sb-\",\n    \"serviceBusNamespacesQueues\": \"sbq-\",\n    \"serviceBusNamespacesTopics\": \"sbt-\",\n    \"serviceEndPointPolicies\": \"se-\",\n    \"serviceFabricClusters\": \"sf-\",\n    \"signalRServiceSignalR\": \"sigr\",\n    \"sqlManagedInstances\": \"sqlmi-\",\n    \"sqlServers\": \"sql-\",\n    \"sqlServersDataWarehouse\": \"sqldw-\",\n    \"sqlServersDatabases\": \"sqldb-\",\n    \"sqlServersDatabasesStretch\": \"sqlstrdb-\",\n    \"storageStorageAccounts\": \"st\",\n    \"storageStorageAccountsVm\": \"stvm\",\n    \"storSimpleManagers\": \"ssimp\",\n    \"streamAnalyticsCluster\": \"asa-\",\n    \"synapseWorkspaces\": \"syn\",\n    \"synapseWorkspacesAnalyticsWorkspaces\": \"synw\",\n    \"synapseWorkspacesSqlPoolsDedicated\": \"syndp\",\n    \"synapseWorkspacesSqlPoolsSpark\": \"synsp\",\n    \"timeSeriesInsightsEnvironments\": \"tsi-\",\n    \"webServerFarms\": \"plan-\",\n    \"webSitesAppService\": \"app-\",\n    \"webSitesAppServiceEnvironment\": \"ase-\",\n    \"webSitesFunctions\": \"func-\",\n    \"webStaticSites\": \"stapp-\"\n}\n"
  },
  {
    "path": "infra/core/ai/cognitiveservices.bicep",
    "content": "metadata description = 'Creates an Azure Cognitive Services instance.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\n@description('The custom subdomain name used to access the API. Defaults to the value of the name parameter.')\nparam customSubDomainName string = name\nparam disableLocalAuth bool = false\nparam deployments array = []\nparam kind string = 'OpenAI'\n\n@allowed([ 'Enabled', 'Disabled' ])\nparam publicNetworkAccess string = 'Enabled'\nparam sku object = {\n  name: 'S0'\n}\n\nparam allowedIpRules array = []\nparam networkAcls object = empty(allowedIpRules) ? {\n  defaultAction: 'Allow'\n} : {\n  ipRules: allowedIpRules\n  defaultAction: 'Deny'\n}\n\nresource account 'Microsoft.CognitiveServices/accounts@2023-05-01' = {\n  name: name\n  location: location\n  tags: tags\n  kind: kind\n  properties: {\n    customSubDomainName: customSubDomainName\n    publicNetworkAccess: publicNetworkAccess\n    networkAcls: networkAcls\n    disableLocalAuth: disableLocalAuth\n  }\n  sku: sku\n}\n\n@batchSize(1)\nresource deployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = [for deployment in deployments: {\n  parent: account\n  name: deployment.name\n  properties: {\n    model: deployment.model\n    raiPolicyName: contains(deployment, 'raiPolicyName') ? deployment.raiPolicyName : null\n  }\n  sku: contains(deployment, 'sku') ? deployment.sku : {\n    name: 'Standard'\n    capacity: 20\n  }\n}]\n\noutput endpoint string = account.properties.endpoint\noutput endpoints object = account.properties.endpoints\noutput id string = account.id\noutput name string = account.name\n"
  },
  {
    "path": "infra/core/ai/hub-dependencies.bicep",
    "content": "param location string = resourceGroup().location\nparam tags object = {}\n\n@description('Name of the key vault')\nparam keyVaultName string\n@description('Name of the storage account')\nparam storageAccountName string\n@description('Name of the OpenAI cognitive services')\nparam openAiName string\n@description('Array of OpenAI model deployments')\nparam openAiModelDeployments array = []\n@description('Name of the Log Analytics workspace')\nparam logAnalyticsName string = ''\n@description('Name of the Application Insights instance')\nparam applicationInsightsName string = ''\n@description('Name of the container registry')\nparam containerRegistryName string = ''\n@description('Name of the Azure Cognitive Search service')\nparam searchServiceName string = ''\n\nmodule keyVault '../security/keyvault.bicep' = {\n  name: 'keyvault'\n  params: {\n    location: location\n    tags: tags\n    name: keyVaultName\n  }\n}\n\nmodule storageAccount '../storage/storage-account.bicep' = {\n  name: 'storageAccount'\n  params: {\n    location: location\n    tags: tags\n    name: storageAccountName\n    containers: [\n      {\n        name: 'default'\n      }\n    ]\n    files: [\n      {\n        name: 'default'\n      }\n    ]\n    queues: [\n      {\n        name: 'default'\n      }\n    ]\n    tables: [\n      {\n        name: 'default'\n      }\n    ]\n    corsRules: [\n      {\n        allowedOrigins: [\n          'https://mlworkspace.azure.ai'\n          'https://ml.azure.com'\n          'https://*.ml.azure.com'\n          'https://ai.azure.com'\n          'https://*.ai.azure.com'\n          'https://mlworkspacecanary.azure.ai'\n          'https://mlworkspace.azureml-test.net'\n        ]\n        allowedMethods: [\n          'GET'\n          'HEAD'\n          'POST'\n          'PUT'\n          'DELETE'\n          'OPTIONS'\n          'PATCH'\n        ]\n        maxAgeInSeconds: 1800\n        exposedHeaders: [\n          '*'\n        ]\n        allowedHeaders: [\n          '*'\n        ]\n      }\n    ]\n    deleteRetentionPolicy: {\n      allowPermanentDelete: false\n      enabled: false\n    }\n    shareDeleteRetentionPolicy: {\n      enabled: true\n      days: 7\n    }\n  }\n}\n\nmodule logAnalytics '../monitor/loganalytics.bicep' =\n  if (!empty(logAnalyticsName)) {\n    name: 'logAnalytics'\n    params: {\n      location: location\n      tags: tags\n      name: logAnalyticsName\n    }\n  }\n\nmodule applicationInsights '../monitor/applicationinsights.bicep' =\n  if (!empty(applicationInsightsName) && !empty(logAnalyticsName)) {\n    name: 'applicationInsights'\n    params: {\n      location: location\n      tags: tags\n      name: applicationInsightsName\n      logAnalyticsWorkspaceId: !empty(logAnalyticsName) ? logAnalytics.outputs.id : ''\n    }\n  }\n\nmodule containerRegistry '../host/container-registry.bicep' =\n  if (!empty(containerRegistryName)) {\n    name: 'containerRegistry'\n    params: {\n      location: location\n      tags: tags\n      name: containerRegistryName\n    }\n  }\n\nmodule cognitiveServices '../ai/cognitiveservices.bicep' = {\n  name: 'cognitiveServices'\n  params: {\n    location: location\n    tags: tags\n    name: openAiName\n    kind: 'AIServices'\n    deployments: openAiModelDeployments\n  }\n}\n\nmodule searchService '../search/search-services.bicep' =\n  if (!empty(searchServiceName)) {\n    name: 'searchService'\n    params: {\n      location: location\n      tags: tags\n      name: searchServiceName\n    }\n  }\n\noutput keyVaultId string = keyVault.outputs.id\noutput keyVaultName string = keyVault.outputs.name\noutput keyVaultEndpoint string = keyVault.outputs.endpoint\n\noutput storageAccountId string = storageAccount.outputs.id\noutput storageAccountName string = storageAccount.outputs.name\n\noutput containerRegistryId string = !empty(containerRegistryName) ? containerRegistry.outputs.id : ''\noutput containerRegistryName string = !empty(containerRegistryName) ? containerRegistry.outputs.name : ''\noutput containerRegistryEndpoint string = !empty(containerRegistryName) ? containerRegistry.outputs.loginServer : ''\n\noutput applicationInsightsId string = !empty(applicationInsightsName) ? applicationInsights.outputs.id : ''\noutput applicationInsightsName string = !empty(applicationInsightsName) ? applicationInsights.outputs.name : ''\noutput logAnalyticsWorkspaceId string = !empty(logAnalyticsName) ? logAnalytics.outputs.id : ''\noutput logAnalyticsWorkspaceName string = !empty(logAnalyticsName) ? logAnalytics.outputs.name : ''\n\noutput openAiId string = cognitiveServices.outputs.id\noutput openAiName string = cognitiveServices.outputs.name\noutput openAiEndpoint string = cognitiveServices.outputs.endpoints['OpenAI Language Model Instance API']\n\noutput searchServiceId string = !empty(searchServiceName) ? searchService.outputs.id : ''\noutput searchServiceName string = !empty(searchServiceName) ? searchService.outputs.name : ''\noutput searchServiceEndpoint string = !empty(searchServiceName) ? searchService.outputs.endpoint : ''\n"
  },
  {
    "path": "infra/core/ai/hub.bicep",
    "content": "@description('The AI Studio Hub Resource name')\nparam name string\n@description('The display name of the AI Studio Hub Resource')\nparam displayName string = name\n@description('The storage account ID to use for the AI Studio Hub Resource')\nparam storageAccountId string\n@description('The key vault ID to use for the AI Studio Hub Resource')\nparam keyVaultId string\n@description('The application insights ID to use for the AI Studio Hub Resource')\nparam applicationInsightsId string = ''\n@description('The container registry ID to use for the AI Studio Hub Resource')\nparam containerRegistryId string = ''\n@description('The OpenAI Cognitive Services account name to use for the AI Studio Hub Resource')\nparam openAiName string\n@description('The OpenAI Cognitive Services account connection name to use for the AI Studio Hub Resource')\nparam openAiConnectionName string\n@description('The Azure Cognitive Search service name to use for the AI Studio Hub Resource')\nparam aiSearchName string = ''\n@description('The Azure Cognitive Search service connection name to use for the AI Studio Hub Resource')\nparam aiSearchConnectionName string\n\n@description('The SKU name to use for the AI Studio Hub Resource')\nparam skuName string = 'Basic'\n@description('The SKU tier to use for the AI Studio Hub Resource')\n@allowed(['Basic', 'Free', 'Premium', 'Standard'])\nparam skuTier string = 'Basic'\n@description('The public network access setting to use for the AI Studio Hub Resource')\n@allowed(['Enabled','Disabled'])\nparam publicNetworkAccess string = 'Enabled'\n\nparam location string = resourceGroup().location\nparam tags object = {}\n\nresource hub 'Microsoft.MachineLearningServices/workspaces@2024-04-01' = {\n  name: name\n  location: location\n  tags: tags\n  sku: {\n    name: skuName\n    tier: skuTier\n  }\n  kind: 'Hub'\n  identity: {\n    type: 'SystemAssigned'\n  }\n  properties: {\n    friendlyName: displayName\n    storageAccount: storageAccountId\n    keyVault: keyVaultId\n    applicationInsights: !empty(applicationInsightsId) ? applicationInsightsId : null\n    containerRegistry: !empty(containerRegistryId) ? containerRegistryId : null\n    hbiWorkspace: false\n    managedNetwork: {\n      isolationMode: 'Disabled'\n    }\n    v1LegacyMode: false\n    publicNetworkAccess: publicNetworkAccess\n  }\n\n  resource contentSafetyDefaultEndpoint 'endpoints' = {\n    name: 'Azure.ContentSafety'\n    properties: {\n      name: 'Azure.ContentSafety'\n      endpointType: 'Azure.ContentSafety'\n      associatedResourceId: openAi.id\n    }\n  }\n\n  resource openAiConnection 'connections' = {\n    name: openAiConnectionName\n    properties: {\n      category: 'AzureOpenAI'\n      authType: 'ApiKey'\n      isSharedToAll: true\n      target: openAi.properties.endpoints['OpenAI Language Model Instance API']\n      metadata: {\n        ApiVersion: '2023-07-01-preview'\n        ApiType: 'azure'\n        ResourceId: openAi.id\n      }\n      credentials: {\n        key: openAi.listKeys().key1\n      }\n    }\n  }\n\n  resource searchConnection 'connections' =\n    if (!empty(aiSearchName)) {\n      name: aiSearchConnectionName\n      properties: {\n        category: 'CognitiveSearch'\n        authType: 'ApiKey'\n        isSharedToAll: true\n        target: 'https://${search.name}.search.windows.net/'\n        credentials: {\n          key: !empty(aiSearchName) ? search.listAdminKeys().primaryKey : ''\n        }\n      }\n    }\n}\n\nresource openAi 'Microsoft.CognitiveServices/accounts@2023-05-01' existing = {\n  name: openAiName\n}\n\nresource search 'Microsoft.Search/searchServices@2021-04-01-preview' existing =\n  if (!empty(aiSearchName)) {\n    name: aiSearchName\n  }\n\noutput name string = hub.name\noutput id string = hub.id\noutput principalId string = hub.identity.principalId\n"
  },
  {
    "path": "infra/core/ai/project.bicep",
    "content": "@description('The AI Studio Hub Resource name')\nparam name string\n@description('The display name of the AI Studio Hub Resource')\nparam displayName string = name\n@description('The name of the AI Studio Hub Resource where this project should be created')\nparam hubName string\n@description('The name of the key vault resource to grant access to the project')\nparam keyVaultName string\n\n@description('The SKU name to use for the AI Studio Hub Resource')\nparam skuName string = 'Basic'\n@description('The SKU tier to use for the AI Studio Hub Resource')\n@allowed(['Basic', 'Free', 'Premium', 'Standard'])\nparam skuTier string = 'Basic'\n@description('The public network access setting to use for the AI Studio Hub Resource')\n@allowed(['Enabled','Disabled'])\nparam publicNetworkAccess string = 'Enabled'\n\nparam location string = resourceGroup().location\nparam tags object = {}\n\nresource project 'Microsoft.MachineLearningServices/workspaces@2024-04-01' = {\n  name: name\n  location: location\n  tags: tags\n  sku: {\n    name: skuName\n    tier: skuTier\n  }\n  kind: 'Project'\n  identity: {\n    type: 'SystemAssigned'\n  }\n  properties: {\n    friendlyName: displayName\n    hbiWorkspace: false\n    v1LegacyMode: false\n    publicNetworkAccess: publicNetworkAccess\n    hubResourceId: hub.id\n  }\n}\n\nmodule keyVaultAccess '../security/keyvault-access.bicep' = {\n  name: 'keyvault-access'\n  params: {\n    keyVaultName: keyVaultName\n    principalId: project.identity.principalId\n  }\n}\n\nmodule mlServiceRoleDataScientist '../security/role.bicep' = {\n  name: 'ml-service-role-data-scientist'\n  params: {\n    principalId: project.identity.principalId\n    roleDefinitionId: 'f6c7c914-8db3-469d-8ca1-694a8f32e121'\n    principalType: 'ServicePrincipal'\n  }\n}\n\nmodule mlServiceRoleSecretsReader '../security/role.bicep' = {\n  name: 'ml-service-role-secrets-reader'\n  params: {\n    principalId: project.identity.principalId\n    roleDefinitionId: 'ea01e6af-a1c1-4350-9563-ad00f8c72ec5'\n    principalType: 'ServicePrincipal'\n  }\n}\n\nresource hub 'Microsoft.MachineLearningServices/workspaces@2024-04-01' existing = {\n  name: hubName\n}\n\noutput id string = project.id\noutput name string = project.name\noutput principalId string = project.identity.principalId\n"
  },
  {
    "path": "infra/core/config/configstore.bicep",
    "content": "metadata description = 'Creates an Azure App Configuration store.'\n\n@description('The name for the Azure App Configuration store')\nparam name string\n\n@description('The Azure region/location for the Azure App Configuration store')\nparam location string = resourceGroup().location\n\n@description('Custom tags to apply to the Azure App Configuration store')\nparam tags object = {}\n\n@description('Specifies the names of the key-value resources. The name is a combination of key and label with $ as delimiter. The label is optional.')\nparam keyValueNames array = []\n\n@description('Specifies the values of the key-value resources.')\nparam keyValueValues array = []\n\n@description('The principal ID to grant access to the Azure App Configuration store')\nparam principalId string\n\nresource configStore 'Microsoft.AppConfiguration/configurationStores@2023-03-01' = {\n  name: name\n  location: location\n  sku: {\n    name: 'standard'\n  }\n  tags: tags\n}\n\nresource configStoreKeyValue 'Microsoft.AppConfiguration/configurationStores/keyValues@2023-03-01' = [for (item, i) in keyValueNames: {\n  parent: configStore\n  name: item\n  properties: {\n    value: keyValueValues[i]\n    tags: tags\n  }\n}]\n\nmodule configStoreAccess '../security/configstore-access.bicep' = {\n  name: 'app-configuration-access'\n  params: {\n    configStoreName: name\n    principalId: principalId\n  }\n  dependsOn: [configStore]\n}\n\noutput endpoint string = configStore.properties.endpoint\n"
  },
  {
    "path": "infra/core/database/cosmos/cosmos-account.bicep",
    "content": "metadata description = 'Creates an Azure Cosmos DB account.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\n\nparam connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING'\nparam keyVaultName string\n\n@allowed([ 'GlobalDocumentDB', 'MongoDB', 'Parse' ])\nparam kind string\n\nresource cosmos 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {\n  name: name\n  kind: kind\n  location: location\n  tags: tags\n  properties: {\n    consistencyPolicy: { defaultConsistencyLevel: 'Session' }\n    locations: [\n      {\n        locationName: location\n        failoverPriority: 0\n        isZoneRedundant: false\n      }\n    ]\n    databaseAccountOfferType: 'Standard'\n    enableAutomaticFailover: false\n    enableMultipleWriteLocations: false\n    apiProperties: (kind == 'MongoDB') ? { serverVersion: '4.2' } : {}\n    capabilities: [ { name: 'EnableServerless' } ]\n    minimalTlsVersion: 'Tls12'\n  }\n}\n\nresource cosmosConnectionString 'Microsoft.KeyVault/vaults/secrets@2022-11-01' = {\n  parent: keyVault\n  name: connectionStringKey\n  properties: {\n    value: cosmos.listConnectionStrings().connectionStrings[0].connectionString\n  }\n}\n\nresource keyVault 'Microsoft.KeyVault/vaults@2022-11-01' existing = {\n  name: keyVaultName\n}\n\noutput connectionStringKey string = connectionStringKey\noutput endpoint string = cosmos.properties.documentEndpoint\noutput id string = cosmos.id\noutput name string = cosmos.name\n"
  },
  {
    "path": "infra/core/database/cosmos/mongo/cosmos-mongo-account.bicep",
    "content": "metadata description = 'Creates an Azure Cosmos DB for MongoDB account.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\n\nparam keyVaultName string\nparam connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING'\n\nmodule cosmos '../../cosmos/cosmos-account.bicep' = {\n  name: 'cosmos-account'\n  params: {\n    name: name\n    location: location\n    connectionStringKey: connectionStringKey\n    keyVaultName: keyVaultName\n    kind: 'MongoDB'\n    tags: tags\n  }\n}\n\noutput connectionStringKey string = cosmos.outputs.connectionStringKey\noutput endpoint string = cosmos.outputs.endpoint\noutput id string = cosmos.outputs.id\n"
  },
  {
    "path": "infra/core/database/cosmos/mongo/cosmos-mongo-db.bicep",
    "content": "metadata description = 'Creates an Azure Cosmos DB for MongoDB account with a database.'\nparam accountName string\nparam databaseName string\nparam location string = resourceGroup().location\nparam tags object = {}\n\nparam collections array = []\nparam connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING'\nparam keyVaultName string\n\nmodule cosmos 'cosmos-mongo-account.bicep' = {\n  name: 'cosmos-mongo-account'\n  params: {\n    name: accountName\n    location: location\n    keyVaultName: keyVaultName\n    tags: tags\n    connectionStringKey: connectionStringKey\n  }\n}\n\nresource database 'Microsoft.DocumentDB/databaseAccounts/mongodbDatabases@2022-11-15' = {\n  name: '${accountName}/${databaseName}'\n  tags: tags\n  properties: {\n    resource: { id: databaseName }\n  }\n\n  resource list 'collections' = [for collection in collections: {\n    name: collection.name\n    properties: {\n      resource: {\n        id: collection.id\n        shardKey: { _id: collection.shardKey }\n        indexes: [ { key: { keys: [ collection.indexKey ] } } ]\n      }\n    }\n  }]\n\n  dependsOn: [\n    cosmos\n  ]\n}\n\noutput connectionStringKey string = connectionStringKey\noutput databaseName string = databaseName\noutput endpoint string = cosmos.outputs.endpoint\n"
  },
  {
    "path": "infra/core/database/cosmos/sql/cosmos-sql-account.bicep",
    "content": "metadata description = 'Creates an Azure Cosmos DB for NoSQL account.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\n\nparam keyVaultName string\n\nmodule cosmos '../../cosmos/cosmos-account.bicep' = {\n  name: 'cosmos-account'\n  params: {\n    name: name\n    location: location\n    tags: tags\n    keyVaultName: keyVaultName\n    kind: 'GlobalDocumentDB'\n  }\n}\n\noutput connectionStringKey string = cosmos.outputs.connectionStringKey\noutput endpoint string = cosmos.outputs.endpoint\noutput id string = cosmos.outputs.id\noutput name string = cosmos.outputs.name\n"
  },
  {
    "path": "infra/core/database/cosmos/sql/cosmos-sql-db.bicep",
    "content": "metadata description = 'Creates an Azure Cosmos DB for NoSQL account with a database.'\nparam accountName string\nparam databaseName string\nparam location string = resourceGroup().location\nparam tags object = {}\n\nparam containers array = []\nparam keyVaultName string\nparam principalIds array = []\n\nmodule cosmos 'cosmos-sql-account.bicep' = {\n  name: 'cosmos-sql-account'\n  params: {\n    name: accountName\n    location: location\n    tags: tags\n    keyVaultName: keyVaultName\n  }\n}\n\nresource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2022-11-15' = {\n  name: '${accountName}/${databaseName}'\n  properties: {\n    resource: { id: databaseName }\n  }\n\n  resource list 'containers' = [for container in containers: {\n    name: container.name\n    properties: {\n      resource: {\n        id: container.id\n        partitionKey: { paths: [ container.partitionKey ] }\n      }\n      options: {}\n    }\n  }]\n\n  dependsOn: [\n    cosmos\n  ]\n}\n\nmodule roleDefinition 'cosmos-sql-role-def.bicep' = {\n  name: 'cosmos-sql-role-definition'\n  params: {\n    accountName: accountName\n  }\n  dependsOn: [\n    cosmos\n    database\n  ]\n}\n\n// We need batchSize(1) here because sql role assignments have to be done sequentially\n@batchSize(1)\nmodule userRole 'cosmos-sql-role-assign.bicep' = [for principalId in principalIds: if (!empty(principalId)) {\n  name: 'cosmos-sql-user-role-${uniqueString(principalId)}'\n  params: {\n    accountName: accountName\n    roleDefinitionId: roleDefinition.outputs.id\n    principalId: principalId\n  }\n  dependsOn: [\n    cosmos\n    database\n  ]\n}]\n\noutput accountId string = cosmos.outputs.id\noutput accountName string = cosmos.outputs.name\noutput connectionStringKey string = cosmos.outputs.connectionStringKey\noutput databaseName string = databaseName\noutput endpoint string = cosmos.outputs.endpoint\noutput roleDefinitionId string = roleDefinition.outputs.id\n"
  },
  {
    "path": "infra/core/database/cosmos/sql/cosmos-sql-role-assign.bicep",
    "content": "metadata description = 'Creates a SQL role assignment under an Azure Cosmos DB account.'\nparam accountName string\n\nparam roleDefinitionId string\nparam principalId string = ''\n\nresource role 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2022-11-15' = {\n  parent: cosmos\n  name: guid(roleDefinitionId, principalId, cosmos.id)\n  properties: {\n    principalId: principalId\n    roleDefinitionId: roleDefinitionId\n    scope: cosmos.id\n  }\n}\n\nresource cosmos 'Microsoft.DocumentDB/databaseAccounts@2022-11-15' existing = {\n  name: accountName\n}\n"
  },
  {
    "path": "infra/core/database/cosmos/sql/cosmos-sql-role-def.bicep",
    "content": "metadata description = 'Creates a SQL role definition under an Azure Cosmos DB account.'\nparam accountName string\n\nresource roleDefinition 'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions@2022-11-15' = {\n  parent: cosmos\n  name: guid(cosmos.id, accountName, 'sql-role')\n  properties: {\n    assignableScopes: [\n      cosmos.id\n    ]\n    permissions: [\n      {\n        dataActions: [\n          'Microsoft.DocumentDB/databaseAccounts/readMetadata'\n          'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*'\n          'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*'\n        ]\n        notDataActions: []\n      }\n    ]\n    roleName: 'Reader Writer'\n    type: 'CustomRole'\n  }\n}\n\nresource cosmos 'Microsoft.DocumentDB/databaseAccounts@2022-11-15' existing = {\n  name: accountName\n}\n\noutput id string = roleDefinition.id\n"
  },
  {
    "path": "infra/core/database/mysql/flexibleserver.bicep",
    "content": "metadata description = 'Creates an Azure Database for MySQL - Flexible Server.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\n\nparam sku object\nparam storage object\nparam administratorLogin string\n@secure()\nparam administratorLoginPassword string\nparam highAvailabilityMode string = 'Disabled'\nparam databaseNames array = []\nparam allowAzureIPsFirewall bool = false\nparam allowAllIPsFirewall bool = false\nparam allowedSingleIPs array = []\n\n// MySQL version\nparam version string\n\nresource mysqlServer 'Microsoft.DBforMySQL/flexibleServers@2023-06-30' = {\n  location: location\n  tags: tags\n  name: name\n  sku: sku\n  properties: {\n    version: version\n    administratorLogin: administratorLogin\n    administratorLoginPassword: administratorLoginPassword\n    storage: storage\n    highAvailability: {\n      mode: highAvailabilityMode\n    }\n  }\n\n  resource database 'databases' = [for name in databaseNames: {\n    name: name\n  }]\n\n  resource firewall_all 'firewallRules' = if (allowAllIPsFirewall) {\n    name: 'allow-all-IPs'\n    properties: {\n      startIpAddress: '0.0.0.0'\n      endIpAddress: '255.255.255.255'\n    }\n  }\n\n  resource firewall_azure 'firewallRules' = if (allowAzureIPsFirewall) {\n    name: 'allow-all-azure-internal-IPs'\n    properties: {\n      startIpAddress: '0.0.0.0'\n      endIpAddress: '0.0.0.0'\n    }\n  }\n\n  resource firewall_single 'firewallRules' = [for ip in allowedSingleIPs: {\n    name: 'allow-single-${replace(ip, '.', '')}'\n    properties: {\n      startIpAddress: ip\n      endIpAddress: ip\n    }\n  }]\n\n}\n\noutput MYSQL_DOMAIN_NAME string = mysqlServer.properties.fullyQualifiedDomainName\n"
  },
  {
    "path": "infra/core/database/postgresql/flexibleserver.bicep",
    "content": "metadata description = 'Creates an Azure Database for PostgreSQL - Flexible Server.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\n\nparam sku object\nparam storage object\nparam appUserLogin string\n@secure()\nparam appUserLoginPassword string\nparam administratorLogin string\n@secure()\nparam administratorLoginPassword string\nparam databaseName string\nparam allowAzureIPsFirewall bool = false\nparam allowAllIPsFirewall bool = false\nparam allowedSingleIPs array = []\nparam keyVaultName string\nparam connectionStringKey string\n\n// PostgreSQL version\nparam version string\n\nparam utcNowString string = utcNow('yyyyMMddHHmm')\n\n// Latest official version 2022-12-01 does not have Bicep types available\nresource postgresServer 'Microsoft.DBforPostgreSQL/flexibleServers@2022-12-01' = {\n  location: location\n  tags: tags\n  name: name\n  sku: sku\n  properties: {\n    version: version\n    administratorLogin: administratorLogin\n    administratorLoginPassword: administratorLoginPassword\n    storage: storage\n    highAvailability: {\n      mode: 'Disabled'\n    }\n  }\n\n  resource database 'databases' = {\n    name: databaseName\n  }\n\n  resource firewall_all 'firewallRules' = if (allowAllIPsFirewall) {\n    name: 'allow-all-IPs'\n    properties: {\n      startIpAddress: '0.0.0.0'\n      endIpAddress: '255.255.255.255'\n    }\n  }\n\n  resource firewall_azure 'firewallRules' = if (allowAzureIPsFirewall) {\n    name: 'allow-all-azure-internal-IPs'\n    properties: {\n      startIpAddress: '0.0.0.0'\n      endIpAddress: '0.0.0.0'\n    }\n  }\n\n  resource firewall_single 'firewallRules' = [for ip in allowedSingleIPs: {\n    name: 'allow-single-${replace(ip, '.', '')}'\n    properties: {\n      startIpAddress: ip\n      endIpAddress: ip\n    }\n  }]\n}\n\nresource psqlDeploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = {\n  name: '${name}-deployment-script'\n  location: location\n  kind: 'AzureCLI'\n  properties: {\n    azCliVersion: '2.37.0'\n    retentionInterval: 'PT1H' // Retain the script resource for 1 hour after it ends running\n    timeout: 'PT5M' // Five minutes\n    cleanupPreference: 'OnSuccess'\n    forceUpdateTag: utcNowString\n    environmentVariables: [\n      {\n        name: 'APPUSERLOGIN'\n        value: appUserLogin\n      }\n      {\n        name: 'APPUSERPASSWORD'\n        secureValue: appUserLoginPassword\n      }\n      {\n        name: 'DBNAME'\n        value: databaseName\n      }\n      {\n        name: 'DBSERVER'\n        value: name\n      }\n      {\n        name: 'ADMINLOGIN'\n        value: administratorLogin\n      }\n      {\n        name: 'ADMINLOGINPASSWORD'\n        secureValue: administratorLoginPassword\n      }\n    ]\n\n    scriptContent: '''\napk add postgresql-client\n\ncat << EOF > create_user.sql\nCREATE ROLE \"$APPUSERLOGIN\" WITH LOGIN PASSWORD '$APPUSERPASSWORD';\nGRANT ALL PRIVILEGES ON DATABASE $DBNAME TO \"$APPUSERLOGIN\";\nEOF\n\npsql \"host=$DBSERVER.postgres.database.azure.com user=$ADMINLOGIN dbname=$DBNAME port=5432 password=$ADMINLOGINPASSWORD sslmode=require\" < create_user.sql\n    '''\n  }\n  dependsOn: [\n    postgresServer\n  ]\n}\n\nresource administratorLoginPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-11-01' = {\n  parent: keyVault\n  name: 'dbAdminPassword'\n  properties: {\n    value: administratorLoginPassword\n  }\n}\n\nresource appUserLoginPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-11-01' = {\n  parent: keyVault\n  name: 'dbAppUserPassword'\n  properties: {\n    value: appUserLoginPassword\n  }\n}\n\nresource sqlAzureConnectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2022-11-01' = {\n  parent: keyVault\n  name: connectionStringKey\n  properties: {\n    value: '${connectionString}; Password=${appUserLoginPassword}'\n  }\n}\n\nresource keyVault 'Microsoft.KeyVault/vaults@2022-11-01' existing = {\n  name: keyVaultName\n}\n\nvar connectionString = 'Host=${postgresServer.properties.fullyQualifiedDomainName};Port=5432;Database=${databaseName};Username=${appUserLogin}'\noutput connectionStringKey string = connectionStringKey\n"
  },
  {
    "path": "infra/core/database/sqlserver/sqlserver.bicep",
    "content": "metadata description = 'Creates an Azure SQL Server instance.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\nparam logAnalyticsWorkspaceId string = ''\n\nparam appUser string = 'appUser'\nparam databaseName string\nparam keyVaultName string\nparam sqlAdmin string = 'sqlAdmin'\nparam connectionStringKey string = 'AZURE-SQL-CONNECTION-STRING'\n\n@secure()\nparam sqlAdminPassword string\n@secure()\nparam appUserPassword string\n\nparam utcNowString string = utcNow('yyyyMMddHHmm')\n\nresource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = {\n  name: name\n  location: location\n  tags: tags\n  properties: {\n    version: '12.0'\n    minimalTlsVersion: '1.2'\n    publicNetworkAccess: 'Enabled'\n    administratorLogin: sqlAdmin\n    administratorLoginPassword: sqlAdminPassword\n  }\n\n  resource firewall 'firewallRules' = {\n    name: 'Azure Services'\n    properties: {\n      // Allow all clients\n      // Note: range [0.0.0.0-0.0.0.0] means \"allow all Azure-hosted clients only\".\n      // This is not sufficient, because we also want to allow direct access from developer machine, for debugging purposes.\n      startIpAddress: '0.0.0.1'\n      endIpAddress: '255.255.255.254'\n    }\n  }\n}\n\nresource sqlServerAuditingSettings 'Microsoft.Sql/servers/auditingSettings@2023-08-01-preview' = {\n  parent: sqlServer\n  name: 'default'\n  properties: {\n    state: 'Enabled'\n    isAzureMonitorTargetEnabled: true\n  }\n}\n\nresource sqlDatabase 'Microsoft.Sql/servers/databases@2023-08-01-preview' = {\n  parent: sqlServer\n  name: databaseName\n  location: location\n}\n\nresource sqlDatabaseDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!(empty(logAnalyticsWorkspaceId))) {\n  scope: sqlDatabase\n  name: 'sqlDatabaseDiagnosticSettings'\n  properties: {\n    workspaceId: logAnalyticsWorkspaceId\n    logs: [\n      {\n        category: 'SQLInsights'\n        enabled: true\n      }\n      {\n        category: 'AutomaticTuning'\n        enabled: true\n      }\n      {\n        category: 'QueryStoreRuntimeStatistics'\n        enabled: true\n      }\n      {\n        category: 'QueryStoreWaitStatistics'\n        enabled: true\n      }\n      {\n        category: 'Errors'\n        enabled: true\n      }\n      {\n        category: 'DatabaseWaitStatistics'\n        enabled: true\n      }\n      {\n        category: 'Timeouts'\n        enabled: true\n      }\n      {\n        category: 'Blocks'\n        enabled: true\n      }\n      {\n        category: 'Deadlocks'\n        enabled: true\n      }\n    ]\n    metrics: [\n      {\n        category: 'Basic'\n        enabled: true\n      }\n      {\n        category: 'InstanceAndAppAdvanced'\n        enabled: true\n      }\n      {\n        category: 'WorkloadManagement'\n        enabled: true\n      }\n    ]\n  }\n}\n\nresource sqlDeploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = {\n  name: '${name}-deployment-script'\n  location: location\n  kind: 'AzureCLI'\n  properties: {\n    azCliVersion: '2.37.0'\n    retentionInterval: 'PT1H' // Retain the script resource for 1 hour after it ends running\n    timeout: 'PT5M' // Five minutes\n    cleanupPreference: 'OnSuccess'\n    forceUpdateTag: utcNowString\n    environmentVariables: [\n      {\n        name: 'APPUSERNAME'\n        value: appUser\n      }\n      {\n        name: 'APPUSERPASSWORD'\n        secureValue: appUserPassword\n      }\n      {\n        name: 'DBNAME'\n        value: databaseName\n      }\n      {\n        name: 'DBSERVER'\n        value: sqlServer.properties.fullyQualifiedDomainName\n      }\n      {\n        name: 'SQLCMDPASSWORD'\n        secureValue: sqlAdminPassword\n      }\n      {\n        name: 'SQLADMIN'\n        value: sqlAdmin\n      }\n    ]\n\n    scriptContent: '''\nwget https://github.com/microsoft/go-sqlcmd/releases/download/v0.8.1/sqlcmd-v0.8.1-linux-x64.tar.bz2\ntar x -f sqlcmd-v0.8.1-linux-x64.tar.bz2 -C .\n\ncat <<SCRIPT_END > ./initDb.sql\ndrop user if exists ${APPUSERNAME}\ngo\ncreate user ${APPUSERNAME} with password = '${APPUSERPASSWORD}'\ngo\nalter role db_owner add member ${APPUSERNAME}\ngo\nSCRIPT_END\n\n./sqlcmd -S ${DBSERVER} -d ${DBNAME} -U ${SQLADMIN} -i ./initDb.sql\n    '''\n  }\n}\n\nresource sqlAdminPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-11-01' = {\n  parent: keyVault\n  name: 'dbAdminPassword'\n  properties: {\n    value: sqlAdminPassword\n  }\n}\n\nresource appUserPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-11-01' = {\n  parent: keyVault\n  name: 'dbAppUserPassword'\n  properties: {\n    value: appUserPassword\n  }\n}\n\nresource sqlAzureConnectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2022-11-01' = {\n  parent: keyVault\n  name: connectionStringKey\n  properties: {\n    value: '${connectionString}; Password=${appUserPassword}'\n  }\n}\n\nresource keyVault 'Microsoft.KeyVault/vaults@2022-11-01' existing = {\n  name: keyVaultName\n}\n\nvar connectionString = 'Server=${sqlServer.properties.fullyQualifiedDomainName}; Database=${sqlDatabase.name}; User=${appUser}'\noutput connectionStringKey string = connectionStringKey\noutput databaseName string = sqlDatabase.name\n"
  },
  {
    "path": "infra/core/gateway/apim.bicep",
    "content": "metadata description = 'Creates an Azure API Management instance.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\n\n@description('The email address of the owner of the service')\n@minLength(1)\nparam publisherEmail string = 'noreply@microsoft.com'\n\n@description('The name of the owner of the service')\n@minLength(1)\nparam publisherName string = 'n/a'\n\n@description('The pricing tier of this API Management service')\n@allowed([\n  'Consumption'\n  'Developer'\n  'Standard'\n  'Premium'\n])\nparam sku string = 'Consumption'\n\n@description('The instance size of this API Management service.')\n@allowed([ 0, 1, 2 ])\nparam skuCount int = 0\n\n@description('Azure Application Insights Name')\nparam applicationInsightsName string\n\nresource apimService 'Microsoft.ApiManagement/service@2021-08-01' = {\n  name: name\n  location: location\n  tags: union(tags, { 'azd-service-name': name })\n  sku: {\n    name: sku\n    capacity: (sku == 'Consumption') ? 0 : ((sku == 'Developer') ? 1 : skuCount)\n  }\n  properties: {\n    publisherEmail: publisherEmail\n    publisherName: publisherName\n    // Custom properties are not supported for Consumption SKU\n    customProperties: sku == 'Consumption' ? {} : {\n      'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA': 'false'\n      'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA': 'false'\n      'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_128_GCM_SHA256': 'false'\n      'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_256_CBC_SHA256': 'false'\n      'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_128_CBC_SHA256': 'false'\n      'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_256_CBC_SHA': 'false'\n      'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_128_CBC_SHA': 'false'\n      'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TripleDes168': 'false'\n      'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls10': 'false'\n      'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls11': 'false'\n      'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Ssl30': 'false'\n      'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls10': 'false'\n      'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls11': 'false'\n      'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Ssl30': 'false'\n    }\n  }\n}\n\nresource apimLogger 'Microsoft.ApiManagement/service/loggers@2021-12-01-preview' = if (!empty(applicationInsightsName)) {\n  name: 'app-insights-logger'\n  parent: apimService\n  properties: {\n    credentials: {\n      instrumentationKey: applicationInsights.properties.InstrumentationKey\n    }\n    description: 'Logger to Azure Application Insights'\n    isBuffered: false\n    loggerType: 'applicationInsights'\n    resourceId: applicationInsights.id\n  }\n}\n\nresource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) {\n  name: applicationInsightsName\n}\n\noutput apimServiceName string = apimService.name\n"
  },
  {
    "path": "infra/core/host/ai-environment.bicep",
    "content": "@minLength(1)\n@description('Primary location for all resources')\nparam location string\n\n@description('The AI Hub resource name.')\nparam hubName string\n@description('The AI Project resource name.')\nparam projectName string\n@description('The Key Vault resource name.')\nparam keyVaultName string\n@description('The Storage Account resource name.')\nparam storageAccountName string\n@description('The Open AI resource name.')\nparam openAiName string\n@description('The Open AI connection name.')\nparam openAiConnectionName string\n@description('The Open AI model deployments.')\nparam openAiModelDeployments array = []\n@description('The Log Analytics resource name.')\nparam logAnalyticsName string = ''\n@description('The Application Insights resource name.')\nparam applicationInsightsName string = ''\n@description('The Container Registry resource name.')\nparam containerRegistryName string = ''\n@description('The Azure Search resource name.')\nparam searchServiceName string = ''\n@description('The Azure Search connection name.')\nparam searchConnectionName string = ''\nparam tags object = {}\n\nmodule hubDependencies '../ai/hub-dependencies.bicep' = {\n  name: 'hubDependencies'\n  params: {\n    location: location\n    tags: tags\n    keyVaultName: keyVaultName\n    storageAccountName: storageAccountName\n    containerRegistryName: containerRegistryName\n    applicationInsightsName: applicationInsightsName\n    logAnalyticsName: logAnalyticsName\n    openAiName: openAiName\n    openAiModelDeployments: openAiModelDeployments\n    searchServiceName: searchServiceName\n  }\n}\n\nmodule hub '../ai/hub.bicep' = {\n  name: 'hub'\n  params: {\n    location: location\n    tags: tags\n    name: hubName\n    displayName: hubName\n    keyVaultId: hubDependencies.outputs.keyVaultId\n    storageAccountId: hubDependencies.outputs.storageAccountId\n    containerRegistryId: hubDependencies.outputs.containerRegistryId\n    applicationInsightsId: hubDependencies.outputs.applicationInsightsId\n    openAiName: hubDependencies.outputs.openAiName\n    openAiConnectionName: openAiConnectionName\n    aiSearchName: hubDependencies.outputs.searchServiceName\n    aiSearchConnectionName: searchConnectionName\n  }\n}\n\nmodule project '../ai/project.bicep' = {\n  name: 'project'\n  params: {\n    location: location\n    tags: tags\n    name: projectName\n    displayName: projectName\n    hubName: hub.outputs.name\n    keyVaultName: hubDependencies.outputs.keyVaultName\n  }\n}\n\n// Outputs\n// Resource Group\noutput resourceGroupName string = resourceGroup().name\n\n// Hub\noutput hubName string = hub.outputs.name\noutput hubPrincipalId string = hub.outputs.principalId\n\n// Project\noutput projectName string = project.outputs.name\noutput projectPrincipalId string = project.outputs.principalId\n\n// Key Vault\noutput keyVaultName string = hubDependencies.outputs.keyVaultName\noutput keyVaultEndpoint string = hubDependencies.outputs.keyVaultEndpoint\n\n// Application Insights\noutput applicationInsightsName string = hubDependencies.outputs.applicationInsightsName\noutput logAnalyticsWorkspaceName string = hubDependencies.outputs.logAnalyticsWorkspaceName\n\n// Container Registry\noutput containerRegistryName string = hubDependencies.outputs.containerRegistryName\noutput containerRegistryEndpoint string = hubDependencies.outputs.containerRegistryEndpoint\n\n// Storage Account\noutput storageAccountName string = hubDependencies.outputs.storageAccountName\n\n// Open AI\noutput openAiName string = hubDependencies.outputs.openAiName\noutput openAiEndpoint string = hubDependencies.outputs.openAiEndpoint\n\n// Search\noutput searchServiceName string = hubDependencies.outputs.searchServiceName\noutput searchServiceEndpoint string = hubDependencies.outputs.searchServiceEndpoint\n"
  },
  {
    "path": "infra/core/host/aks-agent-pool.bicep",
    "content": "metadata description = 'Adds an agent pool to an Azure Kubernetes Service (AKS) cluster.'\nparam clusterName string\n\n@description('The agent pool name')\nparam name string\n\n@description('The agent pool configuration')\nparam config object\n\nresource aksCluster 'Microsoft.ContainerService/managedClusters@2023-11-01' existing = {\n  name: clusterName\n}\n\nresource nodePool 'Microsoft.ContainerService/managedClusters/agentPools@2023-11-01' = {\n  parent: aksCluster\n  name: name\n  properties: config\n}\n"
  },
  {
    "path": "infra/core/host/aks-managed-cluster.bicep",
    "content": "metadata description = 'Creates an Azure Kubernetes Service (AKS) cluster with a system agent pool.'\n@description('The name for the AKS managed cluster')\nparam name string\n\n@description('The name of the resource group for the managed resources of the AKS cluster')\nparam nodeResourceGroupName string = ''\n\n@description('The Azure region/location for the AKS resources')\nparam location string = resourceGroup().location\n\n@description('Custom tags to apply to the AKS resources')\nparam tags object = {}\n\n@description('Kubernetes Version')\nparam kubernetesVersion string = '1.29'\n\n@description('Whether RBAC is enabled for local accounts')\nparam enableRbac bool = true\n\n// Add-ons\n@description('Whether web app routing (preview) add-on is enabled')\nparam webAppRoutingAddon bool = true\n\n// AAD Integration\n@description('Enable Azure Active Directory integration')\nparam enableAad bool = false\n\n@description('Enable RBAC using AAD')\nparam enableAzureRbac bool = false\n\n@description('The Tenant ID associated to the Azure Active Directory')\nparam aadTenantId string = tenant().tenantId\n\n@description('The load balancer SKU to use for ingress into the AKS cluster')\n@allowed([ 'basic', 'standard' ])\nparam loadBalancerSku string = 'standard'\n\n@description('Network plugin used for building the Kubernetes network.')\n@allowed([ 'azure', 'kubenet', 'none' ])\nparam networkPlugin string = 'azure'\n\n@description('Network policy used for building the Kubernetes network.')\n@allowed([ 'azure', 'calico' ])\nparam networkPolicy string = 'azure'\n\n@description('If set to true, getting static credentials will be disabled for this cluster.')\nparam disableLocalAccounts bool = false\n\n@description('The managed cluster SKU.')\n@allowed([ 'Free', 'Paid', 'Standard' ])\nparam sku string = 'Free'\n\n@description('Configuration of AKS add-ons')\nparam addOns object = {}\n\n@description('The log analytics workspace id used for logging & monitoring')\nparam workspaceId string = ''\n\n@description('The node pool configuration for the System agent pool')\nparam systemPoolConfig object\n\n@description('The DNS prefix to associate with the AKS cluster')\nparam dnsPrefix string = ''\n\nresource aks 'Microsoft.ContainerService/managedClusters@2023-11-01' = {\n  name: name\n  location: location\n  tags: tags\n  identity: {\n    type: 'SystemAssigned'\n  }\n  sku: {\n    name: 'Base'\n    tier: sku\n  }\n  properties: {\n    nodeResourceGroup: !empty(nodeResourceGroupName) ? nodeResourceGroupName : 'rg-mc-${name}'\n    kubernetesVersion: kubernetesVersion\n    dnsPrefix: empty(dnsPrefix) ? '${name}-dns' : dnsPrefix\n    enableRBAC: enableRbac\n    aadProfile: enableAad ? {\n      managed: true\n      enableAzureRBAC: enableAzureRbac\n      tenantID: aadTenantId\n    } : null\n    agentPoolProfiles: [\n      systemPoolConfig\n    ]\n    networkProfile: {\n      loadBalancerSku: loadBalancerSku\n      networkPlugin: networkPlugin\n      networkPolicy: networkPolicy\n    }\n    disableLocalAccounts: disableLocalAccounts && enableAad\n    addonProfiles: addOns\n    ingressProfile: {\n      webAppRouting: {\n        enabled: webAppRoutingAddon\n      }\n    }\n  }\n}\n\nvar aksDiagCategories = [\n  'cluster-autoscaler'\n  'kube-controller-manager'\n  'kube-audit-admin'\n  'guard'\n]\n\n// TODO: Update diagnostics to be its own module\n// Blocking issue: https://github.com/Azure/bicep/issues/622\n// Unable to pass in a `resource` scope or unable to use string interpolation in resource types\nresource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) {\n  name: 'aks-diagnostics'\n  scope: aks\n  properties: {\n    workspaceId: workspaceId\n    logs: [for category in aksDiagCategories: {\n      category: category\n      enabled: true\n    }]\n    metrics: [\n      {\n        category: 'AllMetrics'\n        enabled: true\n      }\n    ]\n  }\n}\n\n@description('The resource name of the AKS cluster')\noutput clusterName string = aks.name\n\n@description('The AKS cluster identity')\noutput clusterIdentity object = {\n  clientId: aks.properties.identityProfile.kubeletidentity.clientId\n  objectId: aks.properties.identityProfile.kubeletidentity.objectId\n  resourceId: aks.properties.identityProfile.kubeletidentity.resourceId\n}\n"
  },
  {
    "path": "infra/core/host/aks.bicep",
    "content": "metadata description = 'Creates an Azure Kubernetes Service (AKS) cluster with a system agent pool as well as an additional user agent pool.'\n@description('The name for the AKS managed cluster')\nparam name string\n\n@description('The name for the Azure container registry (ACR)')\nparam containerRegistryName string\n\n@description('The name of the connected log analytics workspace')\nparam logAnalyticsName string = ''\n\n@description('The name of the keyvault to grant access')\nparam keyVaultName string\n\n@description('The Azure region/location for the AKS resources')\nparam location string = resourceGroup().location\n\n@description('Custom tags to apply to the AKS resources')\nparam tags object = {}\n\n@description('AKS add-ons configuration')\nparam addOns object = {\n  azurePolicy: {\n    enabled: true\n    config: {\n      version: 'v2'\n    }\n  }\n  keyVault: {\n    enabled: true\n    config: {\n      enableSecretRotation: 'true'\n      rotationPollInterval: '2m'\n    }\n  }\n  openServiceMesh: {\n    enabled: false\n    config: {}\n  }\n  omsAgent: {\n    enabled: true\n    config: {}\n  }\n  applicationGateway: {\n    enabled: false\n    config: {}\n  }\n}\n\n@description('The managed cluster SKU.')\n@allowed([ 'Free', 'Paid', 'Standard' ])\nparam sku string = 'Free'\n\n@description('The load balancer SKU to use for ingress into the AKS cluster')\n@allowed([ 'basic', 'standard' ])\nparam loadBalancerSku string = 'standard'\n\n@description('Network plugin used for building the Kubernetes network.')\n@allowed([ 'azure', 'kubenet', 'none' ])\nparam networkPlugin string = 'azure'\n\n@description('Network policy used for building the Kubernetes network.')\n@allowed([ 'azure', 'calico' ])\nparam networkPolicy string = 'azure'\n\n@description('The DNS prefix to associate with the AKS cluster')\nparam dnsPrefix string = ''\n\n@description('The name of the resource group for the managed resources of the AKS cluster')\nparam nodeResourceGroupName string = ''\n\n@allowed([\n  'CostOptimised'\n  'Standard'\n  'HighSpec'\n  'Custom'\n])\n@description('The System Pool Preset sizing')\nparam systemPoolType string = 'CostOptimised'\n\n@allowed([\n  ''\n  'CostOptimised'\n  'Standard'\n  'HighSpec'\n  'Custom'\n])\n@description('The User Pool Preset sizing')\nparam agentPoolType string = ''\n\n// Configure system / user agent pools\n@description('Custom configuration of system node pool')\nparam systemPoolConfig object = {}\n@description('Custom configuration of user node pool')\nparam agentPoolConfig object = {}\n\n@description('Id of the user or app to assign application roles')\nparam principalId string = ''\n\n@description('The type of principal to assign application roles')\n@allowed(['Device','ForeignGroup','Group','ServicePrincipal','User'])\nparam principalType string = 'User'\n\n@description('Kubernetes Version')\nparam kubernetesVersion string = '1.29'\n\n@description('The Tenant ID associated to the Azure Active Directory')\nparam aadTenantId string = tenant().tenantId\n\n@description('Whether RBAC is enabled for local accounts')\nparam enableRbac bool = true\n\n@description('If set to true, getting static credentials will be disabled for this cluster.')\nparam disableLocalAccounts bool = false\n\n@description('Enable RBAC using AAD')\nparam enableAzureRbac bool = false\n\n// Add-ons\n@description('Whether web app routing (preview) add-on is enabled')\nparam webAppRoutingAddon bool = true\n\n// Configure AKS add-ons\nvar omsAgentConfig = (!empty(logAnalyticsName) && !empty(addOns.omsAgent) && addOns.omsAgent.enabled) ? union(\n  addOns.omsAgent,\n  {\n    config: {\n      logAnalyticsWorkspaceResourceID: logAnalytics.id\n    }\n  }\n) : {}\n\nvar addOnsConfig = union(\n  (!empty(addOns.azurePolicy) && addOns.azurePolicy.enabled) ? { azurepolicy: addOns.azurePolicy } : {},\n  (!empty(addOns.keyVault) && addOns.keyVault.enabled) ? { azureKeyvaultSecretsProvider: addOns.keyVault } : {},\n  (!empty(addOns.openServiceMesh) && addOns.openServiceMesh.enabled) ? { openServiceMesh: addOns.openServiceMesh } : {},\n  (!empty(addOns.omsAgent) && addOns.omsAgent.enabled) ? { omsagent: omsAgentConfig } : {},\n  (!empty(addOns.applicationGateway) && addOns.applicationGateway.enabled) ? { ingressApplicationGateway: addOns.applicationGateway } : {}\n)\n\n// Link to existing log analytics workspace when available\nresource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' existing = if (!empty(logAnalyticsName)) {\n  name: logAnalyticsName\n}\n\nvar systemPoolSpec = !empty(systemPoolConfig) ? systemPoolConfig : nodePoolPresets[systemPoolType]\n\n// Create the primary AKS cluster resources and system node pool\nmodule managedCluster 'aks-managed-cluster.bicep' = {\n  name: 'managed-cluster'\n  params: {\n    name: name\n    location: location\n    tags: tags\n    systemPoolConfig: union(\n      { name: 'npsystem', mode: 'System' },\n      nodePoolBase,\n      systemPoolSpec\n    )\n    nodeResourceGroupName: nodeResourceGroupName\n    sku: sku\n    dnsPrefix: dnsPrefix\n    kubernetesVersion: kubernetesVersion\n    addOns: addOnsConfig\n    workspaceId: !empty(logAnalyticsName) ? logAnalytics.id : ''\n    enableAad: enableAzureRbac && aadTenantId != ''\n    disableLocalAccounts: disableLocalAccounts\n    aadTenantId: aadTenantId\n    enableRbac: enableRbac\n    enableAzureRbac: enableAzureRbac\n    webAppRoutingAddon: webAppRoutingAddon\n    loadBalancerSku: loadBalancerSku\n    networkPlugin: networkPlugin\n    networkPolicy: networkPolicy\n  }\n}\n\nvar hasAgentPool = !empty(agentPoolConfig) || !empty(agentPoolType)\nvar agentPoolSpec = hasAgentPool && !empty(agentPoolConfig) ? agentPoolConfig : empty(agentPoolType) ? {} : nodePoolPresets[agentPoolType]\n\n// Create additional user agent pool when specified\nmodule agentPool 'aks-agent-pool.bicep' = if (hasAgentPool) {\n  name: 'aks-node-pool'\n  params: {\n    clusterName: managedCluster.outputs.clusterName\n    name: 'npuserpool'\n    config: union({ name: 'npuser', mode: 'User' }, nodePoolBase, agentPoolSpec)\n  }\n}\n\n// Creates container registry (ACR)\nmodule containerRegistry 'container-registry.bicep' = {\n  name: 'container-registry'\n  params: {\n    name: containerRegistryName\n    location: location\n    tags: tags\n    workspaceId: !empty(logAnalyticsName) ? logAnalytics.id : ''\n  }\n}\n\n// Grant ACR Pull access from cluster managed identity to container registry\nmodule containerRegistryAccess '../security/registry-access.bicep' = {\n  name: 'cluster-container-registry-access'\n  params: {\n    containerRegistryName: containerRegistry.outputs.name\n    principalId: managedCluster.outputs.clusterIdentity.objectId\n  }\n}\n\n// Give AKS cluster access to the specified principal\nmodule clusterAccess '../security/aks-managed-cluster-access.bicep' = if (!empty(principalId) && (enableAzureRbac || disableLocalAccounts)) {\n  name: 'cluster-access'\n  params: {\n    clusterName: managedCluster.outputs.clusterName\n    principalId: principalId\n    principalType: principalType\n  }\n}\n\n// Give the AKS Cluster access to KeyVault\nmodule clusterKeyVaultAccess '../security/keyvault-access.bicep' = {\n  name: 'cluster-keyvault-access'\n  params: {\n    keyVaultName: keyVaultName\n    principalId: managedCluster.outputs.clusterIdentity.objectId\n  }\n}\n\n// Helpers for node pool configuration\nvar nodePoolBase = {\n  osType: 'Linux'\n  maxPods: 30\n  type: 'VirtualMachineScaleSets'\n  upgradeSettings: {\n    maxSurge: '33%'\n  }\n}\n\nvar nodePoolPresets = {\n  CostOptimised: {\n    vmSize: 'Standard_B4ms'\n    count: 1\n    minCount: 1\n    maxCount: 3\n    enableAutoScaling: true\n    availabilityZones: []\n  }\n  Standard: {\n    vmSize: 'Standard_DS2_v2'\n    count: 3\n    minCount: 3\n    maxCount: 5\n    enableAutoScaling: true\n    availabilityZones: [\n      '1'\n      '2'\n      '3'\n    ]\n  }\n  HighSpec: {\n    vmSize: 'Standard_D4s_v3'\n    count: 3\n    minCount: 3\n    maxCount: 5\n    enableAutoScaling: true\n    availabilityZones: [\n      '1'\n      '2'\n      '3'\n    ]\n  }\n}\n\n// Module outputs\n@description('The resource name of the AKS cluster')\noutput clusterName string = managedCluster.outputs.clusterName\n\n@description('The AKS cluster identity')\noutput clusterIdentity object = managedCluster.outputs.clusterIdentity\n\n@description('The resource name of the ACR')\noutput containerRegistryName string = containerRegistry.outputs.name\n\n@description('The login server for the container registry')\noutput containerRegistryLoginServer string = containerRegistry.outputs.loginServer\n"
  },
  {
    "path": "infra/core/host/appservice-appsettings.bicep",
    "content": "metadata description = 'Updates app settings for an Azure App Service.'\n@description('The name of the app service resource within the current resource group scope')\nparam name string\n\n@description('The app settings to be applied to the app service')\n@secure()\nparam appSettings object\n\nresource appService 'Microsoft.Web/sites@2022-03-01' existing = {\n  name: name\n}\n\nresource settings 'Microsoft.Web/sites/config@2022-03-01' = {\n  name: 'appsettings'\n  parent: appService\n  properties: appSettings\n}\n"
  },
  {
    "path": "infra/core/host/appservice.bicep",
    "content": "metadata description = 'Creates an Azure App Service in an existing Azure App Service plan.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\n\n// Reference Properties\nparam applicationInsightsName string = ''\nparam appServicePlanId string\nparam keyVaultName string = ''\nparam managedIdentity bool = !empty(keyVaultName)\nparam logAnalyticsWorkspaceId string = ''\n\n// Runtime Properties\n@allowed([\n  'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom'\n])\nparam runtimeName string\nparam runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}'\nparam runtimeVersion string\n\n// Microsoft.Web/sites Properties\nparam kind string = 'app,linux'\n\n// Microsoft.Web/sites/config\nparam allowedOrigins array = []\nparam alwaysOn bool = true\nparam appCommandLine string = ''\n@secure()\nparam appSettings object = {}\nparam clientAffinityEnabled bool = false\nparam enableOryxBuild bool = contains(kind, 'linux')\nparam functionAppScaleLimit int = -1\nparam linuxFxVersion string = runtimeNameAndVersion\nparam minimumElasticInstanceCount int = -1\nparam numberOfWorkers int = -1\nparam scmDoBuildDuringDeployment bool = false\nparam use32BitWorkerProcess bool = false\nparam ftpsState string = 'FtpsOnly'\nparam healthCheckPath string = ''\nparam virtualNetworkSubnetId string = ''\n\nresource appService 'Microsoft.Web/sites@2022-03-01' = {\n  name: name\n  location: location\n  tags: tags\n  kind: kind\n  properties: {\n    serverFarmId: appServicePlanId\n    siteConfig: {\n      linuxFxVersion: linuxFxVersion\n      alwaysOn: alwaysOn\n      ftpsState: ftpsState\n      minTlsVersion: '1.2'\n      appCommandLine: appCommandLine\n      numberOfWorkers: numberOfWorkers != -1 ? numberOfWorkers : null\n      minimumElasticInstanceCount: minimumElasticInstanceCount != -1 ? minimumElasticInstanceCount : null\n      use32BitWorkerProcess: use32BitWorkerProcess\n      functionAppScaleLimit: functionAppScaleLimit != -1 ? functionAppScaleLimit : null\n      healthCheckPath: healthCheckPath\n      cors: {\n        allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins)\n      }\n    }\n    clientAffinityEnabled: clientAffinityEnabled\n    httpsOnly: true\n    virtualNetworkSubnetId: !empty(virtualNetworkSubnetId) ? virtualNetworkSubnetId : null\n  }\n\n  identity: { type: managedIdentity ? 'SystemAssigned' : 'None' }\n\n  resource basicPublishingCredentialsPoliciesFtp 'basicPublishingCredentialsPolicies' = {\n    name: 'ftp'\n    properties: {\n      allow: false\n    }\n  }\n\n  resource basicPublishingCredentialsPoliciesScm 'basicPublishingCredentialsPolicies' = {\n    name: 'scm'\n    properties: {\n      allow: false\n    }\n  }\n}\n\nresource webAppDiagSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!(empty(logAnalyticsWorkspaceId))) {\n  name: '${appService.name}-diagnosticSettings'\n  scope: appService\n  properties: {\n    workspaceId: logAnalyticsWorkspaceId\n    logs: [\n      {\n        category: 'AppServiceHTTPLogs'\n        categoryGroup: null\n        enabled: true\n        retentionPolicy: {\n          days: 7\n          enabled: true\n        }\n      }\n      {\n        category: 'AppServiceConsoleLogs'\n        categoryGroup: null\n        enabled: true\n        retentionPolicy: {\n          days: 7\n          enabled: true\n        }\n      }\n      {\n        category: 'AppServiceAppLogs'\n        categoryGroup: null\n        enabled: true\n        retentionPolicy: {\n          days: 7\n          enabled: true\n        }\n      }\n    ]\n    metrics: [\n      {\n        category: 'AllMetrics'\n        enabled: true\n        retentionPolicy: {\n          days: 7\n          enabled: true\n        }\n      }\n    ]\n  }\n}\n\n// Updates to the single Microsoft.sites/web/config resources that need to be performed sequentially\n// sites/web/config 'appsettings'\nmodule configAppSettings 'appservice-appsettings.bicep' = {\n  name: '${name}-appSettings'\n  params: {\n    name: appService.name\n    appSettings: union(appSettings,\n      {\n        SCM_DO_BUILD_DURING_DEPLOYMENT: string(scmDoBuildDuringDeployment)\n        ENABLE_ORYX_BUILD: string(enableOryxBuild)\n      },\n      runtimeName == 'python' && appCommandLine == '' ? { PYTHON_ENABLE_GUNICORN_MULTIWORKERS: 'true'} : {},\n      !empty(applicationInsightsName) ? { APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString } : {},\n      !empty(keyVaultName) ? { AZURE_KEY_VAULT_ENDPOINT: keyVault.properties.vaultUri } : {})\n  }\n}\n\n// sites/web/config 'logs'\nresource configLogs 'Microsoft.Web/sites/config@2022-03-01' = {\n  name: 'logs'\n  parent: appService\n  properties: {\n    applicationLogs: { fileSystem: { level: 'Verbose' } }\n    detailedErrorMessages: { enabled: true }\n    failedRequestsTracing: { enabled: true }\n    httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } }\n  }\n  dependsOn: [configAppSettings]\n}\n\nresource keyVault 'Microsoft.KeyVault/vaults@2022-11-01' existing = if (!(empty(keyVaultName))) {\n  name: keyVaultName\n}\n\nresource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) {\n  name: applicationInsightsName\n}\n\noutput identityPrincipalId string = managedIdentity ? appService.identity.principalId : ''\noutput name string = appService.name\noutput uri string = 'https://${appService.properties.defaultHostName}'\n"
  },
  {
    "path": "infra/core/host/appserviceplan.bicep",
    "content": "metadata description = 'Creates an Azure App Service plan.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\nparam logAnalyticsWorkspaceId string = ''\n\nparam kind string = ''\nparam reserved bool = true\nparam sku object\n\nresource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = {\n  name: name\n  location: location\n  tags: tags\n  sku: sku\n  kind: kind\n  properties: {\n    reserved: reserved\n  }\n}\n\nresource appServicePlanDiagSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!(empty(logAnalyticsWorkspaceId))) {\n  name: '${appServicePlan.name}-diagnosticSettings'\n  scope: appServicePlan\n  properties: {\n    workspaceId: logAnalyticsWorkspaceId\n    metrics: [\n      {\n        category: 'AllMetrics'\n        enabled: true\n        retentionPolicy: {\n          days: 7\n          enabled: true\n        }\n      }\n    ]\n  }\n}\n\noutput id string = appServicePlan.id\noutput name string = appServicePlan.name\n"
  },
  {
    "path": "infra/core/host/container-app-upsert.bicep",
    "content": "metadata description = 'Creates or updates an existing Azure Container App.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\n\n@description('The environment name for the container apps')\nparam containerAppsEnvironmentName string\n\n@description('The number of CPU cores allocated to a single container instance, e.g., 0.5')\nparam containerCpuCoreCount string = '0.5'\n\n@description('The maximum number of replicas to run. Must be at least 1.')\n@minValue(1)\nparam containerMaxReplicas int = 10\n\n@description('The amount of memory allocated to a single container instance, e.g., 1Gi')\nparam containerMemory string = '1.0Gi'\n\n@description('The minimum number of replicas to run. Must be at least 1.')\n@minValue(1)\nparam containerMinReplicas int = 1\n\n@description('The name of the container')\nparam containerName string = 'main'\n\n@description('The name of the container registry')\nparam containerRegistryName string = ''\n\n@description('Hostname suffix for container registry. Set when deploying to sovereign clouds')\nparam containerRegistryHostSuffix string = 'azurecr.io'\n\n@allowed([ 'http', 'grpc' ])\n@description('The protocol used by Dapr to connect to the app, e.g., HTTP or gRPC')\nparam daprAppProtocol string = 'http'\n\n@description('Enable or disable Dapr for the container app')\nparam daprEnabled bool = false\n\n@description('The Dapr app ID')\nparam daprAppId string = containerName\n\n@description('Specifies if the resource already exists')\nparam exists bool = false\n\n@description('Specifies if Ingress is enabled for the container app')\nparam ingressEnabled bool = true\n\n@description('The type of identity for the resource')\n@allowed([ 'None', 'SystemAssigned', 'UserAssigned' ])\nparam identityType string = 'None'\n\n@description('The name of the user-assigned identity')\nparam identityName string = ''\n\n@description('The name of the container image')\nparam imageName string = ''\n\n@description('The secrets required for the container')\n@secure()\nparam secrets object = {}\n\n@description('The environment variables for the container')\nparam env array = []\n\n@description('Specifies if the resource ingress is exposed externally')\nparam external bool = true\n\n@description('The service binds associated with the container')\nparam serviceBinds array = []\n\n@description('The target port for the container')\nparam targetPort int = 80\n\nresource existingApp 'Microsoft.App/containerApps@2023-05-02-preview' existing = if (exists) {\n  name: name\n}\n\nmodule app 'container-app.bicep' = {\n  name: '${deployment().name}-update'\n  params: {\n    name: name\n    location: location\n    tags: tags\n    identityType: identityType\n    identityName: identityName\n    ingressEnabled: ingressEnabled\n    containerName: containerName\n    containerAppsEnvironmentName: containerAppsEnvironmentName\n    containerRegistryName: containerRegistryName\n    containerRegistryHostSuffix: containerRegistryHostSuffix\n    containerCpuCoreCount: containerCpuCoreCount\n    containerMemory: containerMemory\n    containerMinReplicas: containerMinReplicas\n    containerMaxReplicas: containerMaxReplicas\n    daprEnabled: daprEnabled\n    daprAppId: daprAppId\n    daprAppProtocol: daprAppProtocol\n    secrets: secrets\n    external: external\n    env: env\n    imageName: !empty(imageName) ? imageName : exists ? existingApp.properties.template.containers[0].image : ''\n    targetPort: targetPort\n    serviceBinds: serviceBinds\n  }\n}\n\noutput defaultDomain string = app.outputs.defaultDomain\noutput imageName string = app.outputs.imageName\noutput name string = app.outputs.name\noutput uri string = app.outputs.uri\n"
  },
  {
    "path": "infra/core/host/container-app.bicep",
    "content": "metadata description = 'Creates a container app in an Azure Container App environment.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\n\n@description('Allowed origins')\nparam allowedOrigins array = []\n\n@description('Name of the environment for container apps')\nparam containerAppsEnvironmentName string\n\n@description('CPU cores allocated to a single container instance, e.g., 0.5')\nparam containerCpuCoreCount string = '0.5'\n\n@description('The maximum number of replicas to run. Must be at least 1.')\n@minValue(1)\nparam containerMaxReplicas int = 10\n\n@description('Memory allocated to a single container instance, e.g., 1Gi')\nparam containerMemory string = '1.0Gi'\n\n@description('The minimum number of replicas to run. Must be at least 1.')\nparam containerMinReplicas int = 1\n\n@description('The name of the container')\nparam containerName string = 'main'\n\n@description('The name of the container registry')\nparam containerRegistryName string = ''\n\n@description('Hostname suffix for container registry. Set when deploying to sovereign clouds')\nparam containerRegistryHostSuffix string = 'azurecr.io'\n\n@description('The protocol used by Dapr to connect to the app, e.g., http or grpc')\n@allowed([ 'http', 'grpc' ])\nparam daprAppProtocol string = 'http'\n\n@description('The Dapr app ID')\nparam daprAppId string = containerName\n\n@description('Enable Dapr')\nparam daprEnabled bool = false\n\n@description('The environment variables for the container')\nparam env array = []\n\n@description('Specifies if the resource ingress is exposed externally')\nparam external bool = true\n\n@description('The name of the user-assigned identity')\nparam identityName string = ''\n\n@description('The type of identity for the resource')\n@allowed([ 'None', 'SystemAssigned', 'UserAssigned' ])\nparam identityType string = 'None'\n\n@description('The name of the container image')\nparam imageName string = ''\n\n@description('Specifies if Ingress is enabled for the container app')\nparam ingressEnabled bool = true\n\nparam revisionMode string = 'Single'\n\n@description('The secrets required for the container')\n@secure()\nparam secrets object = {}\n\n@description('The service binds associated with the container')\nparam serviceBinds array = []\n\n@description('The name of the container apps add-on to use. e.g. redis')\nparam serviceType string = ''\n\n@description('The target port for the container')\nparam targetPort int = 80\n\nresource userIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = if (!empty(identityName)) {\n  name: identityName\n}\n\n// Private registry support requires both an ACR name and a User Assigned managed identity\nvar usePrivateRegistry = !empty(identityName) && !empty(containerRegistryName)\n\n// Automatically set to `UserAssigned` when an `identityName` has been set\nvar normalizedIdentityType = !empty(identityName) ? 'UserAssigned' : identityType\n\nmodule containerRegistryAccess '../security/registry-access.bicep' = if (usePrivateRegistry) {\n  name: '${deployment().name}-registry-access'\n  params: {\n    containerRegistryName: containerRegistryName\n    principalId: usePrivateRegistry ? userIdentity.properties.principalId : ''\n  }\n}\n\nresource app 'Microsoft.App/containerApps@2023-05-02-preview' = {\n  name: name\n  location: location\n  tags: tags\n  // It is critical that the identity is granted ACR pull access before the app is created\n  // otherwise the container app will throw a provision error\n  // This also forces us to use an user assigned managed identity since there would no way to \n  // provide the system assigned identity with the ACR pull access before the app is created\n  dependsOn: usePrivateRegistry ? [ containerRegistryAccess ] : []\n  identity: {\n    type: normalizedIdentityType\n    userAssignedIdentities: !empty(identityName) && normalizedIdentityType == 'UserAssigned' ? { '${userIdentity.id}': {} } : null\n  }\n  properties: {\n    managedEnvironmentId: containerAppsEnvironment.id\n    configuration: {\n      activeRevisionsMode: revisionMode\n      ingress: ingressEnabled ? {\n        external: external\n        targetPort: targetPort\n        transport: 'auto'\n        corsPolicy: {\n          allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins)\n        }\n      } : null\n      dapr: daprEnabled ? {\n        enabled: true\n        appId: daprAppId\n        appProtocol: daprAppProtocol\n        appPort: ingressEnabled ? targetPort : 0\n      } : { enabled: false }\n      secrets: [for secret in items(secrets): {\n        name: secret.key\n        value: secret.value\n      }]\n      service: !empty(serviceType) ? { type: serviceType } : null\n      registries: usePrivateRegistry ? [\n        {\n          server: '${containerRegistryName}.${containerRegistryHostSuffix}'\n          identity: userIdentity.id\n        }\n      ] : []\n    }\n    template: {\n      serviceBinds: !empty(serviceBinds) ? serviceBinds : null\n      containers: [\n        {\n          image: !empty(imageName) ? imageName : 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest'\n          name: containerName\n          env: env\n          resources: {\n            cpu: json(containerCpuCoreCount)\n            memory: containerMemory\n          }\n        }\n      ]\n      scale: {\n        minReplicas: containerMinReplicas\n        maxReplicas: containerMaxReplicas\n      }\n    }\n  }\n}\n\nresource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' existing = {\n  name: containerAppsEnvironmentName\n}\n\noutput defaultDomain string = containerAppsEnvironment.properties.defaultDomain\noutput identityPrincipalId string = normalizedIdentityType == 'None' ? '' : (empty(identityName) ? app.identity.principalId : userIdentity.properties.principalId)\noutput imageName string = imageName\noutput name string = app.name\noutput serviceBind object = !empty(serviceType) ? { serviceId: app.id, name: name } : {}\noutput uri string = ingressEnabled ? 'https://${app.properties.configuration.ingress.fqdn}' : ''\n"
  },
  {
    "path": "infra/core/host/container-apps-environment.bicep",
    "content": "metadata description = 'Creates an Azure Container Apps environment.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\n\n@description('Name of the Application Insights resource')\nparam applicationInsightsName string = ''\n\n@description('Specifies if Dapr is enabled')\nparam daprEnabled bool = false\n\n@description('Name of the Log Analytics workspace')\nparam logAnalyticsWorkspaceName string\n\nresource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' = {\n  name: name\n  location: location\n  tags: tags\n  properties: {\n    appLogsConfiguration: {\n      destination: 'log-analytics'\n      logAnalyticsConfiguration: {\n        customerId: logAnalyticsWorkspace.properties.customerId\n        sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey\n      }\n    }\n    daprAIInstrumentationKey: daprEnabled && !empty(applicationInsightsName) ? applicationInsights.properties.InstrumentationKey : ''\n  }\n}\n\nresource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = {\n  name: logAnalyticsWorkspaceName\n}\n\nresource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (daprEnabled && !empty(applicationInsightsName)) {\n  name: applicationInsightsName\n}\n\noutput defaultDomain string = containerAppsEnvironment.properties.defaultDomain\noutput id string = containerAppsEnvironment.id\noutput name string = containerAppsEnvironment.name\n"
  },
  {
    "path": "infra/core/host/container-apps.bicep",
    "content": "metadata description = 'Creates an Azure Container Registry and an Azure Container Apps environment.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\n\nparam containerAppsEnvironmentName string\nparam containerRegistryName string\nparam containerRegistryResourceGroupName string = ''\nparam containerRegistryAdminUserEnabled bool = false\nparam logAnalyticsWorkspaceName string\nparam applicationInsightsName string = ''\nparam daprEnabled bool = false\n\nmodule containerAppsEnvironment 'container-apps-environment.bicep' = {\n  name: '${name}-container-apps-environment'\n  params: {\n    name: containerAppsEnvironmentName\n    location: location\n    tags: tags\n    logAnalyticsWorkspaceName: logAnalyticsWorkspaceName\n    applicationInsightsName: applicationInsightsName\n    daprEnabled: daprEnabled\n  }\n}\n\nmodule containerRegistry 'container-registry.bicep' = {\n  name: '${name}-container-registry'\n  scope: !empty(containerRegistryResourceGroupName) ? resourceGroup(containerRegistryResourceGroupName) : resourceGroup()\n  params: {\n    name: containerRegistryName\n    location: location\n    adminUserEnabled: containerRegistryAdminUserEnabled\n    tags: tags\n  }\n}\n\noutput defaultDomain string = containerAppsEnvironment.outputs.defaultDomain\noutput environmentName string = containerAppsEnvironment.outputs.name\noutput environmentId string = containerAppsEnvironment.outputs.id\n\noutput registryLoginServer string = containerRegistry.outputs.loginServer\noutput registryName string = containerRegistry.outputs.name\n"
  },
  {
    "path": "infra/core/host/container-registry.bicep",
    "content": "metadata description = 'Creates an Azure Container Registry.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\n\n@description('Indicates whether admin user is enabled')\nparam adminUserEnabled bool = false\n\n@description('Indicates whether anonymous pull is enabled')\nparam anonymousPullEnabled bool = false\n\n@description('Azure ad authentication as arm policy settings')\nparam azureADAuthenticationAsArmPolicy object = {\n  status: 'enabled'\n}\n\n@description('Indicates whether data endpoint is enabled')\nparam dataEndpointEnabled bool = false\n\n@description('Encryption settings')\nparam encryption object = {\n  status: 'disabled'\n}\n\n@description('Export policy settings')\nparam exportPolicy object = {\n  status: 'enabled'\n}\n\n@description('Metadata search settings')\nparam metadataSearch string = 'Disabled'\n\n@description('Options for bypassing network rules')\nparam networkRuleBypassOptions string = 'AzureServices'\n\n@description('Public network access setting')\nparam publicNetworkAccess string = 'Enabled'\n\n@description('Quarantine policy settings')\nparam quarantinePolicy object = {\n  status: 'disabled'\n}\n\n@description('Retention policy settings')\nparam retentionPolicy object = {\n  days: 7\n  status: 'disabled'\n}\n\n@description('Scope maps setting')\nparam scopeMaps array = []\n\n@description('SKU settings')\nparam sku object = {\n  name: 'Basic'\n}\n\n@description('Soft delete policy settings')\nparam softDeletePolicy object = {\n  retentionDays: 7\n  status: 'disabled'\n}\n\n@description('Trust policy settings')\nparam trustPolicy object = {\n  type: 'Notary'\n  status: 'disabled'\n}\n\n@description('Zone redundancy setting')\nparam zoneRedundancy string = 'Disabled'\n\n@description('The log analytics workspace ID used for logging and monitoring')\nparam workspaceId string = ''\n\n// 2023-11-01-preview needed for metadataSearch\nresource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' = {\n  name: name\n  location: location\n  tags: tags\n  sku: sku\n  properties: {\n    adminUserEnabled: adminUserEnabled\n    anonymousPullEnabled: anonymousPullEnabled\n    dataEndpointEnabled: dataEndpointEnabled\n    encryption: encryption\n    metadataSearch: metadataSearch\n    networkRuleBypassOptions: networkRuleBypassOptions\n    policies:{\n      quarantinePolicy: quarantinePolicy\n      trustPolicy: trustPolicy\n      retentionPolicy: retentionPolicy\n      exportPolicy: exportPolicy\n      azureADAuthenticationAsArmPolicy: azureADAuthenticationAsArmPolicy\n      softDeletePolicy: softDeletePolicy\n    }\n    publicNetworkAccess: publicNetworkAccess\n    zoneRedundancy: zoneRedundancy\n  }\n\n  resource scopeMap 'scopeMaps' = [for scopeMap in scopeMaps: {\n    name: scopeMap.name\n    properties: scopeMap.properties\n  }]\n}\n\n// TODO: Update diagnostics to be its own module\n// Blocking issue: https://github.com/Azure/bicep/issues/622\n// Unable to pass in a `resource` scope or unable to use string interpolation in resource types\nresource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) {\n  name: 'registry-diagnostics'\n  scope: containerRegistry\n  properties: {\n    workspaceId: workspaceId\n    logs: [\n      {\n        category: 'ContainerRegistryRepositoryEvents'\n        enabled: true\n      }\n      {\n        category: 'ContainerRegistryLoginEvents'\n        enabled: true\n      }\n    ]\n    metrics: [\n      {\n        category: 'AllMetrics'\n        enabled: true\n        timeGrain: 'PT1M'\n      }\n    ]\n  }\n}\n\noutput id string = containerRegistry.id\noutput loginServer string = containerRegistry.properties.loginServer\noutput name string = containerRegistry.name\n"
  },
  {
    "path": "infra/core/host/functions.bicep",
    "content": "metadata description = 'Creates an Azure Function in an existing Azure App Service plan.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\n\n// Reference Properties\nparam applicationInsightsName string = ''\nparam appServicePlanId string\nparam keyVaultName string = ''\nparam managedIdentity bool = !empty(keyVaultName) || storageManagedIdentity\nparam storageAccountName string\nparam storageManagedIdentity bool = false\nparam virtualNetworkSubnetId string = ''\n\n// Runtime Properties\n@allowed([\n  'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom'\n])\nparam runtimeName string\nparam runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}'\nparam runtimeVersion string\n\n// Function Settings\n@allowed([\n  '~4', '~3', '~2', '~1'\n])\nparam extensionVersion string = '~4'\n\n// Microsoft.Web/sites Properties\nparam kind string = 'functionapp,linux'\n\n// Microsoft.Web/sites/config\nparam allowedOrigins array = []\nparam alwaysOn bool = true\nparam appCommandLine string = ''\n@secure()\nparam appSettings object = {}\nparam clientAffinityEnabled bool = false\nparam enableOryxBuild bool = contains(kind, 'linux')\nparam functionAppScaleLimit int = -1\nparam linuxFxVersion string = runtimeNameAndVersion\nparam minimumElasticInstanceCount int = -1\nparam numberOfWorkers int = -1\nparam scmDoBuildDuringDeployment bool = true\nparam use32BitWorkerProcess bool = false\nparam healthCheckPath string = ''\n\nmodule functions 'appservice.bicep' = {\n  name: '${name}-functions'\n  params: {\n    name: name\n    location: location\n    tags: tags\n    allowedOrigins: allowedOrigins\n    alwaysOn: alwaysOn\n    appCommandLine: appCommandLine\n    applicationInsightsName: applicationInsightsName\n    appServicePlanId: appServicePlanId\n    appSettings: union(appSettings, {\n        AzureWebJobsStorage__accountName: storageManagedIdentity ? storage.name : null\n        AzureWebJobsStorage: storageManagedIdentity ? null : 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${storage.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}'\n        FUNCTIONS_EXTENSION_VERSION: extensionVersion\n        FUNCTIONS_WORKER_RUNTIME: runtimeName\n      })\n    clientAffinityEnabled: clientAffinityEnabled\n    enableOryxBuild: enableOryxBuild\n    functionAppScaleLimit: functionAppScaleLimit\n    healthCheckPath: healthCheckPath\n    keyVaultName: keyVaultName\n    kind: kind\n    linuxFxVersion: linuxFxVersion\n    managedIdentity: managedIdentity\n    minimumElasticInstanceCount: minimumElasticInstanceCount\n    numberOfWorkers: numberOfWorkers\n    runtimeName: runtimeName\n    runtimeVersion: runtimeVersion\n    runtimeNameAndVersion: runtimeNameAndVersion\n    scmDoBuildDuringDeployment: scmDoBuildDuringDeployment\n    use32BitWorkerProcess: use32BitWorkerProcess\n    virtualNetworkSubnetId: virtualNetworkSubnetId\n  }\n}\n\nmodule storageOwnerRole '../../core/security/role.bicep' = if (storageManagedIdentity) {\n  name: 'search-index-contrib-role-api'\n  params: {\n    principalId: functions.outputs.identityPrincipalId\n    // Search Index Data Contributor\n    roleDefinitionId: '8ebe5a00-799e-43f5-93ac-243d3dce84a7'\n    principalType: 'ServicePrincipal'\n  }\n}\n\nresource storage 'Microsoft.Storage/storageAccounts@2021-09-01' existing = {\n  name: storageAccountName\n}\n\noutput identityPrincipalId string = managedIdentity ? functions.outputs.identityPrincipalId : ''\noutput name string = functions.outputs.name\noutput uri string = functions.outputs.uri\n"
  },
  {
    "path": "infra/core/host/staticwebapp.bicep",
    "content": "metadata description = 'Creates an Azure Static Web Apps instance.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\n\nparam sku object = {\n  name: 'Free'\n  tier: 'Free'\n}\n\nresource web 'Microsoft.Web/staticSites@2022-03-01' = {\n  name: name\n  location: location\n  tags: tags\n  sku: sku\n  properties: {\n    provider: 'Custom'\n  }\n}\n\noutput name string = web.name\noutput uri string = 'https://${web.properties.defaultHostname}'\n"
  },
  {
    "path": "infra/core/monitor/applicationinsights-dashboard.bicep",
    "content": "metadata description = 'Creates a dashboard for an Application Insights instance.'\nparam name string\nparam applicationInsightsName string\nparam location string = resourceGroup().location\nparam tags object = {}\n\n// 2020-09-01-preview because that is the latest valid version\nresource applicationInsightsDashboard 'Microsoft.Portal/dashboards@2020-09-01-preview' = {\n  name: name\n  location: location\n  tags: tags\n  properties: {\n    lenses: [\n      {\n        order: 0\n        parts: [\n          {\n            position: {\n              x: 0\n              y: 0\n              colSpan: 2\n              rowSpan: 1\n            }\n            metadata: {\n              inputs: [\n                {\n                  name: 'id'\n                  value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                }\n                {\n                  name: 'Version'\n                  value: '1.0'\n                }\n              ]\n              #disable-next-line BCP036\n              type: 'Extension/AppInsightsExtension/PartType/AspNetOverviewPinnedPart'\n              asset: {\n                idInputName: 'id'\n                type: 'ApplicationInsights'\n              }\n              defaultMenuItemId: 'overview'\n            }\n          }\n          {\n            position: {\n              x: 2\n              y: 0\n              colSpan: 1\n              rowSpan: 1\n            }\n            metadata: {\n              inputs: [\n                {\n                  name: 'ComponentId'\n                  value: {\n                    Name: applicationInsights.name\n                    SubscriptionId: subscription().subscriptionId\n                    ResourceGroup: resourceGroup().name\n                  }\n                }\n                {\n                  name: 'Version'\n                  value: '1.0'\n                }\n              ]\n              #disable-next-line BCP036\n              type: 'Extension/AppInsightsExtension/PartType/ProactiveDetectionAsyncPart'\n              asset: {\n                idInputName: 'ComponentId'\n                type: 'ApplicationInsights'\n              }\n              defaultMenuItemId: 'ProactiveDetection'\n            }\n          }\n          {\n            position: {\n              x: 3\n              y: 0\n              colSpan: 1\n              rowSpan: 1\n            }\n            metadata: {\n              inputs: [\n                {\n                  name: 'ComponentId'\n                  value: {\n                    Name: applicationInsights.name\n                    SubscriptionId: subscription().subscriptionId\n                    ResourceGroup: resourceGroup().name\n                  }\n                }\n                {\n                  name: 'ResourceId'\n                  value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                }\n              ]\n              #disable-next-line BCP036\n              type: 'Extension/AppInsightsExtension/PartType/QuickPulseButtonSmallPart'\n              asset: {\n                idInputName: 'ComponentId'\n                type: 'ApplicationInsights'\n              }\n            }\n          }\n          {\n            position: {\n              x: 4\n              y: 0\n              colSpan: 1\n              rowSpan: 1\n            }\n            metadata: {\n              inputs: [\n                {\n                  name: 'ComponentId'\n                  value: {\n                    Name: applicationInsights.name\n                    SubscriptionId: subscription().subscriptionId\n                    ResourceGroup: resourceGroup().name\n                  }\n                }\n                {\n                  name: 'TimeContext'\n                  value: {\n                    durationMs: 86400000\n                    endTime: null\n                    createdTime: '2018-05-04T01:20:33.345Z'\n                    isInitialTime: true\n                    grain: 1\n                    useDashboardTimeRange: false\n                  }\n                }\n                {\n                  name: 'Version'\n                  value: '1.0'\n                }\n              ]\n              #disable-next-line BCP036\n              type: 'Extension/AppInsightsExtension/PartType/AvailabilityNavButtonPart'\n              asset: {\n                idInputName: 'ComponentId'\n                type: 'ApplicationInsights'\n              }\n            }\n          }\n          {\n            position: {\n              x: 5\n              y: 0\n              colSpan: 1\n              rowSpan: 1\n            }\n            metadata: {\n              inputs: [\n                {\n                  name: 'ComponentId'\n                  value: {\n                    Name: applicationInsights.name\n                    SubscriptionId: subscription().subscriptionId\n                    ResourceGroup: resourceGroup().name\n                  }\n                }\n                {\n                  name: 'TimeContext'\n                  value: {\n                    durationMs: 86400000\n                    endTime: null\n                    createdTime: '2018-05-08T18:47:35.237Z'\n                    isInitialTime: true\n                    grain: 1\n                    useDashboardTimeRange: false\n                  }\n                }\n                {\n                  name: 'ConfigurationId'\n                  value: '78ce933e-e864-4b05-a27b-71fd55a6afad'\n                }\n              ]\n              #disable-next-line BCP036\n              type: 'Extension/AppInsightsExtension/PartType/AppMapButtonPart'\n              asset: {\n                idInputName: 'ComponentId'\n                type: 'ApplicationInsights'\n              }\n            }\n          }\n          {\n            position: {\n              x: 0\n              y: 1\n              colSpan: 3\n              rowSpan: 1\n            }\n            metadata: {\n              inputs: []\n              type: 'Extension/HubsExtension/PartType/MarkdownPart'\n              settings: {\n                content: {\n                  settings: {\n                    content: '# Usage'\n                    title: ''\n                    subtitle: ''\n                  }\n                }\n              }\n            }\n          }\n          {\n            position: {\n              x: 3\n              y: 1\n              colSpan: 1\n              rowSpan: 1\n            }\n            metadata: {\n              inputs: [\n                {\n                  name: 'ComponentId'\n                  value: {\n                    Name: applicationInsights.name\n                    SubscriptionId: subscription().subscriptionId\n                    ResourceGroup: resourceGroup().name\n                  }\n                }\n                {\n                  name: 'TimeContext'\n                  value: {\n                    durationMs: 86400000\n                    endTime: null\n                    createdTime: '2018-05-04T01:22:35.782Z'\n                    isInitialTime: true\n                    grain: 1\n                    useDashboardTimeRange: false\n                  }\n                }\n              ]\n              #disable-next-line BCP036\n              type: 'Extension/AppInsightsExtension/PartType/UsageUsersOverviewPart'\n              asset: {\n                idInputName: 'ComponentId'\n                type: 'ApplicationInsights'\n              }\n            }\n          }\n          {\n            position: {\n              x: 4\n              y: 1\n              colSpan: 3\n              rowSpan: 1\n            }\n            metadata: {\n              inputs: []\n              type: 'Extension/HubsExtension/PartType/MarkdownPart'\n              settings: {\n                content: {\n                  settings: {\n                    content: '# Reliability'\n                    title: ''\n                    subtitle: ''\n                  }\n                }\n              }\n            }\n          }\n          {\n            position: {\n              x: 7\n              y: 1\n              colSpan: 1\n              rowSpan: 1\n            }\n            metadata: {\n              inputs: [\n                {\n                  name: 'ResourceId'\n                  value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                }\n                {\n                  name: 'DataModel'\n                  value: {\n                    version: '1.0.0'\n                    timeContext: {\n                      durationMs: 86400000\n                      createdTime: '2018-05-04T23:42:40.072Z'\n                      isInitialTime: false\n                      grain: 1\n                      useDashboardTimeRange: false\n                    }\n                  }\n                  isOptional: true\n                }\n                {\n                  name: 'ConfigurationId'\n                  value: '8a02f7bf-ac0f-40e1-afe9-f0e72cfee77f'\n                  isOptional: true\n                }\n              ]\n              #disable-next-line BCP036\n              type: 'Extension/AppInsightsExtension/PartType/CuratedBladeFailuresPinnedPart'\n              isAdapter: true\n              asset: {\n                idInputName: 'ResourceId'\n                type: 'ApplicationInsights'\n              }\n              defaultMenuItemId: 'failures'\n            }\n          }\n          {\n            position: {\n              x: 8\n              y: 1\n              colSpan: 3\n              rowSpan: 1\n            }\n            metadata: {\n              inputs: []\n              type: 'Extension/HubsExtension/PartType/MarkdownPart'\n              settings: {\n                content: {\n                  settings: {\n                    content: '# Responsiveness\\r\\n'\n                    title: ''\n                    subtitle: ''\n                  }\n                }\n              }\n            }\n          }\n          {\n            position: {\n              x: 11\n              y: 1\n              colSpan: 1\n              rowSpan: 1\n            }\n            metadata: {\n              inputs: [\n                {\n                  name: 'ResourceId'\n                  value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                }\n                {\n                  name: 'DataModel'\n                  value: {\n                    version: '1.0.0'\n                    timeContext: {\n                      durationMs: 86400000\n                      createdTime: '2018-05-04T23:43:37.804Z'\n                      isInitialTime: false\n                      grain: 1\n                      useDashboardTimeRange: false\n                    }\n                  }\n                  isOptional: true\n                }\n                {\n                  name: 'ConfigurationId'\n                  value: '2a8ede4f-2bee-4b9c-aed9-2db0e8a01865'\n                  isOptional: true\n                }\n              ]\n              #disable-next-line BCP036\n              type: 'Extension/AppInsightsExtension/PartType/CuratedBladePerformancePinnedPart'\n              isAdapter: true\n              asset: {\n                idInputName: 'ResourceId'\n                type: 'ApplicationInsights'\n              }\n              defaultMenuItemId: 'performance'\n            }\n          }\n          {\n            position: {\n              x: 12\n              y: 1\n              colSpan: 3\n              rowSpan: 1\n            }\n            metadata: {\n              inputs: []\n              type: 'Extension/HubsExtension/PartType/MarkdownPart'\n              settings: {\n                content: {\n                  settings: {\n                    content: '# Browser'\n                    title: ''\n                    subtitle: ''\n                  }\n                }\n              }\n            }\n          }\n          {\n            position: {\n              x: 15\n              y: 1\n              colSpan: 1\n              rowSpan: 1\n            }\n            metadata: {\n              inputs: [\n                {\n                  name: 'ComponentId'\n                  value: {\n                    Name: applicationInsights.name\n                    SubscriptionId: subscription().subscriptionId\n                    ResourceGroup: resourceGroup().name\n                  }\n                }\n                {\n                  name: 'MetricsExplorerJsonDefinitionId'\n                  value: 'BrowserPerformanceTimelineMetrics'\n                }\n                {\n                  name: 'TimeContext'\n                  value: {\n                    durationMs: 86400000\n                    createdTime: '2018-05-08T12:16:27.534Z'\n                    isInitialTime: false\n                    grain: 1\n                    useDashboardTimeRange: false\n                  }\n                }\n                {\n                  name: 'CurrentFilter'\n                  value: {\n                    eventTypes: [\n                      4\n                      1\n                      3\n                      5\n                      2\n                      6\n                      13\n                    ]\n                    typeFacets: {}\n                    isPermissive: false\n                  }\n                }\n                {\n                  name: 'id'\n                  value: {\n                    Name: applicationInsights.name\n                    SubscriptionId: subscription().subscriptionId\n                    ResourceGroup: resourceGroup().name\n                  }\n                }\n                {\n                  name: 'Version'\n                  value: '1.0'\n                }\n              ]\n              #disable-next-line BCP036\n              type: 'Extension/AppInsightsExtension/PartType/MetricsExplorerBladePinnedPart'\n              asset: {\n                idInputName: 'ComponentId'\n                type: 'ApplicationInsights'\n              }\n              defaultMenuItemId: 'browser'\n            }\n          }\n          {\n            position: {\n              x: 0\n              y: 2\n              colSpan: 4\n              rowSpan: 3\n            }\n            metadata: {\n              inputs: [\n                {\n                  name: 'options'\n                  value: {\n                    chart: {\n                      metrics: [\n                        {\n                          resourceMetadata: {\n                            id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                          }\n                          name: 'sessions/count'\n                          aggregationType: 5\n                          namespace: 'microsoft.insights/components/kusto'\n                          metricVisualization: {\n                            displayName: 'Sessions'\n                            color: '#47BDF5'\n                          }\n                        }\n                        {\n                          resourceMetadata: {\n                            id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                          }\n                          name: 'users/count'\n                          aggregationType: 5\n                          namespace: 'microsoft.insights/components/kusto'\n                          metricVisualization: {\n                            displayName: 'Users'\n                            color: '#7E58FF'\n                          }\n                        }\n                      ]\n                      title: 'Unique sessions and users'\n                      visualization: {\n                        chartType: 2\n                        legendVisualization: {\n                          isVisible: true\n                          position: 2\n                          hideSubtitle: false\n                        }\n                        axisVisualization: {\n                          x: {\n                            isVisible: true\n                            axisType: 2\n                          }\n                          y: {\n                            isVisible: true\n                            axisType: 1\n                          }\n                        }\n                      }\n                      openBladeOnClick: {\n                        openBlade: true\n                        destinationBlade: {\n                          extensionName: 'HubsExtension'\n                          bladeName: 'ResourceMenuBlade'\n                          parameters: {\n                            id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                            menuid: 'segmentationUsers'\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n                {\n                  name: 'sharedTimeRange'\n                  isOptional: true\n                }\n              ]\n              #disable-next-line BCP036\n              type: 'Extension/HubsExtension/PartType/MonitorChartPart'\n              settings: {}\n            }\n          }\n          {\n            position: {\n              x: 4\n              y: 2\n              colSpan: 4\n              rowSpan: 3\n            }\n            metadata: {\n              inputs: [\n                {\n                  name: 'options'\n                  value: {\n                    chart: {\n                      metrics: [\n                        {\n                          resourceMetadata: {\n                            id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                          }\n                          name: 'requests/failed'\n                          aggregationType: 7\n                          namespace: 'microsoft.insights/components'\n                          metricVisualization: {\n                            displayName: 'Failed requests'\n                            color: '#EC008C'\n                          }\n                        }\n                      ]\n                      title: 'Failed requests'\n                      visualization: {\n                        chartType: 3\n                        legendVisualization: {\n                          isVisible: true\n                          position: 2\n                          hideSubtitle: false\n                        }\n                        axisVisualization: {\n                          x: {\n                            isVisible: true\n                            axisType: 2\n                          }\n                          y: {\n                            isVisible: true\n                            axisType: 1\n                          }\n                        }\n                      }\n                      openBladeOnClick: {\n                        openBlade: true\n                        destinationBlade: {\n                          extensionName: 'HubsExtension'\n                          bladeName: 'ResourceMenuBlade'\n                          parameters: {\n                            id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                            menuid: 'failures'\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n                {\n                  name: 'sharedTimeRange'\n                  isOptional: true\n                }\n              ]\n              #disable-next-line BCP036\n              type: 'Extension/HubsExtension/PartType/MonitorChartPart'\n              settings: {}\n            }\n          }\n          {\n            position: {\n              x: 8\n              y: 2\n              colSpan: 4\n              rowSpan: 3\n            }\n            metadata: {\n              inputs: [\n                {\n                  name: 'options'\n                  value: {\n                    chart: {\n                      metrics: [\n                        {\n                          resourceMetadata: {\n                            id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                          }\n                          name: 'requests/duration'\n                          aggregationType: 4\n                          namespace: 'microsoft.insights/components'\n                          metricVisualization: {\n                            displayName: 'Server response time'\n                            color: '#00BCF2'\n                          }\n                        }\n                      ]\n                      title: 'Server response time'\n                      visualization: {\n                        chartType: 2\n                        legendVisualization: {\n                          isVisible: true\n                          position: 2\n                          hideSubtitle: false\n                        }\n                        axisVisualization: {\n                          x: {\n                            isVisible: true\n                            axisType: 2\n                          }\n                          y: {\n                            isVisible: true\n                            axisType: 1\n                          }\n                        }\n                      }\n                      openBladeOnClick: {\n                        openBlade: true\n                        destinationBlade: {\n                          extensionName: 'HubsExtension'\n                          bladeName: 'ResourceMenuBlade'\n                          parameters: {\n                            id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                            menuid: 'performance'\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n                {\n                  name: 'sharedTimeRange'\n                  isOptional: true\n                }\n              ]\n              #disable-next-line BCP036\n              type: 'Extension/HubsExtension/PartType/MonitorChartPart'\n              settings: {}\n            }\n          }\n          {\n            position: {\n              x: 12\n              y: 2\n              colSpan: 4\n              rowSpan: 3\n            }\n            metadata: {\n              inputs: [\n                {\n                  name: 'options'\n                  value: {\n                    chart: {\n                      metrics: [\n                        {\n                          resourceMetadata: {\n                            id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                          }\n                          name: 'browserTimings/networkDuration'\n                          aggregationType: 4\n                          namespace: 'microsoft.insights/components'\n                          metricVisualization: {\n                            displayName: 'Page load network connect time'\n                            color: '#7E58FF'\n                          }\n                        }\n                        {\n                          resourceMetadata: {\n                            id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                          }\n                          name: 'browserTimings/processingDuration'\n                          aggregationType: 4\n                          namespace: 'microsoft.insights/components'\n                          metricVisualization: {\n                            displayName: 'Client processing time'\n                            color: '#44F1C8'\n                          }\n                        }\n                        {\n                          resourceMetadata: {\n                            id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                          }\n                          name: 'browserTimings/sendDuration'\n                          aggregationType: 4\n                          namespace: 'microsoft.insights/components'\n                          metricVisualization: {\n                            displayName: 'Send request time'\n                            color: '#EB9371'\n                          }\n                        }\n                        {\n                          resourceMetadata: {\n                            id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                          }\n                          name: 'browserTimings/receiveDuration'\n                          aggregationType: 4\n                          namespace: 'microsoft.insights/components'\n                          metricVisualization: {\n                            displayName: 'Receiving response time'\n                            color: '#0672F1'\n                          }\n                        }\n                      ]\n                      title: 'Average page load time breakdown'\n                      visualization: {\n                        chartType: 3\n                        legendVisualization: {\n                          isVisible: true\n                          position: 2\n                          hideSubtitle: false\n                        }\n                        axisVisualization: {\n                          x: {\n                            isVisible: true\n                            axisType: 2\n                          }\n                          y: {\n                            isVisible: true\n                            axisType: 1\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n                {\n                  name: 'sharedTimeRange'\n                  isOptional: true\n                }\n              ]\n              #disable-next-line BCP036\n              type: 'Extension/HubsExtension/PartType/MonitorChartPart'\n              settings: {}\n            }\n          }\n          {\n            position: {\n              x: 0\n              y: 5\n              colSpan: 4\n              rowSpan: 3\n            }\n            metadata: {\n              inputs: [\n                {\n                  name: 'options'\n                  value: {\n                    chart: {\n                      metrics: [\n                        {\n                          resourceMetadata: {\n                            id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                          }\n                          name: 'availabilityResults/availabilityPercentage'\n                          aggregationType: 4\n                          namespace: 'microsoft.insights/components'\n                          metricVisualization: {\n                            displayName: 'Availability'\n                            color: '#47BDF5'\n                          }\n                        }\n                      ]\n                      title: 'Average availability'\n                      visualization: {\n                        chartType: 3\n                        legendVisualization: {\n                          isVisible: true\n                          position: 2\n                          hideSubtitle: false\n                        }\n                        axisVisualization: {\n                          x: {\n                            isVisible: true\n                            axisType: 2\n                          }\n                          y: {\n                            isVisible: true\n                            axisType: 1\n                          }\n                        }\n                      }\n                      openBladeOnClick: {\n                        openBlade: true\n                        destinationBlade: {\n                          extensionName: 'HubsExtension'\n                          bladeName: 'ResourceMenuBlade'\n                          parameters: {\n                            id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                            menuid: 'availability'\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n                {\n                  name: 'sharedTimeRange'\n                  isOptional: true\n                }\n              ]\n              #disable-next-line BCP036\n              type: 'Extension/HubsExtension/PartType/MonitorChartPart'\n              settings: {}\n            }\n          }\n          {\n            position: {\n              x: 4\n              y: 5\n              colSpan: 4\n              rowSpan: 3\n            }\n            metadata: {\n              inputs: [\n                {\n                  name: 'options'\n                  value: {\n                    chart: {\n                      metrics: [\n                        {\n                          resourceMetadata: {\n                            id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                          }\n                          name: 'exceptions/server'\n                          aggregationType: 7\n                          namespace: 'microsoft.insights/components'\n                          metricVisualization: {\n                            displayName: 'Server exceptions'\n                            color: '#47BDF5'\n                          }\n                        }\n                        {\n                          resourceMetadata: {\n                            id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                          }\n                          name: 'dependencies/failed'\n                          aggregationType: 7\n                          namespace: 'microsoft.insights/components'\n                          metricVisualization: {\n                            displayName: 'Dependency failures'\n                            color: '#7E58FF'\n                          }\n                        }\n                      ]\n                      title: 'Server exceptions and Dependency failures'\n                      visualization: {\n                        chartType: 2\n                        legendVisualization: {\n                          isVisible: true\n                          position: 2\n                          hideSubtitle: false\n                        }\n                        axisVisualization: {\n                          x: {\n                            isVisible: true\n                            axisType: 2\n                          }\n                          y: {\n                            isVisible: true\n                            axisType: 1\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n                {\n                  name: 'sharedTimeRange'\n                  isOptional: true\n                }\n              ]\n              #disable-next-line BCP036\n              type: 'Extension/HubsExtension/PartType/MonitorChartPart'\n              settings: {}\n            }\n          }\n          {\n            position: {\n              x: 8\n              y: 5\n              colSpan: 4\n              rowSpan: 3\n            }\n            metadata: {\n              inputs: [\n                {\n                  name: 'options'\n                  value: {\n                    chart: {\n                      metrics: [\n                        {\n                          resourceMetadata: {\n                            id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                          }\n                          name: 'performanceCounters/processorCpuPercentage'\n                          aggregationType: 4\n                          namespace: 'microsoft.insights/components'\n                          metricVisualization: {\n                            displayName: 'Processor time'\n                            color: '#47BDF5'\n                          }\n                        }\n                        {\n                          resourceMetadata: {\n                            id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                          }\n                          name: 'performanceCounters/processCpuPercentage'\n                          aggregationType: 4\n                          namespace: 'microsoft.insights/components'\n                          metricVisualization: {\n                            displayName: 'Process CPU'\n                            color: '#7E58FF'\n                          }\n                        }\n                      ]\n                      title: 'Average processor and process CPU utilization'\n                      visualization: {\n                        chartType: 2\n                        legendVisualization: {\n                          isVisible: true\n                          position: 2\n                          hideSubtitle: false\n                        }\n                        axisVisualization: {\n                          x: {\n                            isVisible: true\n                            axisType: 2\n                          }\n                          y: {\n                            isVisible: true\n                            axisType: 1\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n                {\n                  name: 'sharedTimeRange'\n                  isOptional: true\n                }\n              ]\n              #disable-next-line BCP036\n              type: 'Extension/HubsExtension/PartType/MonitorChartPart'\n              settings: {}\n            }\n          }\n          {\n            position: {\n              x: 12\n              y: 5\n              colSpan: 4\n              rowSpan: 3\n            }\n            metadata: {\n              inputs: [\n                {\n                  name: 'options'\n                  value: {\n                    chart: {\n                      metrics: [\n                        {\n                          resourceMetadata: {\n                            id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                          }\n                          name: 'exceptions/browser'\n                          aggregationType: 7\n                          namespace: 'microsoft.insights/components'\n                          metricVisualization: {\n                            displayName: 'Browser exceptions'\n                            color: '#47BDF5'\n                          }\n                        }\n                      ]\n                      title: 'Browser exceptions'\n                      visualization: {\n                        chartType: 2\n                        legendVisualization: {\n                          isVisible: true\n                          position: 2\n                          hideSubtitle: false\n                        }\n                        axisVisualization: {\n                          x: {\n                            isVisible: true\n                            axisType: 2\n                          }\n                          y: {\n                            isVisible: true\n                            axisType: 1\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n                {\n                  name: 'sharedTimeRange'\n                  isOptional: true\n                }\n              ]\n              #disable-next-line BCP036\n              type: 'Extension/HubsExtension/PartType/MonitorChartPart'\n              settings: {}\n            }\n          }\n          {\n            position: {\n              x: 0\n              y: 8\n              colSpan: 4\n              rowSpan: 3\n            }\n            metadata: {\n              inputs: [\n                {\n                  name: 'options'\n                  value: {\n                    chart: {\n                      metrics: [\n                        {\n                          resourceMetadata: {\n                            id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                          }\n                          name: 'availabilityResults/count'\n                          aggregationType: 7\n                          namespace: 'microsoft.insights/components'\n                          metricVisualization: {\n                            displayName: 'Availability test results count'\n                            color: '#47BDF5'\n                          }\n                        }\n                      ]\n                      title: 'Availability test results count'\n                      visualization: {\n                        chartType: 2\n                        legendVisualization: {\n                          isVisible: true\n                          position: 2\n                          hideSubtitle: false\n                        }\n                        axisVisualization: {\n                          x: {\n                            isVisible: true\n                            axisType: 2\n                          }\n                          y: {\n                            isVisible: true\n                            axisType: 1\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n                {\n                  name: 'sharedTimeRange'\n                  isOptional: true\n                }\n              ]\n              #disable-next-line BCP036\n              type: 'Extension/HubsExtension/PartType/MonitorChartPart'\n              settings: {}\n            }\n          }\n          {\n            position: {\n              x: 4\n              y: 8\n              colSpan: 4\n              rowSpan: 3\n            }\n            metadata: {\n              inputs: [\n                {\n                  name: 'options'\n                  value: {\n                    chart: {\n                      metrics: [\n                        {\n                          resourceMetadata: {\n                            id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                          }\n                          name: 'performanceCounters/processIOBytesPerSecond'\n                          aggregationType: 4\n                          namespace: 'microsoft.insights/components'\n                          metricVisualization: {\n                            displayName: 'Process IO rate'\n                            color: '#47BDF5'\n                          }\n                        }\n                      ]\n                      title: 'Average process I/O rate'\n                      visualization: {\n                        chartType: 2\n                        legendVisualization: {\n                          isVisible: true\n                          position: 2\n                          hideSubtitle: false\n                        }\n                        axisVisualization: {\n                          x: {\n                            isVisible: true\n                            axisType: 2\n                          }\n                          y: {\n                            isVisible: true\n                            axisType: 1\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n                {\n                  name: 'sharedTimeRange'\n                  isOptional: true\n                }\n              ]\n              #disable-next-line BCP036\n              type: 'Extension/HubsExtension/PartType/MonitorChartPart'\n              settings: {}\n            }\n          }\n          {\n            position: {\n              x: 8\n              y: 8\n              colSpan: 4\n              rowSpan: 3\n            }\n            metadata: {\n              inputs: [\n                {\n                  name: 'options'\n                  value: {\n                    chart: {\n                      metrics: [\n                        {\n                          resourceMetadata: {\n                            id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}'\n                          }\n                          name: 'performanceCounters/memoryAvailableBytes'\n                          aggregationType: 4\n                          namespace: 'microsoft.insights/components'\n                          metricVisualization: {\n                            displayName: 'Available memory'\n                            color: '#47BDF5'\n                          }\n                        }\n                      ]\n                      title: 'Average available memory'\n                      visualization: {\n                        chartType: 2\n                        legendVisualization: {\n                          isVisible: true\n                          position: 2\n                          hideSubtitle: false\n                        }\n                        axisVisualization: {\n                          x: {\n                            isVisible: true\n                            axisType: 2\n                          }\n                          y: {\n                            isVisible: true\n                            axisType: 1\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n                {\n                  name: 'sharedTimeRange'\n                  isOptional: true\n                }\n              ]\n              #disable-next-line BCP036\n              type: 'Extension/HubsExtension/PartType/MonitorChartPart'\n              settings: {}\n            }\n          }\n        ]\n      }\n    ]\n  }\n}\n\nresource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = {\n  name: applicationInsightsName\n}\n"
  },
  {
    "path": "infra/core/monitor/applicationinsights.bicep",
    "content": "metadata description = 'Creates an Application Insights instance based on an existing Log Analytics workspace.'\nparam name string\nparam dashboardName string = ''\nparam location string = resourceGroup().location\nparam tags object = {}\nparam logAnalyticsWorkspaceId string\n\nresource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {\n  name: name\n  location: location\n  tags: tags\n  kind: 'web'\n  properties: {\n    Application_Type: 'web'\n    WorkspaceResourceId: logAnalyticsWorkspaceId\n  }\n}\n\nmodule applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = if (!empty(dashboardName)) {\n  name: 'application-insights-dashboard'\n  params: {\n    name: dashboardName\n    location: location\n    applicationInsightsName: applicationInsights.name\n  }\n}\n\noutput connectionString string = applicationInsights.properties.ConnectionString\noutput id string = applicationInsights.id\noutput instrumentationKey string = applicationInsights.properties.InstrumentationKey\noutput name string = applicationInsights.name\n"
  },
  {
    "path": "infra/core/monitor/loganalytics.bicep",
    "content": "metadata description = 'Creates a Log Analytics workspace.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\n\nresource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = {\n  name: name\n  location: location\n  tags: tags\n  properties: any({\n    retentionInDays: 30\n    features: {\n      searchVersion: 1\n    }\n    sku: {\n      name: 'PerGB2018'\n    }\n  })\n}\n\noutput id string = logAnalytics.id\noutput name string = logAnalytics.name\n"
  },
  {
    "path": "infra/core/monitor/monitoring.bicep",
    "content": "metadata description = 'Creates an Application Insights instance and a Log Analytics workspace.'\nparam logAnalyticsName string\nparam applicationInsightsName string\nparam applicationInsightsDashboardName string = ''\nparam location string = resourceGroup().location\nparam tags object = {}\n\nmodule logAnalytics 'loganalytics.bicep' = {\n  name: 'loganalytics'\n  params: {\n    name: logAnalyticsName\n    location: location\n    tags: tags\n  }\n}\n\nmodule applicationInsights 'applicationinsights.bicep' = {\n  name: 'applicationinsights'\n  params: {\n    name: applicationInsightsName\n    location: location\n    tags: tags\n    dashboardName: applicationInsightsDashboardName\n    logAnalyticsWorkspaceId: logAnalytics.outputs.id\n  }\n}\n\noutput applicationInsightsConnectionString string = applicationInsights.outputs.connectionString\noutput applicationInsightsId string = applicationInsights.outputs.id\noutput applicationInsightsInstrumentationKey string = applicationInsights.outputs.instrumentationKey\noutput applicationInsightsName string = applicationInsights.outputs.name\noutput logAnalyticsWorkspaceId string = logAnalytics.outputs.id\noutput logAnalyticsWorkspaceName string = logAnalytics.outputs.name\n"
  },
  {
    "path": "infra/core/networking/cdn-endpoint.bicep",
    "content": "metadata description = 'Adds an endpoint to an Azure CDN profile.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\n\n@description('The name of the CDN profile resource')\n@minLength(1)\nparam cdnProfileName string\n\n@description('Delivery policy rules')\nparam deliveryPolicyRules array = []\n\n@description('The origin URL for the endpoint')\n@minLength(1)\nparam originUrl string\n\nresource endpoint 'Microsoft.Cdn/profiles/endpoints@2022-05-01-preview' = {\n  parent: cdnProfile\n  name: name\n  location: location\n  tags: tags\n  properties: {\n    originHostHeader: originUrl\n    isHttpAllowed: false\n    isHttpsAllowed: true\n    queryStringCachingBehavior: 'UseQueryString'\n    optimizationType: 'GeneralWebDelivery'\n    origins: [\n      {\n        name: replace(originUrl, '.', '-')\n        properties: {\n          hostName: originUrl\n          originHostHeader: originUrl\n          priority: 1\n          weight: 1000\n          enabled: true\n        }\n      }\n    ]\n    deliveryPolicy: {\n      rules: deliveryPolicyRules\n    }\n  }\n}\n\nresource cdnProfile 'Microsoft.Cdn/profiles@2022-05-01-preview' existing = {\n  name: cdnProfileName\n}\n\noutput id string = endpoint.id\noutput name string = endpoint.name\noutput uri string = 'https://${endpoint.properties.hostName}'\n"
  },
  {
    "path": "infra/core/networking/cdn-profile.bicep",
    "content": "metadata description = 'Creates an Azure CDN profile.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\n\n@description('The pricing tier of this CDN profile')\n@allowed([\n  'Custom_Verizon'\n  'Premium_AzureFrontDoor'\n  'Premium_Verizon'\n  'StandardPlus_955BandWidth_ChinaCdn'\n  'StandardPlus_AvgBandWidth_ChinaCdn'\n  'StandardPlus_ChinaCdn'\n  'Standard_955BandWidth_ChinaCdn'\n  'Standard_Akamai'\n  'Standard_AvgBandWidth_ChinaCdn'\n  'Standard_AzureFrontDoor'\n  'Standard_ChinaCdn'\n  'Standard_Microsoft'\n  'Standard_Verizon'\n])\nparam sku string = 'Standard_Microsoft'\n\nresource profile 'Microsoft.Cdn/profiles@2022-05-01-preview' = {\n  name: name\n  location: location\n  tags: tags\n  sku: {\n    name: sku\n  }\n}\n\noutput id string = profile.id\noutput name string = profile.name\n"
  },
  {
    "path": "infra/core/networking/cdn.bicep",
    "content": "metadata description = 'Creates an Azure CDN profile with a single endpoint.'\nparam location string = resourceGroup().location\nparam tags object = {}\n\n@description('Name of the CDN endpoint resource')\nparam cdnEndpointName string\n\n@description('Name of the CDN profile resource')\nparam cdnProfileName string\n\n@description('Delivery policy rules')\nparam deliveryPolicyRules array = []\n\n@description('Origin URL for the CDN endpoint')\nparam originUrl string\n\nmodule cdnProfile 'cdn-profile.bicep' = {\n  name: 'cdn-profile'\n  params: {\n    name: cdnProfileName\n    location: location\n    tags: tags\n  }\n}\n\nmodule cdnEndpoint 'cdn-endpoint.bicep' = {\n  name: 'cdn-endpoint'\n  params: {\n    name: cdnEndpointName\n    location: location\n    tags: tags\n    cdnProfileName: cdnProfile.outputs.name\n    originUrl: originUrl\n    deliveryPolicyRules: deliveryPolicyRules\n  }\n}\n\noutput endpointName string = cdnEndpoint.outputs.name\noutput endpointId string = cdnEndpoint.outputs.id\noutput profileName string = cdnProfile.outputs.name\noutput profileId string = cdnProfile.outputs.id\noutput uri string = cdnEndpoint.outputs.uri\n"
  },
  {
    "path": "infra/core/search/search-services.bicep",
    "content": "metadata description = 'Creates an Azure AI Search instance.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\n\nparam sku object = {\n  name: 'standard'\n}\n\nparam authOptions object = {}\nparam disableLocalAuth bool = false\nparam disabledDataExfiltrationOptions array = []\nparam encryptionWithCmk object = {\n  enforcement: 'Unspecified'\n}\n@allowed([\n  'default'\n  'highDensity'\n])\nparam hostingMode string = 'default'\nparam networkRuleSet object = {\n  bypass: 'None'\n  ipRules: []\n}\nparam partitionCount int = 1\n@allowed([\n  'enabled'\n  'disabled'\n])\nparam publicNetworkAccess string = 'enabled'\nparam replicaCount int = 1\n@allowed([\n  'disabled'\n  'free'\n  'standard'\n])\nparam semanticSearch string = 'disabled'\n\nvar searchIdentityProvider = (sku.name == 'free') ? null : {\n  type: 'SystemAssigned'\n}\n\nresource search 'Microsoft.Search/searchServices@2021-04-01-preview' = {\n  name: name\n  location: location\n  tags: tags\n  // The free tier does not support managed identity\n  identity: searchIdentityProvider\n  properties: {\n    authOptions: disableLocalAuth ? null : authOptions\n    disableLocalAuth: disableLocalAuth\n    disabledDataExfiltrationOptions: disabledDataExfiltrationOptions\n    encryptionWithCmk: encryptionWithCmk\n    hostingMode: hostingMode\n    networkRuleSet: networkRuleSet\n    partitionCount: partitionCount\n    publicNetworkAccess: publicNetworkAccess\n    replicaCount: replicaCount\n    semanticSearch: semanticSearch\n  }\n  sku: sku\n}\n\noutput id string = search.id\noutput endpoint string = 'https://${name}.search.windows.net/'\noutput name string = search.name\noutput principalId string = !empty(searchIdentityProvider) ? search.identity.principalId : ''\n\n"
  },
  {
    "path": "infra/core/security/aks-managed-cluster-access.bicep",
    "content": "metadata description = 'Assigns RBAC role to the specified AKS cluster and principal.'\n\n@description('The AKS cluster name used as the target of the role assignments.')\nparam clusterName string\n\n@description('The principal ID to assign the role to.')\nparam principalId string\n\n@description('The principal type to assign the role to.')\n@allowed(['Device','ForeignGroup','Group','ServicePrincipal','User'])\nparam principalType string = 'User'\n\nvar aksClusterAdminRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b1ff04bb-8a4e-4dc4-8eb5-8693973ce19b')\n\nresource aksRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {\n  scope: aksCluster // Use when specifying a scope that is different than the deployment scope\n  name: guid(subscription().id, resourceGroup().id, principalId, aksClusterAdminRole)\n  properties: {\n    roleDefinitionId: aksClusterAdminRole\n    principalType: principalType\n    principalId: principalId\n  }\n}\n\nresource aksCluster 'Microsoft.ContainerService/managedClusters@2023-11-01' existing = {\n  name: clusterName\n}\n"
  },
  {
    "path": "infra/core/security/configstore-access.bicep",
    "content": "@description('Name of Azure App Configuration store')\nparam configStoreName string\n\n@description('The principal ID of the service principal to assign the role to')\nparam principalId string\n\nresource configStore 'Microsoft.AppConfiguration/configurationStores@2023-03-01' existing = {\n  name: configStoreName\n}\n\nvar configStoreDataReaderRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '516239f1-63e1-4d78-a4de-a74fb236a071')\n\nresource configStoreDataReaderRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {\n  name: guid(subscription().id, resourceGroup().id, principalId, configStoreDataReaderRole)\n  scope: configStore\n  properties: {\n    roleDefinitionId: configStoreDataReaderRole\n    principalId: principalId\n    principalType: 'ServicePrincipal' \n  }\n}\n"
  },
  {
    "path": "infra/core/security/keyvault-access.bicep",
    "content": "metadata description = 'Assigns an Azure Key Vault access policy.'\nparam name string = 'add'\n\nparam keyVaultName string\nparam permissions object = { secrets: [ 'get', 'list' ] }\nparam principalId string\n\nresource keyVaultAccessPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2022-11-01' = {\n  parent: keyVault\n  name: name\n  properties: {\n    accessPolicies: [ {\n        objectId: principalId\n        tenantId: subscription().tenantId\n        permissions: permissions\n      } ]\n  }\n}\n\nresource keyVault 'Microsoft.KeyVault/vaults@2022-11-01' existing = {\n  name: keyVaultName\n}\n"
  },
  {
    "path": "infra/core/security/keyvault-secret.bicep",
    "content": "metadata description = 'Creates or updates a secret in an Azure Key Vault.'\nparam name string\nparam tags object = {}\nparam keyVaultName string\nparam contentType string = 'string'\n@description('The value of the secret. Provide only derived values like blob storage access, but do not hard code any secrets in your templates')\n@secure()\nparam secretValue string\n\nparam enabled bool = true\nparam exp int = 0\nparam nbf int = 0\n\nresource keyVaultSecret 'Microsoft.KeyVault/vaults/secrets@2022-11-01' = {\n  name: name\n  tags: tags\n  parent: keyVault\n  properties: {\n    attributes: {\n      enabled: enabled\n      exp: exp\n      nbf: nbf\n    }\n    contentType: contentType\n    value: secretValue\n  }\n}\n\nresource keyVault 'Microsoft.KeyVault/vaults@2022-11-01' existing = {\n  name: keyVaultName\n}\n"
  },
  {
    "path": "infra/core/security/keyvault.bicep",
    "content": "metadata description = 'Creates an Azure Key Vault.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\nparam logAnalyticsWorkspaceId string = ''\n\nparam principalId string = ''\n\n@description('Allow the key vault to be used during resource creation.')\nparam enabledForDeployment bool = false\n@description('Allow the key vault to be used for template deployment.')\nparam enabledForTemplateDeployment bool = false\n\nresource keyVault 'Microsoft.KeyVault/vaults@2022-11-01' = {\n  name: name\n  location: location\n  tags: tags\n  properties: {\n    tenantId: subscription().tenantId\n    sku: { family: 'A', name: 'standard' }\n    accessPolicies: !empty(principalId) ? [\n      {\n        objectId: principalId\n        permissions: { secrets: [ 'get', 'list' ] }\n        tenantId: subscription().tenantId\n      }\n    ] : []\n    enabledForDeployment: enabledForDeployment\n    enabledForTemplateDeployment: enabledForTemplateDeployment\n  }\n}\n\nresource keyVault_DiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!(empty(logAnalyticsWorkspaceId))) {\n  scope: keyVault\n  name: 'keyVaultDiagnosticSettings'\n  properties: {\n    workspaceId: logAnalyticsWorkspaceId\n    logs: [\n      {\n        category: 'AuditEvent'\n        enabled: true\n      }\n    ]\n    metrics: [\n      {\n        category: 'AllMetrics'\n        enabled: true\n      }\n    ]\n  }\n}\n\noutput endpoint string = keyVault.properties.vaultUri\noutput id string = keyVault.id\noutput name string = keyVault.name\n"
  },
  {
    "path": "infra/core/security/registry-access.bicep",
    "content": "metadata description = 'Assigns ACR Pull permissions to access an Azure Container Registry.'\nparam containerRegistryName string\nparam principalId string\n\nvar acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')\n\nresource aksAcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = {\n  scope: containerRegistry // Use when specifying a scope that is different than the deployment scope\n  name: guid(subscription().id, resourceGroup().id, principalId, acrPullRole)\n  properties: {\n    roleDefinitionId: acrPullRole\n    principalType: 'ServicePrincipal'\n    principalId: principalId\n  }\n}\n\nresource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' existing = {\n  name: containerRegistryName\n}\n"
  },
  {
    "path": "infra/core/security/role.bicep",
    "content": "metadata description = 'Creates a role assignment for a service principal.'\nparam principalId string\n\n@allowed([\n  'Device'\n  'ForeignGroup'\n  'Group'\n  'ServicePrincipal'\n  'User'\n])\nparam principalType string = 'ServicePrincipal'\nparam roleDefinitionId string\n\nresource role 'Microsoft.Authorization/roleAssignments@2022-04-01' = {\n  name: guid(subscription().id, resourceGroup().id, principalId, roleDefinitionId)\n  properties: {\n    principalId: principalId\n    principalType: principalType\n    roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId)\n  }\n}\n"
  },
  {
    "path": "infra/core/storage/storage-account.bicep",
    "content": "metadata description = 'Creates an Azure storage account.'\nparam name string\nparam location string = resourceGroup().location\nparam tags object = {}\n\n@allowed([\n  'Cool'\n  'Hot'\n  'Premium' ])\nparam accessTier string = 'Hot'\nparam allowBlobPublicAccess bool = true\nparam allowCrossTenantReplication bool = true\nparam allowSharedKeyAccess bool = true\nparam containers array = []\nparam corsRules array = []\nparam defaultToOAuthAuthentication bool = false\nparam deleteRetentionPolicy object = {}\n@allowed([ 'AzureDnsZone', 'Standard' ])\nparam dnsEndpointType string = 'Standard'\nparam files array = []\nparam isHnsEnabled bool = false\nparam kind string = 'StorageV2'\nparam minimumTlsVersion string = 'TLS1_2'\nparam queues array = []\nparam shareDeleteRetentionPolicy object = {}\nparam supportsHttpsTrafficOnly bool = true\nparam tables array = []\nparam networkAcls object = {\n  bypass: 'AzureServices'\n  defaultAction: 'Allow'\n}\n@allowed([ 'Enabled', 'Disabled' ])\nparam publicNetworkAccess string = 'Enabled'\nparam sku object = { name: 'Standard_LRS' }\n\nresource storage 'Microsoft.Storage/storageAccounts@2023-05-01' = {\n  name: name\n  location: location\n  tags: tags\n  kind: kind\n  sku: sku\n  properties: {\n    accessTier: accessTier\n    allowBlobPublicAccess: allowBlobPublicAccess\n    allowCrossTenantReplication: allowCrossTenantReplication\n    allowSharedKeyAccess: allowSharedKeyAccess\n    defaultToOAuthAuthentication: defaultToOAuthAuthentication\n    dnsEndpointType: dnsEndpointType\n    isHnsEnabled: isHnsEnabled\n    minimumTlsVersion: minimumTlsVersion\n    networkAcls: networkAcls\n    publicNetworkAccess: publicNetworkAccess\n    supportsHttpsTrafficOnly: supportsHttpsTrafficOnly\n  }\n\n  resource blobServices 'blobServices' = if (!empty(containers)) {\n    name: 'default'\n    properties: {\n      cors: {\n        corsRules: corsRules\n      }\n      deleteRetentionPolicy: deleteRetentionPolicy\n    }\n    resource container 'containers' = [for container in containers: {\n      name: container.name\n      properties: {\n        publicAccess: contains(container, 'publicAccess') ? container.publicAccess : 'None'\n      }\n    }]\n  }\n\n  resource fileServices 'fileServices' = if (!empty(files)) {\n    name: 'default'\n    properties: {\n      cors: {\n        corsRules: corsRules\n      }\n      shareDeleteRetentionPolicy: shareDeleteRetentionPolicy\n    }\n  }\n\n  resource queueServices 'queueServices' = if (!empty(queues)) {\n    name: 'default'\n    properties: {\n\n    }\n    resource queue 'queues' = [for queue in queues: {\n      name: queue.name\n      properties: {\n        metadata: {}\n      }\n    }]\n  }\n\n  resource tableServices 'tableServices' = if (!empty(tables)) {\n    name: 'default'\n    properties: {}\n  }\n}\n\noutput id string = storage.id\noutput name string = storage.name\noutput primaryEndpoints object = storage.properties.primaryEndpoints\n"
  },
  {
    "path": "infra/core/testing/loadtesting.bicep",
    "content": "param name string\nparam location string = resourceGroup().location\nparam managedIdentity bool = false\nparam tags object = {}\n\nresource loadTest 'Microsoft.LoadTestService/loadTests@2022-12-01' = {\n  name: name\n  location: location\n  tags: tags\n  identity: { type: managedIdentity ? 'SystemAssigned' : 'None' }\n  properties: {\n  }\n}\n\noutput loadTestingName string = loadTest.name\n"
  },
  {
    "path": "infra/main.bicep",
    "content": "targetScope = 'subscription'\n\n@minLength(1)\n@maxLength(64)\n@description('Name of the the environment which is used to generate a short unique hash used in all resources.')\nparam environmentName string\n\n@minLength(1)\n@description('Primary location for all resources')\nparam location string\n\n@description('Id of the user or app to assign application roles')\nparam principalId string\n\n// Optional parameters to override the default azd resource naming conventions.\n// Add the following to main.parameters.json to provide values:\n// \"resourceGroupName\": {\n//      \"value\": \"myGroupName\"\n// }\nparam resourceGroupName string = ''\nparam logAnalyticsName string = ''\nparam applicationInsightsName string = ''\nparam applicationInsightsDashboardName string = ''\nparam keyVaultName string = ''\nparam appServiceName string = ''\nparam dbServerName string = ''\nparam dbName string = ''\n\n@secure()\nparam dbAdminPassword string\n\n@secure()\nparam dbAppUserPassword string\n\nvar abbrs = loadJsonContent('./abbreviations.json')\n\n// Tags that should be applied to all resources.\n// \n// Note that 'azd-service-name' tags should be applied separately to service host resources.\n// Example usage:\n//   tags: union(tags, { 'azd-service-name': <service name in azure.yaml> })\nvar tags = {\n  'azd-env-name': environmentName\n}\n\n// Generate a unique token to be used in naming resources.\nvar resourceToken = toLower(uniqueString(subscription().id, environmentName, location))\n\n// Name of the service defined in azure.yaml\n// A tag named azd-service-name with this value should be applied to the service host resource, such as:\n//   Microsoft.Web/sites for appservice, function\n// Example usage:\n//   tags: union(tags, { 'azd-service-name': apiServiceName })\nvar webServiceName = 'web'\n\n// Organize resources in a resource group\nresource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {\n  name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}'\n  location: location\n  tags: tags\n}\n\n// Add resources to be provisioned below.\n\nmodule monitoring 'core/monitor/monitoring.bicep' = {\n  name: 'monitoring'\n  params: {\n    location: location\n    tags: tags\n    logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}'\n    applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}'\n    applicationInsightsDashboardName: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}'\n  }\n  scope: rg\n}\n\nmodule keyVault 'core/security/keyvault.bicep' = {\n  name: 'keyvault'\n  params: {\n    location: location\n    tags: tags\n    name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}'\n    principalId: principalId\n  }\n  scope: rg\n}\n\nmodule web 'services/web.bicep' = {\n  name: 'web'\n  params: {\n    name: !empty(appServiceName) ? appServiceName : '${abbrs.webSitesAppService}${resourceToken}'\n    location: location\n    tags: tags\n    serviceName: webServiceName\n    applicationInsightsName: monitoring.outputs.applicationInsightsName\n    keyVaultName: keyVault.outputs.name\n  }\n  scope: rg\n}\n\n//#if (UsePostgreSQL)\nmodule pgsqldatabase 'core/database/postgresql/flexibleserver.bicep' = {\n  name: 'pgsql-database'\n  params: {\n    name: !empty(dbServerName) ? dbServerName : '${abbrs.postgreSQLServers}${resourceToken}'\n    location: location\n    tags: tags\n    sku: {\n      name: 'Standard_B1ms'\n      tier: 'Burstable'\n    }\n    storage: {\n      storageSizeGB: 32\n    }\n    version: '14'\n    appUserLogin: 'appUser'\n    appUserLoginPassword: dbAppUserPassword\n    administratorLogin: 'pgsqlAdmin'\n    administratorLoginPassword: dbAdminPassword\n    databaseName:!empty(dbName) ? dbName : '${abbrs.postgreSQLServersDatabases}${resourceToken}'\n    allowAzureIPsFirewall: true\n    keyVaultName: keyVault.outputs.name\n    connectionStringKey: 'ConnectionStrings--CleanArchitectureDb'\n  }\n  scope: rg\n}\n//#endif\n\n//#if (UseSqlServer)\nmodule database 'core/database/sqlserver/sqlserver.bicep' = {\n  name: 'database'\n  params: {\n    name: !empty(dbServerName) ? dbServerName : '${abbrs.sqlServers}${resourceToken}'\n    location: location\n    tags: tags\n    databaseName: !empty(dbName) ? dbName : '${abbrs.sqlServersDatabases}${resourceToken}'\n    keyVaultName: keyVault.outputs.name\n    connectionStringKey: 'ConnectionStrings--CleanArchitectureDb'\n    sqlAdminPassword: dbAdminPassword\n    appUserPassword: dbAppUserPassword\n  }\n  scope: rg\n}\n//#endif\n\nmodule webKeyVaultAccess 'core/security/keyvault-access.bicep' = {\n  name: 'webKeyVaultAccess'\n  params: {\n    keyVaultName: keyVault.outputs.name\n    principalId: web.outputs.identityPrincipalId\n  }\n  scope: rg\n}\n\n// Add outputs from the deployment here, if needed.\n//\n// This allows the outputs to be referenced by other bicep deployments in the deployment pipeline,\n// or by the local machine as a way to reference created resources in Azure for local development.\n// Secrets should not be added here.\n//\n// Outputs are automatically saved in the local azd environment .env file.\n// To see these outputs, run `azd env get-values`,  or `azd env get-values --output json` for json output.\noutput AZURE_LOCATION string = location\noutput AZURE_TENANT_ID string = tenant().tenantId\noutput AZURE_KEY_VAULT_NAME string = keyVault.outputs.name\noutput AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint\noutput APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString\n//#if (UseSqlServer)\noutput AZURE_SQL_CONNECTION_STRING_KEY string = database.outputs.connectionStringKey\n//#endif\n//#if (UsePostgreSQL)\noutput AZURE_PSQL_CONNECTION_STRING_KEY string = pgsqldatabase.outputs.connectionStringKey\n//#endif\noutput WEB_BASE_URI string = web.outputs.uri\n"
  },
  {
    "path": "infra/main.parameters.json",
    "content": "{\n    \"$schema\": \"https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#\",\n    \"contentVersion\": \"1.0.0.0\",\n    \"parameters\": {\n      \"environmentName\": {\n        \"value\": \"${AZURE_ENV_NAME}\"\n      },\n      \"location\": {\n        \"value\": \"${AZURE_LOCATION}\"\n      },\n      \"principalId\": {\n        \"value\": \"${AZURE_PRINCIPAL_ID}\"\n      },\n      \"dbAdminPassword\": {\n        \"value\": \"$(secretOrRandomPassword ${AZURE_KEY_VAULT_NAME} dbAdminPassword)\"\n      },\n      \"dbAppUserPassword\": {\n        \"value\": \"$(secretOrRandomPassword ${AZURE_KEY_VAULT_NAME} dbAppUserPassword)\"\n      }\n    }\n}\n"
  },
  {
    "path": "infra/services/web.bicep",
    "content": "param name string\nparam location string = resourceGroup().location\nparam tags object = {}\n\nparam serviceName string = 'web'\nparam applicationInsightsName string = ''\nparam keyVaultName string = ''\n\nmodule appServicePlan '../core/host/appserviceplan.bicep' = {\n  name: 'appServicePlan'\n  params: {\n    name: name\n    location: location\n    tags: tags\n    sku: {\n      name: 'B1'\n    }\n  }\n}\n\nmodule appService '../core/host/appservice.bicep' = {\n  name: 'appService'\n  params: {\n    name: name\n    location: location\n    tags: union(tags, { 'azd-service-name': serviceName })\n    appServicePlanId: appServicePlan.outputs.id\n    applicationInsightsName: applicationInsightsName\n    keyVaultName: keyVaultName\n    runtimeName: 'dotnetcore'\n    runtimeVersion: '10.0'\n    healthCheckPath: '/health'\n    appSettings: {\n      ASPNETCORE_ENVIRONMENT: 'Development'\n    }\n  }\n}\n\noutput name string = appService.outputs.name\noutput uri string = appService.outputs.uri\noutput identityPrincipalId string = appService.outputs.identityPrincipalId\n"
  },
  {
    "path": "renovate.json",
    "content": "{\n  \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n  \"extends\": [\n    \"config:recommended\",\n    \":semanticCommits\",\n    \":separateMajorReleases\",\n    \"group:allNonMajor\"\n  ],\n  \"labels\": [\n    \"dependencies\"\n  ],\n  \"schedule\": [\n    \"before 6am on monday\"\n  ],\n  \"timezone\": \"Australia/Brisbane\",\n  \"packageRules\": [\n    {\n      \"description\": \"Automerge non-major updates\",\n      \"matchUpdateTypes\": [\n        \"minor\",\n        \"patch\",\n        \"digest\"\n      ],\n      \"automerge\": true\n    },\n    {\n      \"description\": \"Group GitHub Actions updates\",\n      \"matchManagers\": [\n        \"github-actions\"\n      ],\n      \"groupName\": \"GitHub Actions\",\n      \"automerge\": true\n    },\n    {\n      \"description\": \"Group .NET updates\",\n      \"matchManagers\": [\n        \"nuget\"\n      ],\n      \"groupName\": \".NET dependencies\"\n    },\n    {\n      \"description\": \"Group Angular updates\",\n      \"groupName\": \"Angular\",\n      \"matchPackageNames\": [\n        \"/^@angular/\"\n      ]\n    },\n    {\n      \"description\": \"Group React updates\",\n      \"groupName\": \"React\",\n      \"matchPackageNames\": [\n        \"/^react/\",\n        \"/^@types/react/\"\n      ]\n    },\n    {\n      \"description\": \"Group testing packages\",\n      \"groupName\": \"Testing\",\n      \"matchPackageNames\": [\n        \"/jest/\",\n        \"/karma/\",\n        \"/jasmine/\",\n        \"/playwright/\",\n        \"/nunit/\",\n        \"/moq/\",\n        \"/shouldly/\"\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "src/AppHost/AppHost.csproj",
    "content": "﻿<Project Sdk=\"Aspire.AppHost.Sdk/13.1.3\">\n\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <RootNamespace>CleanArchitecture.AppHost</RootNamespace>\n    <AssemblyName>CleanArchitecture.AppHost</AssemblyName>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Aspire.Hosting.AppHost\" />\n    <PackageReference Include=\"Aspire.Hosting.JavaScript\" />\n    <!--#if (UsePostgreSQL)-->\n    <PackageReference Include=\"Aspire.Hosting.PostgreSQL\" />\n    <!--#endif-->\n    <!--#if (UseSqlServer)-->\n    <PackageReference Include=\"Aspire.Hosting.SqlServer\" />\n    <!--#endif-->\n    <!--#if (UseSqlite)-->\n    <PackageReference Include=\"CommunityToolkit.Aspire.Hosting.SQLite\" />\n    <!--#endif-->\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Shared\\Shared.csproj\" IsAspireProjectResource=\"false\" />\n    <ProjectReference Include=\"..\\Web\\Web.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/AppHost/Program.cs",
    "content": "using CleanArchitecture.Shared;\n\nvar builder = DistributedApplication.CreateBuilder(args);\n\n#if (UsePostgreSQL)\nvar databaseServer = builder\n    .AddPostgres(Services.DatabaseServer)\n    .AddDatabase(Services.Database);\n#elif (UseSqlServer)\nvar databaseServer = builder.AddSqlServer(Services.DatabaseServer)\n    .AddDatabase(Services.Database);\n#else\nvar databaseServer = builder\n    .AddSqlite(Services.Database);\n#endif\n\nvar web = builder.AddProject<Projects.Web>(Services.WebApi)\n    .WithReference(databaseServer)\n    .WaitFor(databaseServer)\n    .WithUrlForEndpoint(\"http\", url =>\n    {\n        url.DisplayText = \"Scalar API Reference\";\n        url.Url = \"/scalar\";\n    });\n\n#if (!UseApiOnly)\nbuilder.AddJavaScriptApp(Services.WebFrontend, \"./../Web/ClientApp\")\n    .WithRunScript(\"start\")\n    .WithReference(web)\n    .WaitFor(web)\n    .WithHttpEndpoint(env: \"PORT\")\n    .WithExternalHttpEndpoints()\n    .PublishAsDockerFile();\n#endif\n\nbuilder.Build().Run();\n"
  },
  {
    "path": "src/AppHost/Properties/launchSettings.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/launchsettings.json\",\n  \"profiles\": {\n    \"https\": {\n      \"commandName\": \"Project\",\n      \"dotnetRunMessages\": true,\n      \"launchBrowser\": true,\n      \"applicationUrl\": \"https://cleanarchitecture.dev.localhost:17000;http://cleanarchitecture.dev.localhost:15000\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"DOTNET_ENVIRONMENT\": \"Development\",\n        \"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL\": \"https://localhost:21000\",\n        \"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL\": \"https://localhost:22000\"\n      }\n    },\n    \"http\": {\n      \"commandName\": \"Project\",\n      \"dotnetRunMessages\": true,\n      \"launchBrowser\": true,\n      \"applicationUrl\": \"http://cleanarchitecture.dev.localhost:15000\",\n      \"environmentVariables\": {\n        \"ASPIRE_ALLOW_UNSECURED_TRANSPORT\": \"true\",\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"DOTNET_ENVIRONMENT\": \"Development\",\n        \"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL\": \"http://localhost:19000\",\n        \"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL\": \"http://localhost:20000\"\n      }\n    },\n    \"manifest\": {\n      \"commandName\": \"Project\",\n      \"commandLineArgs\": \"--publisher manifest --output-path aspire-manifest.json\",\n      \"dotnetRunMessages\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Production\",\n        \"DOTNET_ENVIRONMENT\": \"Production\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/AppHost/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/AppHost/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\",\n      \"Aspire.Hosting.Dcp\": \"Warning\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/Application/Application.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\r\n\r\n  <PropertyGroup>\r\n    <RootNamespace>CleanArchitecture.Application</RootNamespace>\r\n    <AssemblyName>CleanArchitecture.Application</AssemblyName>\r\n  </PropertyGroup>\r\n\r\n  <ItemGroup>\r\n    <PackageReference Include=\"Ardalis.GuardClauses\" />\r\n    <PackageReference Include=\"AutoMapper\" />\r\n    <PackageReference Include=\"FluentValidation.DependencyInjectionExtensions\" />\r\n    <PackageReference Include=\"MediatR\" />\r\n    <PackageReference Include=\"Microsoft.Build.Tasks.Core\" />\r\n    <PackageReference Include=\"Microsoft.Build.Utilities.Core\" />\r\n    <PackageReference Include=\"Microsoft.EntityFrameworkCore\" />\r\n    <PackageReference Include=\"Microsoft.Extensions.Hosting\" />\r\n  </ItemGroup>\r\n\r\n  <ItemGroup>\r\n    <ProjectReference Include=\"..\\Domain\\Domain.csproj\" />\r\n  </ItemGroup>\r\n\r\n</Project>\r\n"
  },
  {
    "path": "src/Application/Common/Behaviours/AuthorizationBehaviour.cs",
    "content": "﻿using System.Reflection;\nusing CleanArchitecture.Application.Common.Exceptions;\nusing CleanArchitecture.Application.Common.Interfaces;\nusing CleanArchitecture.Application.Common.Security;\n\nnamespace CleanArchitecture.Application.Common.Behaviours;\n\npublic class AuthorizationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> \n    where TRequest : notnull\n{\n    private readonly IUser _user;\n    private readonly IIdentityService _identityService;\n\n    public AuthorizationBehaviour(\n        IUser user,\n        IIdentityService identityService)\n    {\n        _user = user;\n        _identityService = identityService;\n    }\n\n    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)\n    {\n        var authorizeAttributes = request.GetType().GetCustomAttributes<AuthorizeAttribute>();\n\n        if (authorizeAttributes.Any())\n        {\n            // Must be authenticated user\n            if (_user.Id == null)\n            {\n                throw new UnauthorizedAccessException();\n            }\n\n            // Role-based authorization\n            var authorizeAttributesWithRoles = authorizeAttributes.Where(a => !string.IsNullOrWhiteSpace(a.Roles));\n\n            if (authorizeAttributesWithRoles.Any())\n            {\n                var authorized = false;\n\n                foreach (var roles in authorizeAttributesWithRoles.Select(a => a.Roles.Split(',')))\n                {\n                    foreach (var role in roles)\n                    {\n                        var isInRole = _user.Roles?.Any(x => role == x)??false;\n                        if (isInRole)\n                        {\n                            authorized = true;\n                            break;\n                        }\n                    }\n                }\n\n                // Must be a member of at least one role in roles\n                if (!authorized)\n                {\n                    throw new ForbiddenAccessException();\n                }\n            }\n\n            // Policy-based authorization\n            var authorizeAttributesWithPolicies = authorizeAttributes.Where(a => !string.IsNullOrWhiteSpace(a.Policy));\n            if (authorizeAttributesWithPolicies.Any())\n            {\n                foreach (var policy in authorizeAttributesWithPolicies.Select(a => a.Policy))\n                {\n                    var authorized = await _identityService.AuthorizeAsync(_user.Id, policy);\n\n                    if (!authorized)\n                    {\n                        throw new ForbiddenAccessException();\n                    }\n                }\n            }\n        }\n\n        // User is authorized / authorization not required\n        return await next();\n    }\n}\n"
  },
  {
    "path": "src/Application/Common/Behaviours/LoggingBehaviour.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Interfaces;\nusing MediatR.Pipeline;\nusing Microsoft.Extensions.Logging;\n\nnamespace CleanArchitecture.Application.Common.Behaviours;\n\npublic class LoggingBehaviour<TRequest> : IRequestPreProcessor<TRequest>\n    where TRequest : notnull\n{\n    private readonly ILogger _logger;\n    private readonly IUser _user;\n    private readonly IIdentityService _identityService;\n\n    public LoggingBehaviour(ILogger<TRequest> logger, IUser user, IIdentityService identityService)\n    {\n        _logger = logger;\n        _user = user;\n        _identityService = identityService;\n    }\n\n    public async Task Process(TRequest request, CancellationToken cancellationToken)\n    {\n        var requestName = typeof(TRequest).Name;\n        var userId = _user.Id ?? string.Empty;\n        string? userName = string.Empty;\n\n        if (!string.IsNullOrEmpty(userId))\n        {\n            userName = await _identityService.GetUserNameAsync(userId);\n        }\n\n        _logger.LogInformation(\"CleanArchitecture Request: {Name} {@UserId} {@UserName} {@Request}\",\n            requestName, userId, userName, request);\n    }\n}\n"
  },
  {
    "path": "src/Application/Common/Behaviours/PerformanceBehaviour.cs",
    "content": "﻿using System.Diagnostics;\nusing CleanArchitecture.Application.Common.Interfaces;\nusing Microsoft.Extensions.Logging;\n\nnamespace CleanArchitecture.Application.Common.Behaviours;\n\npublic class PerformanceBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>\n    where TRequest : notnull\n{\n    private readonly Stopwatch _timer;\n    private readonly ILogger<TRequest> _logger;\n    private readonly IUser _user;\n    private readonly IIdentityService _identityService;\n\n    public PerformanceBehaviour(\n        ILogger<TRequest> logger,\n        IUser user,\n        IIdentityService identityService)\n    {\n        _timer = new Stopwatch();\n\n        _logger = logger;\n        _user = user;\n        _identityService = identityService;\n    }\n\n    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)\n    {\n        _timer.Start();\n\n        var response = await next();\n\n        _timer.Stop();\n\n        var elapsedMilliseconds = _timer.ElapsedMilliseconds;\n\n        if (elapsedMilliseconds > 500)\n        {\n            var requestName = typeof(TRequest).Name;\n            var userId = _user.Id ?? string.Empty;\n            var userName = string.Empty;\n\n            if (!string.IsNullOrEmpty(userId))\n            {\n                userName = await _identityService.GetUserNameAsync(userId);\n            }\n\n            _logger.LogWarning(\"CleanArchitecture Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {@UserId} {@UserName} {@Request}\",\n                requestName, elapsedMilliseconds, userId, userName, request);\n        }\n\n        return response;\n    }\n}\n"
  },
  {
    "path": "src/Application/Common/Behaviours/UnhandledExceptionBehaviour.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\n\nnamespace CleanArchitecture.Application.Common.Behaviours;\n\npublic class UnhandledExceptionBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>\n    where TRequest : notnull\n{\n    private readonly ILogger<TRequest> _logger;\n\n    public UnhandledExceptionBehaviour(ILogger<TRequest> logger)\n    {\n        _logger = logger;\n    }\n\n    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)\n    {\n        try\n        {\n            return await next();\n        }\n        catch (Exception ex)\n        {\n            var requestName = typeof(TRequest).Name;\n\n            _logger.LogError(ex, \"CleanArchitecture Request: Unhandled Exception for Request {Name} {@Request}\", requestName, request);\n\n            throw;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Application/Common/Behaviours/ValidationBehaviour.cs",
    "content": "﻿using ValidationException = CleanArchitecture.Application.Common.Exceptions.ValidationException;\n\nnamespace CleanArchitecture.Application.Common.Behaviours;\n\npublic class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>\n    where TRequest : notnull\n{\n    private readonly IEnumerable<IValidator<TRequest>> _validators;\n\n    public ValidationBehaviour(IEnumerable<IValidator<TRequest>> validators)\n    {\n        _validators = validators;\n    }\n\n    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)\n    {\n        if (_validators.Any())\n        {\n            var validationResults = await Task.WhenAll(\n                _validators.Select(v =>\n                    v.ValidateAsync(new ValidationContext<TRequest>(request), cancellationToken)));\n\n            var failures = validationResults\n                .Where(r => r.Errors.Any())\n                .SelectMany(r => r.Errors)\n                .ToList();\n\n            if (failures.Count != 0)\n                throw new ValidationException(failures);\n        }\n\n        return await next();\n    }\n}\n"
  },
  {
    "path": "src/Application/Common/Exceptions/ForbiddenAccessException.cs",
    "content": "﻿namespace CleanArchitecture.Application.Common.Exceptions;\n\npublic class ForbiddenAccessException : Exception\n{\n    public ForbiddenAccessException() : base() { }\n}\n"
  },
  {
    "path": "src/Application/Common/Exceptions/ValidationException.cs",
    "content": "﻿using FluentValidation.Results;\n\nnamespace CleanArchitecture.Application.Common.Exceptions;\n\npublic class ValidationException : Exception\n{\n    public ValidationException()\n        : base(\"One or more validation failures have occurred.\")\n    {\n        Errors = new Dictionary<string, string[]>();\n    }\n\n    public ValidationException(IEnumerable<ValidationFailure> failures)\n        : this()\n    {\n        Errors = failures\n            .GroupBy(e => e.PropertyName, e => e.ErrorMessage)\n            .ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray());\n    }\n\n    public IDictionary<string, string[]> Errors { get; }\n}\n"
  },
  {
    "path": "src/Application/Common/Interfaces/IApplicationDbContext.cs",
    "content": "﻿using CleanArchitecture.Domain.Entities;\n\nnamespace CleanArchitecture.Application.Common.Interfaces;\n\npublic interface IApplicationDbContext\n{\n    DbSet<TodoList> TodoLists { get; }\n\n    DbSet<TodoItem> TodoItems { get; }\n\n    Task<int> SaveChangesAsync(CancellationToken cancellationToken);\n}\n"
  },
  {
    "path": "src/Application/Common/Interfaces/IIdentityService.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Models;\n\nnamespace CleanArchitecture.Application.Common.Interfaces;\n\npublic interface IIdentityService\n{\n    Task<string?> GetUserNameAsync(string userId);\n\n    Task<bool> IsInRoleAsync(string userId, string role);\n\n    Task<bool> AuthorizeAsync(string userId, string policyName);\n\n    Task<(Result Result, string UserId)> CreateUserAsync(string userName, string password);\n\n    Task<Result> DeleteUserAsync(string userId);\n}\n"
  },
  {
    "path": "src/Application/Common/Interfaces/IUser.cs",
    "content": "﻿namespace CleanArchitecture.Application.Common.Interfaces;\n\npublic interface IUser\n{\n    string? Id { get; }\n    List<string>? Roles { get; }\n\n}\n"
  },
  {
    "path": "src/Application/Common/Mappings/MappingExtensions.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Models;\n\nnamespace CleanArchitecture.Application.Common.Mappings;\n\npublic static class MappingExtensions\n{\n    public static Task<PaginatedList<TDestination>> PaginatedListAsync<TDestination>(this IQueryable<TDestination> queryable, int pageNumber, int pageSize, CancellationToken cancellationToken = default) where TDestination : class\n        => PaginatedList<TDestination>.CreateAsync(queryable.AsNoTracking(), pageNumber, pageSize, cancellationToken);\n\n    public static Task<List<TDestination>> ProjectToListAsync<TDestination>(this IQueryable queryable, IConfigurationProvider configuration, CancellationToken cancellationToken = default) where TDestination : class\n        => queryable.ProjectTo<TDestination>(configuration).AsNoTracking().ToListAsync(cancellationToken);\n}\n"
  },
  {
    "path": "src/Application/Common/Models/LookupDto.cs",
    "content": "﻿using CleanArchitecture.Domain.Entities;\n\nnamespace CleanArchitecture.Application.Common.Models;\n\npublic class LookupDto\n{\n    public int Id { get; init; }\n\n    public string? Title { get; init; }\n\n    private class Mapping : Profile\n    {\n        public Mapping()\n        {\n            CreateMap<TodoList, LookupDto>();\n            CreateMap<TodoItem, LookupDto>();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Application/Common/Models/PaginatedList.cs",
    "content": "﻿namespace CleanArchitecture.Application.Common.Models;\n\npublic class PaginatedList<T>\n{\n    public IReadOnlyCollection<T> Items { get; }\n    public int PageNumber { get; }\n    public int TotalPages { get; }\n    public int TotalCount { get; }\n\n    public PaginatedList(IReadOnlyCollection<T> items, int count, int pageNumber, int pageSize)\n    {\n        PageNumber = pageNumber;\n        TotalPages = (int)Math.Ceiling(count / (double)pageSize);\n        TotalCount = count;\n        Items = items;\n    }\n\n    public bool HasPreviousPage => PageNumber > 1 && PageNumber <= TotalPages + 1;\n\n    public bool HasNextPage => PageNumber < TotalPages;\n\n    public static async Task<PaginatedList<T>> CreateAsync(IQueryable<T> source, int pageNumber, int pageSize, CancellationToken cancellationToken = default)\n    {\n        var count = await source.CountAsync(cancellationToken);\n        var items = await source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken);\n\n        return new PaginatedList<T>(items, count, pageNumber, pageSize);\n    }\n}\n"
  },
  {
    "path": "src/Application/Common/Models/Result.cs",
    "content": "﻿namespace CleanArchitecture.Application.Common.Models;\n\npublic class Result\n{\n    internal Result(bool succeeded, IEnumerable<string> errors)\n    {\n        Succeeded = succeeded;\n        Errors = errors.ToArray();\n    }\n\n    public bool Succeeded { get; init; }\n\n    public string[] Errors { get; init; }\n\n    public static Result Success()\n    {\n        return new Result(true, Array.Empty<string>());\n    }\n\n    public static Result Failure(IEnumerable<string> errors)\n    {\n        return new Result(false, errors);\n    }\n}\n"
  },
  {
    "path": "src/Application/Common/Security/AuthorizeAttribute.cs",
    "content": "﻿namespace CleanArchitecture.Application.Common.Security;\n\n/// <summary>\n/// Specifies the class this attribute is applied to requires authorization.\n/// </summary>\n[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]\npublic class AuthorizeAttribute : Attribute\n{\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"AuthorizeAttribute\"/> class. \n    /// </summary>\n    public AuthorizeAttribute() { }\n\n    /// <summary>\n    /// Gets or sets a comma delimited list of roles that are allowed to access the resource.\n    /// </summary>\n    public string Roles { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Gets or sets the policy name that determines access to the resource.\n    /// </summary>\n    public string Policy { get; set; } = string.Empty;\n}\n"
  },
  {
    "path": "src/Application/DependencyInjection.cs",
    "content": "﻿using System.Reflection;\nusing CleanArchitecture.Application.Common.Behaviours;\nusing Microsoft.Extensions.Hosting;\n\nnamespace Microsoft.Extensions.DependencyInjection;\n\npublic static class DependencyInjection\n{\n    public static void AddApplicationServices(this IHostApplicationBuilder builder)\n    {\n        builder.Services.AddAutoMapper(cfg => \n            cfg.AddMaps(Assembly.GetExecutingAssembly()));\n\n        builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());\n\n        builder.Services.AddMediatR(cfg => {\n            cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());\n            cfg.AddOpenRequestPreProcessor(typeof(LoggingBehaviour<>));\n            cfg.AddOpenBehavior(typeof(UnhandledExceptionBehaviour<,>));\n            cfg.AddOpenBehavior(typeof(AuthorizationBehaviour<,>));\n            cfg.AddOpenBehavior(typeof(ValidationBehaviour<,>));\n            cfg.AddOpenBehavior(typeof(PerformanceBehaviour<,>));\n        });\n    }\n}\n"
  },
  {
    "path": "src/Application/GlobalUsings.cs",
    "content": "﻿global using Ardalis.GuardClauses;\nglobal using AutoMapper;\nglobal using AutoMapper.QueryableExtensions;\nglobal using Microsoft.EntityFrameworkCore;\nglobal using FluentValidation;\nglobal using MediatR;"
  },
  {
    "path": "src/Application/TodoItems/Commands/CreateTodoItem/CreateTodoItem.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Interfaces;\nusing CleanArchitecture.Domain.Entities;\nusing CleanArchitecture.Domain.Events;\n\nnamespace CleanArchitecture.Application.TodoItems.Commands.CreateTodoItem;\n\npublic record CreateTodoItemCommand : IRequest<int>\n{\n    public int ListId { get; init; }\n\n    public string? Title { get; init; }\n}\n\npublic class CreateTodoItemCommandHandler : IRequestHandler<CreateTodoItemCommand, int>\n{\n    private readonly IApplicationDbContext _context;\n\n    public CreateTodoItemCommandHandler(IApplicationDbContext context)\n    {\n        _context = context;\n    }\n\n    public async Task<int> Handle(CreateTodoItemCommand request, CancellationToken cancellationToken)\n    {\n        var entity = new TodoItem\n        {\n            ListId = request.ListId,\n            Title = request.Title,\n            Done = false\n        };\n\n        entity.AddDomainEvent(new TodoItemCreatedEvent(entity));\n\n        _context.TodoItems.Add(entity);\n\n        await _context.SaveChangesAsync(cancellationToken);\n\n        return entity.Id;\n    }\n}\n"
  },
  {
    "path": "src/Application/TodoItems/Commands/CreateTodoItem/CreateTodoItemCommandValidator.cs",
    "content": "﻿namespace CleanArchitecture.Application.TodoItems.Commands.CreateTodoItem;\n\npublic class CreateTodoItemCommandValidator : AbstractValidator<CreateTodoItemCommand>\n{\n    public CreateTodoItemCommandValidator()\n    {\n        RuleFor(v => v.Title)\n            .MaximumLength(200)\n            .NotEmpty();\n    }\n}\n"
  },
  {
    "path": "src/Application/TodoItems/Commands/DeleteTodoItem/DeleteTodoItem.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Interfaces;\nusing CleanArchitecture.Domain.Events;\n\nnamespace CleanArchitecture.Application.TodoItems.Commands.DeleteTodoItem;\n\npublic record DeleteTodoItemCommand(int Id) : IRequest;\n\npublic class DeleteTodoItemCommandHandler : IRequestHandler<DeleteTodoItemCommand>\n{\n    private readonly IApplicationDbContext _context;\n\n    public DeleteTodoItemCommandHandler(IApplicationDbContext context)\n    {\n        _context = context;\n    }\n\n    public async Task Handle(DeleteTodoItemCommand request, CancellationToken cancellationToken)\n    {\n        var entity = await _context.TodoItems\n            .FindAsync(new object[] { request.Id }, cancellationToken);\n\n        Guard.Against.NotFound(request.Id, entity);\n\n        _context.TodoItems.Remove(entity);\n\n        entity.AddDomainEvent(new TodoItemDeletedEvent(entity));\n\n        await _context.SaveChangesAsync(cancellationToken);\n    }\n\n}\n"
  },
  {
    "path": "src/Application/TodoItems/Commands/UpdateTodoItem/UpdateTodoItem.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Interfaces;\n\nnamespace CleanArchitecture.Application.TodoItems.Commands.UpdateTodoItem;\n\npublic record UpdateTodoItemCommand : IRequest\n{\n    public int Id { get; init; }\n\n    public string? Title { get; init; }\n\n    public bool Done { get; init; }\n}\n\npublic class UpdateTodoItemCommandHandler : IRequestHandler<UpdateTodoItemCommand>\n{\n    private readonly IApplicationDbContext _context;\n\n    public UpdateTodoItemCommandHandler(IApplicationDbContext context)\n    {\n        _context = context;\n    }\n\n    public async Task Handle(UpdateTodoItemCommand request, CancellationToken cancellationToken)\n    {\n        var entity = await _context.TodoItems\n            .FindAsync(new object[] { request.Id }, cancellationToken);\n\n        Guard.Against.NotFound(request.Id, entity);\n\n        entity.Title = request.Title;\n        entity.Done = request.Done;\n\n        await _context.SaveChangesAsync(cancellationToken);\n    }\n}\n"
  },
  {
    "path": "src/Application/TodoItems/Commands/UpdateTodoItem/UpdateTodoItemCommandValidator.cs",
    "content": "﻿namespace CleanArchitecture.Application.TodoItems.Commands.UpdateTodoItem;\n\npublic class UpdateTodoItemCommandValidator : AbstractValidator<UpdateTodoItemCommand>\n{\n    public UpdateTodoItemCommandValidator()\n    {\n        RuleFor(v => v.Title)\n            .MaximumLength(200)\n            .NotEmpty();\n    }\n}\n"
  },
  {
    "path": "src/Application/TodoItems/Commands/UpdateTodoItemDetail/UpdateTodoItemDetail.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Interfaces;\nusing CleanArchitecture.Domain.Enums;\n\nnamespace CleanArchitecture.Application.TodoItems.Commands.UpdateTodoItemDetail;\n\npublic record UpdateTodoItemDetailCommand : IRequest\n{\n    public int Id { get; init; }\n\n    public int ListId { get; init; }\n\n    public PriorityLevel Priority { get; init; }\n\n    public string? Note { get; init; }\n}\n\npublic class UpdateTodoItemDetailCommandHandler : IRequestHandler<UpdateTodoItemDetailCommand>\n{\n    private readonly IApplicationDbContext _context;\n\n    public UpdateTodoItemDetailCommandHandler(IApplicationDbContext context)\n    {\n        _context = context;\n    }\n\n    public async Task Handle(UpdateTodoItemDetailCommand request, CancellationToken cancellationToken)\n    {\n        var entity = await _context.TodoItems\n            .FindAsync(new object[] { request.Id }, cancellationToken);\n\n        Guard.Against.NotFound(request.Id, entity);\n\n        entity.ListId = request.ListId;\n        entity.Priority = request.Priority;\n        entity.Note = request.Note;\n\n        await _context.SaveChangesAsync(cancellationToken);\n    }\n}\n"
  },
  {
    "path": "src/Application/TodoItems/EventHandlers/LogTodoItemCompleted.cs",
    "content": "﻿using CleanArchitecture.Domain.Events;\nusing Microsoft.Extensions.Logging;\n\nnamespace CleanArchitecture.Application.TodoItems.EventHandlers;\n\npublic class LogTodoItemCompleted : INotificationHandler<TodoItemCompletedEvent>\n{\n    private readonly ILogger<LogTodoItemCompleted> _logger;\n\n    public LogTodoItemCompleted(ILogger<LogTodoItemCompleted> logger)\n    {\n        _logger = logger;\n    }\n\n    public Task Handle(TodoItemCompletedEvent notification, CancellationToken cancellationToken)\n    {\n        _logger.LogInformation(\"CleanArchitecture Domain Event: {DomainEvent}\", notification.GetType().Name);\n\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "src/Application/TodoItems/EventHandlers/LogTodoItemCreated.cs",
    "content": "﻿using CleanArchitecture.Domain.Events;\nusing Microsoft.Extensions.Logging;\n\nnamespace CleanArchitecture.Application.TodoItems.EventHandlers;\n\npublic class LogTodoItemCreated : INotificationHandler<TodoItemCreatedEvent>\n{\n    private readonly ILogger<LogTodoItemCreated> _logger;\n\n    public LogTodoItemCreated(ILogger<LogTodoItemCreated> logger)\n    {\n        _logger = logger;\n    }\n\n    public Task Handle(TodoItemCreatedEvent notification, CancellationToken cancellationToken)\n    {\n        _logger.LogInformation(\"CleanArchitecture Domain Event: {DomainEvent}\", notification.GetType().Name);\n\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "src/Application/TodoItems/Queries/GetTodoItemsWithPagination/GetTodoItemsWithPagination.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Interfaces;\nusing CleanArchitecture.Application.Common.Mappings;\nusing CleanArchitecture.Application.Common.Models;\n\nnamespace CleanArchitecture.Application.TodoItems.Queries.GetTodoItemsWithPagination;\n\npublic record GetTodoItemsWithPaginationQuery : IRequest<PaginatedList<TodoItemBriefDto>>\n{\n    public int ListId { get; init; }\n    public int PageNumber { get; init; } = 1;\n    public int PageSize { get; init; } = 10;\n}\n\npublic class GetTodoItemsWithPaginationQueryHandler : IRequestHandler<GetTodoItemsWithPaginationQuery, PaginatedList<TodoItemBriefDto>>\n{\n    private readonly IApplicationDbContext _context;\n    private readonly IMapper _mapper;\n\n    public GetTodoItemsWithPaginationQueryHandler(IApplicationDbContext context, IMapper mapper)\n    {\n        _context = context;\n        _mapper = mapper;\n    }\n\n    public async Task<PaginatedList<TodoItemBriefDto>> Handle(GetTodoItemsWithPaginationQuery request, CancellationToken cancellationToken)\n    {\n        return await _context.TodoItems\n            .Where(x => x.ListId == request.ListId)\n            .OrderBy(x => x.Title)\n            .ProjectTo<TodoItemBriefDto>(_mapper.ConfigurationProvider)\n            .PaginatedListAsync(request.PageNumber, request.PageSize, cancellationToken);\n    }\n}\n"
  },
  {
    "path": "src/Application/TodoItems/Queries/GetTodoItemsWithPagination/GetTodoItemsWithPaginationQueryValidator.cs",
    "content": "﻿namespace CleanArchitecture.Application.TodoItems.Queries.GetTodoItemsWithPagination;\n\npublic class GetTodoItemsWithPaginationQueryValidator : AbstractValidator<GetTodoItemsWithPaginationQuery>\n{\n    public GetTodoItemsWithPaginationQueryValidator()\n    {\n        RuleFor(x => x.ListId)\n            .NotEmpty().WithMessage(\"ListId is required.\");\n\n        RuleFor(x => x.PageNumber)\n            .GreaterThanOrEqualTo(1).WithMessage(\"PageNumber at least greater than or equal to 1.\");\n\n        RuleFor(x => x.PageSize)\n            .GreaterThanOrEqualTo(1).WithMessage(\"PageSize at least greater than or equal to 1.\");\n    }\n}\n"
  },
  {
    "path": "src/Application/TodoItems/Queries/GetTodoItemsWithPagination/TodoItemBriefDto.cs",
    "content": "﻿using CleanArchitecture.Domain.Entities;\n\nnamespace CleanArchitecture.Application.TodoItems.Queries.GetTodoItemsWithPagination;\n\npublic class TodoItemBriefDto\n{\n    public int Id { get; init; }\n\n    public int ListId { get; init; }\n\n    public string? Title { get; init; }\n\n    public bool Done { get; init; }\n\n    private class Mapping : Profile\n    {\n        public Mapping()\n        {\n            CreateMap<TodoItem, TodoItemBriefDto>();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Application/TodoLists/Commands/CreateTodoList/CreateTodoList.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Interfaces;\nusing CleanArchitecture.Domain.Entities;\n\nnamespace CleanArchitecture.Application.TodoLists.Commands.CreateTodoList;\n\npublic record CreateTodoListCommand : IRequest<int>\n{\n    public string? Title { get; init; }\n}\n\npublic class CreateTodoListCommandHandler : IRequestHandler<CreateTodoListCommand, int>\n{\n    private readonly IApplicationDbContext _context;\n\n    public CreateTodoListCommandHandler(IApplicationDbContext context)\n    {\n        _context = context;\n    }\n\n    public async Task<int> Handle(CreateTodoListCommand request, CancellationToken cancellationToken)\n    {\n        var entity = new TodoList();\n\n        entity.Title = request.Title;\n\n        _context.TodoLists.Add(entity);\n\n        await _context.SaveChangesAsync(cancellationToken);\n\n        return entity.Id;\n    }\n}\n"
  },
  {
    "path": "src/Application/TodoLists/Commands/CreateTodoList/CreateTodoListCommandValidator.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Interfaces;\n\nnamespace CleanArchitecture.Application.TodoLists.Commands.CreateTodoList;\n\npublic class CreateTodoListCommandValidator : AbstractValidator<CreateTodoListCommand>\n{\n    private readonly IApplicationDbContext _context;\n\n    public CreateTodoListCommandValidator(IApplicationDbContext context)\n    {\n        _context = context;\n\n        RuleFor(v => v.Title)\n            .NotEmpty()\n            .MaximumLength(200)\n            .MustAsync(BeUniqueTitle)\n                .WithMessage(\"'{PropertyName}' must be unique.\")\n                .WithErrorCode(\"Unique\");\n    }\n\n    public async Task<bool> BeUniqueTitle(string title, CancellationToken cancellationToken)\n    {\n        return !await _context.TodoLists\n            .AnyAsync(l => l.Title == title, cancellationToken);\n    }\n}\n"
  },
  {
    "path": "src/Application/TodoLists/Commands/DeleteTodoList/DeleteTodoList.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Interfaces;\n\nnamespace CleanArchitecture.Application.TodoLists.Commands.DeleteTodoList;\n\npublic record DeleteTodoListCommand(int Id) : IRequest;\n\npublic class DeleteTodoListCommandHandler : IRequestHandler<DeleteTodoListCommand>\n{\n    private readonly IApplicationDbContext _context;\n\n    public DeleteTodoListCommandHandler(IApplicationDbContext context)\n    {\n        _context = context;\n    }\n\n    public async Task Handle(DeleteTodoListCommand request, CancellationToken cancellationToken)\n    {\n        var entity = await _context.TodoLists\n            .Where(l => l.Id == request.Id)\n            .SingleOrDefaultAsync(cancellationToken);\n\n        Guard.Against.NotFound(request.Id, entity);\n\n        _context.TodoLists.Remove(entity);\n\n        await _context.SaveChangesAsync(cancellationToken);\n    }\n}\n"
  },
  {
    "path": "src/Application/TodoLists/Commands/PurgeTodoLists/PurgeTodoLists.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Interfaces;\nusing CleanArchitecture.Application.Common.Security;\nusing CleanArchitecture.Domain.Constants;\n\nnamespace CleanArchitecture.Application.TodoLists.Commands.PurgeTodoLists;\n\n[Authorize(Roles = Roles.Administrator)]\n[Authorize(Policy = Policies.CanPurge)]\npublic record PurgeTodoListsCommand : IRequest;\n\npublic class PurgeTodoListsCommandHandler : IRequestHandler<PurgeTodoListsCommand>\n{\n    private readonly IApplicationDbContext _context;\n\n    public PurgeTodoListsCommandHandler(IApplicationDbContext context)\n    {\n        _context = context;\n    }\n\n    public async Task Handle(PurgeTodoListsCommand request, CancellationToken cancellationToken)\n    {\n        _context.TodoLists.RemoveRange(_context.TodoLists);\n\n        await _context.SaveChangesAsync(cancellationToken);\n    }\n}\n"
  },
  {
    "path": "src/Application/TodoLists/Commands/UpdateTodoList/UpdateTodoList.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Interfaces;\n\nnamespace CleanArchitecture.Application.TodoLists.Commands.UpdateTodoList;\n\npublic record UpdateTodoListCommand : IRequest\n{\n    public int Id { get; init; }\n\n    public string? Title { get; init; }\n}\n\npublic class UpdateTodoListCommandHandler : IRequestHandler<UpdateTodoListCommand>\n{\n    private readonly IApplicationDbContext _context;\n\n    public UpdateTodoListCommandHandler(IApplicationDbContext context)\n    {\n        _context = context;\n    }\n\n    public async Task Handle(UpdateTodoListCommand request, CancellationToken cancellationToken)\n    {\n        var entity = await _context.TodoLists\n            .FindAsync(new object[] { request.Id }, cancellationToken);\n\n        Guard.Against.NotFound(request.Id, entity);\n\n        entity.Title = request.Title;\n\n        await _context.SaveChangesAsync(cancellationToken);\n\n    }\n}\n"
  },
  {
    "path": "src/Application/TodoLists/Commands/UpdateTodoList/UpdateTodoListCommandValidator.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Interfaces;\n\nnamespace CleanArchitecture.Application.TodoLists.Commands.UpdateTodoList;\n\npublic class UpdateTodoListCommandValidator : AbstractValidator<UpdateTodoListCommand>\n{\n    private readonly IApplicationDbContext _context;\n\n    public UpdateTodoListCommandValidator(IApplicationDbContext context)\n    {\n        _context = context;\n\n        RuleFor(v => v.Title)\n            .NotEmpty()\n            .MaximumLength(200)\n            .MustAsync(BeUniqueTitle)\n                .WithMessage(\"'{PropertyName}' must be unique.\")\n                .WithErrorCode(\"Unique\");\n    }\n\n    public async Task<bool> BeUniqueTitle(UpdateTodoListCommand model, string title, CancellationToken cancellationToken)\n    {\n        return !await _context.TodoLists\n            .Where(l => l.Id != model.Id)\n            .AnyAsync(l => l.Title == title, cancellationToken);\n    }\n}\n"
  },
  {
    "path": "src/Application/TodoLists/Queries/GetTodos/GetTodos.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Interfaces;\nusing CleanArchitecture.Application.Common.Models;\nusing CleanArchitecture.Application.Common.Security;\nusing CleanArchitecture.Domain.Enums;\n\nnamespace CleanArchitecture.Application.TodoLists.Queries.GetTodos;\n\n[Authorize]\npublic record GetTodosQuery : IRequest<TodosVm>;\n\npublic class GetTodosQueryHandler : IRequestHandler<GetTodosQuery, TodosVm>\n{\n    private readonly IApplicationDbContext _context;\n    private readonly IMapper _mapper;\n\n    public GetTodosQueryHandler(IApplicationDbContext context, IMapper mapper)\n    {\n        _context = context;\n        _mapper = mapper;\n    }\n\n    public async Task<TodosVm> Handle(GetTodosQuery request, CancellationToken cancellationToken)\n    {\n        return new TodosVm\n        {\n            PriorityLevels = Enum.GetValues(typeof(PriorityLevel))\n                .Cast<PriorityLevel>()\n                .Select(p => new LookupDto { Id = (int)p, Title = p.ToString() })\n                .ToList(),\n\n            Lists = await _context.TodoLists\n                .AsNoTracking()\n                .ProjectTo<TodoListDto>(_mapper.ConfigurationProvider)\n                .OrderBy(t => t.Title)\n                .ToListAsync(cancellationToken)\n        };\n    }\n}\n"
  },
  {
    "path": "src/Application/TodoLists/Queries/GetTodos/TodoItemDto.cs",
    "content": "﻿using CleanArchitecture.Domain.Entities;\n\nnamespace CleanArchitecture.Application.TodoLists.Queries.GetTodos;\n\npublic class TodoItemDto\n{\n    public int Id { get; init; }\n\n    public int ListId { get; init; }\n\n    public string? Title { get; init; }\n\n    public bool Done { get; init; }\n\n    public int Priority { get; init; }\n\n    public string? Note { get; init; }\n\n    private class Mapping : Profile\n    {\n        public Mapping()\n        {\n            CreateMap<TodoItem, TodoItemDto>().ForMember(d => d.Priority, \n                opt => opt.MapFrom(s => (int)s.Priority));\n        }\n    }\n}\n"
  },
  {
    "path": "src/Application/TodoLists/Queries/GetTodos/TodoListDto.cs",
    "content": "﻿using CleanArchitecture.Domain.Entities;\n\nnamespace CleanArchitecture.Application.TodoLists.Queries.GetTodos;\n\npublic class TodoListDto\n{\n    public TodoListDto()\n    {\n        Items = Array.Empty<TodoItemDto>();\n    }\n\n    public int Id { get; init; }\n\n    public string? Title { get; init; }\n\n    public string? Colour { get; init; }\n\n    public IReadOnlyCollection<TodoItemDto> Items { get; init; }\n\n    private class Mapping : Profile\n    {\n        public Mapping()\n        {\n            CreateMap<TodoList, TodoListDto>();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Application/TodoLists/Queries/GetTodos/TodosVm.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Models;\n\nnamespace CleanArchitecture.Application.TodoLists.Queries.GetTodos;\n\npublic class TodosVm\n{\n    public IReadOnlyCollection<LookupDto> PriorityLevels { get; init; } = Array.Empty<LookupDto>();\n\n    public IReadOnlyCollection<TodoListDto> Lists { get; init; } = Array.Empty<TodoListDto>();\n}\n"
  },
  {
    "path": "src/Application/WeatherForecasts/Queries/GetWeatherForecasts/GetWeatherForecastsQuery.cs",
    "content": "﻿namespace CleanArchitecture.Application.WeatherForecasts.Queries.GetWeatherForecasts;\n\npublic record GetWeatherForecastsQuery : IRequest<IEnumerable<WeatherForecast>>;\n\npublic class GetWeatherForecastsQueryHandler : IRequestHandler<GetWeatherForecastsQuery, IEnumerable<WeatherForecast>>\n{\n    private static readonly string[] Summaries = new[]\n    {\n        \"Freezing\", \"Bracing\", \"Chilly\", \"Cool\", \"Mild\", \"Warm\", \"Balmy\", \"Hot\", \"Sweltering\", \"Scorching\"\n    };\n\n#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously\n    public async Task<IEnumerable<WeatherForecast>> Handle(GetWeatherForecastsQuery request, CancellationToken cancellationToken)\n#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously\n    {\n        var rng = new Random();\n\n        return Enumerable.Range(1, 5).Select(index => new WeatherForecast\n        {\n            Date = DateTime.Now.AddDays(index),\n            TemperatureC = rng.Next(-20, 55),\n            Summary = Summaries[rng.Next(Summaries.Length)]\n        });\n    }\n}\n"
  },
  {
    "path": "src/Application/WeatherForecasts/Queries/GetWeatherForecasts/WeatherForecast.cs",
    "content": "﻿namespace CleanArchitecture.Application.WeatherForecasts.Queries.GetWeatherForecasts;\n\npublic class WeatherForecast\n{\n    public DateTime Date { get; init; }\n\n    public int TemperatureC { get; init; }\n\n    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);\n\n    public string Summary { get; init; } = string.Empty;\n}\n"
  },
  {
    "path": "src/Domain/Common/BaseAuditableEntity.cs",
    "content": "﻿namespace CleanArchitecture.Domain.Common;\n\npublic abstract class BaseAuditableEntity : BaseEntity\n{\n    public DateTimeOffset Created { get; set; }\n\n    public string? CreatedBy { get; set; }\n\n    public DateTimeOffset LastModified { get; set; }\n\n    public string? LastModifiedBy { get; set; }\n}\n"
  },
  {
    "path": "src/Domain/Common/BaseEntity.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations.Schema;\n\nnamespace CleanArchitecture.Domain.Common;\n\npublic abstract class BaseEntity\n{\n    // This can easily be modified to be BaseEntity<T> and public T Id to support different key types.\n    // Using non-generic integer types for simplicity\n    public int Id { get; set; }\n\n    private readonly List<BaseEvent> _domainEvents = new();\n\n    [NotMapped]\n    public IReadOnlyCollection<BaseEvent> DomainEvents => _domainEvents.AsReadOnly();\n\n    public void AddDomainEvent(BaseEvent domainEvent)\n    {\n        _domainEvents.Add(domainEvent);\n    }\n\n    public void RemoveDomainEvent(BaseEvent domainEvent)\n    {\n        _domainEvents.Remove(domainEvent);\n    }\n\n    public void ClearDomainEvents()\n    {\n        _domainEvents.Clear();\n    }\n}\n"
  },
  {
    "path": "src/Domain/Common/BaseEvent.cs",
    "content": "﻿using MediatR;\n\nnamespace CleanArchitecture.Domain.Common;\n\npublic abstract class BaseEvent : INotification\n{\n}\n"
  },
  {
    "path": "src/Domain/Common/ValueObject.cs",
    "content": "﻿namespace CleanArchitecture.Domain.Common;\n\n// Learn more: https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/implement-value-objects\npublic abstract class ValueObject\n{\n    protected static bool EqualOperator(ValueObject left, ValueObject right)\n    {\n        if (left is null ^ right is null)\n        {\n            return false;\n        }\n\n        return left?.Equals(right!) != false;\n    }\n\n    protected static bool NotEqualOperator(ValueObject left, ValueObject right)\n    {\n        return !(EqualOperator(left, right));\n    }\n\n    protected abstract IEnumerable<object> GetEqualityComponents();\n\n    public override bool Equals(object? obj)\n    {\n        if (obj == null || obj.GetType() != GetType())\n        {\n            return false;\n        }\n\n        var other = (ValueObject)obj;\n        return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());\n    }\n\n    public override int GetHashCode()\n    {\n        var hash = new HashCode();\n\n        foreach (var component in GetEqualityComponents())\n        {\n            hash.Add(component);\n        }\n\n        return hash.ToHashCode();\n    }\n\n    public static bool operator ==(ValueObject left, ValueObject right)\n    {\n        return EqualOperator(left, right);\n    }\n\n    public static bool operator !=(ValueObject left, ValueObject right)\n    {\n        return NotEqualOperator(left, right);\n    }\n}\n"
  },
  {
    "path": "src/Domain/Constants/Policies.cs",
    "content": "﻿namespace CleanArchitecture.Domain.Constants;\n\npublic abstract class Policies\n{\n    public const string CanPurge = nameof(CanPurge);\n}"
  },
  {
    "path": "src/Domain/Constants/Roles.cs",
    "content": "﻿namespace CleanArchitecture.Domain.Constants;\n\npublic abstract class Roles\n{\n    public const string Administrator = nameof(Administrator);\n}"
  },
  {
    "path": "src/Domain/Domain.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\r\n\r\n  <PropertyGroup>\r\n    <RootNamespace>CleanArchitecture.Domain</RootNamespace>\r\n    <AssemblyName>CleanArchitecture.Domain</AssemblyName>\r\n  </PropertyGroup>\r\n\r\n  <ItemGroup>\r\n    <PackageReference Include=\"MediatR.Contracts\" />\r\n  </ItemGroup>\r\n\r\n</Project>\r\n"
  },
  {
    "path": "src/Domain/Entities/TodoItem.cs",
    "content": "﻿namespace CleanArchitecture.Domain.Entities;\n\npublic class TodoItem : BaseAuditableEntity\n{\n    public int ListId { get; set; }\n\n    public string? Title { get; set; }\n\n    public string? Note { get; set; }\n\n    public PriorityLevel Priority { get; set; }\n\n    public DateTime? Reminder { get; set; }\n\n    private bool _done;\n    public bool Done\n    {\n        get => _done;\n        set\n        {\n            if (value && !_done)\n            {\n                AddDomainEvent(new TodoItemCompletedEvent(this));\n            }\n\n            _done = value;\n        }\n    }\n\n    public TodoList List { get; set; } = null!;\n}\n"
  },
  {
    "path": "src/Domain/Entities/TodoList.cs",
    "content": "﻿namespace CleanArchitecture.Domain.Entities;\n\npublic class TodoList : BaseAuditableEntity\n{\n    public string? Title { get; set; }\n\n    public Colour Colour { get; set; } = Colour.White;\n\n    public IList<TodoItem> Items { get; private set; } = new List<TodoItem>();\n}\n"
  },
  {
    "path": "src/Domain/Enums/PriorityLevel.cs",
    "content": "﻿namespace CleanArchitecture.Domain.Enums;\n\npublic enum PriorityLevel\n{\n    None = 0,\n    Low = 1,\n    Medium = 2,\n    High = 3\n}\n"
  },
  {
    "path": "src/Domain/Events/TodoItemCompletedEvent.cs",
    "content": "﻿namespace CleanArchitecture.Domain.Events;\n\npublic class TodoItemCompletedEvent : BaseEvent\n{\n    public TodoItemCompletedEvent(TodoItem item)\n    {\n        Item = item;\n    }\n\n    public TodoItem Item { get; }\n}\n"
  },
  {
    "path": "src/Domain/Events/TodoItemCreatedEvent.cs",
    "content": "﻿namespace CleanArchitecture.Domain.Events;\n\npublic class TodoItemCreatedEvent : BaseEvent\n{\n    public TodoItemCreatedEvent(TodoItem item)\n    {\n        Item = item;\n    }\n\n    public TodoItem Item { get; }\n}\n"
  },
  {
    "path": "src/Domain/Events/TodoItemDeletedEvent.cs",
    "content": "﻿namespace CleanArchitecture.Domain.Events;\n\npublic class TodoItemDeletedEvent : BaseEvent\n{\n    public TodoItemDeletedEvent(TodoItem item)\n    {\n        Item = item;\n    }\n\n    public TodoItem Item { get; }\n}\n"
  },
  {
    "path": "src/Domain/Exceptions/UnsupportedColourException.cs",
    "content": "﻿namespace CleanArchitecture.Domain.Exceptions;\n\npublic class UnsupportedColourException : Exception\n{\n    public UnsupportedColourException(string code)\n        : base($\"Colour \\\"{code}\\\" is unsupported.\")\n    {\n    }\n}\n"
  },
  {
    "path": "src/Domain/GlobalUsings.cs",
    "content": "﻿global using CleanArchitecture.Domain.Common;\nglobal using CleanArchitecture.Domain.Entities;\nglobal using CleanArchitecture.Domain.Enums;\nglobal using CleanArchitecture.Domain.Events;\nglobal using CleanArchitecture.Domain.Exceptions;\nglobal using CleanArchitecture.Domain.ValueObjects;"
  },
  {
    "path": "src/Domain/ValueObjects/Colour.cs",
    "content": "﻿namespace CleanArchitecture.Domain.ValueObjects;\n\npublic class Colour(string code) : ValueObject\n{\n    public static Colour From(string code)\n    {\n        var colour = new Colour(code);\n\n        if (!SupportedColours.Contains(colour))\n        {\n            throw new UnsupportedColourException(code);\n        }\n\n        return colour;\n    }\n\n    public static Colour White => new(\"#FFFFFF\");\n\n    public static Colour Red => new(\"#FF5733\");\n\n    public static Colour Orange => new(\"#FFC300\");\n\n    public static Colour Yellow => new(\"#FFFF66\");\n\n    public static Colour Green => new(\"#CCFF99\");\n\n    public static Colour Blue => new(\"#6666FF\");\n\n    public static Colour Purple => new(\"#9966CC\");\n\n    public static Colour Grey => new(\"#999999\");\n\n    public string Code { get; private set; } = string.IsNullOrWhiteSpace(code)?\"#000000\":code;\n\n    public static implicit operator string(Colour colour)\n    {\n        return colour.ToString();\n    }\n\n    public static explicit operator Colour(string code)\n    {\n        return From(code);\n    }\n\n    public override string ToString()\n    {\n        return Code;\n    }\n\n    protected static IEnumerable<Colour> SupportedColours\n    {\n        get\n        {\n            yield return White;\n            yield return Red;\n            yield return Orange;\n            yield return Yellow;\n            yield return Green;\n            yield return Blue;\n            yield return Purple;\n            yield return Grey;\n        }\n    }\n\n    protected override IEnumerable<object> GetEqualityComponents()\n    {\n        yield return Code;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure/Data/ApplicationDbContext.cs",
    "content": "﻿using System.Reflection;\nusing CleanArchitecture.Application.Common.Interfaces;\nusing CleanArchitecture.Domain.Entities;\nusing CleanArchitecture.Infrastructure.Identity;\nusing Microsoft.AspNetCore.Identity.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore;\n\nnamespace CleanArchitecture.Infrastructure.Data;\n\npublic class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IApplicationDbContext\n{\n    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }\n\n    public DbSet<TodoList> TodoLists => Set<TodoList>();\n\n    public DbSet<TodoItem> TodoItems => Set<TodoItem>();\n\n    protected override void OnModelCreating(ModelBuilder builder)\n    {\n        base.OnModelCreating(builder);\n        builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure/Data/ApplicationDbContextInitialiser.cs",
    "content": "﻿using CleanArchitecture.Domain.Constants;\nusing CleanArchitecture.Domain.Entities;\nusing CleanArchitecture.Infrastructure.Identity;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\n\nnamespace CleanArchitecture.Infrastructure.Data;\n\npublic static class InitialiserExtensions\n{\n    public static async Task InitialiseDatabaseAsync(this WebApplication app)\n    {\n        using var scope = app.Services.CreateScope();\n\n        var initialiser = scope.ServiceProvider.GetRequiredService<ApplicationDbContextInitialiser>();\n\n        await initialiser.InitialiseAsync();\n        await initialiser.SeedAsync();\n    }\n}\n\npublic class ApplicationDbContextInitialiser\n{\n    private readonly ILogger<ApplicationDbContextInitialiser> _logger;\n    private readonly ApplicationDbContext _context;\n    private readonly UserManager<ApplicationUser> _userManager;\n    private readonly RoleManager<IdentityRole> _roleManager;\n\n    public ApplicationDbContextInitialiser(ILogger<ApplicationDbContextInitialiser> logger, ApplicationDbContext context, UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager)\n    {\n        _logger = logger;\n        _context = context;\n        _userManager = userManager;\n        _roleManager = roleManager;\n    }\n\n    public async Task InitialiseAsync()\n    {\n        try\n        {\n            // See https://jasontaylor.dev/ef-core-database-initialisation-strategies\n            await _context.Database.EnsureDeletedAsync();\n            await _context.Database.EnsureCreatedAsync();\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(ex, \"An error occurred while initialising the database.\");\n            throw;\n        }\n    }\n\n    public async Task SeedAsync()\n    {\n        try\n        {\n            await TrySeedAsync();\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(ex, \"An error occurred while seeding the database.\");\n            throw;\n        }\n    }\n\n    public async Task TrySeedAsync()\n    {\n        // Default roles\n        var administratorRole = new IdentityRole(Roles.Administrator);\n\n        if (_roleManager.Roles.All(r => r.Name != administratorRole.Name))\n        {\n            await _roleManager.CreateAsync(administratorRole);\n        }\n\n        // Default users\n        var administrator = new ApplicationUser { UserName = \"administrator@localhost\", Email = \"administrator@localhost\" };\n\n        if (_userManager.Users.All(u => u.UserName != administrator.UserName))\n        {\n            await _userManager.CreateAsync(administrator, \"Administrator1!\");\n            if (!string.IsNullOrWhiteSpace(administratorRole.Name))\n            {\n                await _userManager.AddToRolesAsync(administrator, new [] { administratorRole.Name });\n            }\n        }\n\n        // Default data\n        // Seed, if necessary\n        if (!_context.TodoLists.Any())\n        {\n            _context.TodoLists.Add(new TodoList\n            {\n                Title = \"Todo List\",\n                Items =\n                {\n                    new TodoItem { Title = \"Make a todo list 📃\" },\n                    new TodoItem { Title = \"Check off the first item ✅\" },\n                    new TodoItem { Title = \"Realise you've already done two things on the list! 🤯\"},\n                    new TodoItem { Title = \"Reward yourself with a nice, long nap 🏆\" },\n                }\n            });\n\n            await _context.SaveChangesAsync();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure/Data/Configurations/TodoItemConfiguration.cs",
    "content": "﻿using CleanArchitecture.Domain.Entities;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace CleanArchitecture.Infrastructure.Data.Configurations;\n\npublic class TodoItemConfiguration : IEntityTypeConfiguration<TodoItem>\n{\n    public void Configure(EntityTypeBuilder<TodoItem> builder)\n    {\n        builder.Property(t => t.Title)\n            .HasMaxLength(200)\n            .IsRequired();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure/Data/Configurations/TodoListConfiguration.cs",
    "content": "﻿using CleanArchitecture.Domain.Entities;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace CleanArchitecture.Infrastructure.Data.Configurations;\n\npublic class TodoListConfiguration : IEntityTypeConfiguration<TodoList>\n{\n    public void Configure(EntityTypeBuilder<TodoList> builder)\n    {\n        builder.Property(t => t.Title)\n            .HasMaxLength(200)\n            .IsRequired();\n\n        builder\n            .OwnsOne(b => b.Colour);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure/Data/Interceptors/AuditableEntityInterceptor.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Interfaces;\nusing CleanArchitecture.Domain.Common;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.ChangeTracking;\nusing Microsoft.EntityFrameworkCore.Diagnostics;\n\nnamespace CleanArchitecture.Infrastructure.Data.Interceptors;\n\npublic class AuditableEntityInterceptor : SaveChangesInterceptor\n{\n    private readonly IUser _user;\n    private readonly TimeProvider _dateTime;\n\n    public AuditableEntityInterceptor(\n        IUser user,\n        TimeProvider dateTime)\n    {\n        _user = user;\n        _dateTime = dateTime;\n    }\n\n    public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)\n    {\n        UpdateEntities(eventData.Context);\n\n        return base.SavingChanges(eventData, result);\n    }\n\n    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)\n    {\n        UpdateEntities(eventData.Context);\n\n        return base.SavingChangesAsync(eventData, result, cancellationToken);\n    }\n\n    public void UpdateEntities(DbContext? context)\n    {\n        if (context == null) return;\n\n        foreach (var entry in context.ChangeTracker.Entries<BaseAuditableEntity>())\n        {\n            if (entry.State is EntityState.Added or EntityState.Modified || entry.HasChangedOwnedEntities())\n            {\n                var utcNow = _dateTime.GetUtcNow();\n                if (entry.State == EntityState.Added)\n                {\n                    entry.Entity.CreatedBy = _user.Id;\n                    entry.Entity.Created = utcNow;\n                } \n                entry.Entity.LastModifiedBy = _user.Id;\n                entry.Entity.LastModified = utcNow;\n            }\n        }\n    }\n}\n\npublic static class Extensions\n{\n    public static bool HasChangedOwnedEntities(this EntityEntry entry) =>\n        entry.References.Any(r => \n            r.TargetEntry != null && \n            r.TargetEntry.Metadata.IsOwned() && \n            (r.TargetEntry.State == EntityState.Added || r.TargetEntry.State == EntityState.Modified));\n}\n"
  },
  {
    "path": "src/Infrastructure/Data/Interceptors/DispatchDomainEventsInterceptor.cs",
    "content": "﻿using CleanArchitecture.Domain.Common;\nusing MediatR;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Diagnostics;\n\nnamespace CleanArchitecture.Infrastructure.Data.Interceptors;\n\npublic class DispatchDomainEventsInterceptor : SaveChangesInterceptor\n{\n    private readonly IMediator _mediator;\n\n    public DispatchDomainEventsInterceptor(IMediator mediator)\n    {\n        _mediator = mediator;\n    }\n\n    public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)\n    {\n        DispatchDomainEvents(eventData.Context).GetAwaiter().GetResult();\n\n        return base.SavingChanges(eventData, result);\n\n    }\n\n    public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)\n    {\n        await DispatchDomainEvents(eventData.Context);\n\n        return await base.SavingChangesAsync(eventData, result, cancellationToken);\n    }\n\n    public async Task DispatchDomainEvents(DbContext? context)\n    {\n        if (context == null) return;\n\n        var entities = context.ChangeTracker\n            .Entries<BaseEntity>()\n            .Where(e => e.Entity.DomainEvents.Any())\n            .Select(e => e.Entity);\n\n        var domainEvents = entities\n            .SelectMany(e => e.DomainEvents)\n            .ToList();\n\n        entities.ToList().ForEach(e => e.ClearDomainEvents());\n\n        foreach (var domainEvent in domainEvents)\n            await _mediator.Publish(domainEvent);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure/DependencyInjection.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Interfaces;\r\nusing CleanArchitecture.Domain.Constants;\r\nusing CleanArchitecture.Infrastructure.Data;\r\nusing CleanArchitecture.Infrastructure.Data.Interceptors;\r\nusing CleanArchitecture.Infrastructure.Identity;\r\nusing Microsoft.AspNetCore.Identity;\r\nusing Microsoft.EntityFrameworkCore;\r\nusing Microsoft.EntityFrameworkCore.Diagnostics;\r\nusing Microsoft.Extensions.Configuration;\r\nusing Microsoft.Extensions.Hosting;\r\n\r\nnamespace Microsoft.Extensions.DependencyInjection;\r\n\r\npublic static class DependencyInjection\r\n{\r\n    public static void AddInfrastructureServices(this IHostApplicationBuilder builder)\r\n    {\r\n        var connectionString = builder.Configuration.GetConnectionString(Services.Database);\r\n        Guard.Against.Null(connectionString, message: $\"Connection string '{Services.Database}' not found.\");\r\n\r\n        builder.Services.AddScoped<ISaveChangesInterceptor, AuditableEntityInterceptor>();\r\n        builder.Services.AddScoped<ISaveChangesInterceptor, DispatchDomainEventsInterceptor>();\r\n\r\n        builder.Services.AddDbContext<ApplicationDbContext>((sp, options) =>\r\n        {\r\n            options.AddInterceptors(sp.GetServices<ISaveChangesInterceptor>());\r\n#if (UsePostgreSQL)\r\n            options.UseNpgsql(connectionString);\r\n#elif (UseSqlServer)\r\n            options.UseSqlServer(connectionString);\r\n#else\r\n            options.UseSqlite(connectionString);\r\n#endif\r\n            options.ConfigureWarnings(warnings => warnings.Ignore(RelationalEventId.PendingModelChangesWarning));\r\n        });\r\n\r\n#if UsePostgreSQL\r\n        builder.EnrichNpgsqlDbContext<ApplicationDbContext>();\r\n#elif (UseSqlServer)\r\n        builder.EnrichSqlServerDbContext<ApplicationDbContext>();\r\n#endif\r\n\r\n        builder.Services.AddScoped<IApplicationDbContext>(provider => provider.GetRequiredService<ApplicationDbContext>());\r\n\r\n        builder.Services.AddScoped<ApplicationDbContextInitialiser>();\r\n\r\n#if (UseApiOnly)\r\n        builder.Services.AddAuthentication()\r\n            .AddBearerToken(IdentityConstants.BearerScheme);\r\n\r\n        builder.Services.AddAuthorizationBuilder();\r\n\r\n        builder.Services\r\n            .AddIdentityCore<ApplicationUser>()\r\n            .AddRoles<IdentityRole>()\r\n            .AddEntityFrameworkStores<ApplicationDbContext>()\r\n            .AddApiEndpoints();\r\n#else\r\n        builder.Services.AddAuthentication(options =>\r\n            {\r\n                options.DefaultScheme = IdentityConstants.ApplicationScheme;\r\n                options.DefaultSignInScheme = IdentityConstants.ExternalScheme;\r\n            })\r\n            .AddIdentityCookies();\r\n\r\n        builder.Services.AddAuthorizationBuilder();\r\n\r\n        builder.Services\r\n            .AddIdentityCore<ApplicationUser>()\r\n            .AddRoles<IdentityRole>()\r\n            .AddEntityFrameworkStores<ApplicationDbContext>()\r\n            .AddSignInManager()\r\n            .AddDefaultTokenProviders()\r\n            .AddApiEndpoints();\r\n#endif\r\n\r\n        builder.Services.AddSingleton(TimeProvider.System);\r\n        builder.Services.AddTransient<IIdentityService, IdentityService>();\r\n\r\n        builder.Services.AddAuthorization(options =>\r\n            options.AddPolicy(Policies.CanPurge, policy => policy.RequireRole(Roles.Administrator)));\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/Infrastructure/GlobalUsings.cs",
    "content": "﻿global using Ardalis.GuardClauses;\nglobal using CleanArchitecture.Shared;"
  },
  {
    "path": "src/Infrastructure/Identity/ApplicationUser.cs",
    "content": "﻿using Microsoft.AspNetCore.Identity;\n\nnamespace CleanArchitecture.Infrastructure.Identity;\n\npublic class ApplicationUser : IdentityUser\n{\n}\n"
  },
  {
    "path": "src/Infrastructure/Identity/IdentityResultExtensions.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Models;\nusing Microsoft.AspNetCore.Identity;\n\nnamespace CleanArchitecture.Infrastructure.Identity;\n\npublic static class IdentityResultExtensions\n{\n    public static Result ToApplicationResult(this IdentityResult result)\n    {\n        return result.Succeeded\n            ? Result.Success()\n            : Result.Failure(result.Errors.Select(e => e.Description));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure/Identity/IdentityService.cs",
    "content": "using CleanArchitecture.Application.Common.Interfaces;\nusing CleanArchitecture.Application.Common.Models;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.EntityFrameworkCore;\n\nnamespace CleanArchitecture.Infrastructure.Identity;\n\npublic class IdentityService : IIdentityService\n{\n    private readonly UserManager<ApplicationUser> _userManager;\n    private readonly IUserClaimsPrincipalFactory<ApplicationUser> _userClaimsPrincipalFactory;\n    private readonly IAuthorizationService _authorizationService;\n\n    public IdentityService(\n        UserManager<ApplicationUser> userManager,\n        IUserClaimsPrincipalFactory<ApplicationUser> userClaimsPrincipalFactory,\n        IAuthorizationService authorizationService)\n    {\n        _userManager = userManager;\n        _userClaimsPrincipalFactory = userClaimsPrincipalFactory;\n        _authorizationService = authorizationService;\n    }\n\n    public async Task<string?> GetUserNameAsync(string userId)\n    {\n        var user = await _userManager.FindByIdAsync(userId);\n\n        return user?.UserName;\n    }\n\n    public async Task<(Result Result, string UserId)> CreateUserAsync(string userName, string password)\n    {\n        var user = new ApplicationUser\n        {\n            UserName = userName,\n            Email = userName,\n        };\n\n        var result = await _userManager.CreateAsync(user, password);\n\n        return (result.ToApplicationResult(), user.Id);\n    }\n\n    public async Task<bool> IsInRoleAsync(string userId, string role)\n    {\n        var user = await _userManager.FindByIdAsync(userId);\n\n        return user != null && await _userManager.IsInRoleAsync(user, role);\n    }\n\n    public async Task<bool> AuthorizeAsync(string userId, string policyName)\n    {\n        var user = await _userManager.FindByIdAsync(userId);\n\n        if (user == null)\n        {\n            return false;\n        }\n\n        var principal = await _userClaimsPrincipalFactory.CreateAsync(user);\n\n        var result = await _authorizationService.AuthorizeAsync(principal, policyName);\n\n        return result.Succeeded;\n    }\n\n    public async Task<Result> DeleteUserAsync(string userId)\n    {\n        var user = await _userManager.FindByIdAsync(userId);\n\n        return user != null ? await DeleteUserAsync(user) : Result.Success();\n    }\n\n    public async Task<Result> DeleteUserAsync(ApplicationUser user)\n    {\n        var result = await _userManager.DeleteAsync(user);\n\n        return result.ToApplicationResult();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure/Infrastructure.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\r\n\r\n  <PropertyGroup>\r\n    <RootNamespace>CleanArchitecture.Infrastructure</RootNamespace>\r\n    <AssemblyName>CleanArchitecture.Infrastructure</AssemblyName>\r\n  </PropertyGroup>\r\n\r\n  <ItemGroup>\r\n    <PackageReference Include=\"Microsoft.EntityFrameworkCore\" />\r\n    <!--#if (UseSqlServer)-->\r\n    <PackageReference Include=\"Aspire.Microsoft.EntityFrameworkCore.SqlServer\" />\r\n    <!--#endif-->\r\n    <!--#if (UsePostgreSQL)-->\r\n    <PackageReference Include=\"Aspire.Npgsql.EntityFrameworkCore.PostgreSQL\" />\r\n    <!--#endif-->\r\n    <PackageReference Include=\"Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore\" />\r\n    <PackageReference Include=\"Microsoft.AspNetCore.Identity.EntityFrameworkCore\" />\r\n    <!--#if (UsePostgreSQL)-->\r\n    <PackageReference Include=\"Npgsql.EntityFrameworkCore.PostgreSQL\" />\r\n    <!--#endif-->\r\n    <!--#if (UseSqlite)-->\r\n    <PackageReference Include=\"Microsoft.EntityFrameworkCore.Sqlite\" />\r\n    <!--#endif-->\r\n  </ItemGroup>\r\n\r\n  <ItemGroup>\r\n    <ProjectReference Include=\"..\\Application\\Application.csproj\" />\r\n    <ProjectReference Include=\"..\\Shared\\Shared.csproj\" />\r\n  </ItemGroup>\r\n\r\n</Project>\r\n"
  },
  {
    "path": "src/ServiceDefaults/Extensions.cs",
    "content": "using Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Diagnostics.HealthChecks;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Diagnostics.HealthChecks;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.ServiceDiscovery;\nusing OpenTelemetry;\nusing OpenTelemetry.Metrics;\nusing OpenTelemetry.Trace;\n\nnamespace Microsoft.Extensions.Hosting;\n\n// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry.\n// This project should be referenced by each service project in your solution.\n// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults\npublic static class Extensions\n{\n    public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder\n    {\n        builder.ConfigureOpenTelemetry();\n\n        builder.AddDefaultHealthChecks();\n\n        builder.Services.AddServiceDiscovery();\n\n        builder.Services.ConfigureHttpClientDefaults(http =>\n        {\n            // Turn on resilience by default\n            http.AddStandardResilienceHandler();\n\n            // Turn on service discovery by default\n            http.AddServiceDiscovery();\n        });\n\n        // Uncomment the following to restrict the allowed schemes for service discovery.\n        // builder.Services.Configure<ServiceDiscoveryOptions>(options =>\n        // {\n        //     options.AllowedSchemes = [\"https\"];\n        // });\n\n        return builder;\n    }\n\n    public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder\n    {\n        builder.Logging.AddOpenTelemetry(logging =>\n        {\n            logging.IncludeFormattedMessage = true;\n            logging.IncludeScopes = true;\n        });\n\n        builder.Services.AddOpenTelemetry()\n            .WithMetrics(metrics =>\n            {\n                metrics.AddAspNetCoreInstrumentation()\n                    .AddHttpClientInstrumentation()\n                    .AddRuntimeInstrumentation();\n            })\n            .WithTracing(tracing =>\n            {\n                tracing.AddSource(builder.Environment.ApplicationName)\n                    .AddAspNetCoreInstrumentation()\n                    // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)\n                    //.AddGrpcClientInstrumentation()\n                    .AddHttpClientInstrumentation();\n            });\n\n        builder.AddOpenTelemetryExporters();\n\n        return builder;\n    }\n\n    private static TBuilder AddOpenTelemetryExporters<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder\n    {\n        var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration[\"OTEL_EXPORTER_OTLP_ENDPOINT\"]);\n\n        if (useOtlpExporter)\n        {\n            builder.Services.AddOpenTelemetry().UseOtlpExporter();\n        }\n\n        // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)\n        //if (!string.IsNullOrEmpty(builder.Configuration[\"APPLICATIONINSIGHTS_CONNECTION_STRING\"]))\n        //{\n        //    builder.Services.AddOpenTelemetry()\n        //       .UseAzureMonitor();\n        //}\n\n        return builder;\n    }\n\n    public static TBuilder AddDefaultHealthChecks<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder\n    {\n        builder.Services.AddHealthChecks()\n            // Add a default liveness check to ensure app is responsive\n            .AddCheck(\"self\", () => HealthCheckResult.Healthy(), [\"live\"]);\n\n        return builder;\n    }\n\n    public static WebApplication MapDefaultEndpoints(this WebApplication app)\n    {\n        // Adding health checks endpoints to applications in non-development environments has security implications.\n        // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.\n        if (app.Environment.IsDevelopment())\n        {\n            // All health checks must pass for app to be considered ready to accept traffic after starting\n            app.MapHealthChecks(\"/health\");\n\n            // Only health checks tagged with the \"live\" tag must pass for app to be considered alive\n            app.MapHealthChecks(\"/alive\", new HealthCheckOptions\n            {\n                Predicate = r => r.Tags.Contains(\"live\")\n            });\n        }\n\n        return app;\n    }\n}\n"
  },
  {
    "path": "src/ServiceDefaults/ServiceDefaults.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <IsAspireSharedProject>true</IsAspireSharedProject>\n    <RootNamespace>CleanArchitecture.ServiceDefaults</RootNamespace>\n    <AssemblyName>CleanArchitecture.ServiceDefaults</AssemblyName>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <FrameworkReference Include=\"Microsoft.AspNetCore.App\" />\n    \n    <PackageReference Include=\"Microsoft.Extensions.Http.Resilience\" />\n    <PackageReference Include=\"Microsoft.Extensions.ServiceDiscovery\" />\n    <PackageReference Include=\"OpenTelemetry.Exporter.OpenTelemetryProtocol\" />\n    <PackageReference Include=\"OpenTelemetry.Extensions.Hosting\" />\n    <PackageReference Include=\"OpenTelemetry.Instrumentation.AspNetCore\" />\n    <PackageReference Include=\"OpenTelemetry.Instrumentation.Http\" />\n    <PackageReference Include=\"OpenTelemetry.Instrumentation.Runtime\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Shared/Services.cs",
    "content": "﻿namespace CleanArchitecture.Shared;\n\npublic static class Services\n{\n    /// <summary>\n    /// The name of the Web Frontend service.\n    /// This service is responsible for hosting the frontend application.\n    /// </summary>\n    public const string WebFrontend = \"webfrontend\";\n\n    /// <summary>\n    /// The name of the Web API service.\n    /// This service is responsible for hosting the Web API application.\n    /// </summary>\n    public const string WebApi = \"webapi\";\n\n    /// <summary>\n    /// The name of the Database Server service.\n    /// This service is responsible for hosting the database server (e.g., PostgreSQL, SQL Server, or SQLite).\n    /// </summary>\n    public const string DatabaseServer = \"dbserver\";\n\n    /// <summary>\n    /// The name of the Database.\n    /// This is the name of the database that will be created and used by the application.\n    /// </summary>\n    public const string Database = \"CleanArchitectureDb\";\n}\n"
  },
  {
    "path": "src/Shared/Shared.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <RootNamespace>CleanArchitecture.Shared</RootNamespace>\n    <AssemblyName>CleanArchitecture.Shared</AssemblyName>\n  </PropertyGroup>\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Web/ClientApp/.editorconfig",
    "content": "# Editor configuration, see http://editorconfig.org\nroot = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.ts]\nquote_type = single\n\n[*.md]\nmax_line_length = off\ntrim_trailing_whitespace = false\n\n[*.{razor,cshtml}]\ncharset = utf-8-bom\n"
  },
  {
    "path": "src/Web/ClientApp/.gitignore",
    "content": "# See http://help.github.com/ignore-files/ for more about ignoring files.\n\n# compiled output\n/dist\n/dist-server\n/tmp\n/out-tsc\n/.angular\n\n# dependencies\n/node_modules\n\n# IDEs and editors\n/.idea\n.project\n.classpath\n.c9/\n*.launch\n.settings/\n*.sublime-workspace\n\n# IDE - VSCode\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n\n# misc\n/.sass-cache\n/connect.lock\n/coverage\n/libpeerconnection.log\nnpm-debug.log\nyarn-error.log\ntestem.log\n/typings\n\n# System Files\n.DS_Store\nThumbs.db\n"
  },
  {
    "path": "src/Web/ClientApp/Dockerfile",
    "content": "FROM node:24 as build\n\nWORKDIR /app\n\nCOPY package.json package.json\nCOPY package-lock.json package-lock.json\n\nRUN npm install\n\nCOPY . .\n\nRUN npm run build\n\nFROM nginx:alpine\n\nCOPY --from=build /app/default.conf.template /etc/nginx/templates/default.conf.template\nCOPY --from=build /app/dist/weather/browser /usr/share/nginx/html\n\n# Expose the default nginx port\nEXPOSE 80\n\nCMD [\"nginx\", \"-g\", \"daemon off;\"]"
  },
  {
    "path": "src/Web/ClientApp/README.md",
    "content": "# CleanArchitecture.Web\n\nThis project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 21.1.5.\n\n## Development server\n\nRun `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.\n\n## Code scaffolding\n\nRun `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.\n\n## Build\n\nRun `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.\n\n## Running unit tests\n\nRun `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).\n\n## Running end-to-end tests\n\nRun `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.\n\n## Further help\n\nTo get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.\n"
  },
  {
    "path": "src/Web/ClientApp/angular.json",
    "content": "{\n  \"$schema\": \"./node_modules/@angular/cli/lib/config/schema.json\",\n  \"version\": 1,\n  \"newProjectRoot\": \"projects\",\n  \"projects\": {\n    \"CleanArchitecture.Web\": {\n      \"projectType\": \"application\",\n      \"schematics\": {\n        \"@schematics/angular:application\": {\n          \"strict\": true\n        }\n      },\n      \"root\": \"\",\n      \"sourceRoot\": \"src\",\n      \"prefix\": \"app\",\n      \"architect\": {\n        \"build\": {\n          \"builder\": \"@angular-devkit/build-angular:application\",\n          \"options\": {\n            \"outputPath\": \"dist\",\n            \"index\": \"src/index.html\",\n            \"browser\": \"src/main.ts\",\n            \"polyfills\": [\n              \"zone.js\"\n            ],\n            \"tsConfig\": \"tsconfig.app.json\",\n            \"allowedCommonJsDependencies\": [\n              \"oidc-client\"\n            ],\n            \"assets\": [\n              \"src/assets\",\n              {\n                \"glob\": \"favicon.png\",\n                \"input\": \"src\",\n                \"output\": \"/\"\n              }\n            ],\n            \"styles\": [\n              \"src/styles.scss\"\n            ],\n            \"scripts\": []\n          },\n          \"configurations\": {\n            \"production\": {\n              \"budgets\": [\n                {\n                  \"type\": \"initial\",\n                  \"maximumWarning\": \"1mb\",\n                  \"maximumError\": \"5mb\"\n                },\n                {\n                  \"type\": \"anyComponentStyle\",\n                  \"maximumWarning\": \"2kb\",\n                  \"maximumError\": \"4kb\"\n                }\n              ],\n              \"fileReplacements\": [\n                {\n                  \"replace\": \"src/environments/environment.ts\",\n                  \"with\": \"src/environments/environment.prod.ts\"\n                }\n              ],\n              \"outputHashing\": \"all\"\n            },\n            \"development\": {\n              \"optimization\": false,\n              \"extractLicenses\": false,\n              \"sourceMap\": true,\n              \"namedChunks\": true\n            }\n          },\n          \"defaultConfiguration\": \"production\"\n        },\n        \"serve\": {\n          \"builder\": \"@angular-devkit/build-angular:dev-server\",\n          \"configurations\": {\n            \"production\": {\n              \"buildTarget\": \"CleanArchitecture.Web:build:production\"\n            },\n            \"development\": {\n              \"proxyConfig\": \"proxy.conf.js\",\n              \"buildTarget\": \"CleanArchitecture.Web:build:development\"\n            }\n          },\n          \"defaultConfiguration\": \"development\"\n        },\n        \"extract-i18n\": {\n          \"builder\": \"@angular-devkit/build-angular:extract-i18n\",\n          \"options\": {\n            \"buildTarget\": \"CleanArchitecture.Web:build\"\n          }\n        },\n        \"test\": {\n          \"builder\": \"@angular-devkit/build-angular:karma\",\n          \"options\": {\n            \"polyfills\": [\n              \"zone.js\",\n              \"zone.js/testing\"\n            ],\n            \"tsConfig\": \"tsconfig.spec.json\",\n            \"karmaConfig\": \"karma.conf.js\",\n            \"assets\": [\n              \"src/assets\"\n            ],\n            \"styles\": [\n              \"src/styles.scss\"\n            ],\n            \"scripts\": []\n          }\n        }\n      }\n    }\n  },\n  \"cli\": {\n    \"analytics\": false\n  }\n}"
  },
  {
    "path": "src/Web/ClientApp/default.conf.template",
    "content": "server {\n    listen       ${PORT};\n    listen  [::]:${PORT};\n    server_name  localhost;\n\n    access_log  /var/log/nginx/server.access.log  main;\n\n    location / {\n        root /usr/share/nginx/html;\n        try_files $uri $uri/ /index.html;\n    }\n\n    location /api/ {\n        proxy_pass ${services__web__https__0};\n        proxy_http_version 1.1;\n        proxy_ssl_server_name on;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    }\n}"
  },
  {
    "path": "src/Web/ClientApp/karma.conf.js",
    "content": "// Karma configuration file, see link for more information\n// https://karma-runner.github.io/1.0/config/configuration-file.html\n\nmodule.exports = function (config) {\n  config.set({\n    basePath: '',\n    frameworks: ['jasmine', '@angular-devkit/build-angular'],\n    plugins: [\n      require('karma-jasmine'),\n      require('karma-chrome-launcher'),\n      require('karma-jasmine-html-reporter'),\n      require('karma-coverage'),\n      require('@angular-devkit/build-angular/plugins/karma')\n    ],\n    client: {\n      jasmine: {\n        // you can add configuration options for Jasmine here\n        // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html\n        // for example, you can disable the random execution with `random: false`\n        // or set a specific seed with `seed: 4321`\n      },\n      clearContext: false // leave Jasmine Spec Runner output visible in browser\n    },\n    jasmineHtmlReporter: {\n      suppressAll: true // removes the duplicated traces\n    },\n    coverageReporter: {\n      dir: require('path').join(__dirname, './coverage/angularapp'),\n      subdir: '.',\n      reporters: [\n        { type: 'html' },\n        { type: 'text-summary' }\n      ]\n    },\n    reporters: ['progress', 'kjhtml'],\n    port: 9876,\n    colors: true,\n    logLevel: config.LOG_INFO,\n    autoWatch: true,\n    browsers: ['Chrome'],\n    singleRun: false,\n    restartOnFileChange: true\n  });\n};\n"
  },
  {
    "path": "src/Web/ClientApp/nswag.json",
    "content": "{\n  \"runtime\": \"Net100\",\n  \"defaultVariables\": null,\n  \"documentGenerator\": {\n    \"fromDocument\": {\n      \"json\": null,\n      \"url\": \"../wwwroot/openapi/v1.json\",\n      \"output\": null,\n      \"newLineBehavior\": \"Auto\"\n    }\n  },\n  \"codeGenerators\": {\n    \"openApiToTypeScriptClient\": {\n      \"className\": \"{controller}Client\",\n      \"moduleName\": \"\",\n      \"namespace\": \"\",\n      \"typeScriptVersion\": 4.3,\n      \"template\": \"Angular\",\n      \"promiseType\": \"Promise\",\n      \"httpClass\": \"HttpClient\",\n      \"withCredentials\": false,\n      \"useSingletonProvider\": true,\n      \"injectionTokenType\": \"InjectionToken\",\n      \"rxJsVersion\": 7.0,\n      \"dateTimeType\": \"Date\",\n      \"nullValue\": \"Undefined\",\n      \"generateClientClasses\": true,\n      \"generateClientInterfaces\": true,\n      \"generateOptionalParameters\": false,\n      \"exportTypes\": true,\n      \"wrapDtoExceptions\": false,\n      \"exceptionClass\": \"SwaggerException\",\n      \"clientBaseClass\": null,\n      \"wrapResponses\": false,\n      \"wrapResponseMethods\": [],\n      \"generateResponseClasses\": true,\n      \"responseClass\": \"SwaggerResponse\",\n      \"protectedMethods\": [],\n      \"configurationClass\": null,\n      \"useTransformOptionsMethod\": false,\n      \"useTransformResultMethod\": false,\n      \"generateDtoTypes\": true,\n      \"operationGenerationMode\": \"MultipleClientsFromFirstTagAndOperationId\",\n      \"includedOperationIds\": [],\n      \"excludedOperationIds\": [],\n      \"markOptionalProperties\": true,\n      \"generateCloneMethod\": false,\n      \"typeStyle\": \"Class\",\n      \"enumStyle\": \"Enum\",\n      \"useLeafType\": false,\n      \"classTypes\": [],\n      \"extendedClasses\": [],\n      \"extensionCode\": null,\n      \"generateDefaultValues\": true,\n      \"excludedTypeNames\": [],\n      \"excludedParameterNames\": [],\n      \"handleReferences\": false,\n      \"generateTypeCheckFunctions\": false,\n      \"generateConstructorInterface\": true,\n      \"convertConstructorInterfaceData\": false,\n      \"importRequiredTypes\": true,\n      \"useGetBaseUrlMethod\": false,\n      \"baseUrlTokenName\": \"API_BASE_URL\",\n      \"queryNullValue\": \"\",\n      \"useAbortSignal\": false,\n      \"inlineNamedDictionaries\": false,\n      \"inlineNamedAny\": false,\n      \"includeHttpContext\": false,\n      \"templateDirectory\": null,\n      \"serviceHost\": null,\n      \"serviceSchemes\": null,\n      \"output\": \"src/app/web-api-client.ts\",\n      \"newLineBehavior\": \"Auto\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/Web/ClientApp/package.json",
    "content": "{\n  \"name\": \"cleanarchitecture.web\",\n  \"version\": \"0.0.0\",\n  \"scripts\": {\n    \"ng\": \"ng\",\n    \"start\": \"run-script-os\",\n    \"prestart\": \"npm run generate-api\",\n    \"start:windows\": \"ng serve --port %PORT%\",\n    \"start:default\": \"ng serve --port $PORT\",\n    \"prebuild\": \"npm run generate-api\",\n    \"build\": \"ng build\",\n    \"watch\": \"ng build --watch --configuration development\",\n    \"test\": \"ng test\",\n    \"generate-api\": \"nswag run /runtime:Net100\"\n  },\n  \"private\": true,\n  \"dependencies\": {\n    \"@angular/animations\": \"^21.1.5\",\n    \"@angular/common\": \"^21.1.5\",\n    \"@angular/compiler\": \"^21.1.5\",\n    \"@angular/core\": \"^21.1.5\",\n    \"@angular/forms\": \"^21.1.5\",\n    \"@angular/platform-browser\": \"^21.1.5\",\n    \"@angular/platform-browser-dynamic\": \"^21.1.5\",\n    \"@angular/platform-server\": \"^21.1.5\",\n    \"@angular/router\": \"^21.1.5\",\n    \"@picocss/pico\": \"^2.0.0\",\n    \"lucide-angular\": \"^0.577.0\",\n    \"run-script-os\": \"^1.1.6\",\n    \"rxjs\": \"~7.8.1\",\n    \"tslib\": \"^2.5.0\",\n    \"zone.js\": \"~0.16.0\"\n  },\n  \"devDependencies\": {\n    \"@angular-devkit/build-angular\": \"^21.1.5\",\n    \"@angular/cli\": \"^21.1.5\",\n    \"@angular/compiler-cli\": \"^21.1.5\",\n    \"@types/jasmine\": \"~6.0.0\",\n    \"@types/node\": \"^24.0.0\",\n    \"jasmine-core\": \"~6.1.0\",\n    \"karma\": \"~6.4.2\",\n    \"karma-chrome-launcher\": \"~3.2.0\",\n    \"karma-coverage\": \"~2.2.0\",\n    \"karma-jasmine\": \"~5.1.0\",\n    \"karma-jasmine-html-reporter\": \"~2.2.0\",\n    \"nswag\": \"latest\",\n    \"typescript\": \"~5.9.3\"\n  }\n}\n"
  },
  {
    "path": "src/Web/ClientApp/proxy.conf.js",
    "content": "const { env } = require('process');\n\nconst target =\n  env[\"services__webapi__https__0\"] ||\n  env[\"services__webapi__http__0\"];\n\nconst PROXY_CONFIG = [\n  {\n    context: [\n      \"/api\",\n      \"/openapi\",\n      \"/scalar\",\n      \"/weatherforecast\",\n      \"/WeatherForecast\"\n    ],\n    target: target,\n    secure: env[\"NODE_ENV\"] !== \"development\",\n  }\n];\n\nmodule.exports = PROXY_CONFIG;\n"
  },
  {
    "path": "src/Web/ClientApp/src/api-authorization/auth.guard.ts",
    "content": "import { Injectable } from '@angular/core';\nimport { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';\nimport { Observable } from 'rxjs';\nimport { tap, take } from 'rxjs/operators';\nimport { AuthService } from './auth.service';\n\n@Injectable({\n  providedIn: 'root'\n})\nexport class AuthGuard implements CanActivate {\n  constructor(private authService: AuthService, private router: Router) {}\n\n  canActivate(_route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {\n    return this.authService.isAuthenticated$.pipe(\n      take(1),\n      tap(isAuthenticated => {\n        if (!isAuthenticated) {\n          this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });\n        }\n      })\n    );\n  }\n}"
  },
  {
    "path": "src/Web/ClientApp/src/api-authorization/auth.service.ts",
    "content": "import { Injectable } from '@angular/core';\nimport { BehaviorSubject, Observable, of } from 'rxjs';\nimport { tap, catchError, map } from 'rxjs/operators';\nimport { LoginRequest, RegisterRequest, UsersClient } from '../app/web-api-client';\n\n@Injectable({\n  providedIn: 'root'\n})\nexport class AuthService {\n  private _isAuthenticated = new BehaviorSubject<boolean>(false);\n  isAuthenticated$ = this._isAuthenticated.asObservable();\n\n  constructor(private usersClient: UsersClient) {}\n\n  initialize(): Observable<boolean> {\n    return this.usersClient.infoGET().pipe(\n      map(() => true),\n      catchError(() => of(false)),\n      tap(isAuth => this._isAuthenticated.next(isAuth))\n    );\n  }\n\n  login(email: string, password: string): Observable<void> {\n    return this.usersClient.login(true, undefined, new LoginRequest({ email, password })).pipe(\n      tap(() => this._isAuthenticated.next(true)),\n      map(() => void 0)\n    );\n  }\n\n  register(email: string, password: string): Observable<void> {\n    return this.usersClient.register(new RegisterRequest({ email, password }));\n  }\n\n  logout(): Observable<void> {\n    return this.usersClient.logout({}).pipe(\n      tap(() => this._isAuthenticated.next(false))\n    );\n  }\n}"
  },
  {
    "path": "src/Web/ClientApp/src/api-authorization/authorize.interceptor.spec.ts",
    "content": "import { TestBed, inject } from '@angular/core/testing';\nimport { provideRouter } from '@angular/router';\n\nimport { AuthorizeInterceptor } from './authorize.interceptor';\n\ndescribe('AuthorizeInterceptor', () => {\n  beforeEach(() => {\n    TestBed.configureTestingModule({\n      providers: [\n        AuthorizeInterceptor,\n        provideRouter([])\n      ]\n    });\n  });\n\n  it('should be created', inject([AuthorizeInterceptor], (service: AuthorizeInterceptor) => {\n    expect(service).toBeTruthy();\n  }));\n});\n"
  },
  {
    "path": "src/Web/ClientApp/src/api-authorization/authorize.interceptor.ts",
    "content": "import { Injectable } from '@angular/core';\nimport { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';\nimport { Observable, throwError } from 'rxjs';\nimport { catchError } from 'rxjs/operators';\nimport { Router } from '@angular/router';\n\n@Injectable({\n  providedIn: 'root'\n})\nexport class AuthorizeInterceptor implements HttpInterceptor {\n  constructor(private router: Router) {}\n\n  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {\n    const authReq = req.clone({ withCredentials: true });\n    return next.handle(authReq).pipe(\n      catchError(error => {\n        if (error instanceof HttpErrorResponse\n          && error.status === 401\n          && !error.url?.includes('/manage/info')\n          && !this.router.url.startsWith('/login')) {\n          this.router.navigate(['/login'], { queryParams: { returnUrl: window.location.pathname } });\n        }\n        return throwError(() => error);\n      })\n    );\n  }\n}"
  },
  {
    "path": "src/Web/ClientApp/src/api-authorization/login/login.component.html",
    "content": "<article>\n  <h2>Log in</h2>\n  <form (ngSubmit)=\"login()\" #loginForm=\"ngForm\">\n    <label for=\"email\">Email</label>\n    <input type=\"email\" id=\"email\" name=\"email\" [(ngModel)]=\"email\"\n      [attr.aria-invalid]=\"invalid ? true : null\"\n      aria-describedby=\"login-error\"\n      autocomplete=\"username\"\n      (ngModelChange)=\"invalid = false\" />\n    <label for=\"password\">Password</label>\n    <input type=\"password\" id=\"password\" name=\"password\" [(ngModel)]=\"password\"\n      [attr.aria-invalid]=\"invalid ? true : null\"\n      aria-describedby=\"login-error\"\n      autocomplete=\"current-password\"\n      (ngModelChange)=\"invalid = false\" />\n    @if (invalid) {\n      <small id=\"login-error\">Invalid email or password.</small>\n    }\n    <button type=\"submit\">Log in</button>\n    <p style=\"margin-top: 1rem\">Don't have an account? <a [routerLink]=\"['/register']\">Register</a></p>\n  </form>\n</article>\n"
  },
  {
    "path": "src/Web/ClientApp/src/api-authorization/login/login.component.ts",
    "content": "import { Component, ChangeDetectorRef } from '@angular/core';\nimport { Router, ActivatedRoute } from '@angular/router';\nimport { AuthService } from '../auth.service';\nimport { firstValueFrom } from 'rxjs';\n\n@Component({\n  standalone: false,\n  selector: 'app-login',\n  templateUrl: './login.component.html'\n})\nexport class LoginComponent {\n  email = '';\n  password = '';\n  invalid = false;\n\n  constructor(\n    private authService: AuthService,\n    private router: Router,\n    private route: ActivatedRoute,\n    private cdr: ChangeDetectorRef\n  ) {}\n\n  async login() {\n    this.invalid = false;\n    try {\n      await firstValueFrom(this.authService.login(this.email, this.password));\n      const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';\n      await this.router.navigateByUrl(returnUrl);\n    } catch {\n      this.invalid = true;\n      this.cdr.detectChanges();\n    }\n  }\n}\n"
  },
  {
    "path": "src/Web/ClientApp/src/api-authorization/register/register.component.html",
    "content": "<article>\n  <h2>Register</h2>\n  @if (error) {\n    <p class=\"error\">{{ error }}</p>\n  }\n  <form (ngSubmit)=\"register()\">\n    <label for=\"email\">Email</label>\n    <input type=\"email\" id=\"email\" name=\"email\" [(ngModel)]=\"email\"\n      [attr.aria-invalid]=\"emailTouched ? !emailValid : null\"\n      aria-describedby=\"email-helper\"\n      autocomplete=\"username\"\n      (blur)=\"emailTouched = true\" />\n    <small id=\"email-helper\">\n      {{ emailTouched && !emailValid ? 'Please enter a valid email address.' : '' }}\n    </small>\n    <label for=\"password\">Password</label>\n    <input type=\"password\" id=\"password\" name=\"password\" [(ngModel)]=\"password\"\n      [attr.aria-invalid]=\"passwordTouched ? !passwordValid : null\"\n      aria-describedby=\"password-helper\"\n      autocomplete=\"new-password\"\n      (blur)=\"passwordTouched = true\" />\n    <small id=\"password-helper\">\n      {{ passwordTouched && !passwordValid ? 'Password must be at least ' + minPasswordLength + ' characters.' : '' }}\n    </small>\n    <button type=\"submit\">Register</button>\n    <p style=\"margin-top: 1rem\">Already have an account? <a [routerLink]=\"['/login']\">Log in</a></p>\n  </form>\n</article>\n"
  },
  {
    "path": "src/Web/ClientApp/src/api-authorization/register/register.component.ts",
    "content": "import { Component, ChangeDetectorRef } from '@angular/core';\nimport { Router } from '@angular/router';\nimport { AuthService } from '../auth.service';\nimport { firstValueFrom } from 'rxjs';\n\nconst MIN_PASSWORD_LENGTH = 6;\n\n@Component({\n  standalone: false,\n  selector: 'app-register',\n  templateUrl: './register.component.html'\n})\nexport class RegisterComponent {\n  email = '';\n  password = '';\n  emailTouched = false;\n  passwordTouched = false;\n  error = '';\n\n  readonly minPasswordLength = MIN_PASSWORD_LENGTH;\n\n  get emailValid() { return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(this.email); }\n  get passwordValid() { return this.password.length >= MIN_PASSWORD_LENGTH; }\n\n  constructor(private authService: AuthService, private router: Router, private cdr: ChangeDetectorRef) {}\n\n  async register() {\n    this.error = '';\n    this.emailTouched = true;\n    this.passwordTouched = true;\n    if (!this.emailValid || !this.passwordValid) return;\n    try {\n      await firstValueFrom(this.authService.register(this.email, this.password));\n      await this.router.navigate(['/login']);\n    } catch {\n      this.error = 'Registration failed. Please try again.';\n      this.cdr.detectChanges();\n    }\n  }\n}\n"
  },
  {
    "path": "src/Web/ClientApp/src/app/app.component.html",
    "content": "<app-nav-menu></app-nav-menu>\n<main>\n  <router-outlet></router-outlet>\n</main>\n"
  },
  {
    "path": "src/Web/ClientApp/src/app/app.component.ts",
    "content": "import { Component } from '@angular/core';\n\n@Component({\n  standalone: false,\n  selector: 'app-root',\n  templateUrl: './app.component.html'\n})\nexport class AppComponent {\n  title = 'app';\n}\n"
  },
  {
    "path": "src/Web/ClientApp/src/app/app.module.ts",
    "content": "import { APP_ID, NgModule, inject, provideAppInitializer } from '@angular/core';\nimport { BrowserModule } from '@angular/platform-browser';\nimport { FormsModule } from '@angular/forms';\nimport { RouterModule } from '@angular/router';\nimport { LucideAngularModule, Sun, Moon, Laptop, Plus, Settings, MoreHorizontal } from 'lucide-angular';\nimport { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';\n\nimport { AppComponent } from './app.component';\nimport { NavMenuComponent } from './nav-menu/nav-menu.component';\nimport { HomeComponent } from './home/home.component';\nimport { CounterComponent } from './counter/counter.component';\nimport { WeatherComponent } from './weather/weather.component';\nimport { TasksComponent } from './todo/todo.component';\nimport { ThemeToggleComponent } from './theme-toggle/theme-toggle.component';\nimport { API_BASE_URL } from './web-api-client';\nimport { AuthorizeInterceptor } from 'src/api-authorization/authorize.interceptor';\nimport { LoginComponent } from 'src/api-authorization/login/login.component';\nimport { RegisterComponent } from 'src/api-authorization/register/register.component';\nimport { AuthGuard } from 'src/api-authorization/auth.guard';\nimport { AuthService } from 'src/api-authorization/auth.service';\n\nexport function getApiBaseUrl(): string {\n  const url = document.getElementsByTagName('base')[0].href;\n  return url.endsWith('/') ? url.slice(0, -1) : url;\n}\n\n@NgModule({\n    declarations: [\n        AppComponent,\n        NavMenuComponent,\n        HomeComponent,\n        CounterComponent,\n        WeatherComponent,\n        TasksComponent,\n        ThemeToggleComponent,\n        LoginComponent,\n        RegisterComponent\n    ],\n    bootstrap: [AppComponent],\n    imports: [\n        BrowserModule,\n        FormsModule,\n        LucideAngularModule.pick({ Sun, Moon, Laptop, Plus, Settings, MoreHorizontal }),\n        RouterModule.forRoot([\n            { path: '', component: HomeComponent, pathMatch: 'full' },\n            { path: 'counter', component: CounterComponent },\n            { path: 'weather', component: WeatherComponent, canActivate: [AuthGuard] },\n            { path: 'todo', component: TasksComponent, canActivate: [AuthGuard] },\n            { path: 'login', component: LoginComponent },\n            { path: 'register', component: RegisterComponent }\n        ])\n    ],\n    providers: [\n        { provide: APP_ID, useValue: 'ng-cli-universal' },\n        { provide: HTTP_INTERCEPTORS, useClass: AuthorizeInterceptor, multi: true },\n        { provide: API_BASE_URL, useFactory: getApiBaseUrl, deps: [] },\n        provideAppInitializer(() => inject(AuthService).initialize()),\n        provideHttpClient(withInterceptorsFromDi())\n    ]\n})\nexport class AppModule { }\n"
  },
  {
    "path": "src/Web/ClientApp/src/app/app.server.module.ts",
    "content": "import { NgModule } from '@angular/core';\nimport { ServerModule } from '@angular/platform-server';\nimport { AppComponent } from './app.component';\nimport { AppModule } from './app.module';\n\n@NgModule({\n    imports: [AppModule, ServerModule],\n    bootstrap: [AppComponent]\n})\nexport class AppServerModule { }\n"
  },
  {
    "path": "src/Web/ClientApp/src/app/counter/counter.component.html",
    "content": "<h1>Counter</h1>\n\n<p>This is a simple example of an Angular component.</p>\n\n<p aria-live=\"polite\">Current count: <strong>{{ currentCount }}</strong></p>\n\n<button (click)=\"incrementCounter()\">Increment</button>\n"
  },
  {
    "path": "src/Web/ClientApp/src/app/counter/counter.component.spec.ts",
    "content": "import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';\n\nimport { CounterComponent } from './counter.component';\n\ndescribe('CounterComponent', () => {\n  let fixture: ComponentFixture<CounterComponent>;\n\n  beforeEach(waitForAsync(() => {\n    TestBed.configureTestingModule({\n      declarations: [ CounterComponent ]\n    })\n    .compileComponents();\n  }));\n\n  beforeEach(() => {\n    fixture = TestBed.createComponent(CounterComponent);\n    fixture.detectChanges();\n  });\n\n  it('should display a title', waitForAsync(() => {\n    const titleText = fixture.nativeElement.querySelector('h1').textContent;\n    expect(titleText).toEqual('Counter');\n  }));\n\n  it('should start with count 0, then increments by 1 when clicked', waitForAsync(() => {\n    const countElement = fixture.nativeElement.querySelector('strong');\n    expect(countElement.textContent).toEqual('0');\n\n    const incrementButton = fixture.nativeElement.querySelector('button');\n    incrementButton.click();\n    fixture.detectChanges();\n    expect(countElement.textContent).toEqual('1');\n  }));\n});\n"
  },
  {
    "path": "src/Web/ClientApp/src/app/counter/counter.component.ts",
    "content": "import { Component } from '@angular/core';\n\n@Component({\n  standalone: false,\n  selector: 'app-counter-component',\n  templateUrl: './counter.component.html'\n})\nexport class CounterComponent {\n  public currentCount = 0;\n\n  public incrementCounter() {\n    this.currentCount++;\n  }\n}\n"
  },
  {
    "path": "src/Web/ClientApp/src/app/fetch-data/fetch-data.component.html",
    "content": "<h1 id=\"tableLabel\">Weather forecast</h1>\n\n<p>This component demonstrates fetching data from the server.</p>\n@if (!forecasts?.length) {\n<p><em>Loading...</em></p>\n}\n@else{\n<table class='table table-striped' aria-labelledby=\"tableLabel\">\n  <thead>\n    <tr>\n      <th>Date</th>\n      <th>Temp. (C)</th>\n      <th>Temp. (F)</th>\n      <th>Summary</th>\n    </tr>\n  </thead>\n  <tbody>\n    @for (forecast of forecasts; track $index) {\n    <tr>\n      <td>{{ forecast.date | date }}</td>\n      <td>{{ forecast.temperatureC }}</td>\n      <td>{{ forecast.temperatureF }}</td>\n      <td>{{ forecast.summary }}</td>\n    </tr>\n    }\n  </tbody>\n</table>\n}"
  },
  {
    "path": "src/Web/ClientApp/src/app/fetch-data/fetch-data.component.ts",
    "content": "import { ChangeDetectorRef, Component } from '@angular/core';\nimport { WeatherForecastsClient, WeatherForecast } from '../web-api-client';\n\n@Component({\n  standalone: false,\n  selector: 'app-fetch-data',\n  templateUrl: './fetch-data.component.html'\n})\nexport class FetchDataComponent {\n  public forecasts: WeatherForecast[] = [];\n\n  constructor(private client: WeatherForecastsClient, private cdr: ChangeDetectorRef) {\n      client.getWeatherForecasts().subscribe({\n          next: result => {\n              this.forecasts = result;\n              this.cdr.detectChanges();\n          },\n          error: error => console.error(error)\n      });\n  }\n}\n"
  },
  {
    "path": "src/Web/ClientApp/src/app/home/home.component.html",
    "content": "<h1>Welcome</h1>\n<p>A full-stack application with an <a href='https://angular.dev/'>Angular</a> frontend and an <a href='https://get.asp.net/'>ASP.NET Core</a> backend, built with:</p>\n<ul>\n  <li><a href='https://get.asp.net/'>ASP.NET Core</a> and <a href='https://learn.microsoft.com/en-us/dotnet/csharp/'>C#</a> for cross-platform server-side code</li>\n  <li><a href='https://angular.dev/'>Angular</a> and <a href='http://www.typescriptlang.org/'>TypeScript</a> for client-side code</li>\n  <li><a href='https://picocss.com/'>Pico CSS</a> for layout and styling</li>\n  <li><a href='https://lucide.dev/'>Lucide</a> for icons</li>\n</ul>\n<p>To help you get started:</p>\n<ul>\n  <li><strong>Client-side navigation</strong>. Click <em>Counter</em> then <em>Back</em> to return here.</li>\n  <li><strong>Angular CLI integration</strong>. In development mode, there's no need to run <code>ng serve</code>. It runs in the background automatically, so your client-side resources are dynamically built on demand and the page refreshes when you modify any file.</li>\n  <li><strong>Efficient production builds</strong>. In production mode, development-time features are disabled, and your <code>dotnet publish</code> configuration automatically invokes <code>ng build</code> to produce minified, ahead-of-time compiled JavaScript files.</li>\n</ul>\n<p>The <code>ClientApp</code> subdirectory is a standard Angular CLI application. Open a command prompt there to run any <code>ng</code> command (e.g., <code>ng test</code>), or use <code>npm</code> to install extra packages.</p>\n"
  },
  {
    "path": "src/Web/ClientApp/src/app/home/home.component.ts",
    "content": "import { Component } from '@angular/core';\n\n@Component({\n  standalone: false,\n  selector: 'app-home',\n  templateUrl: './home.component.html',\n})\nexport class HomeComponent {\n}\n"
  },
  {
    "path": "src/Web/ClientApp/src/app/nav-menu/nav-menu.component.html",
    "content": "<header>\n  <nav>\n    <ul>\n      <li><a class=\"nav-brand\" [routerLink]=\"['/']\">Clean Architecture</a></li>\n    </ul>\n    <ul>\n      <li><a [routerLink]=\"['/']\" [routerLinkActive]=\"['pico-active']\" [routerLinkActiveOptions]=\"{ exact: true }\">Home</a></li>\n      <li><a [routerLink]=\"['/counter']\" [routerLinkActive]=\"['pico-active']\">Counter</a></li>\n      <li><a [routerLink]=\"['/weather']\" [routerLinkActive]=\"['pico-active']\">Weather</a></li>\n      <li><a [routerLink]=\"['/todo']\" [routerLinkActive]=\"['pico-active']\">Tasks</a></li>\n    </ul>\n    <ul>\n      @if (isAuthenticated$ | async) {\n        <li><a href=\"#\" (click)=\"logout($event)\">Log out</a></li>\n      } @else {\n        <li><a [routerLink]=\"['/login']\" [routerLinkActive]=\"['pico-active']\">Log in</a></li>\n        <li><a [routerLink]=\"['/register']\" [routerLinkActive]=\"['pico-active']\">Register</a></li>\n      }\n      <li aria-hidden=\"true\" class=\"nav-separator\"></li>\n      <li><app-theme-toggle /></li>\n    </ul>\n  </nav>\n</header>\n"
  },
  {
    "path": "src/Web/ClientApp/src/app/nav-menu/nav-menu.component.scss",
    "content": "// Styles handled globally in styles.scss\n"
  },
  {
    "path": "src/Web/ClientApp/src/app/nav-menu/nav-menu.component.ts",
    "content": "import { Component } from '@angular/core';\nimport { Router } from '@angular/router';\nimport { AuthService } from 'src/api-authorization/auth.service';\n\n@Component({\n  standalone: false,\n  selector: 'app-nav-menu',\n  templateUrl: './nav-menu.component.html',\n  styleUrls: ['./nav-menu.component.scss']\n})\nexport class NavMenuComponent {\n  isAuthenticated$ = this.authService.isAuthenticated$;\n\n  constructor(private authService: AuthService, private router: Router) {}\n\n  logout(event: Event): void {\n    event.preventDefault();\n    this.authService.logout().subscribe({\n      next: () => this.router.navigate(['/login'])\n    });\n  }\n}\n"
  },
  {
    "path": "src/Web/ClientApp/src/app/theme-toggle/theme-toggle.component.html",
    "content": "<button class=\"theme-toggle-btn\" (click)=\"cycle()\" [attr.aria-label]=\"label()\">\n  <lucide-icon [name]=\"icon()\" [size]=\"22\" [strokeWidth]=\"2\"></lucide-icon>\n</button>\n"
  },
  {
    "path": "src/Web/ClientApp/src/app/theme-toggle/theme-toggle.component.ts",
    "content": "import { Component, computed } from '@angular/core';\nimport { ThemeService, Theme } from '../theme.service';\n\n@Component({\n  standalone: false,\n  selector: 'app-theme-toggle',\n  templateUrl: './theme-toggle.component.html'\n})\nexport class ThemeToggleComponent {\n  constructor(public themeService: ThemeService) {}\n\n  icon = computed(() => {\n    switch (this.themeService.theme()) {\n      case 'light': return 'sun';\n      case 'dark':  return 'moon';\n      default:      return 'laptop';\n    }\n  });\n\n  label = computed(() => {\n    switch (this.themeService.theme()) {\n      case 'light': return 'Light';\n      case 'dark':  return 'Dark';\n      default:      return 'Auto';\n    }\n  });\n\n  cycle(): void {\n    const next: Record<Theme, Theme> = { auto: 'light', light: 'dark', dark: 'auto' };\n    this.themeService.setTheme(next[this.themeService.theme()]);\n  }\n}\n"
  },
  {
    "path": "src/Web/ClientApp/src/app/theme.service.ts",
    "content": "import { Injectable, signal } from '@angular/core';\n\nexport type Theme = 'auto' | 'light' | 'dark';\n\n@Injectable({ providedIn: 'root' })\nexport class ThemeService {\n  private readonly STORAGE_KEY = 'picoColorScheme';\n\n  theme = signal<Theme>('auto');\n\n  constructor() {\n    const stored = localStorage.getItem(this.STORAGE_KEY) as Theme | null;\n    const initial: Theme = stored ?? 'auto';\n    this.theme.set(initial);\n    this.applyTheme(initial);\n  }\n\n  setTheme(value: Theme): void {\n    this.theme.set(value);\n    localStorage.setItem(this.STORAGE_KEY, value);\n    this.applyTheme(value);\n  }\n\n  private applyTheme(theme: Theme): void {\n    if (theme === 'auto') {\n      document.documentElement.removeAttribute('data-theme');\n    } else {\n      document.documentElement.setAttribute('data-theme', theme);\n    }\n  }\n}\n"
  },
  {
    "path": "src/Web/ClientApp/src/app/todo/todo.component.html",
    "content": "<hgroup>\n  <h1>Tasks</h1>\n  <p>Manage your todo lists and tasks.</p>\n</hgroup>\n\n@if (!lists()) {\n  <span aria-busy=\"true\">Loading&hellip;</span>\n} @else {\n  <div class=\"todo-layout\">\n\n    <!-- Sidebar -->\n    <div class=\"todo-sidebar\">\n      <div class=\"todo-panel-header\">\n        <h2>Lists</h2>\n        <button class=\"icon-btn\" (click)=\"showNewListDialog()\"><lucide-icon name=\"plus\" [size]=\"20\" [strokeWidth]=\"2\" /></button>\n      </div>\n      <ul>\n        @for (list of lists(); track $index) {\n          <li [attr.aria-current]=\"selectedList() === list ? 'true' : null\"\n              (click)=\"selectedListId.set(list.id)\">\n            <span>{{ list.title }}</span>\n            <small>{{ remainingItems(list) }}</small>\n          </li>\n        }\n      </ul>\n    </div>\n\n    <!-- Items panel -->\n    @if (selectedList()) {\n      <div class=\"todo-main\">\n        <div class=\"todo-panel-header\">\n          <h2>{{ selectedList()!.title }}</h2>\n          <button class=\"icon-btn\" (click)=\"showListOptionsDialog()\"><lucide-icon name=\"settings\" [size]=\"20\" [strokeWidth]=\"2\" /></button>\n        </div>\n\n        @for (item of selectedList()!.items; track $index; let i = $index) {\n          <div class=\"todo-item\">\n            <input type=\"checkbox\" [(ngModel)]=\"item.done\" (change)=\"updateItem(item)\" />\n            @if (item === editingItem()) {\n              <input id=\"{{ 'itemTitle' + i }}\" type=\"text\" class=\"todo-item-input\"\n                [(ngModel)]=\"item.title\" (keyup.enter)=\"updateItem(item)\"\n                (keyup.escape)=\"cancelEdit(); $event.target.blur()\"\n                (blur)=\"editingItem() && updateItem(item)\"\n                autofocus maxlength=\"200\" />\n            } @else {\n              <span class=\"todo-item-text\" [class.todo-done]=\"item.done\"\n                (click)=\"editItem(item, 'itemTitle' + i)\">{{ item.title }}</span>\n            }\n            @if (item.id !== 0) {\n              <button class=\"icon-btn\" (click)=\"showItemDetailsDialog(item)\"><lucide-icon name=\"more-horizontal\" [size]=\"20\" [strokeWidth]=\"2\" /></button>\n            }\n          </div>\n        }\n\n        <div class=\"todo-item todo-new-item\">\n          <input type=\"checkbox\" disabled />\n          @if (addingItem()) {\n            <input id=\"newItemInput\" type=\"text\" class=\"todo-item-input\"\n              [(ngModel)]=\"newItemTitle\"\n              (keyup.enter)=\"commitNewItem()\"\n              (keyup.escape)=\"cancelNewItem(); $event.target.blur()\"\n              (blur)=\"addingItem() && commitNewItem()\"\n              maxlength=\"200\" />\n          } @else {\n            <span class=\"todo-item-text todo-new-item-placeholder\" (click)=\"startAddingItem()\">New task…</span>\n          }\n        </div>\n      </div>\n    }\n\n  </div>\n}\n\n<!-- New List dialog -->\n<dialog #newListDialog>\n  <article>\n    <header>\n      <h3>New List</h3>\n      <button rel=\"prev\" aria-label=\"Close\" (click)=\"newListCancelled()\"></button>\n    </header>\n    <label for=\"title\">Title</label>\n    <input type=\"text\" id=\"title\" placeholder=\"List title&hellip;\" [(ngModel)]=\"newListEditor.title\"\n      [attr.aria-invalid]=\"newListError() ? 'true' : null\"\n      (keyup.enter)=\"addList()\" maxlength=\"200\" />\n    @if (newListError()) {\n      <small>{{ newListError() }}</small>\n    }\n    <footer>\n      <button class=\"secondary\" (click)=\"newListCancelled()\">Cancel</button>\n      <button (click)=\"addList()\">Create</button>\n    </footer>\n  </article>\n</dialog>\n\n<!-- List Options dialog -->\n<dialog #listOptionsDialog>\n  <article>\n    <header>\n      <h3>List Options</h3>\n      <button rel=\"prev\" aria-label=\"Close\" (click)=\"closeListOptionsDialog()\"></button>\n    </header>\n    <label for=\"inputListTitle\">Title</label>\n    <input type=\"text\" id=\"inputListTitle\" placeholder=\"List name&hellip;\"\n      [(ngModel)]=\"listOptionsEditor.title\" (keyup.enter)=\"updateListOptions()\" maxlength=\"200\" />\n    <footer>\n      <button class=\"danger\" style=\"margin-inline-end: auto\" (click)=\"confirmDeleteList()\">Delete</button>\n      <button class=\"secondary\" (click)=\"closeListOptionsDialog()\">Cancel</button>\n      <button (click)=\"updateListOptions()\">Update</button>\n    </footer>\n  </article>\n</dialog>\n\n<!-- Delete List dialog -->\n<dialog #deleteListDialog>\n  <article>\n    <header>\n      <h3>Delete \"{{ selectedList()?.title }}\"?</h3>\n      <button rel=\"prev\" aria-label=\"Close\" (click)=\"closeDeleteListDialog()\"></button>\n    </header>\n    <p>All items will be permanently deleted.</p>\n    <footer>\n      <button class=\"secondary\" (click)=\"closeDeleteListDialog()\">Cancel</button>\n      <button class=\"danger\" style=\"--pico-background-color: var(--pico-del-color); --pico-border-color: var(--pico-del-color); --pico-color: #fff;\" (click)=\"deleteListConfirmed()\">Delete</button>\n    </footer>\n  </article>\n</dialog>\n\n<!-- Item Details dialog -->\n<dialog #itemDetailsDialog>\n  <article>\n    <header>\n      <h3>Item Details</h3>\n      <button rel=\"prev\" aria-label=\"Close\" (click)=\"closeItemDetailsDialog()\"></button>\n    </header>\n    <label for=\"itemList\">List</label>\n    <select id=\"itemList\" [(ngModel)]=\"itemDetailsEditor.listId\">\n      @for (list of lists(); track $index) {\n        <option [ngValue]=\"list.id\">{{ list.title }}</option>\n      }\n    </select>\n    <label for=\"priority\">Priority</label>\n    <select id=\"priority\" [(ngModel)]=\"itemDetailsEditor.priority\">\n      @for (level of priorityLevels(); track $index) {\n        <option [ngValue]=\"level.id\">{{ level.title }}</option>\n      }\n    </select>\n    <label for=\"note\">Note</label>\n    <textarea id=\"note\" rows=\"3\" [(ngModel)]=\"itemDetailsEditor.note\"></textarea>\n    <footer>\n      <button class=\"danger\" style=\"margin-inline-end: auto\" (click)=\"deleteItem(selectedItem()!)\">Delete</button>\n      <button class=\"secondary\" (click)=\"closeItemDetailsDialog()\">Cancel</button>\n      <button (click)=\"updateItemDetails()\">Update</button>\n    </footer>\n  </article>\n</dialog>\n"
  },
  {
    "path": "src/Web/ClientApp/src/app/todo/todo.component.scss",
    "content": "// Styles handled globally in styles.scss\n"
  },
  {
    "path": "src/Web/ClientApp/src/app/todo/todo.component.ts",
    "content": "import { Component, OnInit, ViewChild, ElementRef, signal, computed, effect } from '@angular/core';\nimport { TodoListsClient, TodoItemsClient,\n  TodoListDto, TodoItemDto, LookupDto,\n  CreateTodoListCommand, UpdateTodoListCommand,\n  CreateTodoItemCommand, UpdateTodoItemCommand, UpdateTodoItemDetailCommand\n} from '../web-api-client';\n\n@Component({\n  standalone: false,\n  selector: 'app-todo-component',\n  templateUrl: './todo.component.html',\n  styleUrls: ['./todo.component.scss']\n})\nexport class TasksComponent implements OnInit {\n  @ViewChild('newListDialog') newListDialogRef: ElementRef<HTMLDialogElement>;\n  @ViewChild('listOptionsDialog') listOptionsDialogRef: ElementRef<HTMLDialogElement>;\n  @ViewChild('deleteListDialog') deleteListDialogRef: ElementRef<HTMLDialogElement>;\n  @ViewChild('itemDetailsDialog') itemDetailsDialogRef: ElementRef<HTMLDialogElement>;\n\n  lists = signal<TodoListDto[] | null>(null);\n  priorityLevels = signal<LookupDto[]>([]);\n  selectedListId = signal<number | null>(null);\n  selectedList = computed(() => this.lists()?.find(l => l.id === this.selectedListId()) ?? null);\n  selectedItem = signal<TodoItemDto | null>(null);\n  editingItem = signal<TodoItemDto | null>(null);\n  newListEditor: any = {};\n  newListError = signal('');\n  listOptionsEditor: any = {};\n  itemDetailsEditor: any = {};\n  newItemTitle = '';\n  addingItem = signal(false);\n  private originalTitle = '';\n\n  constructor(\n    private listsClient: TodoListsClient,\n    private itemsClient: TodoItemsClient\n  ) {\n    effect(() => { this.selectedListId(); this.newItemTitle = ''; this.addingItem.set(false); });\n  }\n\n  ngOnInit(): void {\n    this.listsClient.getTodoLists().subscribe({\n      next: result => {\n        this.lists.set(result.lists);\n        this.priorityLevels.set(result.priorityLevels);\n        if (result.lists.length) {\n          this.selectedListId.set(result.lists[0].id);\n        }\n      },\n      error: error => console.error(error)\n    });\n  }\n\n  // Lists\n  remainingItems(list: TodoListDto): number {\n    return list.items.filter(t => !t.done).length;\n  }\n\n  showNewListDialog(): void {\n    this.newListEditor = {};\n    this.newListError.set('');\n    this.newListDialogRef.nativeElement.showModal();\n    setTimeout(() => document.getElementById('title')?.focus(), 50);\n  }\n\n  newListCancelled(): void {\n    this.newListDialogRef.nativeElement.close();\n    this.newListEditor = {};\n    this.newListError.set('');\n  }\n\n  addList(): void {\n    const list = {\n      id: 0,\n      title: this.newListEditor.title,\n      items: []\n    } as TodoListDto;\n\n    this.listsClient.createTodoList(list as CreateTodoListCommand).subscribe({\n      next: result => {\n        list.id = result;\n        this.lists.update(ls => [...ls, list]);\n        this.selectedListId.set(list.id);\n        this.newListDialogRef.nativeElement.close();\n        this.newListEditor = {};\n        this.newListError.set('');\n      },\n      error: error => {\n        const errors = JSON.parse(error.response).errors;\n        if (errors && errors.Title) {\n          this.newListError.set(errors.Title[0]);\n        }\n        setTimeout(() => document.getElementById('title')?.focus(), 50);\n      }\n    });\n  }\n\n  showListOptionsDialog(): void {\n    this.listOptionsEditor = {\n      id: this.selectedList()!.id,\n      title: this.selectedList()!.title\n    };\n    this.listOptionsDialogRef.nativeElement.showModal();\n  }\n\n  closeListOptionsDialog(): void {\n    this.listOptionsDialogRef.nativeElement.close();\n    this.listOptionsEditor = {};\n  }\n\n  updateListOptions(): void {\n    const id = this.selectedList()!.id;\n    const newTitle = this.listOptionsEditor.title;\n    this.listsClient.updateTodoList(id, this.listOptionsEditor as UpdateTodoListCommand).subscribe({\n      next: () => {\n        this.lists.update(ls => ls.map(l => l.id === id ? { ...l, title: newTitle } as TodoListDto : l));\n        this.closeListOptionsDialog();\n      },\n      error: error => console.error(error)\n    });\n  }\n\n  confirmDeleteList(): void {\n    this.closeListOptionsDialog();\n    this.deleteListDialogRef.nativeElement.showModal();\n  }\n\n  closeDeleteListDialog(): void {\n    this.deleteListDialogRef.nativeElement.close();\n  }\n\n  deleteListConfirmed(): void {\n    const deletedId = this.selectedList()!.id;\n    this.listsClient.deleteTodoList(deletedId).subscribe({\n      next: () => {\n        this.closeDeleteListDialog();\n        this.lists.update(ls => ls.filter(l => l.id !== deletedId));\n        const remaining = this.lists()!;\n        this.selectedListId.set(remaining.length ? remaining[0].id : null);\n      },\n      error: error => console.error(error)\n    });\n  }\n\n  // Items\n  showItemDetailsDialog(item: TodoItemDto): void {\n    this.selectedItem.set(item);\n    this.itemDetailsEditor = { ...item };\n    this.itemDetailsDialogRef.nativeElement.showModal();\n  }\n\n  closeItemDetailsDialog(): void {\n    this.itemDetailsDialogRef.nativeElement.close();\n    this.selectedItem.set(null);\n    this.itemDetailsEditor = {};\n  }\n\n  updateItemDetails(): void {\n    const currentItem = this.selectedItem()!;\n    const isMoving = currentItem.listId !== this.itemDetailsEditor.listId;\n    this.itemsClient.updateTodoItemDetail(currentItem.id, this.itemDetailsEditor as UpdateTodoItemDetailCommand).subscribe({\n      next: () => {\n        this.lists.update(ls => ls.map(l => {\n          if (l.id === currentItem.listId && isMoving) {\n            return { ...l, items: l.items.filter(i => i.id !== currentItem.id) } as TodoListDto;\n          }\n          if (l.id === this.itemDetailsEditor.listId && isMoving) {\n            const moved = { ...currentItem, listId: this.itemDetailsEditor.listId, priority: this.itemDetailsEditor.priority, note: this.itemDetailsEditor.note } as TodoItemDto;\n            return { ...l, items: [...l.items, moved] } as TodoListDto;\n          }\n          if (l.id === currentItem.listId) {\n            return { ...l, items: l.items.map(i => i.id === currentItem.id\n              ? { ...i, priority: this.itemDetailsEditor.priority, note: this.itemDetailsEditor.note } as TodoItemDto\n              : i\n            )} as TodoListDto;\n          }\n          return l;\n        }));\n        this.closeItemDetailsDialog();\n      },\n      error: error => console.error(error)\n    });\n  }\n\n  startAddingItem(): void {\n    this.addingItem.set(true);\n    setTimeout(() => document.getElementById('newItemInput')?.focus(), 50);\n  }\n\n  cancelNewItem(): void {\n    this.addingItem.set(false);\n    this.newItemTitle = '';\n  }\n\n  commitNewItem(): void {\n    this.addingItem.set(false);\n    if (!this.newItemTitle.trim()) {\n      this.newItemTitle = '';\n      return;\n    }\n    const listId = this.selectedListId()!;\n    const title = this.newItemTitle.trim();\n    this.itemsClient.createTodoItem({ title, listId } as CreateTodoItemCommand).subscribe({\n      next: result => {\n        this.lists.update(ls => ls.map(l => l.id === listId\n          ? { ...l, items: [...l.items, { id: result, listId, title, done: false, priority: this.priorityLevels()[0].id } as TodoItemDto] } as TodoListDto\n          : l\n        ));\n        this.newItemTitle = '';\n      },\n      error: error => console.error(error)\n    });\n  }\n\n  editItem(item: TodoItemDto, inputId: string): void {\n    this.originalTitle = item.title;\n    this.editingItem.set(item);\n    setTimeout(() => document.getElementById(inputId)?.focus(), 100);\n  }\n\n  cancelEdit(): void {\n    if (this.editingItem()) {\n      this.editingItem()!.title = this.originalTitle;\n    }\n    this.editingItem.set(null);\n  }\n\n  updateItem(item: TodoItemDto): void {\n    if (!item.title.trim()) {\n      this.deleteItem(item);\n      return;\n    }\n\n    if (item.id === 0) {\n      const listId = this.selectedListId()!;\n      this.itemsClient\n        .createTodoItem({ title: item.title, listId } as CreateTodoItemCommand)\n        .subscribe({\n          next: result => {\n            this.lists.update(ls => ls.map(l => l.id === listId\n              ? { ...l, items: l.items.map(i => i === item ? { ...i, id: result } as TodoItemDto : i) } as TodoListDto\n              : l\n            ));\n          },\n          error: error => console.error(error)\n        });\n    } else {\n      this.itemsClient.updateTodoItem(item.id, item as UpdateTodoItemCommand).subscribe({\n        next: () => console.log('Update succeeded.'),\n        error: error => console.error(error)\n      });\n    }\n\n    this.editingItem.set(null);\n  }\n\n  deleteItem(item: TodoItemDto): void {\n    if (this.itemDetailsDialogRef?.nativeElement.open) {\n      this.itemDetailsDialogRef.nativeElement.close();\n    }\n\n    const listId = this.selectedListId()!;\n    if (item.id === 0) {\n      const currentItem = this.selectedItem()!;\n      this.lists.update(ls => ls.map(l => l.id === listId\n        ? { ...l, items: l.items.filter(i => i !== currentItem) } as TodoListDto\n        : l\n      ));\n      this.editingItem.set(null);\n    } else {\n      this.itemsClient.deleteTodoItem(item.id).subscribe({\n        next: () => {\n          this.lists.update(ls => ls.map(l => l.id === listId\n            ? { ...l, items: l.items.filter(i => i.id !== item.id) } as TodoListDto\n            : l\n          ));\n        },\n        error: error => console.error(error)\n      });\n    }\n  }\n}\n"
  },
  {
    "path": "src/Web/ClientApp/src/app/weather/weather.component.html",
    "content": "<h1>Weather</h1>\n<p>This component demonstrates fetching data from the server.</p>\n\n@if (loading) {\n  <span aria-busy=\"true\">Fetching your weather forecast&hellip;</span>\n}\n@if (error) {\n  <p class=\"error\">{{ error }}</p>\n}\n@if (!loading && !error) {\n  <table aria-label=\"Weather forecast\">\n    <thead>\n      <tr>\n        <th>Date</th>\n        <th>Temp. (C)</th>\n        <th>Temp. (F)</th>\n        <th>Summary</th>\n      </tr>\n    </thead>\n    <tbody>\n      @for (forecast of forecasts; track forecast.date) {\n        <tr>\n          <td>{{ forecast.date | date }}</td>\n          <td>{{ forecast.temperatureC }}</td>\n          <td>{{ forecast.temperatureF }}</td>\n          <td>{{ forecast.summary }}</td>\n        </tr>\n      }\n    </tbody>\n  </table>\n}\n"
  },
  {
    "path": "src/Web/ClientApp/src/app/weather/weather.component.ts",
    "content": "import { ChangeDetectorRef, Component } from '@angular/core';\nimport { WeatherForecastsClient, WeatherForecast } from '../web-api-client';\n\n@Component({\n  standalone: false,\n  selector: 'app-weather',\n  templateUrl: './weather.component.html'\n})\nexport class WeatherComponent {\n  forecasts: WeatherForecast[] = [];\n  loading = true;\n  error: string | null = null;\n\n  constructor(private client: WeatherForecastsClient, private cdr: ChangeDetectorRef) {\n    client.getWeatherForecasts().subscribe({\n      next: result => {\n        this.forecasts = result;\n        this.loading = false;\n        this.cdr.detectChanges();\n      },\n      error: () => {\n        this.error = 'Unable to load weather forecasts. Please try again later.';\n        this.loading = false;\n        this.cdr.detectChanges();\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "src/Web/ClientApp/src/assets/.gitkeep",
    "content": ""
  },
  {
    "path": "src/Web/ClientApp/src/environments/environment.prod.ts",
    "content": "export const environment = {\n  production: true\n};\n"
  },
  {
    "path": "src/Web/ClientApp/src/environments/environment.ts",
    "content": "// This file can be replaced during build by using the `fileReplacements` array.\n// `ng build` replaces `environment.ts` with `environment.prod.ts`.\n// The list of file replacements can be found in `angular.json`.\n\nexport const environment = {\n  production: false\n};\n\n/*\n * For easier debugging in development mode, you can import the following file\n * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.\n *\n * This import should be commented out in production mode because it will have a negative impact\n * on performance if an error is thrown.\n */\n// import 'zone.js/plugins/zone-error';  // Included with Angular CLI.\n"
  },
  {
    "path": "src/Web/ClientApp/src/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Clean Architecture</title>\n    <base href=\"/\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"favicon.png\" />\n    <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n    <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n    <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Outfit:wght@600;700&display=swap\" rel=\"stylesheet\" />\n    <script>\n      (function () {\n        var theme = localStorage.getItem('picoColorScheme');\n        if (theme && theme !== 'auto') {\n          document.documentElement.setAttribute('data-theme', theme);\n        }\n      })();\n    </script>\n  </head>\n  <body>\n    <app-root>Loading...</app-root>\n  </body>\n</html>\n"
  },
  {
    "path": "src/Web/ClientApp/src/main.ts",
    "content": "import { enableProdMode } from '@angular/core';\nimport { platformBrowserDynamic } from '@angular/platform-browser-dynamic';\n\nimport { AppModule } from './app/app.module';\nimport { environment } from './environments/environment';\n\nexport function getBaseUrl() {\n  return document.getElementsByTagName('base')[0].href;\n}\n\nconst providers = [\n  { provide: 'BASE_URL', useFactory: getBaseUrl, deps: [] }\n];\n\nif (environment.production) {\n  enableProdMode();\n}\n\nplatformBrowserDynamic(providers).bootstrapModule(AppModule)\n  .catch(err => console.log(err));\n"
  },
  {
    "path": "src/Web/ClientApp/src/polyfills.ts",
    "content": "/**\n * This file includes polyfills needed by Angular and is loaded before the app.\n * You can add your own extra polyfills to this file.\n *\n * This file is divided into 2 sections:\n *   1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.\n *   2. Application imports. Files imported after ZoneJS that should be loaded before your main\n *      file.\n *\n * The current setup is for so-called \"evergreen\" browsers; the last versions of browsers that\n * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),\n * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.\n *\n * Learn more in https://angular.io/guide/browser-support\n */\n\n/***************************************************************************************************\n * BROWSER POLYFILLS\n */\n\n/**\n * IE11 requires the following for NgClass support on SVG elements\n */\n// import 'classlist.js';  // Run `npm install --save classlist.js`.\n\n/**\n * Web Animations `@angular/platform-browser/animations`\n * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.\n * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).\n */\n// import 'web-animations-js';  // Run `npm install --save web-animations-js`.\n\n/**\n * By default, zone.js will patch all possible macroTask and DomEvents\n * user can disable parts of macroTask/DomEvents patch by setting following flags\n * because those flags need to be set before `zone.js` being loaded, and webpack\n * will put import in the top of bundle, so user need to create a separate file\n * in this directory (for example: zone-flags.ts), and put the following flags\n * into that file, and then add the following code before importing zone.js.\n * import './zone-flags';\n *\n * The flags allowed in zone-flags.ts are listed here.\n *\n * The following flags will work for all browsers.\n *\n * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame\n * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick\n * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames\n *\n *  in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js\n *  with the following flag, it will bypass `zone.js` patch for IE/Edge\n *\n *  (window as any).__Zone_enable_cross_context_check = true;\n *\n */\n\n/***************************************************************************************************\n * Zone JS is required by default for Angular itself.\n */\nimport 'zone.js';  // Included with Angular CLI.\n\n\n/***************************************************************************************************\n * APPLICATION IMPORTS\n */\n"
  },
  {
    "path": "src/Web/ClientApp/src/styles.scss",
    "content": "@use \"sass:map\";\n@use \"@picocss/pico/scss/index\" as * with (\n  $theme-color: \"violet\",\n  $semantic-root-element: \"app-root\",\n  $enable-semantic-container: true,\n  $enable-classes: false\n);\n\n// ── Base font size & typography ───────────────────────────────────────────────\nhtml {\n  font-size: 95%;\n}\n\n:root {\n  --pico-font-family-sans-serif: \"Inter\", system-ui, sans-serif;\n  --pico-font-family-headings: \"Outfit\", sans-serif;\n  --pico-font-family-monospace: \"JetBrains Mono\", monospace;\n  --pico-border-radius: 0.375rem;\n  --pico-line-height: 1.5;\n}\n\n// ── Header ────────────────────────────────────────────────────────────────────\n:root {\n  --header-height: 4.5rem;\n}\n\napp-nav-menu > header {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  width: 100%;\n  max-width: none;\n  height: var(--header-height);\n  padding: 0;\n  margin: 0;\n  z-index: 100;\n  background: color-mix(in srgb, var(--pico-background-color) 85%, transparent);\n  backdrop-filter: blur(8px);\n  border-bottom: 1px solid var(--pico-muted-border-color);\n}\n\napp-nav-menu > header nav {\n  height: 100%;\n  display: flex;\n  align-items: center;\n  width: 100%;\n  margin-inline: auto;\n  padding-inline: var(--pico-block-spacing-horizontal);\n\n  @media (min-width: 576px)  { max-width: 510px;  padding-inline: 0; }\n  @media (min-width: 768px)  { max-width: 700px; }\n  @media (min-width: 1024px) { max-width: 950px; }\n  @media (min-width: 1280px) { max-width: 1200px; }\n  @media (min-width: 1536px) { max-width: 1450px; }\n}\n\n// ── Main content ──────────────────────────────────────────────────────────────\napp-root > main {\n  padding-top: calc(var(--header-height) + var(--pico-block-spacing-vertical) * 2);\n}\n\n// ── Nav brand ─────────────────────────────────────────────────────────────────\napp-nav-menu > header nav > ul:first-child a {\n  font-family: \"Outfit\", sans-serif;\n  font-size: 1.75rem;\n  font-weight: 700;\n  text-decoration: none;\n  color: var(--pico-contrast);\n}\n\n// ── Login / Register centered layout ──────────────────────────────────────────\napp-root > main:has(> app-login, > app-register) {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  min-height: 100vh;\n\n  > app-login,\n  > app-register {\n    display: contents;\n  }\n\n  > app-login > article,\n  > app-register > article {\n    width: 100%;\n    max-width: map.get(map.get($breakpoints, \"md\"), \"viewport\");\n  }\n}\n\n// ── Danger color — more vivid in dark mode ────────────────────────────────────\n[data-theme=\"dark\"] {\n  --pico-del-color: #f05050;\n}\n\n@media (prefers-color-scheme: dark) {\n  :root:not([data-theme=\"light\"]) {\n    --pico-del-color: #f05050;\n  }\n}\n\n// ── Nav separator ─────────────────────────────────────────────────────────────\n.nav-separator {\n  width: 1px;\n  height: 1.2rem;\n  background: var(--pico-muted-border-color);\n  padding: 0;\n  margin-inline: 0.25rem;\n}\n\n// ── Theme toggle ───────────────────────────────────────────────────────────────\n.theme-toggle-btn {\n  background: none;\n  border: none;\n  box-shadow: none;\n  padding: 0;\n  line-height: 1;\n  color: var(--pico-muted-color);\n  cursor: pointer;\n  --pico-form-element-spacing-vertical: 0;\n  --pico-form-element-spacing-horizontal: 0;\n\n  &:hover {\n    background: none;\n    color: var(--pico-primary);\n  }\n}\n\n// ── Icon buttons ───────────────────────────────────────────────────────────────\n.icon-btn {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  width: 2rem;\n  height: 2rem;\n  padding: 0;\n  font-size: 1.25rem;\n  font-weight: bold;\n  line-height: 1;\n  flex-shrink: 0;\n  background: transparent !important;\n  border-color: transparent !important;\n  color: var(--pico-muted-color);\n  box-shadow: none !important;\n  --pico-form-element-spacing-vertical: 0;\n  --pico-form-element-spacing-horizontal: 0;\n\n  &:hover {\n    color: var(--pico-primary);\n  }\n\n  lucide-icon {\n    display: flex;\n  }\n}\n\n// ── Todo layout ───────────────────────────────────────────────────────────────\n.todo-layout {\n  display: grid;\n  grid-template-columns: 220px 1fr;\n  gap: calc(var(--pico-block-spacing-horizontal) * 2);\n  align-items: start;\n}\n\n.todo-sidebar {\n  border-right: 1px solid var(--pico-muted-border-color);\n  padding-right: calc(var(--pico-block-spacing-horizontal) * 2);\n\n  ul {\n    list-style: none;\n    padding: 0;\n    margin: 0;\n  }\n\n  li {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: calc(var(--pico-spacing) * 0.4) calc(var(--pico-spacing) * 0.5);\n    border-radius: var(--pico-border-radius);\n    cursor: pointer;\n    color: var(--pico-muted-color);\n\n    &[aria-current=\"true\"] {\n      background: var(--pico-primary-background);\n      color: var(--pico-primary-inverse);\n    }\n\n    &:hover:not([aria-current=\"true\"]) {\n      background: var(--pico-card-sectioning-background-color);\n      color: var(--pico-color);\n    }\n\n    small {\n      font-size: 0.75rem;\n      opacity: 0.7;\n    }\n  }\n}\n\n.todo-main {\n  min-width: 0;\n  padding-left: var(--pico-spacing);\n}\n\n.todo-panel-header {\n  display: flex;\n  align-items: baseline;\n  gap: 0.25rem;\n  margin-bottom: var(--pico-spacing);\n}\n\n.todo-item {\n  display: flex;\n  align-items: baseline;\n  gap: calc(var(--pico-spacing) * 0.5);\n  padding-block: calc(var(--pico-spacing) * 0.35);\n  border-bottom: 1px solid var(--pico-muted-border-color);\n\n  input[type=\"checkbox\"] {\n    margin: 0;\n    flex-shrink: 0;\n    align-self: center;\n  }\n}\n\n.todo-item-text {\n  flex: 1;\n  cursor: pointer;\n  word-break: break-word;\n}\n\n.todo-done {\n  text-decoration: line-through;\n  color: var(--pico-muted-color);\n}\n\n.todo-item-input {\n  flex: 1;\n  align-self: center;\n  margin: 0 !important;\n  padding: 0;\n  min-height: unset;\n  --pico-form-element-spacing-horizontal: 0;\n  --pico-form-element-spacing-vertical: 0;\n  border: none;\n  border-bottom: 1px solid var(--pico-primary);\n  border-radius: 0;\n  background: transparent;\n  box-shadow: none;\n\n  &:not(:focus) {\n    border-bottom-color: transparent;\n  }\n}\n\n.todo-new-item {\n  border-bottom: none;\n\n  input[type=\"checkbox\"] {\n    opacity: 0.3;\n  }\n}\n\n.todo-new-item-placeholder {\n  color: var(--pico-muted-color);\n  cursor: pointer;\n}\n\n// ── Dialog header/footer ───────────────────────────────────────────────────────\ndialog article header {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n\n  > :first-child {\n    margin: 0;\n  }\n\n  > button[rel=\"prev\"] {\n    position: static;\n    margin: 0;\n    margin-inline-start: auto;\n    flex-shrink: 0;\n  }\n}\n\ndialog article footer {\n  display: flex;\n  justify-content: flex-end;\n  align-items: center;\n  gap: calc(var(--pico-spacing) * 0.5);\n}\n\n// ── Button variants (not compiled in classless mode — defined manually) ───────\nbutton.secondary,\n[type=\"submit\"].secondary,\n[type=\"button\"].secondary,\n[role=\"button\"].secondary {\n  --pico-background-color: transparent;\n  --pico-border-color: var(--pico-secondary);\n  --pico-color: var(--pico-secondary);\n\n  &:is(:hover, :active, :focus) {\n    --pico-background-color: color-mix(in srgb, var(--pico-secondary) 10%, transparent);\n    --pico-border-color: var(--pico-secondary-hover);\n    --pico-color: var(--pico-secondary-hover);\n  }\n}\n\nbutton.danger,\n[type=\"submit\"].danger,\n[type=\"button\"].danger,\n[role=\"button\"].danger {\n  --pico-background-color: transparent;\n  --pico-border-color: var(--pico-del-color);\n  --pico-color: var(--pico-del-color);\n\n  &:is(:hover, :active, :focus) {\n    --pico-background-color: color-mix(in srgb, var(--pico-del-color) 10%, transparent);\n    --pico-border-color: color-mix(in srgb, var(--pico-del-color) 80%, black);\n    --pico-color: color-mix(in srgb, var(--pico-del-color) 80%, black);\n  }\n}\n\n.error {\n  color: var(--pico-del-color);\n}\n"
  },
  {
    "path": "src/Web/ClientApp/src/test.ts",
    "content": "// This file is required by karma.conf.js and loads recursively all the .spec and framework files\n\nimport 'zone.js/testing';\nimport { getTestBed } from '@angular/core/testing';\nimport {\n  BrowserDynamicTestingModule,\n  platformBrowserDynamicTesting\n} from '@angular/platform-browser-dynamic/testing';\n\ngetTestBed().initTestEnvironment(\n  BrowserDynamicTestingModule,\n  platformBrowserDynamicTesting()\n);\n"
  },
  {
    "path": "src/Web/ClientApp/tsconfig.app.json",
    "content": "/* To learn more about this file see: https://angular.io/config/tsconfig. */\n{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"./out-tsc/app\",\n    \"types\": []\n  },\n  \"files\": [\n    \"src/main.ts\"\n  ],\n  \"include\": [\n    \"src/**/*.d.ts\"\n  ]\n}\n"
  },
  {
    "path": "src/Web/ClientApp/tsconfig.json",
    "content": "{\n  \"compileOnSave\": false,\n  \"compilerOptions\": {\n    \"baseUrl\": \"./\",\n    \"outDir\": \"./dist/out-tsc\",\n    \"forceConsistentCasingInFileNames\": true,\n    \"strict\": false,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"sourceMap\": true,\n    \"declaration\": false,\n    \"downlevelIteration\": true,\n    \"experimentalDecorators\": true,\n    \"moduleResolution\": \"bundler\",\n    \"importHelpers\": true,\n    \"target\": \"es2022\",\n    \"module\": \"es2022\",\n    \"lib\": [\n      \"es2022\",\n      \"dom\"\n    ],\n    \"useDefineForClassFields\": false\n  },\n  \"angularCompilerOptions\": {\n    \"enableI18nLegacyMessageIdFormat\": false,\n    \"strictInjectionParameters\": true,\n    \"strictInputAccessModifiers\": true,\n    \"strictTemplates\": true\n  }\n}\n"
  },
  {
    "path": "src/Web/ClientApp/tsconfig.spec.json",
    "content": "/* To learn more about this file see: https://angular.io/config/tsconfig. */\n{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"./out-tsc/spec\",\n    \"types\": [\n      \"jasmine\",\n      \"node\"\n    ]\n  },\n  \"files\": [\n    \"src/test.ts\"\n  ],\n  \"include\": [\n    \"src/**/*.spec.ts\",\n    \"src/**/*.d.ts\"\n  ]\n}\n"
  },
  {
    "path": "src/Web/ClientApp-React/.gitignore",
    "content": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n\n# testing\n/coverage\n\n# production\n/build\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "src/Web/ClientApp-React/README.md",
    "content": "# CleanArchitecture React Client\n\nThis project uses [Vite](https://vitejs.dev/) with React 19 and TypeScript.\n\n## Available Scripts\n\n### `npm start`\n\nRuns the app in development mode with hot module replacement.\nOpens at [https://localhost:44447](https://localhost:44447).\n\nThe development server proxies API requests to the ASP.NET Core backend.\n\n### `npm run build`\n\nBuilds the app for production to the `build` folder.\nOptimizes the build for best performance.\n\n### `npm run preview`\n\nPreviews the production build locally.\n\n### `npm run lint`\n\nRuns ESLint on the src directory.\n\n## Project Structure\n\n- `src/` - React source code\n- `src/main.jsx` - Application entry point\n- `src/App.js` - Root component\n- `src/components/` - React components\n- `public/` - Static assets (favicon, manifest)\n- `vite.config.ts` - Vite configuration with proxy settings\n- `index.html` - HTML template\n\n## Environment Variables\n\nVite environment variables must be prefixed with `VITE_` to be exposed to client code.\n\nExample:\n```\nVITE_API_URL=https://api.example.com\n```\n\nAccess in code:\n```javascript\nconst apiUrl = import.meta.env.VITE_API_URL;\n```\n\n## HTTPS Configuration\n\nThe development server uses ASP.NET Core development certificates for HTTPS.\nRun `npm start` to automatically set up certificates via `aspnetcore-https.js`.\n\n## Learn More\n\n- [Vite Documentation](https://vitejs.dev/)\n- [React Documentation](https://react.dev/)\n"
  },
  {
    "path": "src/Web/ClientApp-React/aspnetcore-https.cjs",
    "content": "// This script sets up HTTPS for the application using the ASP.NET Core HTTPS certificate\nconst fs = require('fs');\nconst spawn = require('child_process').spawn;\nconst path = require('path');\n\nconst baseFolder =\n  process.env.APPDATA !== undefined && process.env.APPDATA !== ''\n    ? `${process.env.APPDATA}/ASP.NET/https`\n    : `${process.env.HOME}/.aspnet/https`;\n\nif (!fs.existsSync(baseFolder)) { fs.mkdirSync(baseFolder, { recursive: true }); }\n\nconst certificateArg = process.argv.map(arg => arg.match(/--name=(?<value>.+)/i)).filter(Boolean)[0];\nconst certificateName = certificateArg ? certificateArg.groups.value : process.env.npm_package_name;\n\nif (!certificateName) {\n  console.error('Invalid certificate name. Run this script in the context of an npm/yarn script or pass --name=<<app>> explicitly.')\n  process.exit(-1);\n}\n\nconst certFilePath = path.join(baseFolder, `${certificateName}.pem`);\nconst keyFilePath = path.join(baseFolder, `${certificateName}.key`);\n\nif (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {\n  spawn('dotnet', [\n    'dev-certs',\n    'https',\n    '--export-path',\n    certFilePath,\n    '--format',\n    'Pem',\n    '--no-password',\n  ], { stdio: 'inherit', })\n  .on('exit', (code) => process.exit(code));\n}\n"
  },
  {
    "path": "src/Web/ClientApp-React/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\" />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <base href=\"/\" />\n    <link rel=\"manifest\" href=\"/manifest.webmanifest\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/favicon.png\" />\n    <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n    <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n    <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Outfit:wght@600;700&display=swap\" rel=\"stylesheet\" />\n    <title>Clean Architecture</title>\n    <script>\n      (function () {\n        var theme = localStorage.getItem('picoColorScheme');\n        if (theme && theme !== 'auto') {\n          document.documentElement.setAttribute('data-theme', theme);\n        }\n      })();\n    </script>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.jsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "src/Web/ClientApp-React/nswag.json",
    "content": "{\n  \"runtime\": \"Net100\",\n  \"defaultVariables\": null,\n  \"documentGenerator\": {\n    \"fromDocument\": {\n      \"json\": null,\n      \"url\": \"../wwwroot/openapi/v1.json\",\n      \"output\": null,\n      \"newLineBehavior\": \"Auto\"\n    }\n  },\n  \"codeGenerators\": {\n    \"openApiToTypeScriptClient\": {\n      \"className\": \"{controller}Client\",\n      \"moduleName\": \"\",\n      \"namespace\": \"\",\n      \"typeScriptVersion\": 4.3,\n      \"template\": \"Fetch\",\n      \"promiseType\": \"Promise\",\n      \"httpClass\": \"HttpClient\",\n      \"withCredentials\": false,\n      \"requestCredentials\": \"include\",\n      \"useSingletonProvider\": true,\n      \"injectionTokenType\": \"InjectionToken\",\n      \"rxJsVersion\": 7.0,\n      \"dateTimeType\": \"Date\",\n      \"nullValue\": \"Undefined\",\n      \"generateClientClasses\": true,\n      \"generateClientInterfaces\": false,\n      \"generateOptionalParameters\": false,\n      \"exportTypes\": true,\n      \"wrapDtoExceptions\": false,\n      \"exceptionClass\": \"SwaggerException\",\n      \"clientBaseClass\": null,\n      \"wrapResponses\": false,\n      \"wrapResponseMethods\": [],\n      \"generateResponseClasses\": true,\n      \"responseClass\": \"SwaggerResponse\",\n      \"protectedMethods\": [],\n      \"configurationClass\": null,\n      \"useTransformOptionsMethod\": false,\n      \"useTransformResultMethod\": false,\n      \"generateDtoTypes\": true,\n      \"operationGenerationMode\": \"MultipleClientsFromFirstTagAndOperationId\",\n      \"includedOperationIds\": [],\n      \"excludedOperationIds\": [],\n      \"markOptionalProperties\": true,\n      \"generateCloneMethod\": false,\n      \"typeStyle\": \"Class\",\n      \"enumStyle\": \"Enum\",\n      \"useLeafType\": false,\n      \"classTypes\": [],\n      \"extendedClasses\": [],\n      \"extensionCode\": null,\n      \"generateDefaultValues\": true,\n      \"excludedTypeNames\": [],\n      \"excludedParameterNames\": [],\n      \"handleReferences\": false,\n      \"generateTypeCheckFunctions\": false,\n      \"generateConstructorInterface\": true,\n      \"convertConstructorInterfaceData\": false,\n      \"importRequiredTypes\": true,\n      \"useGetBaseUrlMethod\": false,\n      \"baseUrlTokenName\": \"API_BASE_URL\",\n      \"queryNullValue\": \"\",\n      \"useAbortSignal\": false,\n      \"inlineNamedDictionaries\": false,\n      \"inlineNamedAny\": false,\n      \"includeHttpContext\": false,\n\"serviceHost\": null,\n      \"serviceSchemes\": null,\n      \"output\": \"src/web-api-client.ts\",\n      \"newLineBehavior\": \"Auto\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/Web/ClientApp-React/package.json",
    "content": "{\n  \"name\": \"cleanarchitecture.web\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@picocss/pico\": \"^2.0.6\",\n    \"lucide-react\": \"^0.577.0\",\n    \"react\": \"^19.1.0\",\n    \"react-dom\": \"^19.1.0\",\n    \"react-router-dom\": \"^7.6.1\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^24.0.0\",\n    \"@types/react\": \"^19.1.0\",\n    \"@types/react-dom\": \"^19.1.0\",\n    \"@vitejs/plugin-react\": \"^6.0.0\",\n    \"eslint\": \"^9.26.0\",\n    \"eslint-plugin-react-hooks\": \"^7.0.0\",\n    \"eslint-plugin-react-refresh\": \"^0.5.0\",\n    \"globals\": \"^17.0.0\",\n    \"nswag\": \"latest\",\n    \"sass\": \"^1.98.0\",\n    \"typescript\": \"~5.9.0\",\n    \"vite\": \"^8.0.0\"\n  },\n  \"scripts\": {\n    \"prestart\": \"npm run generate-api\",\n    \"start\": \"vite\",\n    \"prebuild\": \"npm run generate-api\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview\",\n    \"lint\": \"eslint ./src/\",\n    \"generate-api\": \"nswag run /runtime:Net100\"\n  }\n}\n"
  },
  {
    "path": "src/Web/ClientApp-React/public/manifest.webmanifest",
    "content": "{\n  \"short_name\": \"CleanArchitecture.Web\",\n  \"name\": \"CleanArchitecture.Web\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    }\n  ],\n  \"start_url\": \"./index.html\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "src/Web/ClientApp-React/src/App.jsx",
    "content": "import React, { Component } from 'react';\nimport { Route, Routes } from 'react-router-dom';\nimport AppRoutes from './AppRoutes';\nimport { Layout } from './components/Layout';\nimport { AuthProvider } from './components/api-authorization/AuthContext';\nimport { ThemeProvider } from './components/ThemeContext';\n\nexport default class App extends Component {\n  static displayName = App.name;\n\n  render() {\n    return (\n      <ThemeProvider>\n      <AuthProvider>\n        <Layout>\n        <Routes>\n          {AppRoutes.map((route, index) => {\n            const { element, ...rest } = route;\n            return <Route key={index} {...rest} element={element} />;\n          })}\n        </Routes>\n        </Layout>\n      </AuthProvider>\n      </ThemeProvider>\n    );\n  }\n}\n"
  },
  {
    "path": "src/Web/ClientApp-React/src/AppRoutes.jsx",
    "content": "import { Counter } from \"./components/Counter\";\nimport { Weather } from \"./components/Weather\";\nimport { Tasks } from \"./components/Todo\";\nimport { Home } from \"./components/Home\";\nimport { LoginPage } from \"./components/api-authorization/LoginPage\";\nimport { RegisterPage } from \"./components/api-authorization/RegisterPage\";\nimport { ProtectedRoute } from \"./components/api-authorization/ProtectedRoute\";\n\nconst AppRoutes = [\n  {\n    index: true,\n    element: <Home />\n  },\n  {\n    path: '/counter',\n    element: <Counter />\n  },\n  {\n    path: '/weather',\n    element: <ProtectedRoute><Weather /></ProtectedRoute>\n  },\n  {\n    path: '/todo',\n    element: <ProtectedRoute><Tasks /></ProtectedRoute>\n  },\n  {\n    path: '/login',\n    element: <LoginPage />\n  },\n  {\n    path: '/register',\n    element: <RegisterPage />\n  }\n];\n\nexport default AppRoutes;\n"
  },
  {
    "path": "src/Web/ClientApp-React/src/components/Counter.jsx",
    "content": "import { useState } from 'react';\n\nexport function Counter() {\n  const [count, setCount] = useState(0);\n\n  return (\n    <div>\n      <h1>Counter</h1>\n      <p>This is a simple example of a React component.</p>\n      <p aria-live=\"polite\">Current count: <strong>{count}</strong></p>\n      <button onClick={() => setCount(c => c + 1)}>Increment</button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/Web/ClientApp-React/src/components/Home.jsx",
    "content": "export function Home() {\n  return (\n    <div>\n      <h1>Welcome</h1>\n      <p>A full-stack application with a <a href=\"https://react.dev/\">React</a> frontend and an <a href=\"https://get.asp.net/\">ASP.NET Core</a> backend, built with:</p>\n      <ul>\n        <li><a href=\"https://get.asp.net/\">ASP.NET Core</a> and <a href=\"https://learn.microsoft.com/en-us/dotnet/csharp/\">C#</a> for cross-platform server-side code</li>\n        <li><a href=\"https://react.dev/\">React</a> and <a href=\"https://vite.dev/\">Vite</a> for client-side code</li>\n        <li><a href=\"https://picocss.com/\">Pico CSS</a> for layout and styling</li>\n        <li><a href=\"https://lucide.dev/\">Lucide</a> for icons</li>\n      </ul>\n      <p>To help you get started:</p>\n      <ul>\n        <li><strong>Client-side navigation</strong>. Click <em>Counter</em> then <em>Back</em> to return here.</li>\n        <li><strong>Vite dev server</strong>. In development mode, Vite runs in the background with hot module replacement, so the page updates instantly when you modify any file.</li>\n        <li><strong>Efficient production builds</strong>. In production mode, development-time features are disabled, and your <code>dotnet publish</code> configuration produces minified, efficiently bundled JavaScript files.</li>\n      </ul>\n      <p>The <code>ClientApp</code> subdirectory is a Vite + React application. Open a command prompt there to run <code>npm</code> commands such as <code>npm run dev</code> or <code>npm install</code>.</p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/Web/ClientApp-React/src/components/Layout.jsx",
    "content": "import { NavMenu } from './NavMenu';\n\nexport function Layout({ children }) {\n  return (\n    <>\n      <NavMenu />\n      <main>{children}</main>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/Web/ClientApp-React/src/components/NavMenu.jsx",
    "content": "import { Link, useNavigate } from 'react-router-dom';\nimport { useAuth } from './api-authorization/AuthContext';\nimport { ThemeToggle } from './ThemeToggle';\n\nfunction AuthLinks() {\n  const { isAuthenticated, logout } = useAuth();\n  const navigate = useNavigate();\n\n  const handleLogout = async (e) => {\n    e.preventDefault();\n    await logout();\n    navigate('/login');\n  };\n\n  if (isAuthenticated) {\n    return <li><a href=\"#\" onClick={handleLogout}>Log out</a></li>;\n  }\n  return (\n    <>\n      <li><Link to=\"/login\">Log in</Link></li>\n      <li><Link to=\"/register\">Register</Link></li>\n    </>\n  );\n}\n\nexport function NavMenu() {\n  return (\n    <header>\n      <nav>\n        <ul>\n          <li><Link to=\"/\">Clean Architecture</Link></li>\n        </ul>\n        <ul>\n          <li><Link to=\"/\">Home</Link></li>\n          <li><Link to=\"/counter\">Counter</Link></li>\n          <li><Link to=\"/weather\">Weather</Link></li>\n          <li><Link to=\"/todo\">Tasks</Link></li>\n        </ul>\n        <ul>\n          <AuthLinks />\n          <li aria-hidden=\"true\" className=\"nav-separator\"></li>\n          <li><ThemeToggle /></li>\n        </ul>\n      </nav>\n    </header>\n  );\n}\n"
  },
  {
    "path": "src/Web/ClientApp-React/src/components/ThemeContext.jsx",
    "content": "import { createContext, useContext, useEffect, useState } from 'react';\n\nconst STORAGE_KEY = 'picoColorScheme';\n\nconst ThemeContext = createContext({});\n\nexport function useTheme() {\n  return useContext(ThemeContext);\n}\n\nexport function ThemeProvider({ children }) {\n  const [theme, setTheme] = useState(\n    () => localStorage.getItem(STORAGE_KEY) || 'auto'\n  );\n\n  useEffect(() => {\n    if (theme === 'auto') {\n      document.documentElement.removeAttribute('data-theme');\n    } else {\n      document.documentElement.setAttribute('data-theme', theme);\n    }\n    localStorage.setItem(STORAGE_KEY, theme);\n  }, [theme]);\n\n  return (\n    <ThemeContext.Provider value={{ theme, setTheme }}>\n      {children}\n    </ThemeContext.Provider>\n  );\n}\n"
  },
  {
    "path": "src/Web/ClientApp-React/src/components/ThemeToggle.jsx",
    "content": "import { Sun, Moon, Laptop } from 'lucide-react';\nimport { useTheme } from './ThemeContext';\n\nconst icons = {\n  auto:  <Laptop size={22} strokeWidth={2} />,\n  light: <Sun    size={22} strokeWidth={2} />,\n  dark:  <Moon   size={22} strokeWidth={2} />,\n};\n\nconst next = { auto: 'light', light: 'dark', dark: 'auto' };\n\nexport function ThemeToggle() {\n  const { theme, setTheme } = useTheme();\n\n  return (\n    <button\n      className=\"theme-toggle-btn\"\n      onClick={() => setTheme(next[theme])}\n      aria-label={theme}\n    >\n      {icons[theme]}\n    </button>\n  );\n}\n"
  },
  {
    "path": "src/Web/ClientApp-React/src/components/Todo.jsx",
    "content": "import { useState, useEffect, useRef } from 'react';\nimport { Plus, Settings, MoreHorizontal } from 'lucide-react';\nimport { TodoListsClient, TodoItemsClient } from '../web-api-client.ts';\n\nconst listsClient = new TodoListsClient();\nconst itemsClient = new TodoItemsClient();\n\nexport function Tasks() {\n  const [lists, setLists] = useState(null);\n  const [priorityLevels, setPriorityLevels] = useState([]);\n  const [selectedListId, setSelectedListId] = useState(null);\n  const [selectedItem, setSelectedItem] = useState(null);\n  const [editingItem, setEditingItem] = useState(null);\n  const [editValue, setEditValue] = useState('');\n  const [newItemTitle, setNewItemTitle] = useState('');\n  const [addingItem, setAddingItem] = useState(false);\n  const [listOptionsEditor, setListOptionsEditor] = useState({});\n  const [itemDetailsEditor, setItemDetailsEditor] = useState({});\n  const [newListTitle, setNewListTitle] = useState('');\n  const [newListError, setNewListError] = useState('');\n\n  const originalTitle = useRef('');\n  const editCancelledRef = useRef(false);\n  const newItemCancelledRef = useRef(false);\n\n  const newListDialogRef = useRef(null);\n  const listOptionsDialogRef = useRef(null);\n  const deleteListDialogRef = useRef(null);\n  const itemDetailsDialogRef = useRef(null);\n\n  useEffect(() => {\n    listsClient.getTodoLists().then(result => {\n      setLists(result.lists);\n      setPriorityLevels(result.priorityLevels);\n      if (result.lists.length) setSelectedListId(result.lists[0].id);\n    }).catch(console.error);\n  }, []);\n\n  useEffect(() => {\n    setNewItemTitle('');\n    setAddingItem(false);\n  }, [selectedListId]);\n\n  const selectedList = lists?.find(l => l.id === selectedListId) ?? null;\n  const remainingItems = list => list.items.filter(t => !t.done).length;\n\n  // ── Lists ──────────────────────────────────────────────────────────────────\n\n  const showNewListDialog = () => {\n    setNewListTitle('');\n    setNewListError('');\n    newListDialogRef.current.showModal();\n    setTimeout(() => document.getElementById('newListTitle')?.focus(), 50);\n  };\n\n  const closeNewListDialog = () => {\n    newListDialogRef.current.close();\n    setNewListTitle('');\n    setNewListError('');\n  };\n\n  const commitNewList = async () => {\n    if (!newListTitle.trim()) return;\n    try {\n      const id = await listsClient.createTodoList({ title: newListTitle.trim(), items: [] });\n      const newList = { id, title: newListTitle.trim(), items: [] };\n      setLists(ls => [...ls, newList]);\n      setSelectedListId(id);\n      closeNewListDialog();\n    } catch (e) {\n      try {\n        const errors = JSON.parse(e.response).errors;\n        if (errors?.Title) { setNewListError(errors.Title[0]); return; }\n      } catch { /* ignore */ }\n      setNewListError('Failed to create list.');\n    }\n  };\n\n  const showListOptionsDialog = () => {\n    setListOptionsEditor({ id: selectedList.id, title: selectedList.title });\n    listOptionsDialogRef.current.showModal();\n  };\n\n  const closeListOptionsDialog = () => {\n    listOptionsDialogRef.current.close();\n    setListOptionsEditor({});\n  };\n\n  const updateListOptions = async () => {\n    try {\n      await listsClient.updateTodoList(selectedList.id, listOptionsEditor);\n      setLists(ls => ls.map(l => l.id === selectedList.id ? { ...l, title: listOptionsEditor.title } : l));\n      closeListOptionsDialog();\n    } catch (e) { console.error(e); }\n  };\n\n  const confirmDeleteList = () => {\n    closeListOptionsDialog();\n    deleteListDialogRef.current.showModal();\n  };\n\n  const closeDeleteListDialog = () => deleteListDialogRef.current.close();\n\n  const deleteListConfirmed = async () => {\n    try {\n      await listsClient.deleteTodoList(selectedList.id);\n      const remaining = lists.filter(l => l.id !== selectedList.id);\n      setLists(remaining);\n      setSelectedListId(remaining.length ? remaining[0].id : null);\n      closeDeleteListDialog();\n    } catch (e) { console.error(e); }\n  };\n\n  // ── Items ──────────────────────────────────────────────────────────────────\n\n  const showItemDetailsDialog = (item) => {\n    setSelectedItem(item);\n    setItemDetailsEditor({ ...item });\n    itemDetailsDialogRef.current.showModal();\n  };\n\n  const closeItemDetailsDialog = () => {\n    itemDetailsDialogRef.current.close();\n    setSelectedItem(null);\n    setItemDetailsEditor({});\n  };\n\n  const updateItemDetails = async () => {\n    const isMoving = selectedItem.listId !== itemDetailsEditor.listId;\n    try {\n      await itemsClient.updateTodoItemDetail(selectedItem.id, itemDetailsEditor);\n      setLists(ls => ls.map(l => {\n        if (l.id === selectedItem.listId && isMoving)\n          return { ...l, items: l.items.filter(i => i.id !== selectedItem.id) };\n        if (l.id === itemDetailsEditor.listId && isMoving)\n          return { ...l, items: [...l.items, { ...selectedItem, ...itemDetailsEditor }] };\n        if (l.id === selectedItem.listId)\n          return { ...l, items: l.items.map(i => i.id === selectedItem.id ? { ...i, priority: itemDetailsEditor.priority, note: itemDetailsEditor.note } : i) };\n        return l;\n      }));\n      closeItemDetailsDialog();\n    } catch (e) { console.error(e); }\n  };\n\n  const deleteItem = async (item) => {\n    if (itemDetailsDialogRef.current?.open) closeItemDetailsDialog();\n    try {\n      await itemsClient.deleteTodoItem(item.id);\n      setLists(ls => ls.map(l => l.id === selectedListId\n        ? { ...l, items: l.items.filter(i => i.id !== item.id) } : l));\n    } catch (e) { console.error(e); }\n  };\n\n  const updateCheckbox = async (item, done) => {\n    const updated = { ...item, done };\n    setLists(ls => ls.map(l => l.id === selectedListId\n      ? { ...l, items: l.items.map(i => i.id === item.id ? updated : i) } : l));\n    try { await itemsClient.updateTodoItem(item.id, updated); }\n    catch (e) { console.error(e); }\n  };\n\n  const editItem = (item, inputId) => {\n    originalTitle.current = item.title;\n    setEditValue(item.title);\n    setEditingItem(item);\n    setTimeout(() => document.getElementById(inputId)?.focus(), 100);\n  };\n\n  const cancelEdit = (e) => {\n    editCancelledRef.current = true;\n    setLists(ls => ls.map(l => ({\n      ...l, items: l.items.map(i => i === editingItem ? { ...i, title: originalTitle.current } : i)\n    })));\n    setEditingItem(null);\n    e?.target.blur();\n  };\n\n  const commitEdit = async () => {\n    if (!editValue.trim()) {\n      await deleteItem(editingItem);\n      setEditingItem(null);\n      return;\n    }\n    const updated = { ...editingItem, title: editValue.trim() };\n    setLists(ls => ls.map(l => l.id === selectedListId\n      ? { ...l, items: l.items.map(i => i === editingItem ? updated : i) } : l));\n    setEditingItem(null);\n    try { await itemsClient.updateTodoItem(updated.id, updated); }\n    catch (e) { console.error(e); }\n  };\n\n  const startAddingItem = () => {\n    setAddingItem(true);\n    setTimeout(() => document.getElementById('newItemInput')?.focus(), 50);\n  };\n\n  const cancelNewItem = (e) => {\n    newItemCancelledRef.current = true;\n    setAddingItem(false);\n    setNewItemTitle('');\n    e?.target.blur();\n  };\n\n  const commitNewItem = async () => {\n    setAddingItem(false);\n    if (!newItemTitle.trim()) { setNewItemTitle(''); return; }\n    const title = newItemTitle.trim();\n    const listId = selectedListId;\n    setNewItemTitle('');\n    try {\n      const id = await itemsClient.createTodoItem({ title, listId });\n      setLists(ls => ls.map(l => l.id === listId\n        ? { ...l, items: [...l.items, { id, listId, title, done: false, priority: priorityLevels[0]?.id }] } : l));\n    } catch (e) { console.error(e); }\n  };\n\n  if (!lists) return <span aria-busy=\"true\">Loading&hellip;</span>;\n\n  return (\n    <>\n      <hgroup>\n        <h1>Tasks</h1>\n        <p>Manage your todo lists and tasks.</p>\n      </hgroup>\n\n      <div className=\"todo-layout\">\n\n        {/* Sidebar */}\n        <div className=\"todo-sidebar\">\n          <div className=\"todo-panel-header\">\n            <h2>Lists</h2>\n            <button className=\"icon-btn\" onClick={showNewListDialog}><Plus size={20} strokeWidth={2} /></button>\n          </div>\n          <ul>\n            {lists.map(list => (\n              <li key={list.id}\n                aria-current={selectedList === list ? 'true' : undefined}\n                onClick={() => setSelectedListId(list.id)}>\n                <span>{list.title}</span>\n                <small>{remainingItems(list)}</small>\n              </li>\n            ))}\n          </ul>\n        </div>\n\n        {/* Items panel */}\n        {selectedList && (\n          <div className=\"todo-main\">\n            <div className=\"todo-panel-header\">\n              <h2>{selectedList.title}</h2>\n              <button className=\"icon-btn\" onClick={showListOptionsDialog}><Settings size={20} strokeWidth={2} /></button>\n            </div>\n\n            {selectedList.items.map((item, i) => (\n              <div key={item.id} className=\"todo-item\">\n                <input type=\"checkbox\" checked={item.done}\n                  onChange={e => updateCheckbox(item, e.target.checked)} />\n                {editingItem === item ? (\n                  <input id={`itemTitle${i}`} type=\"text\" className=\"todo-item-input\"\n                    value={editValue}\n                    onChange={e => setEditValue(e.target.value)}\n                    onKeyDown={e => {\n                      if (e.key === 'Enter') { e.target.blur(); }\n                      if (e.key === 'Escape') cancelEdit(e);\n                    }}\n                    onBlur={() => {\n                      if (editCancelledRef.current) { editCancelledRef.current = false; return; }\n                      commitEdit();\n                    }}\n                    autoFocus maxLength={200} />\n                ) : (\n                  <span className={`todo-item-text${item.done ? ' todo-done' : ''}`}\n                    onClick={() => editItem(item, `itemTitle${i}`)}>\n                    {item.title}\n                  </span>\n                )}\n                {item.id !== 0 && (\n                  <button className=\"icon-btn\" onClick={() => showItemDetailsDialog(item)}><MoreHorizontal size={20} strokeWidth={2} /></button>\n                )}\n              </div>\n            ))}\n\n            <div className=\"todo-item todo-new-item\">\n              <input type=\"checkbox\" disabled />\n              {addingItem ? (\n                <input id=\"newItemInput\" type=\"text\" className=\"todo-item-input\"\n                  value={newItemTitle}\n                  onChange={e => setNewItemTitle(e.target.value)}\n                  onKeyDown={e => {\n                    if (e.key === 'Enter') commitNewItem();\n                    if (e.key === 'Escape') cancelNewItem(e);\n                  }}\n                  onBlur={() => {\n                    if (newItemCancelledRef.current) { newItemCancelledRef.current = false; return; }\n                    commitNewItem();\n                  }}\n                  maxLength={200} />\n              ) : (\n                <span className=\"todo-item-text todo-new-item-placeholder\"\n                  onClick={startAddingItem}>New task…</span>\n              )}\n            </div>\n          </div>\n        )}\n      </div>\n\n      {/* New List dialog */}\n      <dialog ref={newListDialogRef}>\n        <article>\n          <header>\n            <h3>New List</h3>\n            <button rel=\"prev\" aria-label=\"Close\" onClick={closeNewListDialog}></button>\n          </header>\n          <label htmlFor=\"newListTitle\">Title</label>\n          <input type=\"text\" id=\"newListTitle\" placeholder=\"List title…\"\n            value={newListTitle} onChange={e => setNewListTitle(e.target.value)}\n            aria-invalid={newListError ? 'true' : undefined}\n            onKeyDown={e => e.key === 'Enter' && commitNewList()}\n            maxLength={200} />\n          {newListError && <small>{newListError}</small>}\n          <footer>\n            <button className=\"secondary\" onClick={closeNewListDialog}>Cancel</button>\n            <button onClick={commitNewList}>Create</button>\n          </footer>\n        </article>\n      </dialog>\n\n      {/* List Options dialog */}\n      <dialog ref={listOptionsDialogRef}>\n        <article>\n          <header>\n            <h3>List Options</h3>\n            <button rel=\"prev\" aria-label=\"Close\" onClick={closeListOptionsDialog}></button>\n          </header>\n          <label htmlFor=\"listOptionsTitle\">Title</label>\n          <input type=\"text\" id=\"listOptionsTitle\" placeholder=\"List name…\"\n            value={listOptionsEditor.title || ''}\n            onChange={e => setListOptionsEditor(ed => ({ ...ed, title: e.target.value }))}\n            onKeyDown={e => e.key === 'Enter' && updateListOptions()}\n            maxLength={200} />\n          <footer>\n            <button className=\"danger\" style={{ marginInlineEnd: 'auto' }} onClick={confirmDeleteList}>Delete</button>\n            <button className=\"secondary\" onClick={closeListOptionsDialog}>Cancel</button>\n            <button onClick={updateListOptions}>Update</button>\n          </footer>\n        </article>\n      </dialog>\n\n      {/* Delete List dialog */}\n      <dialog ref={deleteListDialogRef}>\n        <article>\n          <header>\n            <h3>Delete \"{selectedList?.title}\"?</h3>\n            <button rel=\"prev\" aria-label=\"Close\" onClick={closeDeleteListDialog}></button>\n          </header>\n          <p>All items will be permanently deleted.</p>\n          <footer>\n            <button className=\"secondary\" onClick={closeDeleteListDialog}>Cancel</button>\n            <button className=\"danger\"\n              style={{ '--pico-background-color': 'var(--pico-del-color)', '--pico-border-color': 'var(--pico-del-color)', '--pico-color': '#fff' }}\n              onClick={deleteListConfirmed}>Delete</button>\n          </footer>\n        </article>\n      </dialog>\n\n      {/* Item Details dialog */}\n      <dialog ref={itemDetailsDialogRef}>\n        <article>\n          <header>\n            <h3>Item Details</h3>\n            <button rel=\"prev\" aria-label=\"Close\" onClick={closeItemDetailsDialog}></button>\n          </header>\n          <label htmlFor=\"itemList\">List</label>\n          <select id=\"itemList\" value={itemDetailsEditor.listId || ''}\n            onChange={e => setItemDetailsEditor(ed => ({ ...ed, listId: +e.target.value }))}>\n            {lists.map(list => (\n              <option key={list.id} value={list.id}>{list.title}</option>\n            ))}\n          </select>\n          <label htmlFor=\"itemPriority\">Priority</label>\n          <select id=\"itemPriority\" value={itemDetailsEditor.priority || ''}\n            onChange={e => setItemDetailsEditor(ed => ({ ...ed, priority: +e.target.value }))}>\n            {priorityLevels.map(level => (\n              <option key={level.id} value={level.id}>{level.title}</option>\n            ))}\n          </select>\n          <label htmlFor=\"itemNote\">Note</label>\n          <textarea id=\"itemNote\" rows={3} value={itemDetailsEditor.note || ''}\n            onChange={e => setItemDetailsEditor(ed => ({ ...ed, note: e.target.value }))}></textarea>\n          <footer>\n            <button className=\"danger\" style={{ marginInlineEnd: 'auto' }} onClick={() => deleteItem(selectedItem)}>Delete</button>\n            <button className=\"secondary\" onClick={closeItemDetailsDialog}>Cancel</button>\n            <button onClick={updateItemDetails}>Update</button>\n          </footer>\n        </article>\n      </dialog>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/Web/ClientApp-React/src/components/Weather.jsx",
    "content": "import { useState, useEffect } from 'react';\nimport { WeatherForecastsClient } from '../web-api-client.ts';\n\nexport function Weather() {\n  const [forecasts, setForecasts] = useState([]);\n  const [loading, setLoading] = useState(true);\n  const [error, setError] = useState(null);\n\n  useEffect(() => {\n    async function fetchData() {\n      try {\n        const client = new WeatherForecastsClient();\n        const data = await client.getWeatherForecasts();\n        setForecasts(data);\n      } catch (e) {\n        setError('Unable to load weather forecasts. Please try again later.');\n      } finally {\n        setLoading(false);\n      }\n    }\n    fetchData();\n  }, []);\n\n  return (\n    <div>\n      <h1>Weather</h1>\n      <p>This component demonstrates fetching data from the server.</p>\n      {loading && <span aria-busy=\"true\">Fetching your weather forecast...</span>}\n      {error && <p className=\"error\">{error}</p>}\n      {!loading && !error && (\n        <table>\n          <thead>\n            <tr>\n              <th>Date</th>\n              <th>Temp. (C)</th>\n              <th>Temp. (F)</th>\n              <th>Summary</th>\n            </tr>\n          </thead>\n          <tbody>\n            {forecasts.map(forecast =>\n              <tr key={forecast.date}>\n                <td>{new Date(forecast.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</td>\n                <td>{forecast.temperatureC}</td>\n                <td>{forecast.temperatureF}</td>\n                <td>{forecast.summary}</td>\n              </tr>\n            )}\n          </tbody>\n        </table>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/Web/ClientApp-React/src/components/api-authorization/AuthContext.jsx",
    "content": "import { createContext, useContext, useState, useEffect } from 'react';\nimport { UsersClient, LoginRequest, RegisterRequest } from '../../web-api-client';\n\nconst AuthContext = createContext(null);\n\nconst client = new UsersClient();\n\nexport function AuthProvider({ children }) {\n  const [isAuthenticated, setIsAuthenticated] = useState(false);\n  const [isLoading, setIsLoading] = useState(true);\n\n  useEffect(() => {\n    client.infoGET()\n      .then(() => setIsAuthenticated(true))\n      .catch(() => setIsAuthenticated(false))\n      .finally(() => setIsLoading(false));\n  }, []);\n\n  const login = (email, password) =>\n    client.login(true, undefined, new LoginRequest({ email, password }))\n      .then(() => setIsAuthenticated(true));\n\n  const register = (email, password) =>\n    client.register(new RegisterRequest({ email, password }));\n\n  const logout = () =>\n    client.logout({})\n      .then(() => setIsAuthenticated(false));\n\n  return (\n    <AuthContext.Provider value={{ isAuthenticated, isLoading, login, register, logout }}>\n      {children}\n    </AuthContext.Provider>\n  );\n}\n\nexport const useAuth = () => useContext(AuthContext);"
  },
  {
    "path": "src/Web/ClientApp-React/src/components/api-authorization/LoginPage.jsx",
    "content": "import { useState } from 'react';\nimport { useNavigate, useLocation, Link } from 'react-router-dom';\nimport { useAuth } from './AuthContext';\n\nexport function LoginPage() {\n  const [email, setEmail] = useState('');\n  const [password, setPassword] = useState('');\n  const [invalid, setInvalid] = useState(false);\n  const { login } = useAuth();\n  const navigate = useNavigate();\n  const location = useLocation();\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    try {\n      await login(email, password);\n      const returnUrl = location.state?.returnUrl || '/';\n      navigate(returnUrl, { replace: true });\n    } catch {\n      setInvalid(true);\n    }\n  };\n\n  const handleChange = (setter) => (e) => {\n    setInvalid(false);\n    setter(e.target.value);\n  };\n\n  return (\n    <article>\n      <h2>Log in</h2>\n      <form onSubmit={handleSubmit}>\n        <label htmlFor=\"email\">Email</label>\n        <input type=\"email\" id=\"email\" autoComplete=\"username\"\n          value={email} onChange={handleChange(setEmail)}\n          aria-invalid={invalid || undefined}\n          aria-describedby={invalid ? 'login-error' : undefined} />\n        <label htmlFor=\"password\">Password</label>\n        <input type=\"password\" id=\"password\" autoComplete=\"current-password\"\n          value={password} onChange={handleChange(setPassword)}\n          aria-invalid={invalid || undefined}\n          aria-describedby={invalid ? 'login-error' : undefined} />\n        {invalid && <small id=\"login-error\">Invalid email or password.</small>}\n        <button type=\"submit\">Log in</button>\n        <p style={{ marginTop: '1rem' }}>Don't have an account? <Link to=\"/register\">Register</Link></p>\n      </form>\n    </article>\n  );\n}\n"
  },
  {
    "path": "src/Web/ClientApp-React/src/components/api-authorization/ProtectedRoute.jsx",
    "content": "import { Navigate, useLocation } from 'react-router-dom';\nimport { useAuth } from './AuthContext';\n\nexport function ProtectedRoute({ children }) {\n  const { isAuthenticated, isLoading } = useAuth();\n  const location = useLocation();\n\n  if (isLoading) return null;\n  if (!isAuthenticated) return <Navigate to=\"/login\" state={{ returnUrl: location.pathname }} replace />;\n  return children;\n}\n"
  },
  {
    "path": "src/Web/ClientApp-React/src/components/api-authorization/RegisterPage.jsx",
    "content": "import { useState } from 'react';\nimport { useNavigate, Link } from 'react-router-dom';\nimport { useAuth } from './AuthContext';\n\nconst MIN_PASSWORD_LENGTH = 6;\n\nfunction validateEmail(value) {\n  return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value);\n}\n\nexport function RegisterPage() {\n  const [email, setEmail] = useState('');\n  const [password, setPassword] = useState('');\n  const [emailTouched, setEmailTouched] = useState(false);\n  const [passwordTouched, setPasswordTouched] = useState(false);\n  const [error, setError] = useState('');\n  const { register } = useAuth();\n  const navigate = useNavigate();\n\n  const emailValid = validateEmail(email);\n  const passwordValid = password.length >= MIN_PASSWORD_LENGTH;\n\n  const emailInvalid = emailTouched ? !emailValid : undefined;\n  const passwordInvalid = passwordTouched ? !passwordValid : undefined;\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    setError('');\n    setEmailTouched(true);\n    setPasswordTouched(true);\n    if (!emailValid || !passwordValid) return;\n    try {\n      await register(email, password);\n      navigate('/login');\n    } catch {\n      setError('Registration failed. Please try again.');\n    }\n  };\n\n  return (\n    <article>\n      <h2>Register</h2>\n      {error && <p className=\"error\">{error}</p>}\n      <form onSubmit={handleSubmit}>\n        <label htmlFor=\"email\">Email</label>\n        <input type=\"email\" id=\"email\" autoComplete=\"username\"\n          value={email}\n          onChange={e => setEmail(e.target.value)}\n          onBlur={() => setEmailTouched(true)}\n          aria-invalid={emailInvalid}\n          aria-describedby=\"email-helper\" />\n        <small id=\"email-helper\">\n          {emailTouched && !emailValid ? 'Please enter a valid email address.' : ''}\n        </small>\n        <label htmlFor=\"password\">Password</label>\n        <input type=\"password\" id=\"password\" autoComplete=\"new-password\"\n          value={password}\n          onChange={e => setPassword(e.target.value)}\n          onBlur={() => setPasswordTouched(true)}\n          aria-invalid={passwordInvalid}\n          aria-describedby=\"password-helper\" />\n        <small id=\"password-helper\">\n          {passwordTouched && !passwordValid\n            ? `Password must be at least ${MIN_PASSWORD_LENGTH} characters.`\n            : ''}\n        </small>\n        <button type=\"submit\">Register</button>\n        <p style={{ marginTop: '1rem' }}>Already have an account? <Link to=\"/login\">Log in</Link></p>\n      </form>\n    </article>\n  );\n}\n"
  },
  {
    "path": "src/Web/ClientApp-React/src/main.jsx",
    "content": "import './styles.scss';\nimport React from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { BrowserRouter } from 'react-router-dom';\nimport App from './App';\n\nconst baseUrl = document.getElementsByTagName('base')[0].getAttribute('href');\nconst root = createRoot(document.getElementById('root'));\n\nroot.render(\n  <BrowserRouter basename={baseUrl}>\n    <App />\n  </BrowserRouter>\n);\n"
  },
  {
    "path": "src/Web/ClientApp-React/src/styles.scss",
    "content": "@use \"sass:map\";\n@use \"@picocss/pico/scss/index\" as * with (\n  $theme-color: \"violet\",\n  $semantic-root-element: \"#root\",\n  $enable-semantic-container: true,\n  $enable-classes: false\n);\n\n// ── Base font size & typography ───────────────────────────────────────────────\nhtml {\n  font-size: 95%;\n}\n\n:root {\n  --pico-font-family-sans-serif: \"Inter\", system-ui, sans-serif;\n  --pico-font-family-headings: \"Outfit\", sans-serif;\n  --pico-font-family-monospace: \"JetBrains Mono\", monospace;\n  --pico-border-radius: 0.375rem;\n  --pico-line-height: 1.5;\n}\n\n// ── Header ────────────────────────────────────────────────────────────────────\n:root {\n  --header-height: 4.5rem;\n}\n\n#root > header {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  width: 100%;\n  max-width: none;\n  height: var(--header-height);\n  padding: 0;\n  margin: 0;\n  z-index: 100;\n  background: color-mix(in srgb, var(--pico-background-color) 85%, transparent);\n  backdrop-filter: blur(8px);\n  border-bottom: 1px solid var(--pico-muted-border-color);\n}\n\n#root > header nav {\n  height: 100%;\n  display: flex;\n  align-items: center;\n  width: 100%;\n  margin-inline: auto;\n  padding-inline: var(--pico-block-spacing-horizontal);\n\n  @media (min-width: 576px)  { max-width: 510px;  padding-inline: 0; }\n  @media (min-width: 768px)  { max-width: 700px; }\n  @media (min-width: 1024px) { max-width: 950px; }\n  @media (min-width: 1280px) { max-width: 1200px; }\n  @media (min-width: 1536px) { max-width: 1450px; }\n}\n\n// ── Main content ──────────────────────────────────────────────────────────────\n#root > main {\n  padding-top: calc(var(--header-height) + var(--pico-block-spacing-vertical) * 2);\n}\n\n// ── Nav brand ─────────────────────────────────────────────────────────────────\n#root > header nav > ul:first-child a {\n  font-family: \"Outfit\", sans-serif;\n  font-size: 1.75rem;\n  font-weight: 700;\n  text-decoration: none;\n  color: var(--pico-contrast);\n}\n\n// ── Login / Register centered layout ──────────────────────────────────────────\n#root > main:has(> article:only-child) {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  min-height: 100vh;\n\n  > article {\n    width: 100%;\n    max-width: map.get(map.get($breakpoints, \"md\"), \"viewport\");\n  }\n}\n\n// ── Danger color — more vivid in dark mode ────────────────────────────────────\n[data-theme=\"dark\"] {\n  --pico-del-color: #f05050;\n}\n\n@media (prefers-color-scheme: dark) {\n  :root:not([data-theme=\"light\"]) {\n    --pico-del-color: #f05050;\n  }\n}\n\n// ── Nav separator ─────────────────────────────────────────────────────────────\n.nav-separator {\n  width: 1px;\n  height: 1.2rem;\n  background: var(--pico-muted-border-color);\n  padding: 0;\n  margin-inline: 0.25rem;\n}\n\n// ── Theme toggle ───────────────────────────────────────────────────────────────\n.theme-toggle-btn {\n  background: none;\n  border: none;\n  box-shadow: none;\n  padding: 0;\n  line-height: 1;\n  color: var(--pico-muted-color);\n  cursor: pointer;\n  --pico-form-element-spacing-vertical: 0;\n  --pico-form-element-spacing-horizontal: 0;\n\n  &:hover {\n    background: none;\n    color: var(--pico-primary);\n  }\n}\n\n// ── Icon buttons ───────────────────────────────────────────────────────────────\n.icon-btn {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  width: 2rem;\n  height: 2rem;\n  padding: 0;\n  font-size: 1.25rem;\n  font-weight: bold;\n  line-height: 1;\n  flex-shrink: 0;\n  background: transparent !important;\n  border-color: transparent !important;\n  color: var(--pico-muted-color);\n  box-shadow: none !important;\n  --pico-form-element-spacing-vertical: 0;\n  --pico-form-element-spacing-horizontal: 0;\n\n  &:hover {\n    color: var(--pico-primary);\n  }\n}\n\n// ── Todo layout ───────────────────────────────────────────────────────────────\n.todo-layout {\n  display: grid;\n  grid-template-columns: 220px 1fr;\n  gap: calc(var(--pico-block-spacing-horizontal) * 2);\n  align-items: start;\n}\n\n.todo-sidebar {\n  border-right: 1px solid var(--pico-muted-border-color);\n  padding-right: calc(var(--pico-block-spacing-horizontal) * 2);\n\n  ul {\n    list-style: none;\n    padding: 0;\n    margin: 0;\n  }\n\n  li {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: calc(var(--pico-spacing) * 0.4) calc(var(--pico-spacing) * 0.5);\n    border-radius: var(--pico-border-radius);\n    cursor: pointer;\n    color: var(--pico-muted-color);\n\n    &[aria-current=\"true\"] {\n      background: var(--pico-primary-background);\n      color: var(--pico-primary-inverse);\n    }\n\n    &:hover:not([aria-current=\"true\"]) {\n      background: var(--pico-card-sectioning-background-color);\n      color: var(--pico-color);\n    }\n\n    small {\n      font-size: 0.75rem;\n      opacity: 0.7;\n    }\n  }\n}\n\n.todo-main {\n  min-width: 0;\n  padding-left: var(--pico-spacing);\n}\n\n.todo-panel-header {\n  display: flex;\n  align-items: baseline;\n  gap: 0.25rem;\n  margin-bottom: var(--pico-spacing);\n}\n\n.todo-item {\n  display: flex;\n  align-items: baseline;\n  gap: calc(var(--pico-spacing) * 0.5);\n  padding-block: calc(var(--pico-spacing) * 0.35);\n  border-bottom: 1px solid var(--pico-muted-border-color);\n\n  input[type=\"checkbox\"] {\n    margin: 0;\n    flex-shrink: 0;\n    align-self: center;\n  }\n}\n\n.todo-item-text {\n  flex: 1;\n  cursor: pointer;\n  word-break: break-word;\n}\n\n.todo-done {\n  text-decoration: line-through;\n  color: var(--pico-muted-color);\n}\n\n.todo-item-input {\n  flex: 1;\n  align-self: center;\n  margin: 0 !important;\n  padding: 0;\n  min-height: unset;\n  --pico-form-element-spacing-horizontal: 0;\n  --pico-form-element-spacing-vertical: 0;\n  border: none;\n  border-bottom: 1px solid var(--pico-primary);\n  border-radius: 0;\n  background: transparent;\n  box-shadow: none;\n\n  &:not(:focus) {\n    border-bottom-color: transparent;\n  }\n}\n\n.todo-new-item {\n  border-bottom: none;\n\n  input[type=\"checkbox\"] {\n    opacity: 0.3;\n  }\n}\n\n.todo-new-item-placeholder {\n  color: var(--pico-muted-color);\n  cursor: pointer;\n}\n\n// ── Dialog header/footer ───────────────────────────────────────────────────────\ndialog article header {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n\n  > :first-child {\n    margin: 0;\n  }\n\n  > button[rel=\"prev\"] {\n    position: static;\n    margin: 0;\n    margin-inline-start: auto;\n    flex-shrink: 0;\n  }\n}\n\ndialog article footer {\n  display: flex;\n  justify-content: flex-end;\n  align-items: center;\n  gap: calc(var(--pico-spacing) * 0.5);\n}\n\n// ── Button variants (not compiled in classless mode — defined manually) ───────\nbutton.secondary,\n[type=\"submit\"].secondary,\n[type=\"button\"].secondary,\n[role=\"button\"].secondary {\n  --pico-background-color: transparent;\n  --pico-border-color: var(--pico-secondary);\n  --pico-color: var(--pico-secondary);\n\n  &:is(:hover, :active, :focus) {\n    --pico-background-color: color-mix(in srgb, var(--pico-secondary) 10%, transparent);\n    --pico-border-color: var(--pico-secondary-hover);\n    --pico-color: var(--pico-secondary-hover);\n  }\n}\n\nbutton.danger,\n[type=\"submit\"].danger,\n[type=\"button\"].danger,\n[role=\"button\"].danger {\n  --pico-background-color: transparent;\n  --pico-border-color: var(--pico-del-color);\n  --pico-color: var(--pico-del-color);\n\n  &:is(:hover, :active, :focus) {\n    --pico-background-color: color-mix(in srgb, var(--pico-del-color) 10%, transparent);\n    --pico-border-color: color-mix(in srgb, var(--pico-del-color) 80%, black);\n    --pico-color: color-mix(in srgb, var(--pico-del-color) 80%, black);\n  }\n}\n\n.error {\n  color: var(--pico-del-color);\n}\n"
  },
  {
    "path": "src/Web/ClientApp-React/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "src/Web/ClientApp-React/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"isolatedModules\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true,\n\n    /* Compatibility */\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"resolveJsonModule\": true,\n    \"allowJs\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "src/Web/ClientApp-React/vite.config.ts",
    "content": "import { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\n\nconst target =\n  process.env['services__webapi__https__0'] ||\n  process.env['services__webapi__http__0'];\n\nconst proxyOptions = target\n  ? { target, secure: false, changeOrigin: true }\n  : undefined;\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [react()],\n  server: {\n    port: parseInt(process.env.PORT!),\n    proxy: proxyOptions\n      ? {\n          '/api': proxyOptions,\n          '/openapi': proxyOptions,\n          '/scalar': proxyOptions,\n          '/weatherforecast': proxyOptions,\n          '/WeatherForecast': proxyOptions,\n        }\n      : undefined,\n  },\n  build: {\n    outDir: 'build',\n  },\n});\n"
  },
  {
    "path": "src/Web/DependencyInjection.cs",
    "content": "using Azure.Identity;\nusing CleanArchitecture.Application.Common.Interfaces;\nusing CleanArchitecture.Infrastructure.Data;\nusing CleanArchitecture.Web.Services;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Microsoft.Extensions.DependencyInjection;\n\npublic static class DependencyInjection\n{\n    public static void AddWebServices(this IHostApplicationBuilder builder)\n    {\n        builder.Services.AddDatabaseDeveloperPageExceptionFilter();\n\n        builder.Services.AddScoped<IUser, CurrentUser>();\n\n        builder.Services.AddHttpContextAccessor();\n\n        builder.Services.AddExceptionHandler<ProblemDetailsExceptionHandler>();\n\n        // Customise default API behaviour\n        builder.Services.Configure<ApiBehaviorOptions>(options =>\n            options.SuppressModelStateInvalidFilter = true);\n\n        builder.Services.AddEndpointsApiExplorer();\n\n        builder.Services.AddOpenApi(options =>\n        {\n            options.AddOperationTransformer<ApiExceptionOperationTransformer>();\n            options.AddOperationTransformer<IdentityApiOperationTransformer>();\n#if (UseApiOnly)\n            options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();\n#endif\n        });\n\n        builder.Services.AddCors();\n    }\n\n    public static void AddKeyVaultIfConfigured(this IHostApplicationBuilder builder)\n    {\n        var keyVaultUri = builder.Configuration[\"AZURE_KEY_VAULT_ENDPOINT\"];\n        if (!string.IsNullOrWhiteSpace(keyVaultUri))\n        {\n            builder.Configuration.AddAzureKeyVault(\n                new Uri(keyVaultUri),\n                new DefaultAzureCredential());\n        }\n    }\n}\n"
  },
  {
    "path": "src/Web/Endpoints/TodoItems.cs",
    "content": "using CleanArchitecture.Application.Common.Models;\nusing CleanArchitecture.Application.TodoItems.Commands.CreateTodoItem;\nusing CleanArchitecture.Application.TodoItems.Commands.DeleteTodoItem;\nusing CleanArchitecture.Application.TodoItems.Commands.UpdateTodoItem;\nusing CleanArchitecture.Application.TodoItems.Commands.UpdateTodoItemDetail;\nusing CleanArchitecture.Application.TodoItems.Queries.GetTodoItemsWithPagination;\nusing Microsoft.AspNetCore.Http.HttpResults;\n\nnamespace CleanArchitecture.Web.Endpoints;\n\npublic class TodoItems : IEndpointGroup\n{\n    public static void Map(RouteGroupBuilder groupBuilder)\n    {\n        groupBuilder.RequireAuthorization();\n\n        groupBuilder.MapGet(GetTodoItemsWithPagination);\n        groupBuilder.MapPost(CreateTodoItem);\n        groupBuilder.MapPut(UpdateTodoItem, \"{id}\");\n        groupBuilder.MapPatch(UpdateTodoItemDetail, \"UpdateDetail/{id}\");\n        groupBuilder.MapDelete(DeleteTodoItem, \"{id}\");\n    }\n\n    [EndpointSummary(\"Get Todo Items with Pagination\")]\n    [EndpointDescription(\"Retrieves a paginated list of todo items based on the provided query parameters.\")]\n    public static async Task<Ok<PaginatedList<TodoItemBriefDto>>> GetTodoItemsWithPagination(\n        ISender sender,\n        [AsParameters] GetTodoItemsWithPaginationQuery query)\n    {\n        var result = await sender.Send(query);\n\n        return TypedResults.Ok(result);\n    }\n\n    [EndpointSummary(\"Create a new Todo Item\")]\n    [EndpointDescription(\"Creates a new todo item using the provided details and returns the ID of the created item.\")]\n    public static async Task<Created<int>> CreateTodoItem(ISender sender, CreateTodoItemCommand command)\n    {\n        var id = await sender.Send(command);\n\n        return TypedResults.Created($\"/{nameof(TodoItems)}/{id}\", id);\n    }\n\n    [EndpointSummary(\"Update a Todo Item\")]\n    [EndpointDescription(\"Updates the specified todo item. The ID in the URL must match the ID in the payload.\")]\n    public static async Task<Results<NoContent, BadRequest>> UpdateTodoItem(ISender sender, int id, UpdateTodoItemCommand command)\n    {\n        if (id != command.Id)\n            return TypedResults.BadRequest();\n\n        await sender.Send(command);\n\n        return TypedResults.NoContent();\n    }\n\n    [EndpointSummary(\"Update Todo Item Details\")]\n    [EndpointDescription(\"Updates the detail fields of a specific todo item. The ID in the URL must match the ID in the payload.\")]\n    public static async Task<Results<NoContent, BadRequest>> UpdateTodoItemDetail(ISender sender, int id, UpdateTodoItemDetailCommand command)\n    {\n        if (id != command.Id) return TypedResults.BadRequest();\n\n        await sender.Send(command);\n\n        return TypedResults.NoContent();\n    }\n\n    [EndpointSummary(\"Delete a Todo Item\")]\n    [EndpointDescription(\"Deletes the todo item with the specified ID.\")]\n    public static async Task<NoContent> DeleteTodoItem(ISender sender, int id)\n    {\n        await sender.Send(new DeleteTodoItemCommand(id));\n\n        return TypedResults.NoContent();\n    }\n}\n"
  },
  {
    "path": "src/Web/Endpoints/TodoLists.cs",
    "content": "using CleanArchitecture.Application.TodoLists.Commands.CreateTodoList;\nusing CleanArchitecture.Application.TodoLists.Commands.DeleteTodoList;\nusing CleanArchitecture.Application.TodoLists.Commands.UpdateTodoList;\nusing CleanArchitecture.Application.TodoLists.Queries.GetTodos;\nusing Microsoft.AspNetCore.Http.HttpResults;\n\nnamespace CleanArchitecture.Web.Endpoints;\n\npublic class TodoLists : IEndpointGroup\n{\n    public static void Map(RouteGroupBuilder groupBuilder)\n    {\n        groupBuilder.RequireAuthorization();\n\n        groupBuilder.MapGet(GetTodoLists);\n        groupBuilder.MapPost(CreateTodoList);\n        groupBuilder.MapPut(UpdateTodoList, \"{id}\");\n        groupBuilder.MapDelete(DeleteTodoList, \"{id}\");\n    }\n\n    [EndpointSummary(\"Get all Todo Lists\")]\n    [EndpointDescription(\"Retrieves all todo lists along with their items.\")]\n    public static async Task<Ok<TodosVm>> GetTodoLists(ISender sender)\n    {\n        var vm = await sender.Send(new GetTodosQuery());\n\n        return TypedResults.Ok(vm);\n    }\n\n    [EndpointSummary(\"Create a new Todo List\")]\n    [EndpointDescription(\"Creates a new todo list using the provided details and returns the ID of the created list.\")]\n    public static async Task<Created<int>> CreateTodoList(ISender sender, CreateTodoListCommand command)\n    {\n        var id = await sender.Send(command);\n\n        return TypedResults.Created($\"/{nameof(TodoLists)}/{id}\", id);\n    }\n\n    [EndpointSummary(\"Update a Todo List\")]\n    [EndpointDescription(\"Updates the specified todo list. The ID in the URL must match the ID in the payload.\")]\n    public static async Task<Results<NoContent, BadRequest>> UpdateTodoList(ISender sender, int id, UpdateTodoListCommand command)\n    {\n        if (id != command.Id) return TypedResults.BadRequest();\n\n        await sender.Send(command);\n\n        return TypedResults.NoContent();\n    }\n\n    [EndpointSummary(\"Delete a Todo List\")]\n    [EndpointDescription(\"Deletes the todo list with the specified ID.\")]\n    public static async Task<NoContent> DeleteTodoList(ISender sender, int id)\n    {\n        await sender.Send(new DeleteTodoListCommand(id));\n\n        return TypedResults.NoContent();\n    }\n}\n"
  },
  {
    "path": "src/Web/Endpoints/Users.cs",
    "content": "﻿using CleanArchitecture.Infrastructure.Identity;\nusing Microsoft.AspNetCore.Http.HttpResults;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace CleanArchitecture.Web.Endpoints;\n\npublic class Users : IEndpointGroup\n{\n    public static void Map(RouteGroupBuilder groupBuilder)\n    {\n        groupBuilder.MapIdentityApi<ApplicationUser>();\n\n        groupBuilder.MapPost(Logout, \"logout\").RequireAuthorization();\n    }\n\n    [EndpointSummary(\"Log out\")]\n    [EndpointDescription(\"Logs out the current user by clearing the authentication cookie.\")]\n    public static async Task<Results<Ok, UnauthorizedHttpResult>> Logout(SignInManager<ApplicationUser> signInManager, [FromBody] object empty)\n    {\n        if (empty != null)\n        {\n            await signInManager.SignOutAsync();\n            return TypedResults.Ok();\n        }\n\n        return TypedResults.Unauthorized();\n    }\n}\n"
  },
  {
    "path": "src/Web/Endpoints/WeatherForecasts.cs",
    "content": "using CleanArchitecture.Application.WeatherForecasts.Queries.GetWeatherForecasts;\nusing Microsoft.AspNetCore.Http.HttpResults;\n\nnamespace CleanArchitecture.Web.Endpoints;\n\npublic class WeatherForecasts : IEndpointGroup\n{\n    public static void Map(RouteGroupBuilder groupBuilder)\n    {\n        groupBuilder.RequireAuthorization();\n\n        groupBuilder.MapGet(GetWeatherForecasts);\n    }\n\n    [EndpointSummary(\"Get Weather Forecasts\")]\n    [EndpointDescription(\"Retrieves a list of weather forecasts for the next few days.\")]\n    public static async Task<Ok<IEnumerable<WeatherForecast>>> GetWeatherForecasts(ISender sender)\n    {\n        var forecasts = await sender.Send(new GetWeatherForecastsQuery());\n\n        return TypedResults.Ok(forecasts);\n    }\n}\n"
  },
  {
    "path": "src/Web/GlobalUsings.cs",
    "content": "global using Ardalis.GuardClauses;\nglobal using CleanArchitecture.Web.Infrastructure;\nglobal using MediatR;\n"
  },
  {
    "path": "src/Web/Infrastructure/ApiExceptionOperationTransformer.cs",
    "content": "using Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.OpenApi;\nusing Microsoft.OpenApi;\n\nnamespace CleanArchitecture.Web.Infrastructure;\n\n/// <summary>\n/// Adds standard error responses to every OpenAPI operation. A 400 Bad Request is added to all\n/// operations because every request passes through <c>ValidationBehaviour</c> in the MediatR\n/// pipeline. 401 Unauthorized and 403 Forbidden are added only to operations that carry\n/// <see cref=\"IAuthorizeData\"/> metadata.\n/// </summary>\ninternal sealed class ApiExceptionOperationTransformer : IOpenApiOperationTransformer\n{\n    public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)\n    {\n        operation.Responses ??= [];\n        operation.Responses.TryAdd(\"400\", new OpenApiResponse { Description = \"Bad Request\" });\n\n        var requiresAuth = context.Description.ActionDescriptor.EndpointMetadata\n            .Any(m => m is IAuthorizeData);\n\n        if (requiresAuth)\n        {\n            operation.Responses.TryAdd(\"401\", new OpenApiResponse { Description = \"Unauthorized\" });\n            operation.Responses.TryAdd(\"403\", new OpenApiResponse { Description = \"Forbidden\" });\n        }\n\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "src/Web/Infrastructure/BearerSecuritySchemeTransformer.cs",
    "content": "using Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.OpenApi;\nusing Microsoft.OpenApi;\n\nnamespace CleanArchitecture.Web.Infrastructure;\n\n/// <summary>\n/// Adds the Bearer JWT security scheme to the OpenAPI document when Bearer authentication\n/// is configured, enabling the Scalar UI to send <c>Authorization: Bearer &lt;token&gt;</c>\n/// headers from the interactive documentation.\n/// </summary>\ninternal sealed class BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider) : IOpenApiDocumentTransformer\n{\n    public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)\n    {\n        var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync();\n        if (authenticationSchemes.Any(authScheme => authScheme.Name == \"Bearer\"))\n        {\n            var requirements = new Dictionary<string, IOpenApiSecurityScheme>\n            {\n                [\"Bearer\"] = new OpenApiSecurityScheme\n                {\n                    Type = SecuritySchemeType.Http,\n                    Scheme = \"bearer\",\n                    In = ParameterLocation.Header,\n                    BearerFormat = \"Json Web Token\"\n                }\n            };\n            document.Components ??= new OpenApiComponents();\n            document.Components.SecuritySchemes = requirements;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Web/Infrastructure/EndpointRouteBuilderExtensions.cs",
    "content": "﻿using System.Diagnostics.CodeAnalysis;\n\nnamespace CleanArchitecture.Web.Infrastructure;\n\n/// <summary>\n/// Extends <see cref=\"IEndpointRouteBuilder\"/> with convenience overloads used inside\n/// <see cref=\"IEndpointGroup.Map\"/>. Each method wraps the standard ASP.NET Core\n/// <c>Map{Verb}</c> call and automatically derives the endpoint name from the handler's\n/// method name, which becomes the OpenAPI <c>operationId</c> and is used for typed\n/// client generation (e.g. <c>nswag</c>).\n/// <para>\n/// <c>pattern</c> is optional for GET and POST (collection-level operations that typically\n/// have no route parameter) but required for PUT, PATCH, and DELETE (resource-level\n/// operations that almost always target a specific item by ID, e.g. <c>\"{id}\"</c>).\n/// </para>\n/// </summary>\npublic static class EndpointRouteBuilderExtensions\n{\n    /// <inheritdoc cref=\"EndpointRouteBuilderExtensions\"/>\n    public static RouteHandlerBuilder MapGet(this IEndpointRouteBuilder builder, Delegate handler, [StringSyntax(\"Route\")] string pattern = \"\")\n    {\n        Guard.Against.AnonymousMethod(handler);\n\n        return builder.MapGet(pattern, handler)\n              .WithName(handler.Method.Name);\n    }\n\n    /// <inheritdoc cref=\"EndpointRouteBuilderExtensions\"/>\n    public static RouteHandlerBuilder MapPost(this IEndpointRouteBuilder builder, Delegate handler, [StringSyntax(\"Route\")] string pattern = \"\")\n    {\n        Guard.Against.AnonymousMethod(handler);\n\n        return builder.MapPost(pattern, handler)\n            .WithName(handler.Method.Name);\n    }\n\n    /// <inheritdoc cref=\"EndpointRouteBuilderExtensions\"/>\n    public static RouteHandlerBuilder MapPut(this IEndpointRouteBuilder builder, Delegate handler, [StringSyntax(\"Route\")] string pattern)\n    {\n        Guard.Against.AnonymousMethod(handler);\n\n        return builder.MapPut(pattern, handler)\n            .WithName(handler.Method.Name);\n    }\n\n    /// <inheritdoc cref=\"EndpointRouteBuilderExtensions\"/>\n    public static RouteHandlerBuilder MapPatch(this IEndpointRouteBuilder builder, Delegate handler, [StringSyntax(\"Route\")] string pattern)\n    {\n        Guard.Against.AnonymousMethod(handler);\n\n        return builder.MapPatch(pattern, handler)\n            .WithName(handler.Method.Name);\n    }\n\n    /// <inheritdoc cref=\"EndpointRouteBuilderExtensions\"/>\n    public static RouteHandlerBuilder MapDelete(this IEndpointRouteBuilder builder, Delegate handler, [StringSyntax(\"Route\")] string pattern)\n    {\n        Guard.Against.AnonymousMethod(handler);\n\n        return builder.MapDelete(pattern, handler)\n            .WithName(handler.Method.Name);\n    }\n}\n"
  },
  {
    "path": "src/Web/Infrastructure/IEndpointGroup.cs",
    "content": "namespace CleanArchitecture.Web.Infrastructure;\n\n/// <summary>\n/// Defines a group of related Minimal API endpoints.\n/// Implementations are automatically discovered and registered as a route group with a matching\n/// OpenAPI tag. By default the route prefix is <c>/api/{ClassName}</c>; override\n/// <see cref=\"RoutePrefix\"/> to use a custom path, including nested resource paths such as\n/// <c>/api/TodoLists/{todoListId}/TodoItems</c>.\n/// </summary>\npublic interface IEndpointGroup\n{\n    /// <summary>\n    /// The route prefix for this endpoint group.\n    /// Defaults to <c>/api/{ClassName}</c>. Override to specify a custom or nested path.\n    /// </summary>\n    static virtual string? RoutePrefix => null;\n\n    static abstract void Map(RouteGroupBuilder groupBuilder);\n}\n"
  },
  {
    "path": "src/Web/Infrastructure/IdentityApiOperationTransformer.cs",
    "content": "using Microsoft.AspNetCore.OpenApi;\nusing Microsoft.OpenApi;\n\nnamespace CleanArchitecture.Web.Infrastructure;\n\n/// <summary>\n/// Adds human-readable summaries and descriptions to the ASP.NET Core Identity endpoints\n/// registered by <c>MapIdentityApi</c>. Those endpoints are generated by the framework and\n/// cannot be annotated with <see cref=\"EndpointSummaryAttribute\"/> or\n/// <see cref=\"EndpointDescriptionAttribute\"/> directly, so metadata is applied here by\n/// matching on the operation's relative path (and HTTP method for ambiguous paths).\n/// </summary>\ninternal sealed class IdentityApiOperationTransformer : IOpenApiOperationTransformer\n{\n    private static readonly Dictionary<string, (string Summary, string Description)> _metadata = new()\n    {\n        [\"api/Users/register\"]                  = (\"Register\", \"Creates a new user account.\"),\n        [\"api/Users/login\"]                     = (\"Log in\", \"Authenticates a user. Use ?useCookies=true for cookie-based authentication.\"),\n        [\"api/Users/refresh\"]                   = (\"Refresh token\", \"Returns a new access token using a valid refresh token.\"),\n        [\"api/Users/confirmEmail\"]              = (\"Confirm email\", \"Confirms a user's email address using the token sent by email.\"),\n        [\"api/Users/resendConfirmationEmail\"]   = (\"Resend confirmation email\", \"Sends a new email confirmation link to the specified address.\"),\n        [\"api/Users/forgotPassword\"]            = (\"Forgot password\", \"Sends a password reset link to the specified email address.\"),\n        [\"api/Users/resetPassword\"]             = (\"Reset password\", \"Resets a user's password using the token sent by email.\"),\n        [\"api/Users/manage/2fa\"]                = (\"Manage two-factor authentication\", \"Enables, disables, or retrieves two-factor authentication settings.\"),\n        [\"api/Users/manage/info GET\"]           = (\"Get account info\", \"Returns the current user's email and two-factor authentication status.\"),\n        [\"api/Users/manage/info POST\"]          = (\"Update account info\", \"Updates the current user's email or password.\"),\n    };\n\n    public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)\n    {\n        var path = context.Description.RelativePath ?? \"\";\n        var key = $\"{path} {context.Description.HttpMethod?.ToUpperInvariant()}\";\n\n        if (_metadata.TryGetValue(key, out var meta) || _metadata.TryGetValue(path, out meta))\n        {\n            operation.Summary = meta.Summary;\n            operation.Description = meta.Description;\n        }\n\n        return Task.CompletedTask;\n    }\n}"
  },
  {
    "path": "src/Web/Infrastructure/MethodInfoExtensions.cs",
    "content": "﻿using System.Reflection;\n\nnamespace CleanArchitecture.Web.Infrastructure;\n\npublic static class MethodInfoExtensions\n{\n    // Compiler-generated anonymous methods (lambdas, local functions) contain '<' and '>' in their names.\n    private static readonly char[] AnonymousMethodChars = ['<', '>'];\n\n    /// <summary>\n    /// Returns <see langword=\"true\"/> if the method was compiler-generated from an anonymous delegate or lambda\n    /// (i.e. it has no stable, human-readable name that can be used as an OpenAPI <c>operationId</c>).\n    /// </summary>\n    public static bool IsAnonymous(this MethodInfo method) =>\n        method.Name.Any(AnonymousMethodChars.Contains);\n\n    /// <summary>\n    /// Throws <see cref=\"ArgumentException\"/> if <paramref name=\"input\"/> is an anonymous handler.\n    /// Endpoint handlers must be named methods so that a meaningful <c>operationId</c> can be derived\n    /// from <see cref=\"MethodInfo.Name\"/> for use in OpenAPI and typed client generation.\n    /// </summary>\n    public static void AnonymousMethod(this IGuardClause guardClause, Delegate input)\n    {\n        if (input.Method.IsAnonymous())\n            throw new ArgumentException(\"The endpoint name must be specified when using anonymous handlers.\");\n    }\n}"
  },
  {
    "path": "src/Web/Infrastructure/ProblemDetailsExceptionHandler.cs",
    "content": "using CleanArchitecture.Application.Common.Exceptions;\nusing Microsoft.AspNetCore.Diagnostics;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace CleanArchitecture.Web.Infrastructure;\n\n/// <summary>\n/// Converts well-known application exceptions into RFC 9110-compliant <see cref=\"ProblemDetails\"/> responses,\n/// mapping <see cref=\"ValidationException\"/> → 400, <see cref=\"NotFoundException\"/> → 404,\n/// <see cref=\"UnauthorizedAccessException\"/> → 401, and <see cref=\"ForbiddenAccessException\"/> → 403.\n/// Unrecognised exceptions are not handled and fall through to the default middleware.\n/// </summary>\npublic class ProblemDetailsExceptionHandler : IExceptionHandler\n{\n    public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)\n    {\n        var (statusCode, problemDetails) = exception switch\n        {\n            ValidationException ve => (StatusCodes.Status400BadRequest, (ProblemDetails)new ValidationProblemDetails(ve.Errors)\n            {\n                Status = StatusCodes.Status400BadRequest,\n                Type = \"https://tools.ietf.org/html/rfc9110#section-15.5.1\"\n            }),\n            NotFoundException ne => (StatusCodes.Status404NotFound, new ProblemDetails\n            {\n                Status = StatusCodes.Status404NotFound,\n                Type = \"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\n                Title = \"The specified resource was not found.\",\n                Detail = ne.Message\n            }),\n            UnauthorizedAccessException => (StatusCodes.Status401Unauthorized, new ProblemDetails\n            {\n                Status = StatusCodes.Status401Unauthorized,\n                Title = \"Unauthorized\",\n                Type = \"https://tools.ietf.org/html/rfc9110#section-15.5.2\"\n            }),\n            ForbiddenAccessException => (StatusCodes.Status403Forbidden, new ProblemDetails\n            {\n                Status = StatusCodes.Status403Forbidden,\n                Title = \"Forbidden\",\n                Type = \"https://tools.ietf.org/html/rfc9110#section-15.5.4\"\n            }),\n            _ => (-1, null)\n        };\n\n        if (problemDetails is null) return false;\n\n        httpContext.Response.StatusCode = statusCode;\n        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/Web/Infrastructure/WebApplicationExtensions.cs",
    "content": "using System.Reflection;\n\nnamespace CleanArchitecture.Web.Infrastructure;\n\npublic static class WebApplicationExtensions\n{\n    /// <summary>\n    /// Discovers all <see cref=\"IEndpointGroup\"/> implementations in <paramref name=\"assembly\"/>\n    /// and registers each as a route group with a matching OpenAPI tag. The route prefix defaults\n    /// to <c>/api/{ClassName}</c> but can be overridden via <see cref=\"IEndpointGroup.RoutePrefix\"/>.\n    /// </summary>\n    public static WebApplication MapEndpoints(this WebApplication app, Assembly assembly)\n    {\n        var endpointGroupTypes = assembly.GetExportedTypes()\n            .Where(t => t is { IsAbstract: false, IsInterface: false }\n                     && t.IsAssignableTo(typeof(IEndpointGroup)));\n\n        foreach (var type in endpointGroupTypes)\n        {\n            var groupName = type.Name;\n            var routePrefix = type.GetProperty(nameof(IEndpointGroup.RoutePrefix))\n                ?.GetValue(null) as string ?? $\"/api/{groupName}\";\n            var group = app.MapGroup(routePrefix).WithTags(groupName);\n            type.GetMethod(nameof(IEndpointGroup.Map))!.Invoke(null, [group]);\n        }\n\n        return app;\n    }\n}\n"
  },
  {
    "path": "src/Web/Program.cs",
    "content": "using CleanArchitecture.Infrastructure.Data;\r\nusing Scalar.AspNetCore;\r\n\r\nvar builder = WebApplication.CreateBuilder(args);\r\n\r\n// Add services to the container.\r\nbuilder.AddServiceDefaults();\r\n\r\nbuilder.AddKeyVaultIfConfigured();\r\nbuilder.AddApplicationServices();\r\nbuilder.AddInfrastructureServices();\r\nbuilder.AddWebServices();\r\n\r\nvar app = builder.Build();\r\n\r\n// Configure the HTTP request pipeline.\r\nif (app.Environment.IsDevelopment())\r\n{\r\n    await app.InitialiseDatabaseAsync();\r\n}\r\nelse\r\n{\r\n    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.\r\n    app.UseHsts();\r\n}\r\n\r\napp.UseHttpsRedirection();\r\napp.UseCors(static builder => \r\n    builder.AllowAnyMethod()\r\n        .AllowAnyHeader()\r\n        .AllowAnyOrigin());\r\n\r\napp.MapOpenApi();\r\napp.MapScalarApiReference();\r\n\r\n#if (!UseApiOnly)\r\napp.MapFallbackToFile(\"index.html\");\r\n#endif\r\n\r\napp.UseExceptionHandler(options => { });\r\n\r\n#if (UseApiOnly)\r\napp.Map(\"/\", () => Results.Redirect(\"/scalar\"));\r\n#endif\r\n\r\napp.MapDefaultEndpoints();\r\napp.MapEndpoints(typeof(Program).Assembly);\r\n\r\napp.UseFileServer();\r\n\r\napp.Run();\r\n"
  },
  {
    "path": "src/Web/Properties/launchSettings.json",
    "content": "﻿{\n  \"$schema\": \"https://json.schemastore.org/launchsettings.json\",\n  \"profiles\": {\n    \"http\": {\n      \"commandName\": \"Project\",\n      \"dotnetRunMessages\": true,\n      \"launchBrowser\": true,\n      \"launchUrl\": \"scalar\",\n      \"applicationUrl\": \"http://localhost:5000\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"https\": {\n      \"commandName\": \"Project\",\n      \"dotnetRunMessages\": true,\n      \"launchBrowser\": true,\n      \"launchUrl\": \"scalar\",\n      \"applicationUrl\": \"https://localhost:5001;http://localhost:5000\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Web/Services/CurrentUser.cs",
    "content": "﻿using System.Security.Claims;\n\nusing CleanArchitecture.Application.Common.Interfaces;\n\nnamespace CleanArchitecture.Web.Services;\n\npublic class CurrentUser : IUser\n{\n    private readonly IHttpContextAccessor _httpContextAccessor;\n\n    public CurrentUser(IHttpContextAccessor httpContextAccessor)\n    {\n        _httpContextAccessor = httpContextAccessor;\n    }\n\n    public string? Id => _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);\n    public List<string>? Roles => _httpContextAccessor.HttpContext?.User?.FindAll(ClaimTypes.Role).Select(x => x.Value).ToList();\n\n}\n"
  },
  {
    "path": "src/Web/Web-webapi.http",
    "content": "﻿# For more info on HTTP files go to https://aka.ms/vs/httpfile\n@Web_HostAddress = https://localhost:5001\n\n@Email=administrator@localhost\n@Password=Administrator1!\n@BearerToken=<YourToken>\n\n# POST Users Register\nPOST {{Web_HostAddress}}/api/Users/Register\nContent-Type: application/json\n\n{\n  \"email\": \"{{Email}}\",\n  \"password\": \"{{Password}}\"\n}\n\n###\n\n# POST Users Login\nPOST {{Web_HostAddress}}/api/Users/Login\nContent-Type: application/json\n\n{\n  \"email\": \"{{Email}}\",\n  \"password\": \"{{Password}}\"\n}\n\n###\n\n# POST Users Refresh\nPOST {{Web_HostAddress}}/api/Users/Refresh\nAuthorization: Bearer {{BearerToken}}\nContent-Type: application/json\n\n{\n  \"refreshToken\": \"\"\n}\n\n###\n\n# GET WeatherForecast\nGET {{Web_HostAddress}}/api/WeatherForecasts\nAuthorization: Bearer {{BearerToken}}\n\n###\n\n# GET TodoLists\nGET {{Web_HostAddress}}/api/TodoLists\nAuthorization: Bearer {{BearerToken}}\n\n###\n\n# POST TodoLists\nPOST {{Web_HostAddress}}/api/TodoLists\nAuthorization: Bearer {{BearerToken}}\nContent-Type: application/json\n\n// CreateTodoListCommand\n{\n  \"Title\": \"Backlog\"\n}\n\n###\n\n# PUT TodoLists\nPUT {{Web_HostAddress}}/api/TodoLists/1\nAuthorization: Bearer {{BearerToken}}\nContent-Type: application/json\n\n// UpdateTodoListCommand\n{\n  \"Id\": 1,\n  \"Title\": \"Product Backlog\"\n}\n\n###\n\n# DELETE TodoLists\nDELETE {{Web_HostAddress}}/api/TodoLists/1\nAuthorization: Bearer {{BearerToken}}\n\n###\n\n# GET TodoItems\n@PageNumber = 1\n@PageSize = 10\nGET {{Web_HostAddress}}/api/TodoItems?ListId=1&PageNumber={{PageNumber}}&PageSize={{PageSize}}\n\nAuthorization: Bearer {{BearerToken}}\n\n###\n\n# POST TodoItems\nPOST {{Web_HostAddress}}/api/TodoItems\nAuthorization: Bearer {{BearerToken}}\nContent-Type: application/json\n\n// CreateTodoItemCommand\n{\n  \"ListId\": 1,\n  \"Title\": \"Eat a burrito 🌯\"\n}\n\n###\n\n#PUT TodoItems UpdateItemDetails\nPUT {{Web_HostAddress}}/api/TodoItems/UpdateItemDetails?Id=1\nAuthorization: Bearer {{BearerToken}}\nContent-Type: application/json\n\n// UpdateTodoItemDetailCommand\n{\n  \"Id\": 1,\n  \"ListId\": 1,\n  \"Priority\": 3,\n  \"Note\": \"This is a good idea!\"\n}\n\n###\n\n# PUT TodoItems\nPUT {{Web_HostAddress}}/api/TodoItems/1\nAuthorization: Bearer {{BearerToken}}\nContent-Type: application/json\n\n// UpdateTodoItemCommand\n{\n  \"Id\": 1,\n  \"Title\": \"Eat a yummy burrito 🌯\",\n  \"Done\": true\n}\n\n###\n\n# DELETE TodoItem\nDELETE {{Web_HostAddress}}/api/TodoItems/1\nAuthorization: Bearer {{BearerToken}}\n\n###"
  },
  {
    "path": "src/Web/Web.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <RootNamespace>CleanArchitecture.Web</RootNamespace>\n    <AssemblyName>CleanArchitecture.Web</AssemblyName>\n    <!--#if (!UseApiOnly)-->\n    <OpenApiDocumentsDirectory>./wwwroot/openapi/</OpenApiDocumentsDirectory>\n    <OpenApiGenerateDocumentsOptions>--file-name v1</OpenApiGenerateDocumentsOptions>\n    <!--#endif-->\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Application\\Application.csproj\" />\n    <ProjectReference Include=\"..\\Infrastructure\\Infrastructure.csproj\" />\n    <ProjectReference Include=\"..\\ServiceDefaults\\ServiceDefaults.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Azure.Extensions.AspNetCore.Configuration.Secrets\" />\n    <PackageReference Include=\"Azure.Identity\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.Identity.EntityFrameworkCore\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.OpenApi\" />\n    <!--#if (!UseApiOnly)-->\n    <PackageReference Include=\"Microsoft.Extensions.ApiDescription.Server\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <!--#endif-->\n    <PackageReference Include=\"Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore\" />\n    <PackageReference Include=\"Microsoft.EntityFrameworkCore.Design\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <PackageReference Include=\"Scalar.AspNetCore\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Web/Web.http",
    "content": "﻿# For more info on HTTP files go to https://aka.ms/vs/httpfile\n@Web_HostAddress = https://localhost:5001\n@AuthCookieName = .AspNetCore.Identity.Application\n@AuthCookieValue = <AuthCookieValue>\n\n# GET Identity Account Login\n# Get the @RequestVerificationToken necessary for logging in.\nGET {{Web_HostAddress}}/Identity/Account/Login\n\n###\n\n# POST Identity Account Login\n# Get the @AuthCookieValue necessary for authenticating requests.\n@Email=administrator@localhost\n@Password=Administrator1!\n@RequestVerificationToken=<RequestVerificationToken>\nPOST {{Web_HostAddress}}/Identity/Account/Login\nContent-Type: application/x-www-form-urlencoded\n\nInput.Email={{Email}}&Input.Password={{Password}}&__RequestVerificationToken={{RequestVerificationToken}}\n\n###\n\n# GET WeatherForecast\nGET {{Web_HostAddress}}/api/WeatherForecasts\nCookie: {{AuthCookieName}}={{AuthCookieValue}}\n\n###\n\n# GET TodoLists\nGET {{Web_HostAddress}}/api/TodoLists\nCookie: {{AuthCookieName}}={{AuthCookieValue}}\n\n###\n\n#GET TodoList\nGET {{Web_HostAddress}}/api/TodoLists/1\nCookie: {{AuthCookieName}}={{AuthCookieValue}}\n\n###\n\n# POST TodoLists\nPOST {{Web_HostAddress}}/api/TodoLists\nCookie: {{AuthCookieName}}={{AuthCookieValue}}\nContent-Type: application/json\n\n// CreateTodoListCommand\n{\n  \"Title\": \"Backlog\"\n}\n\n###\n\n# PUT TodoLists\nPUT {{Web_HostAddress}}/api/TodoLists/1\nCookie: {{AuthCookieName}}={{AuthCookieValue}}\nContent-Type: application/json\n\n// UpdateTodoListCommand\n{\n  \"Id\": 1,\n  \"Title\": \"Product Backlog\"\n}\n\n###\n\n# DELETE TodoLists\nDELETE {{Web_HostAddress}}/api/TodoLists/1\nCookie: {{AuthCookieName}}={{AuthCookieValue}}\n\n###\n\n# GET TodoItems\n@PageNumber = 1\n@PageSize = 10\nGET {{Web_HostAddress}}/api/TodoItems?ListId=1&PageNumber={{PageNumber}}&PageSize={{PageSize}}\nCookie: {{AuthCookieName}}={{AuthCookieValue}}\n\n###\n\n# POST TodoItems\nPOST {{Web_HostAddress}}/api/TodoItems\nCookie: {{AuthCookieName}}={{AuthCookieValue}}\nContent-Type: application/json\n\n// CreateTodoItemCommand\n{\n  \"ListId\": 1,\n  \"Title\": \"Eat a burrito 🌯\"\n}\n\n###\n\n#PUT TodoItems UpdateItemDetails\nPUT {{Web_HostAddress}}/api/TodoItems/UpdateItemDetails?Id=1\nCookie: {{AuthCookieName}}={{AuthCookieValue}}\nContent-Type: application/json\n\n// UpdateTodoItemDetailCommand\n{\n  \"Id\": 1,\n  \"ListId\": 1,\n  \"Priority\": 3,\n  \"Note\": \"This is a good idea!\"\n}\n\n###\n\n# PUT TodoItems\nPUT {{Web_HostAddress}}/api/TodoItems/1\nCookie: {{AuthCookieName}}={{AuthCookieValue}}\nContent-Type: application/json\n\n// UpdateTodoItemCommand\n{\n  \"Id\": 1,\n  \"Title\": \"Eat a yummy burrito 🌯\",\n  \"Done\": true\n}\n\n###\n\n# DELETE TodoItem\nDELETE {{Web_HostAddress}}/api/TodoItems/1\nCookie: {{AuthCookieName}}={{AuthCookieValue}}\n\n###"
  },
  {
    "path": "src/Web/appsettings.PostgreSQL.json",
    "content": "{\n  \"ConnectionStrings\": {\n    \"CleanArchitectureDb\": \"Server=127.0.0.1;Port=5432;Database=CleanArchitectureDb;Username=admin;Password=password;\"\n  },\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/Web/appsettings.SQLServer.json",
    "content": "{\n  \"ConnectionStrings\": {\n    \"CleanArchitectureDb\": \"Server=(localdb)\\\\mssqllocaldb;Database=CleanArchitectureDb;Trusted_Connection=True;MultipleActiveResultSets=true\"\n  },\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/Web/appsettings.SQLite.json",
    "content": "{\n  \"ConnectionStrings\": {\n    \"CleanArchitectureDb\": \"DataSource=CleanArchitecture.db;Cache=Shared\"\n  },\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/Web/appsettings.json",
    "content": "{\n  \"ConnectionStrings\": {\n    \"CleanArchitectureDb\": \"DataSource=CleanArchitecture.db;Cache=Shared\"\n  },\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  },\n  \"AllowedHosts\": \"*\"\n}\n"
  },
  {
    "path": "templates/ca-use-case/.template.config/dotnetcli.host.json",
    "content": "{\n    \"symbolInfo\": {\n        \"featureName\": {\n            \"longName\": \"feature-name\",\n            \"shortName\": \"fn\"\n        },\n        \"returnType\": {\n            \"longName\": \"return-type\",\n            \"shortName\": \"rt\"\n        },\n        \"useCaseType\": {\n            \"longName\": \"usecase-type\",\n            \"shortName\": \"ut\"\n        },\n        \"parentNamespace\": {\n            \"longName\": \"parent-namespace\",\n            \"shortName\": \"pn\"\n        }\n    }\n}"
  },
  {
    "path": "templates/ca-use-case/.template.config/template.json",
    "content": "{\n  \"$schema\": \"http://json.schemastore.org/template\",\n  \"author\": \"JasonTaylorDev\",\n  \"classifications\": [\n    \"Clean Architecture\"\n  ],\n  \"name\": \"Clean Architecture Solution Use Case\",\n  \"description\":  \"Create a new use case (query or command)\",\n  \"identity\": \"Clean.Architecture.Solution.UseCase.CSharp\",\n  \"groupIdentity\": \"Clean.Architecture.Solution.UseCase\",\n  \"shortName\": \"ca-usecase\",\n  \"tags\": {\n    \"language\": \"C#\",\n    \"type\": \"item\"\n  },\n  \"sourceName\": \"CleanArchitectureUseCase\",\n  \"preferNameDirectory\": false,\n  \"symbols\": {\n    \"RootNamespace\": {\n      \"type\": \"bind\",\n      \"binding\": \"msbuild:RootNamespace\",\n      \"defaultValue\": \"CleanArchitecture.Application\"\n    },\n    \"parentNamespace\": {\n      \"type\": \"parameter\",\n      \"datatype\": \"string\",\n      \"isRequired\": false,\n      \"description\": \"Parent namespace when creating use case in a subfolder (e.g., 'Accounting' when in src/Application/Accounting)\",\n      \"defaultValue\": \"\"\n    },\n    \"featureName\": {\n      \"type\": \"parameter\",\n      \"datatype\": \"string\",\n      \"isRequired\": true,\n      \"fileRename\": \"FeatureName\"\n    },\n    \"ComputedNamespaceWithFeature\": {\n      \"type\": \"generated\",\n      \"generator\": \"join\",\n      \"parameters\": {\n        \"symbols\": [\n          { \"type\": \"ref\", \"value\": \"RootNamespace\" },\n          { \"type\": \"ref\", \"value\": \"parentNamespace\" },\n          { \"type\": \"ref\", \"value\": \"featureName\" }\n        ],\n        \"separator\": \".\",\n        \"removeEmptyValues\": true\n      },\n      \"replaces\": \"CleanArchitecture.Application.FeatureName\"\n    },\n    \"useCaseType\": {\n      \"type\": \"parameter\",\n      \"datatype\": \"choice\",\n      \"isRequired\": true,\n      \"choices\": [\n        {\n          \"choice\": \"command\",\n          \"description\": \"Create a new command\"\n        },\n        {\n          \"choice\": \"query\",\n          \"description\": \"Create a new query\"\n        }\n      ],\n      \"description\": \"The type of use case to create\"\n    },\n    \"createCommand\": {\n      \"type\": \"computed\",\n      \"value\": \"(useCaseType == \\\"command\\\")\"\n    },\n    \"createQuery\": {\n      \"type\": \"computed\",\n      \"value\": \"(useCaseType == \\\"query\\\")\"\n    },\n    \"returnType\": {\n      \"type\": \"parameter\",\n      \"datatype\": \"string\",\n      \"isRequired\": false,\n      \"replaces\": \"TReturnType\",\n      \"defaultValue\": \"\"\n    },\n    \"hasReturnType\": {\n      \"type\": \"computed\",\n      \"value\": \"(returnType != \\\"\\\")\"\n    },\n    \"CommonInterfacesNamespace\": {\n      \"type\": \"generated\",\n      \"generator\": \"join\",\n      \"parameters\": {\n        \"symbols\": [\n          { \"type\": \"ref\", \"value\": \"RootNamespace\" },\n          { \"type\": \"const\", \"value\": \"Common.Interfaces\" }\n        ],\n        \"separator\": \".\"\n      },\n      \"replaces\": \"CleanArchitecture.Application.Common.Interfaces\"\n    }\n  },\n  \"sources\": [\n    {\n      \"modifiers\": [\n        {\n          \"condition\": \"(createCommand)\",\n          \"exclude\": [ \"FeatureName/Queries/**/*\" ]\n        },\n        {\n          \"condition\": \"(createQuery)\",\n          \"exclude\": [ \"FeatureName/Commands/**/*\" ]\n        }\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "templates/ca-use-case/FeatureName/Commands/CleanArchitectureUseCase/CleanArchitectureUseCase.cs",
    "content": "using CleanArchitecture.Application.Common.Interfaces;\n\nnamespace CleanArchitecture.Application.FeatureName.Commands.CleanArchitectureUseCase;\n\n//#if (hasReturnType)\npublic record CleanArchitectureUseCaseCommand : IRequest<TReturnType>\n//#else\npublic record CleanArchitectureUseCaseCommand : IRequest\n//#endif\n{\n}\n\npublic class CleanArchitectureUseCaseCommandValidator : AbstractValidator<CleanArchitectureUseCaseCommand>\n{\n    public CleanArchitectureUseCaseCommandValidator()\n    {\n    }\n}\n\n//#if (hasReturnType)\npublic class CleanArchitectureUseCaseCommandHandler : IRequestHandler<CleanArchitectureUseCaseCommand, TReturnType>\n//#else\npublic class CleanArchitectureUseCaseCommandHandler : IRequestHandler<CleanArchitectureUseCaseCommand>\n//#endif\n{\n    private readonly IApplicationDbContext _context;\n\n    public CleanArchitectureUseCaseCommandHandler(IApplicationDbContext context)\n    {\n        _context = context;\n    }\n\n//#if (hasReturnType)\n    public async Task<TReturnType> Handle(CleanArchitectureUseCaseCommand request, CancellationToken cancellationToken)\n    {\n        throw new NotImplementedException();\n    }\n//#else\n    public async Task Handle(CleanArchitectureUseCaseCommand request, CancellationToken cancellationToken)\n    {\n        throw new NotImplementedException();\n    }\n//#endif\n}\n"
  },
  {
    "path": "templates/ca-use-case/FeatureName/Queries/CleanArchitectureUseCase/CleanArchitectureUseCase.cs",
    "content": "using CleanArchitecture.Application.Common.Interfaces;\n\nnamespace CleanArchitecture.Application.FeatureName.Queries.CleanArchitectureUseCase;\n\n//#if (hasReturnType)\npublic record CleanArchitectureUseCaseQuery : IRequest<TReturnType>\n//#else\npublic record CleanArchitectureUseCaseQuery : IRequest\n//#endif\n{\n}\n\npublic class CleanArchitectureUseCaseQueryValidator : AbstractValidator<CleanArchitectureUseCaseQuery>\n{\n    public CleanArchitectureUseCaseQueryValidator()\n    {\n    }\n}\n\n//#if (hasReturnType)\npublic class CleanArchitectureUseCaseQueryHandler : IRequestHandler<CleanArchitectureUseCaseQuery, TReturnType>\n//#else\npublic class CleanArchitectureUseCaseQueryHandler : IRequestHandler<CleanArchitectureUseCaseQuery>\n//#endif\n{\n    private readonly IApplicationDbContext _context;\n\n    public CleanArchitectureUseCaseQueryHandler(IApplicationDbContext context)\n    {\n        _context = context;\n    }\n\n//#if (hasReturnType)\n    public async Task<TReturnType> Handle(CleanArchitectureUseCaseQuery request, CancellationToken cancellationToken)\n    {\n        throw new NotImplementedException();\n    }\n//#else\n    public async Task Handle(CleanArchitectureUseCaseQuery request, CancellationToken cancellationToken)\n    {\n        throw new NotImplementedException();\n    }\n//#endif\n}\n"
  },
  {
    "path": "tests/Application.FunctionalTests/Application.FunctionalTests.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\r\n\r\n    <PropertyGroup>\r\n        <RootNamespace>CleanArchitecture.Application.FunctionalTests</RootNamespace>\r\n        <AssemblyName>CleanArchitecture.Application.FunctionalTests</AssemblyName>\r\n    </PropertyGroup>\r\n\r\n    <ItemGroup>\r\n      <PackageReference Include=\"System.IdentityModel.Tokens.Jwt\" />\r\n      <PackageReference Include=\"Microsoft.AspNetCore.Mvc.Testing\" />\r\n      <PackageReference Include=\"Microsoft.NET.Test.Sdk\" />\r\n      <PackageReference Include=\"nunit\" />\r\n      <PackageReference Include=\"NUnit.Analyzers\">\r\n        <PrivateAssets>all</PrivateAssets>\r\n        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\r\n      </PackageReference>\r\n      <PackageReference Include=\"NUnit3TestAdapter\" />\r\n      <PackageReference Include=\"coverlet.collector\">\r\n        <PrivateAssets>all</PrivateAssets>\r\n        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\r\n      </PackageReference>\r\n      <PackageReference Include=\"Moq\" />\r\n      <PackageReference Include=\"Respawn\" />\r\n      <PackageReference Include=\"Shouldly\" />\r\n      <PackageReference Include=\"Aspire.Hosting.Testing\" />\r\n    </ItemGroup>\r\n\r\n    <ItemGroup>\r\n      <Using Include=\"Aspire.Hosting\" />\r\n      <Using Include=\"Aspire.Hosting.ApplicationModel\" />\r\n      <Using Include=\"Aspire.Hosting.Testing\" />\r\n    </ItemGroup>\r\n\r\n    <ItemGroup>\r\n        <ProjectReference Include=\"..\\..\\src\\Shared\\Shared.csproj\" />\r\n        <ProjectReference Include=\"..\\..\\src\\Web\\Web.csproj\" />\r\n        <ProjectReference Include=\"..\\TestAppHost\\TestAppHost.csproj\" />\r\n    </ItemGroup>\r\n\r\n</Project>\r\n"
  },
  {
    "path": "tests/Application.FunctionalTests/FunctionalTestSetup.cs",
    "content": "using Microsoft.Extensions.DependencyInjection;\n\nnamespace CleanArchitecture.Application.FunctionalTests;\n\n[SetUpFixture]\npublic class FunctionalTestSetup\n{\n    internal static IServiceScopeFactory ScopeFactory { get; private set; } = null!;\n    internal static DatabaseResetter? DbResetter { get; private set; }\n\n    private static WebApiFactory? _factory;\n    private static DistributedApplication? _app;\n\n    [OneTimeSetUp]\n    public async Task OneTimeSetUp()\n    {\n        var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60));\n        var cancellationToken = cts.Token;\n\n        var builder = await DistributedApplicationTestingBuilder\n            .CreateAsync<Projects.TestAppHost>(\n                args: [],\n                configureBuilder: (options, _) =>\n                {\n                    options.DisableDashboard = true;\n                });\n\n        builder.Configuration[\"ASPIRE_ALLOW_UNSECURED_TRANSPORT\"] = \"true\";\n\n        _app = await builder\n            .BuildAsync(cancellationToken)\n            .WaitAsync(cancellationToken);\n\n        await _app\n            .StartAsync(cancellationToken)\n            .WaitAsync(cancellationToken);\n\n        await _app.ResourceNotifications.WaitForResourceHealthyAsync(\n            Services.Database, cancellationToken);\n\n        var connectionString = (await _app.GetConnectionStringAsync(Services.Database))!;\n\n        _factory = new WebApiFactory(connectionString);\n        ScopeFactory = _factory.Services.GetRequiredService<IServiceScopeFactory>();\n        DbResetter = await DatabaseResetter.CreateAsync(connectionString);\n    }\n\n    [OneTimeTearDown]\n    public async Task OneTimeTearDown()\n    {\n        if (DbResetter is not null) await DbResetter.DisposeAsync();\n        if (_app is not null) await _app.DisposeAsync();\n        if (_factory is not null) await _factory.DisposeAsync();\n    }\n}\n"
  },
  {
    "path": "tests/Application.FunctionalTests/GlobalUsings.cs",
    "content": "﻿global using Ardalis.GuardClauses;\nglobal using CleanArchitecture.Application.FunctionalTests.Infrastructure;\nglobal using CleanArchitecture.Shared;\nglobal using Moq;\nglobal using NUnit.Framework;\nglobal using Shouldly;\n"
  },
  {
    "path": "tests/Application.FunctionalTests/Infrastructure/DatabaseResetter.cs",
    "content": "#if (UsePostgreSQL)\nusing Npgsql;\n#elif (UseSqlServer)\nusing Microsoft.Data.SqlClient;\n#else\nusing Microsoft.Data.Sqlite;\n#endif\nusing Respawn;\nusing System.Data.Common;\n\nnamespace CleanArchitecture.Application.FunctionalTests.Infrastructure;\n\ninternal sealed class DatabaseResetter : IAsyncDisposable\n{\n    private readonly DbConnection _connection;\n    private readonly Respawner _respawner;\n\n    private DatabaseResetter(DbConnection connection, Respawner respawner)\n    {\n        _connection = connection;\n        _respawner = respawner;\n    }\n\n    public static async Task<DatabaseResetter> CreateAsync(string connectionString)\n    {\n#if (UsePostgreSQL)\n        var connection = new NpgsqlConnection(connectionString);\n#elif (UseSqlServer)\n        var connection = new SqlConnection(connectionString);\n#else\n        var connection = new SqliteConnection(connectionString);\n#endif\n\n        await connection.OpenAsync();\n        var respawner = await Respawner.CreateAsync(connection);\n        await connection.CloseAsync();\n        return new DatabaseResetter(connection, respawner);\n    }\n\n    public async Task ResetAsync()\n    {\n        await _connection.OpenAsync();\n        await _respawner.ResetAsync(_connection);\n        await _connection.CloseAsync();\n    }\n\n    public async ValueTask DisposeAsync() => await _connection.DisposeAsync();\n}\n"
  },
  {
    "path": "tests/Application.FunctionalTests/Infrastructure/TestApp.cs",
    "content": "using CleanArchitecture.Domain.Constants;\nusing CleanArchitecture.Infrastructure.Data;\nusing CleanArchitecture.Infrastructure.Identity;\nusing MediatR;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace CleanArchitecture.Application.FunctionalTests.Infrastructure;\n\npublic static class TestApp\n{\n    private static string? _userId;\n    private static List<string>? _roles;\n\n    public static async Task<TResponse> SendAsync<TResponse>(IRequest<TResponse> request)\n    {\n        using var scope = FunctionalTestSetup.ScopeFactory.CreateScope();\n\n        var mediator = scope.ServiceProvider.GetRequiredService<ISender>();\n\n        return await mediator.Send(request);\n    }\n\n    public static async Task SendAsync(IBaseRequest request)\n    {\n        using var scope = FunctionalTestSetup.ScopeFactory.CreateScope();\n\n        var mediator = scope.ServiceProvider.GetRequiredService<ISender>();\n\n        await mediator.Send(request);\n    }\n\n    public static string? GetUserId() => _userId;\n\n    public static List<string>? GetRoles() => _roles;\n\n    public static async Task<string> RunAsDefaultUserAsync()\n    {\n        return await RunAsUserAsync(\"test@local\", \"Testing1234!\", []);\n    }\n\n    public static async Task<string> RunAsAdministratorAsync()\n    {\n        return await RunAsUserAsync(\"administrator@local\", \"Administrator1234!\", [Roles.Administrator]);\n    }\n\n    public static async Task<string> RunAsUserAsync(string userName, string password, string[] roles)\n    {\n        using var scope = FunctionalTestSetup.ScopeFactory.CreateScope();\n\n        var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();\n\n        var user = new ApplicationUser { UserName = userName, Email = userName };\n\n        var result = await userManager.CreateAsync(user, password);\n\n        if (roles.Length > 0)\n        {\n            var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();\n\n            foreach (var role in roles)\n            {\n                await roleManager.CreateAsync(new IdentityRole(role));\n            }\n\n            await userManager.AddToRolesAsync(user, roles);\n        }\n\n        if (result.Succeeded)\n        {\n            _userId = user.Id;\n            _roles = [..roles];\n            return _userId;\n        }\n\n        var errors = string.Join(Environment.NewLine, result.ToApplicationResult().Errors);\n\n        throw new Exception($\"Unable to create {userName}.{Environment.NewLine}{errors}\");\n    }\n\n    public static async Task ResetState()\n    {\n        if (FunctionalTestSetup.DbResetter is not null)\n        {\n            await FunctionalTestSetup.DbResetter.ResetAsync();\n        }\n\n        _userId = null;\n        _roles = null;\n    }\n\n    public static async Task<TEntity?> FindAsync<TEntity>(params object[] keyValues)\n        where TEntity : class\n    {\n        using var scope = FunctionalTestSetup.ScopeFactory.CreateScope();\n\n        var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();\n\n        return await context.FindAsync<TEntity>(keyValues);\n    }\n\n    public static async Task AddAsync<TEntity>(TEntity entity)\n        where TEntity : class\n    {\n        using var scope = FunctionalTestSetup.ScopeFactory.CreateScope();\n\n        var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();\n\n        context.Add(entity);\n\n        await context.SaveChangesAsync();\n    }\n\n    public static async Task<int> CountAsync<TEntity>() where TEntity : class\n    {\n        using var scope = FunctionalTestSetup.ScopeFactory.CreateScope();\n\n        var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();\n\n        return await context.Set<TEntity>().CountAsync();\n    }\n}\n"
  },
  {
    "path": "tests/Application.FunctionalTests/Infrastructure/TestBase.cs",
    "content": "namespace CleanArchitecture.Application.FunctionalTests.Infrastructure;\n\npublic abstract class TestBase\n{\n    [SetUp]\n    public async Task SetUp()\n    {\n        await TestApp.ResetState();\n    }\n}\n"
  },
  {
    "path": "tests/Application.FunctionalTests/Infrastructure/WebApiFactory.cs",
    "content": "using CleanArchitecture.Application.Common.Interfaces;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.AspNetCore.Mvc.Testing;\nusing Microsoft.AspNetCore.TestHost;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\n\nnamespace CleanArchitecture.Application.FunctionalTests.Infrastructure;\n\npublic class WebApiFactory(string connectionString) : WebApplicationFactory<Program>\n{\n    protected override void ConfigureWebHost(IWebHostBuilder builder)\n    {\n        builder\n            .UseSetting(\"ConnectionStrings:CleanArchitectureDb\", connectionString);\n\n        builder.ConfigureTestServices(services =>\n        {\n            services\n                .RemoveAll<IUser>()\n                .AddTransient(provider =>\n                {\n                    var mock = new Mock<IUser>();\n                    mock.SetupGet(x => x.Roles).Returns(TestApp.GetRoles());\n                    mock.SetupGet(x => x.Id).Returns(TestApp.GetUserId());\n                    return mock.Object;\n                });\n        });\n    }\n}\n"
  },
  {
    "path": "tests/Application.FunctionalTests/TodoItems/Commands/CreateTodoItemTests.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Exceptions;\nusing CleanArchitecture.Application.TodoItems.Commands.CreateTodoItem;\nusing CleanArchitecture.Application.TodoLists.Commands.CreateTodoList;\nusing CleanArchitecture.Domain.Entities;\n\nnamespace CleanArchitecture.Application.FunctionalTests.TodoItems.Commands;\n\npublic class CreateTodoItemTests : TestBase\n{\n    [Test]\n    public async Task ShouldRequireMinimumFields()\n    {\n        var command = new CreateTodoItemCommand();\n\n        await Should.ThrowAsync<ValidationException>(() => TestApp.SendAsync(command));\n    }\n\n    [Test]\n    public async Task ShouldCreateTodoItem()\n    {\n        var userId = await TestApp.RunAsDefaultUserAsync();\n\n        var listId = await TestApp.SendAsync(new CreateTodoListCommand\n        {\n            Title = \"New List\"\n        });\n\n        var command = new CreateTodoItemCommand\n        {\n            ListId = listId,\n            Title = \"Tasks\"\n        };\n\n        var itemId = await TestApp.SendAsync(command);\n\n        var item = await TestApp.FindAsync<TodoItem>(itemId);\n\n        item.ShouldNotBeNull();\n        item!.ListId.ShouldBe(command.ListId);\n        item.Title.ShouldBe(command.Title);\n        item.CreatedBy.ShouldBe(userId);\n        item.Created.ShouldBe(DateTime.Now, TimeSpan.FromMilliseconds(10000));\n        item.LastModifiedBy.ShouldBe(userId);\n        item.LastModified.ShouldBe(DateTime.Now, TimeSpan.FromMilliseconds(10000));\n    }\n}\n"
  },
  {
    "path": "tests/Application.FunctionalTests/TodoItems/Commands/DeleteTodoItemTests.cs",
    "content": "﻿using CleanArchitecture.Application.TodoItems.Commands.CreateTodoItem;\nusing CleanArchitecture.Application.TodoItems.Commands.DeleteTodoItem;\nusing CleanArchitecture.Application.TodoLists.Commands.CreateTodoList;\nusing CleanArchitecture.Domain.Entities;\n\nnamespace CleanArchitecture.Application.FunctionalTests.TodoItems.Commands;\n\npublic class DeleteTodoItemTests : TestBase\n{\n    [Test]\n    public async Task ShouldRequireValidTodoItemId()\n    {\n        var command = new DeleteTodoItemCommand(99);\n\n        await Should.ThrowAsync<NotFoundException>(() => TestApp.SendAsync(command));\n    }\n\n    [Test]\n    public async Task ShouldDeleteTodoItem()\n    {\n        var listId = await TestApp.SendAsync(new CreateTodoListCommand\n        {\n            Title = \"New List\"\n        });\n\n        var itemId = await TestApp.SendAsync(new CreateTodoItemCommand\n        {\n            ListId = listId,\n            Title = \"New Item\"\n        });\n\n        await TestApp.SendAsync(new DeleteTodoItemCommand(itemId));\n\n        var item = await TestApp.FindAsync<TodoItem>(itemId);\n\n        item.ShouldBeNull();\n    }\n}\n"
  },
  {
    "path": "tests/Application.FunctionalTests/TodoItems/Commands/UpdateTodoItemDetailTests.cs",
    "content": "﻿using CleanArchitecture.Application.TodoItems.Commands.CreateTodoItem;\nusing CleanArchitecture.Application.TodoItems.Commands.UpdateTodoItem;\nusing CleanArchitecture.Application.TodoItems.Commands.UpdateTodoItemDetail;\nusing CleanArchitecture.Application.TodoLists.Commands.CreateTodoList;\nusing CleanArchitecture.Domain.Entities;\nusing CleanArchitecture.Domain.Enums;\n\nnamespace CleanArchitecture.Application.FunctionalTests.TodoItems.Commands;\n\npublic class UpdateTodoItemDetailTests : TestBase\n{\n    [Test]\n    public async Task ShouldRequireValidTodoItemId()\n    {\n        var command = new UpdateTodoItemCommand { Id = 99, Title = \"New Title\" };\n\n        await Should.ThrowAsync<NotFoundException>(() => TestApp.SendAsync(command));\n    }\n\n    [Test]\n    public async Task ShouldUpdateTodoItem()\n    {\n        var userId = await TestApp.RunAsDefaultUserAsync();\n\n        var listId = await TestApp.SendAsync(new CreateTodoListCommand\n        {\n            Title = \"New List\"\n        });\n\n        var itemId = await TestApp.SendAsync(new CreateTodoItemCommand\n        {\n            ListId = listId,\n            Title = \"New Item\"\n        });\n\n        var command = new UpdateTodoItemDetailCommand\n        {\n            Id = itemId,\n            ListId = listId,\n            Note = \"This is the note.\",\n            Priority = PriorityLevel.High\n        };\n\n        await TestApp.SendAsync(command);\n\n        var item = await TestApp.FindAsync<TodoItem>(itemId);\n\n        item.ShouldNotBeNull();\n        item!.ListId.ShouldBe(command.ListId);\n        item.Note.ShouldBe(command.Note);\n        item.Priority.ShouldBe(command.Priority);\n        item.LastModifiedBy.ShouldNotBeNull();\n        item.LastModifiedBy.ShouldBe(userId);\n        item.LastModified.ShouldBe(DateTime.Now, TimeSpan.FromMilliseconds(10000));\n    }\n}\n"
  },
  {
    "path": "tests/Application.FunctionalTests/TodoItems/Commands/UpdateTodoItemTests.cs",
    "content": "﻿using CleanArchitecture.Application.TodoItems.Commands.CreateTodoItem;\nusing CleanArchitecture.Application.TodoItems.Commands.UpdateTodoItem;\nusing CleanArchitecture.Application.TodoLists.Commands.CreateTodoList;\nusing CleanArchitecture.Domain.Entities;\n\nnamespace CleanArchitecture.Application.FunctionalTests.TodoItems.Commands;\n\npublic class UpdateTodoItemTests : TestBase\n{\n    [Test]\n    public async Task ShouldRequireValidTodoItemId()\n    {\n        var command = new UpdateTodoItemCommand { Id = 99, Title = \"New Title\" };\n        await Should.ThrowAsync<NotFoundException>(() => TestApp.SendAsync(command));\n    }\n\n    [Test]\n    public async Task ShouldUpdateTodoItem()\n    {\n        var userId = await TestApp.RunAsDefaultUserAsync();\n\n        var listId = await TestApp.SendAsync(new CreateTodoListCommand\n        {\n            Title = \"New List\"\n        });\n\n        var itemId = await TestApp.SendAsync(new CreateTodoItemCommand\n        {\n            ListId = listId,\n            Title = \"New Item\"\n        });\n\n        var command = new UpdateTodoItemCommand\n        {\n            Id = itemId,\n            Title = \"Updated Item Title\"\n        };\n\n        await TestApp.SendAsync(command);\n\n        var item = await TestApp.FindAsync<TodoItem>(itemId);\n\n        item.ShouldNotBeNull();\n        item!.Title.ShouldBe(command.Title);\n        item.LastModifiedBy.ShouldNotBeNull();\n        item.LastModifiedBy.ShouldBe(userId);\n        item.LastModified.ShouldBe(DateTime.Now, TimeSpan.FromMilliseconds(10000));\n    }\n}\n"
  },
  {
    "path": "tests/Application.FunctionalTests/TodoLists/Commands/CreateTodoListTests.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Exceptions;\nusing CleanArchitecture.Application.TodoLists.Commands.CreateTodoList;\nusing CleanArchitecture.Domain.Entities;\n\nnamespace CleanArchitecture.Application.FunctionalTests.TodoLists.Commands;\n\npublic class CreateTodoListTests : TestBase\n{\n    [Test]\n    public async Task ShouldRequireMinimumFields()\n    {\n        var command = new CreateTodoListCommand();\n        await Should.ThrowAsync<ValidationException>(() => TestApp.SendAsync(command));\n    }\n\n    [Test]\n    public async Task ShouldRequireUniqueTitle()\n    {\n        await TestApp.SendAsync(new CreateTodoListCommand\n        {\n            Title = \"Shopping\"\n        });\n\n        var command = new CreateTodoListCommand\n        {\n            Title = \"Shopping\"\n        };\n\n        await Should.ThrowAsync<ValidationException>(() => TestApp.SendAsync(command));\n    }\n\n    [Test]\n    public async Task ShouldCreateTodoList()\n    {\n        var userId = await TestApp.RunAsDefaultUserAsync();\n\n        var command = new CreateTodoListCommand\n        {\n            Title = \"Tasks\"\n        };\n\n        var id = await TestApp.SendAsync(command);\n\n        var list = await TestApp.FindAsync<TodoList>(id);\n\n        list.ShouldNotBeNull();\n        list!.Title.ShouldBe(command.Title);\n        list.CreatedBy.ShouldBe(userId);\n        list.Created.ShouldBe(DateTime.Now, TimeSpan.FromMilliseconds(10000));\n    }\n}\n"
  },
  {
    "path": "tests/Application.FunctionalTests/TodoLists/Commands/DeleteTodoListTests.cs",
    "content": "﻿using CleanArchitecture.Application.TodoLists.Commands.CreateTodoList;\nusing CleanArchitecture.Application.TodoLists.Commands.DeleteTodoList;\nusing CleanArchitecture.Domain.Entities;\n\nnamespace CleanArchitecture.Application.FunctionalTests.TodoLists.Commands;\n\npublic class DeleteTodoListTests : TestBase\n{\n    [Test]\n    public async Task ShouldRequireValidTodoListId()\n    {\n        var command = new DeleteTodoListCommand(99);\n        await Should.ThrowAsync<NotFoundException>(() => TestApp.SendAsync(command));\n    }\n\n    [Test]\n    public async Task ShouldDeleteTodoList()\n    {\n        var listId = await TestApp.SendAsync(new CreateTodoListCommand\n        {\n            Title = \"New List\"\n        });\n\n        await TestApp.SendAsync(new DeleteTodoListCommand(listId));\n\n        var list = await TestApp.FindAsync<TodoList>(listId);\n\n        list.ShouldBeNull();\n    }\n}\n"
  },
  {
    "path": "tests/Application.FunctionalTests/TodoLists/Commands/PurgeTodoListsTests.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Exceptions;\nusing CleanArchitecture.Application.Common.Security;\nusing CleanArchitecture.Application.TodoLists.Commands.CreateTodoList;\nusing CleanArchitecture.Application.TodoLists.Commands.PurgeTodoLists;\nusing CleanArchitecture.Domain.Entities;\n\nnamespace CleanArchitecture.Application.FunctionalTests.TodoLists.Commands;\n\npublic class PurgeTodoListsTests : TestBase\n{\n    [Test]\n    public async Task ShouldDenyAnonymousUser()\n    {\n        var command = new PurgeTodoListsCommand();\n\n        command.GetType().ShouldSatisfyAllConditions(\n            type => type.ShouldBeDecoratedWith<AuthorizeAttribute>()\n        );\n\n        var action = () => TestApp.SendAsync(command);\n\n        await Should.ThrowAsync<UnauthorizedAccessException>(action);\n    }\n\n    [Test]\n    public async Task ShouldDenyNonAdministrator()\n    {\n        await TestApp.RunAsDefaultUserAsync();\n\n        var command = new PurgeTodoListsCommand();\n\n        var action = () => TestApp.SendAsync(command);\n\n        await Should.ThrowAsync<ForbiddenAccessException>(action);\n    }\n\n    [Test]\n    public async Task ShouldAllowAdministrator()\n    {\n        await TestApp.RunAsAdministratorAsync();\n\n        var command = new PurgeTodoListsCommand();\n\n        var action = () => TestApp.SendAsync(command);\n\n        Func<Task> asyncAction = async () => await TestApp.SendAsync(command);\n        await asyncAction.ShouldNotThrowAsync();\n    }\n\n    [Test]\n    public async Task ShouldDeleteAllLists()\n    {\n        await TestApp.RunAsAdministratorAsync();\n\n        await TestApp.SendAsync(new CreateTodoListCommand\n        {\n            Title = \"New List #1\"\n        });\n\n        await TestApp.SendAsync(new CreateTodoListCommand\n        {\n            Title = \"New List #2\"\n        });\n\n        await TestApp.SendAsync(new CreateTodoListCommand\n        {\n            Title = \"New List #3\"\n        });\n\n        await TestApp.SendAsync(new PurgeTodoListsCommand());\n\n        var count = await TestApp.CountAsync<TodoList>();\n\n        count.ShouldBe(0);\n    }\n}\n"
  },
  {
    "path": "tests/Application.FunctionalTests/TodoLists/Commands/UpdateTodoListTests.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Exceptions;\nusing CleanArchitecture.Application.TodoLists.Commands.CreateTodoList;\nusing CleanArchitecture.Application.TodoLists.Commands.UpdateTodoList;\nusing CleanArchitecture.Domain.Entities;\n\nnamespace CleanArchitecture.Application.FunctionalTests.TodoLists.Commands;\n\npublic class UpdateTodoListTests : TestBase\n{\n    [Test]\n    public async Task ShouldRequireValidTodoListId()\n    {\n        var command = new UpdateTodoListCommand { Id = 99, Title = \"New Title\" };\n        await Should.ThrowAsync<NotFoundException>(() => TestApp.SendAsync(command));\n    }\n\n    [Test]\n    public async Task ShouldRequireUniqueTitle()\n    {\n        var listId = await TestApp.SendAsync(new CreateTodoListCommand\n        {\n            Title = \"New List\"\n        });\n\n        await TestApp.SendAsync(new CreateTodoListCommand\n        {\n            Title = \"Other List\"\n        });\n\n        var command = new UpdateTodoListCommand\n        {\n            Id = listId,\n            Title = \"Other List\"\n        };\n\n        var ex = await Should.ThrowAsync<ValidationException>(() => TestApp.SendAsync(command));\n\n        ex.Errors.ShouldContainKey(\"Title\");\n        ex.Errors[\"Title\"].ShouldContain(\"'Title' must be unique.\");\n    }\n\n    [Test]\n    public async Task ShouldUpdateTodoList()\n    {\n        var userId = await TestApp.RunAsDefaultUserAsync();\n\n        var listId = await TestApp.SendAsync(new CreateTodoListCommand\n        {\n            Title = \"New List\"\n        });\n\n        var command = new UpdateTodoListCommand\n        {\n            Id = listId,\n            Title = \"Updated List Title\"\n        };\n\n        await TestApp.SendAsync(command);\n\n        var list = await TestApp.FindAsync<TodoList>(listId);\n\n        list.ShouldNotBeNull();\n        list!.Title.ShouldBe(command.Title);\n        list.LastModifiedBy.ShouldNotBeNull();\n        list.LastModifiedBy.ShouldBe(userId);\n        list.LastModified.ShouldBe(DateTime.Now, TimeSpan.FromMilliseconds(10000));\n    }\n}\n"
  },
  {
    "path": "tests/Application.FunctionalTests/TodoLists/Queries/GetTodosTests.cs",
    "content": "﻿using CleanArchitecture.Application.TodoLists.Queries.GetTodos;\nusing CleanArchitecture.Domain.Entities;\nusing CleanArchitecture.Domain.ValueObjects;\n\nnamespace CleanArchitecture.Application.FunctionalTests.TodoLists.Queries;\n\npublic class GetTodosTests : TestBase\n{\n    [Test]\n    public async Task ShouldReturnPriorityLevels()\n    {\n        await TestApp.RunAsDefaultUserAsync();\n\n        var query = new GetTodosQuery();\n\n        var result = await TestApp.SendAsync(query);\n\n        result.PriorityLevels.ShouldNotBeEmpty();\n    }\n\n    [Test]\n    public async Task ShouldReturnAllListsAndItems()\n    {\n        await TestApp.RunAsDefaultUserAsync();\n\n        await TestApp.AddAsync(new TodoList\n        {\n            Title = \"Shopping\",\n            Colour = Colour.Blue,\n            Items =\n                {\n                    new TodoItem { Title = \"Apples\", Done = true },\n                    new TodoItem { Title = \"Milk\", Done = true },\n                    new TodoItem { Title = \"Bread\", Done = true },\n                    new TodoItem { Title = \"Toilet paper\" },\n                    new TodoItem { Title = \"Pasta\" },\n                    new TodoItem { Title = \"Tissues\" },\n                    new TodoItem { Title = \"Tuna\" }\n                }\n        });\n\n        var query = new GetTodosQuery();\n\n        var result = await TestApp.SendAsync(query);\n\n        result.Lists.Count.ShouldBe(1);\n        result.Lists.First().Items.Count.ShouldBe(7);\n    }\n\n    [Test]\n    public async Task ShouldDenyAnonymousUser()\n    {\n        var query = new GetTodosQuery();\n\n        var action = () => TestApp.SendAsync(query);\n\n        await Should.ThrowAsync<UnauthorizedAccessException>(action);\n    }\n}\n"
  },
  {
    "path": "tests/Application.UnitTests/Application.UnitTests.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\r\n\r\n    <PropertyGroup>\r\n        <RootNamespace>CleanArchitecture.Application.UnitTests</RootNamespace>\r\n        <AssemblyName>CleanArchitecture.Application.UnitTests</AssemblyName>\r\n    </PropertyGroup>\r\n\r\n    <ItemGroup>\r\n        <PackageReference Include=\"Azure.Identity\" />\r\n        <PackageReference Include=\"Microsoft.NET.Test.Sdk\" />\r\n        <PackageReference Include=\"nunit\" />\r\n        <PackageReference Include=\"NUnit.Analyzers\">\r\n          <PrivateAssets>all</PrivateAssets>\r\n          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\r\n        </PackageReference>\r\n        <PackageReference Include=\"NUnit3TestAdapter\" />\r\n        <PackageReference Include=\"coverlet.collector\">\r\n          <PrivateAssets>all</PrivateAssets>\r\n          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\r\n        </PackageReference>\r\n        <PackageReference Include=\"Moq\" />\r\n        <PackageReference Include=\"Shouldly\" />\r\n    </ItemGroup>\r\n\r\n    <ItemGroup>\r\n        <ProjectReference Include=\"..\\..\\src\\Application\\Application.csproj\" />\r\n        <ProjectReference Include=\"..\\..\\src\\Infrastructure\\Infrastructure.csproj\" />\r\n    </ItemGroup>\r\n\r\n</Project>\r\n"
  },
  {
    "path": "tests/Application.UnitTests/Common/Behaviours/RequestLoggerTests.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Behaviours;\nusing CleanArchitecture.Application.Common.Interfaces;\nusing CleanArchitecture.Application.TodoItems.Commands.CreateTodoItem;\nusing Microsoft.Extensions.Logging;\nusing Moq;\nusing NUnit.Framework;\n\nnamespace CleanArchitecture.Application.UnitTests.Common.Behaviours;\n\npublic class RequestLoggerTests\n{\n    private Mock<ILogger<CreateTodoItemCommand>> _logger = null!;\n    private Mock<IUser> _user = null!;\n    private Mock<IIdentityService> _identityService = null!;\n\n    [SetUp]\n    public void Setup()\n    {\n        _logger = new Mock<ILogger<CreateTodoItemCommand>>();\n        _user = new Mock<IUser>();\n        _identityService = new Mock<IIdentityService>();\n    }\n\n    [Test]\n    public async Task ShouldCallGetUserNameAsyncOnceIfAuthenticated()\n    {\n        _user.Setup(x => x.Id).Returns(Guid.NewGuid().ToString());\n\n        var requestLogger = new LoggingBehaviour<CreateTodoItemCommand>(_logger.Object, _user.Object, _identityService.Object);\n\n        await requestLogger.Process(new CreateTodoItemCommand { ListId = 1, Title = \"title\" }, new CancellationToken());\n\n        _identityService.Verify(i => i.GetUserNameAsync(It.IsAny<string>()), Times.Once);\n    }\n\n    [Test]\n    public async Task ShouldNotCallGetUserNameAsyncOnceIfUnauthenticated()\n    {\n        var requestLogger = new LoggingBehaviour<CreateTodoItemCommand>(_logger.Object, _user.Object, _identityService.Object);\n\n        await requestLogger.Process(new CreateTodoItemCommand { ListId = 1, Title = \"title\" }, new CancellationToken());\n\n        _identityService.Verify(i => i.GetUserNameAsync(It.IsAny<string>()), Times.Never);\n    }\n}\n"
  },
  {
    "path": "tests/Application.UnitTests/Common/Exceptions/ValidationExceptionTests.cs",
    "content": "﻿using CleanArchitecture.Application.Common.Exceptions;\nusing FluentValidation.Results;\nusing NUnit.Framework;\nusing Shouldly;\n\nnamespace CleanArchitecture.Application.UnitTests.Common.Exceptions;\n\npublic class ValidationExceptionTests\n{\n    [Test]\n    public void DefaultConstructorCreatesAnEmptyErrorDictionary()\n    {\n        var actual = new ValidationException().Errors;\n\n        actual.Keys.ShouldBeEmpty();\n    }\n\n    [Test]\n    public void SingleValidationFailureCreatesASingleElementErrorDictionary()\n    {\n        var failures = new List<ValidationFailure>\n            {\n                new ValidationFailure(\"Age\", \"must be over 18\"),\n            };\n\n        var actual = new ValidationException(failures).Errors;\n\n        actual.Keys.ShouldBe(new string[] { \"Age\" });\n        actual[\"Age\"].ShouldBe(new string[] { \"must be over 18\" });\n    }\n\n    [Test]\n    public void MulitpleValidationFailureForMultiplePropertiesCreatesAMultipleElementErrorDictionaryEachWithMultipleValues()\n    {\n        var failures = new List<ValidationFailure>\n            {\n                new ValidationFailure(\"Age\", \"must be 18 or older\"),\n                new ValidationFailure(\"Age\", \"must be 25 or younger\"),\n                new ValidationFailure(\"Password\", \"must contain at least 8 characters\"),\n                new ValidationFailure(\"Password\", \"must contain a digit\"),\n                new ValidationFailure(\"Password\", \"must contain upper case letter\"),\n                new ValidationFailure(\"Password\", \"must contain lower case letter\"),\n            };\n\n        var actual = new ValidationException(failures).Errors;\n\n        actual.Keys.ShouldBe(new string[] { \"Password\", \"Age\" }, ignoreOrder: true);\n\n        actual[\"Age\"].ShouldBe(new string[]\n        {\n                \"must be 25 or younger\",\n                \"must be 18 or older\",\n        }, ignoreOrder: true);\n\n        actual[\"Password\"].ShouldBe(new string[]\n        {\n                \"must contain lower case letter\",\n                \"must contain upper case letter\",\n                \"must contain at least 8 characters\",\n                \"must contain a digit\",\n        }, ignoreOrder: true);\n    }\n}\n"
  },
  {
    "path": "tests/Application.UnitTests/Common/Mappings/MappingTests.cs",
    "content": "﻿using System.Runtime.CompilerServices;\nusing AutoMapper;\nusing CleanArchitecture.Application.Common.Interfaces;\nusing CleanArchitecture.Application.Common.Models;\nusing CleanArchitecture.Application.TodoItems.Queries.GetTodoItemsWithPagination;\nusing CleanArchitecture.Application.TodoLists.Queries.GetTodos;\nusing CleanArchitecture.Domain.Entities;\nusing Microsoft.Extensions.Logging;\nusing NUnit.Framework;\n\nnamespace CleanArchitecture.Application.UnitTests.Common.Mappings;\n\npublic class MappingTests\n{\n    private ILoggerFactory? _loggerFactory;\n    private MapperConfiguration? _configuration;\n    private IMapper? _mapper;\n\n    [OneTimeSetUp]\n    public void OneTimeSetUp()\n    {\n        // Minimal logger factory for tests\n        _loggerFactory = LoggerFactory.Create(b => b.AddDebug().SetMinimumLevel(LogLevel.Debug));\n\n        _configuration = new MapperConfiguration(cfg =>\n            cfg.AddMaps(typeof(IApplicationDbContext).Assembly),\n            loggerFactory: _loggerFactory);\n\n        _mapper = _configuration.CreateMapper();\n    }\n\n    [Test]\n    public void ShouldHaveValidConfiguration()\n    {\n        _configuration!.AssertConfigurationIsValid();\n    }\n\n    [Test]\n    [TestCase(typeof(TodoList), typeof(TodoListDto))]\n    [TestCase(typeof(TodoItem), typeof(TodoItemDto))]\n    [TestCase(typeof(TodoList), typeof(LookupDto))]\n    [TestCase(typeof(TodoItem), typeof(LookupDto))]\n    [TestCase(typeof(TodoItem), typeof(TodoItemBriefDto))]\n    public void ShouldSupportMappingFromSourceToDestination(Type source, Type destination)\n    {\n        var instance = GetInstanceOf(source);\n\n        _mapper!.Map(instance, source, destination);\n    }\n\n    private static object GetInstanceOf(Type type)\n    {\n        if (type.GetConstructor(Type.EmptyTypes) != null)\n            return Activator.CreateInstance(type)!;\n\n        // Type without parameterless constructor\n        return RuntimeHelpers.GetUninitializedObject(type);\n    }\n\n\n    [OneTimeTearDown]\n    public void OneTimeTearDown()\n    {\n        _loggerFactory?.Dispose();\n    }\n}\n"
  },
  {
    "path": "tests/Application.UnitTests/Common/Models/PaginatedListTests.cs",
    "content": "using CleanArchitecture.Application.Common.Models;\nusing NUnit.Framework;\nusing Shouldly;\n\nnamespace CleanArchitecture.Application.UnitTests.Common.Models;\n\npublic class PaginatedListTests\n{\n    [Test]\n    public void HasPreviousPage_ShouldBeFalse_WhenOnFirstPage()\n    {\n        var list = new PaginatedList<int>([], 100, pageNumber: 1, pageSize: 10);\n\n        list.HasPreviousPage.ShouldBeFalse();\n    }\n\n    [Test]\n    public void HasPreviousPage_ShouldBeTrue_WhenOnSecondPage()\n    {\n        var list = new PaginatedList<int>([], 100, pageNumber: 2, pageSize: 10);\n\n        list.HasPreviousPage.ShouldBeTrue();\n    }\n\n    [Test]\n    public void HasPreviousPage_ShouldBeTrue_WhenOnePageBeyondLastPage()\n    {\n        var list = new PaginatedList<int>([], 100, pageNumber: 11, pageSize: 10);\n\n        list.HasPreviousPage.ShouldBeTrue();\n    }\n\n    [Test]\n    public void HasPreviousPage_ShouldBeFalse_WhenTwoPagesOrMoreBeyondLastPage()\n    {\n        var list = new PaginatedList<int>([], 100, pageNumber: 12, pageSize: 10);\n\n        list.HasPreviousPage.ShouldBeFalse();\n    }\n\n    [Test]\n    public void HasNextPage_ShouldBeFalse_WhenOnLastPage()\n    {\n        var list = new PaginatedList<int>([], 100, pageNumber: 10, pageSize: 10);\n\n        list.HasNextPage.ShouldBeFalse();\n    }\n\n    [Test]\n    public void HasNextPage_ShouldBeTrue_WhenNotOnLastPage()\n    {\n        var list = new PaginatedList<int>([], 100, pageNumber: 9, pageSize: 10);\n\n        list.HasNextPage.ShouldBeTrue();\n    }\n}\n"
  },
  {
    "path": "tests/Domain.UnitTests/Domain.UnitTests.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\r\n\r\n    <PropertyGroup>\r\n        <RootNamespace>CleanArchitecture.Domain.UnitTests</RootNamespace>\r\n        <AssemblyName>CleanArchitecture.Domain.UnitTests</AssemblyName>\r\n    </PropertyGroup>\r\n\r\n    <ItemGroup>\r\n        <PackageReference Include=\"Microsoft.NET.Test.Sdk\" />\r\n        <PackageReference Include=\"nunit\" />\r\n        <PackageReference Include=\"NUnit.Analyzers\">\r\n          <PrivateAssets>all</PrivateAssets>\r\n          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\r\n        </PackageReference>\r\n        <PackageReference Include=\"NUnit3TestAdapter\" />\r\n        <PackageReference Include=\"coverlet.collector\">\r\n          <PrivateAssets>all</PrivateAssets>\r\n          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\r\n        </PackageReference>\r\n        <PackageReference Include=\"Shouldly\" />\r\n    </ItemGroup>\r\n\r\n    <ItemGroup>\r\n        <ProjectReference Include=\"..\\..\\src\\Domain\\Domain.csproj\" />\r\n    </ItemGroup>\r\n\r\n</Project>\r\n"
  },
  {
    "path": "tests/Domain.UnitTests/ValueObjects/ColourTests.cs",
    "content": "﻿using CleanArchitecture.Domain.Exceptions;\nusing CleanArchitecture.Domain.ValueObjects;\nusing NUnit.Framework;\nusing Shouldly;\n\nnamespace CleanArchitecture.Domain.UnitTests.ValueObjects;\n\npublic class ColourTests\n{\n    [Test]\n    public void ShouldReturnCorrectColourCode()\n    {\n        var code = \"#FFFFFF\";\n\n        var colour = Colour.From(code);\n\n        colour.Code.ShouldBe(code);\n    }\n\n    [Test]\n    public void ToStringReturnsCode()\n    {\n        var colour = Colour.White;\n\n        colour.ToString().ShouldBe(colour.Code);\n    }\n\n    [Test]\n    public void ShouldPerformImplicitConversionToColourCodeString()\n    {\n        string code = Colour.White;\n\n        code.ShouldBe(\"#FFFFFF\");\n    }\n\n    [Test]\n    public void ShouldPerformExplicitConversionGivenSupportedColourCode()\n    {\n        var colour = (Colour)\"#FFFFFF\";\n\n        colour.ShouldBe(Colour.White);\n    }\n\n    [Test]\n    public void ShouldThrowUnsupportedColourExceptionGivenNotSupportedColourCode()\n    {\n        Should.Throw<UnsupportedColourException>(() => Colour.From(\"##FF33CC\"));\n    }\n\n    [Test]\n    public void ShouldBeComparableWithOperators()\n    {\n        var color1 = new Colour(\"#FFFFFF\");\n        var color2 = new Colour(\"#FFFFFF\");\n        var color3 = new Colour(\"#AAAAAA\");\n        (color1 == color2).ShouldBe(true);\n        (color1 == color3).ShouldBe(false);\n    }\n}\n"
  },
  {
    "path": "tests/Infrastructure.IntegrationTests/GlobalUsings.cs",
    "content": "global using NUnit.Framework;"
  },
  {
    "path": "tests/Infrastructure.IntegrationTests/Infrastructure.IntegrationTests.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\r\n\r\n    <PropertyGroup>\r\n        <RootNamespace>CleanArchitecture.Infrastructure.IntegrationTests</RootNamespace>\r\n        <AssemblyName>CleanArchitecture.Infrastructure.IntegrationTests</AssemblyName>\r\n    </PropertyGroup>\r\n\r\n    <ItemGroup>\r\n        <PackageReference Include=\"Microsoft.NET.Test.Sdk\" />\r\n        <PackageReference Include=\"NUnit\" />\r\n        <PackageReference Include=\"NUnit3TestAdapter\" />\r\n        <PackageReference Include=\"NUnit.Analyzers\">\r\n          <PrivateAssets>all</PrivateAssets>\r\n          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\r\n        </PackageReference>\r\n        <PackageReference Include=\"coverlet.collector\">\r\n          <PrivateAssets>all</PrivateAssets>\r\n          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\r\n        </PackageReference>\r\n    </ItemGroup>\r\n\r\n</Project>\r\n"
  },
  {
    "path": "tests/TestAppHost/Program.cs",
    "content": "using CleanArchitecture.Shared;\n\nnamespace CleanArchitecture.TestAppHost;\n\npublic class Program\n{\n    public static void Main(string[] args)\n    {\n        var builder = DistributedApplication.CreateBuilder(args);\n\n        #if (UsePostgreSQL)\n        builder.AddPostgres(Services.DatabaseServer)\n            .AddDatabase(Services.Database);\n        #elif (UseSqlServer)\n        builder.AddSqlServer(Services.DatabaseServer)\n            .AddDatabase(Services.Database);\n        #else\n        builder\n            .AddSqlite(Services.Database);\n        #endif\n\n        builder.Build().Run();\n    }\n}"
  },
  {
    "path": "tests/TestAppHost/TestAppHost.csproj",
    "content": "<Project Sdk=\"Aspire.AppHost.Sdk/13.1.3\">\n\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <RootNamespace>CleanArchitecture.TestAppHost</RootNamespace>\n    <AssemblyName>CleanArchitecture.TestAppHost</AssemblyName>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Aspire.Hosting.AppHost\" />\n    <!--#if (UsePostgreSQL)-->\n    <PackageReference Include=\"Aspire.Hosting.PostgreSQL\" />\n    <!--#endif-->\n    <!--#if (UseSqlServer)-->\n    <PackageReference Include=\"Aspire.Hosting.SqlServer\" />\n    <!--#endif-->\n    <!--#if (UseSqlite)-->\n    <PackageReference Include=\"CommunityToolkit.Aspire.Hosting.SQLite\" />\n    <!--#endif-->\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Shared\\Shared.csproj\" IsAspireProjectResource=\"false\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "tests/Web.AcceptanceTests/AspireSetup.cs",
    "content": "using Aspire.Hosting;\n\nnamespace CleanArchitecture.Web.AcceptanceTests;\n\n[SetUpFixture]\npublic class AspireSetup\n{\n    private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(60);\n\n    public static IDistributedApplicationTestingBuilder Builder { get; private set; } = null!;\n    public static DistributedApplication App { get; private set; } = null!;\n\n    [OneTimeSetUp]\n    public async Task OneTimeSetup()\n    {\n        var cts = new CancellationTokenSource(DefaultTimeout);\n        var cancellationToken = cts.Token;\n\n        Builder = await DistributedApplicationTestingBuilder\n             .CreateAsync<Projects.AppHost>(\n                args: [],\n                configureBuilder: (options, _) =>\n                {\n                    options.DisableDashboard = false; // Enable the dashboard for testing purposes\n                });\n\n        Builder.Configuration[\"ASPIRE_ALLOW_UNSECURED_TRANSPORT\"] = \"true\";\n\n        Builder.Services.AddLogging(logging =>\n        {\n            logging.SetMinimumLevel(LogLevel.Debug);\n            // Override the logging filters from the app's configuration\n            logging.AddFilter(Builder.Environment.ApplicationName, LogLevel.Debug);\n            logging.AddFilter(\"Aspire.\", LogLevel.Debug);\n        });\n\n        Builder.Services.ConfigureHttpClientDefaults(clientBuilder =>\n        {\n            clientBuilder.AddStandardResilienceHandler();\n        });\n\n        App = await Builder\n            .BuildAsync(cancellationToken)\n            .WaitAsync(cancellationToken);\n\n        await App\n            .StartAsync(cancellationToken)\n            .WaitAsync(cancellationToken);\n\n        await Task.WhenAll(\n            App.ResourceNotifications.WaitForResourceHealthyAsync(Services.WebApi, cancellationToken).WaitAsync(cancellationToken),\n            App.ResourceNotifications.WaitForResourceHealthyAsync(Services.WebFrontend, cancellationToken).WaitAsync(cancellationToken));\n    }\n\n    [OneTimeTearDown]\n    public async Task OneTimeTearDown()\n    {\n        await App.DisposeAsync();\n    }\n}\n"
  },
  {
    "path": "tests/Web.AcceptanceTests/Features/Counter.feature",
    "content": "@Counter\nFeature: Counter\n\nScenario: Counter page is displayed\n    Given a user visits the counter page\n    Then the counter heading is \"Counter\"\n\nScenario: Counter increments when button is clicked\n    Given a user visits the counter page\n    Then the current count is 0\n    When the user clicks increment\n    Then the current count is 1\n"
  },
  {
    "path": "tests/Web.AcceptanceTests/Features/Home.feature",
    "content": "@Home\nFeature: Home\n\nScenario: Welcome heading is displayed\n    Given a user visits the home page\n    Then the heading \"Welcome\" is visible\n"
  },
  {
    "path": "tests/Web.AcceptanceTests/Features/Login.feature",
    "content": "﻿@Login\nFeature: Login\n    User can log in\n\nScenario: User can log in with valid credentials\n\tGiven a logged out user\n\tWhen the user logs in with valid credentials\n\tThen they log in successfully\n\nScenario: User cannot log in with invalid credentials\n    Given a logged out user\n    When the user logs in with invalid credentials\n    Then an error is displayed"
  },
  {
    "path": "tests/Web.AcceptanceTests/Features/Weather.feature",
    "content": "@Weather\nFeature: Weather\n\nScenario: Weather forecast data is displayed\n    Given an authenticated user visits the weather page\n    Then the weather forecast heading is \"Weather\"\n    And the weather forecast table is displayed\n    And 5 weather forecasts are shown\n"
  },
  {
    "path": "tests/Web.AcceptanceTests/GlobalUsings.cs",
    "content": "﻿global using CleanArchitecture.Web.AcceptanceTests.Pages;\nglobal using CleanArchitecture.Shared;\nglobal using Microsoft.Playwright;\nglobal using Reqnroll;\nglobal using Reqnroll.BoDi;\nglobal using Shouldly;\n"
  },
  {
    "path": "tests/Web.AcceptanceTests/Pages/BasePage.cs",
    "content": "namespace CleanArchitecture.Web.AcceptanceTests.Pages;\n\npublic abstract class BasePage(IPage page)\n{\n    protected static string BaseUrl => AspireSetup.App.GetEndpoint(Services.WebFrontend).ToString().TrimEnd('/');\n\n    public abstract string PagePath { get; }\n\n    protected IPage Page { get; } = page;\n\n    public Task GotoAsync() => Page.GotoAsync(PagePath);\n}\n"
  },
  {
    "path": "tests/Web.AcceptanceTests/Pages/CounterPage.cs",
    "content": "namespace CleanArchitecture.Web.AcceptanceTests.Pages;\n\npublic class CounterPage(IPage page) : BasePage(page)\n{\n    public override string PagePath => $\"{BaseUrl}/counter\";\n\n    public Task AssertHeading(string text)\n        => Assertions.Expect(Page.Locator(\"h1\")).ToHaveTextAsync(text);\n\n    public Task AssertCurrentCount(int count)\n        => Assertions.Expect(Page.Locator(\"p[aria-live='polite'] strong\")).ToHaveTextAsync(count.ToString());\n\n    public Task ClickIncrement()\n        => Page.Locator(\"button:has-text('Increment')\").ClickAsync();\n}\n"
  },
  {
    "path": "tests/Web.AcceptanceTests/Pages/HomePage.cs",
    "content": "namespace CleanArchitecture.Web.AcceptanceTests.Pages;\n\npublic class HomePage(IPage page) : BasePage(page)\n{\n    public override string PagePath => BaseUrl;\n\n    public Task AssertHeading(string text)\n        => Assertions.Expect(Page.Locator(\"h1\")).ToHaveTextAsync(text);\n}\n"
  },
  {
    "path": "tests/Web.AcceptanceTests/Pages/LoginPage.cs",
    "content": "namespace CleanArchitecture.Web.AcceptanceTests.Pages;\n\npublic class LoginPage(IPage page) : BasePage(page)\n{\n    public override string PagePath => $\"{BaseUrl}/login\";\n\n    public Task SetEmail(string email)\n        => Page.FillAsync(\"#email\", email);\n\n    public Task SetPassword(string password)\n        => Page.FillAsync(\"#password\", password);\n\n    public Task ClickLogin()\n        => Page.Locator(\"button[type='submit']\").ClickAsync();\n\n    public Task<string?> LogoutButtonText()\n        => Page.Locator(\"a:has-text('Log out')\").TextContentAsync();\n\n    public Task AssertErrorVisible()\n        => Assertions.Expect(Page.Locator(\"#login-error\")).ToBeVisibleAsync();\n}\n"
  },
  {
    "path": "tests/Web.AcceptanceTests/Pages/WeatherPage.cs",
    "content": "namespace CleanArchitecture.Web.AcceptanceTests.Pages;\n\npublic class WeatherPage(IPage page) : BasePage(page)\n{\n    public override string PagePath => $\"{BaseUrl}/weather\";\n\n    public Task AssertHeading(string text)\n        => Assertions.Expect(Page.Locator(\"h1\")).ToHaveTextAsync(text);\n\n    public Task AssertTableVisible()\n        => Assertions.Expect(Page.Locator(\"table\")).ToBeVisibleAsync();\n\n    public Task AssertRowCount(int count)\n        => Assertions.Expect(Page.Locator(\"tbody tr\")).ToHaveCountAsync(count);\n}\n"
  },
  {
    "path": "tests/Web.AcceptanceTests/PlaywrightSetup.cs",
    "content": "using System.Diagnostics;\n\nnamespace CleanArchitecture.Web.AcceptanceTests;\n\n[SetUpFixture]\npublic class PlaywrightSetup\n{\n    private static bool IsHeadless => Debugger.IsAttached is false;\n    private static IPlaywright? _playwright;\n\n    public static IBrowser Browser { get; private set; } = null!;\n\n    [OneTimeSetUp]\n    public async Task OneTimeSetUp()\n    {\n        Assertions.SetDefaultExpectTimeout(10_000);\n\n        _playwright = await Playwright.CreateAsync();\n\n        Browser = await _playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions\n        {\n            Headless = IsHeadless,\n            SlowMo = IsHeadless ? 0 : 500\n        });\n    }\n\n    [OneTimeTearDown]\n    public async Task OneTimeTearDown()\n    {\n        await Browser.CloseAsync();\n        _playwright?.Dispose();\n    }\n}\n"
  },
  {
    "path": "tests/Web.AcceptanceTests/StepDefinitions/CounterStepDefinitions.cs",
    "content": "namespace CleanArchitecture.Web.AcceptanceTests.StepDefinitions;\n\n[Binding]\npublic sealed class CounterStepDefinitions(CounterPage counterPage)\n{\n    [BeforeFeature(\"Counter\")]\n    public static async Task BeforeCounterFeature(IObjectContainer container)\n    {\n        var context = await PlaywrightSetup.Browser.NewContextAsync();\n        var page = await context.NewPageAsync();\n        container.RegisterInstanceAs(context);\n        container.RegisterInstanceAs(new CounterPage(page));\n    }\n\n    [AfterFeature]\n    public static async Task AfterCounterFeature(IObjectContainer container)\n    {\n        var context = container.Resolve<IBrowserContext>();\n        await context.DisposeAsync();\n    }\n\n    [Given(\"a user visits the counter page\")]\n    public Task GivenAUserVisitsTheCounterPage() => counterPage.GotoAsync();\n\n    [Then(\"the counter heading is {string}\")]\n    public Task ThenTheCounterHeadingIs(string text) => counterPage.AssertHeading(text);\n\n    [Then(\"the current count is {int}\")]\n    public Task ThenTheCurrentCountIs(int count) => counterPage.AssertCurrentCount(count);\n\n    [When(\"the user clicks increment\")]\n    public Task WhenTheUserClicksIncrement() => counterPage.ClickIncrement();\n}\n"
  },
  {
    "path": "tests/Web.AcceptanceTests/StepDefinitions/HomeStepDefinitions.cs",
    "content": "namespace CleanArchitecture.Web.AcceptanceTests.StepDefinitions;\n\n[Binding]\npublic sealed class HomeStepDefinitions(HomePage homePage)\n{\n    [BeforeFeature(\"Home\")]\n    public static async Task BeforeHomeFeature(IObjectContainer container)\n    {\n        var context = await PlaywrightSetup.Browser.NewContextAsync();\n        var page = await context.NewPageAsync();\n        container.RegisterInstanceAs(context);\n        container.RegisterInstanceAs(new HomePage(page));\n    }\n\n    [AfterFeature]\n    public static async Task AfterHomeFeature(IObjectContainer container)\n    {\n        var context = container.Resolve<IBrowserContext>();\n        await context.DisposeAsync();\n    }\n\n    [Given(\"a user visits the home page\")]\n    public Task GivenAUserVisitsTheHomePage() => homePage.GotoAsync();\n\n    [Then(\"the heading {string} is visible\")]\n    public Task ThenTheHeadingIsVisible(string text) => homePage.AssertHeading(text);\n}\n"
  },
  {
    "path": "tests/Web.AcceptanceTests/StepDefinitions/LoginStepDefinitions.cs",
    "content": "namespace CleanArchitecture.Web.AcceptanceTests.StepDefinitions;\n\n[Binding]\npublic sealed class LoginStepDefinitions(LoginPage loginPage)\n{\n    [BeforeFeature(\"Login\")]\n    public static async Task BeforeLoginFeature(IObjectContainer container)\n    {\n        var context = await PlaywrightSetup.Browser.NewContextAsync();\n        var page = await context.NewPageAsync();\n        container.RegisterInstanceAs(context);\n        container.RegisterInstanceAs(new LoginPage(page));\n    }\n\n    [AfterFeature]\n    public static async Task AfterLoginFeature(IObjectContainer container)\n    {\n        var context = container.Resolve<IBrowserContext>();\n        await context.DisposeAsync();\n    }\n\n    [Given(\"a logged out user\")]\n    public Task GivenALoggedOutUser() => loginPage.GotoAsync();\n\n    [When(\"the user logs in with valid credentials\")]\n    public async Task TheUserLogsInWithValidCredentials()\n    {\n        await loginPage.SetEmail(\"administrator@localhost\");\n        await loginPage.SetPassword(\"Administrator1!\");\n        await loginPage.ClickLogin();\n    }\n\n    [Then(\"they log in successfully\")]\n    public async Task TheyLogInSuccessfully()\n    {\n        var logoutButtonText = await loginPage.LogoutButtonText();\n\n        logoutButtonText.ShouldNotBeNull();\n        logoutButtonText.ShouldBe(\"Log out\");\n    }\n\n    [When(\"the user logs in with invalid credentials\")]\n    public async Task TheUserLogsInWithInvalidCredentials()\n    {\n        await loginPage.SetEmail(\"hacker@localhost\");\n        await loginPage.SetPassword(\"l337hax!\");\n        await loginPage.ClickLogin();\n    }\n\n    [Then(\"an error is displayed\")]\n    public Task AnErrorIsDisplayed() => loginPage.AssertErrorVisible();\n}\n"
  },
  {
    "path": "tests/Web.AcceptanceTests/StepDefinitions/WeatherStepDefinitions.cs",
    "content": "namespace CleanArchitecture.Web.AcceptanceTests.StepDefinitions;\n\n[Binding]\npublic sealed class WeatherStepDefinitions(WeatherPage weatherPage)\n{\n    [BeforeFeature(\"Weather\")]\n    public static async Task BeforeWeatherFeature(IObjectContainer container)\n    {\n        var context = await PlaywrightSetup.Browser.NewContextAsync();\n        var page = await context.NewPageAsync();\n\n        var loginPage = new LoginPage(page);\n        await loginPage.GotoAsync();\n        await loginPage.SetEmail(\"administrator@localhost\");\n        await loginPage.SetPassword(\"Administrator1!\");\n        await loginPage.ClickLogin();\n        await Assertions.Expect(page.Locator(\"a:has-text('Log out')\")).ToBeVisibleAsync();\n\n        container.RegisterInstanceAs(context);\n        container.RegisterInstanceAs(new WeatherPage(page));\n    }\n\n    [AfterFeature]\n    public static async Task AfterWeatherFeature(IObjectContainer container)\n    {\n        var context = container.Resolve<IBrowserContext>();\n        await context.DisposeAsync();\n    }\n\n    [Given(\"an authenticated user visits the weather page\")]\n    public Task GivenAnAuthenticatedUserVisitsTheWeatherPage() => weatherPage.GotoAsync();\n\n    [Then(\"the weather forecast heading is {string}\")]\n    public Task ThenTheWeatherForecastHeadingIs(string text) => weatherPage.AssertHeading(text);\n\n    [Then(\"the weather forecast table is displayed\")]\n    public Task ThenTheWeatherForecastTableIsDisplayed() => weatherPage.AssertTableVisible();\n\n    [Then(\"{int} weather forecasts are shown\")]\n    public Task ThenWeatherForecastsAreShown(int count) => weatherPage.AssertRowCount(count);\n}\n"
  },
  {
    "path": "tests/Web.AcceptanceTests/Web.AcceptanceTests.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n    <PropertyGroup>\n        <RootNamespace>CleanArchitecture.Web.AcceptanceTests</RootNamespace>\n        <AssemblyName>CleanArchitecture.Web.AcceptanceTests</AssemblyName>\n        <ReqnrollUseIntermediateOutputPathForCodeBehind>true</ReqnrollUseIntermediateOutputPathForCodeBehind>\n    </PropertyGroup>\n\n    <ItemGroup>\n        <PackageReference Include=\"Aspire.Hosting.Testing\" />\n        <PackageReference Include=\"Microsoft.NET.Test.Sdk\" />\n        <PackageReference Include=\"Microsoft.Playwright\" />\n        <PackageReference Include=\"Reqnroll.NUnit\" />\n        <PackageReference Include=\"Shouldly\" />\n        <PackageReference Include=\"nunit\" />\n        <PackageReference Include=\"NUnit3TestAdapter\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <Using Include=\"Aspire.Hosting.ApplicationModel\" />\n        <Using Include=\"Aspire.Hosting.Testing\" />\n        <Using Include=\"Microsoft.Extensions.DependencyInjection\" />\n        <Using Include=\"Microsoft.Extensions.Logging\" />\n        <Using Include=\"NUnit.Framework\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <ProjectReference Include=\"..\\..\\src\\AppHost\\AppHost.csproj\" />\n        <ProjectReference Include=\"..\\..\\src\\Shared\\Shared.csproj\" />\n    </ItemGroup>\n\n</Project>\n"
  }
]