diff --git a/.github/workflows/AZ2007_Zip_Code_M2.yml b/.github/workflows/AZ2007_Zip_Code_M2.yml new file mode 100644 index 0000000..981c9bd --- /dev/null +++ b/.github/workflows/AZ2007_Zip_Code_M2.yml @@ -0,0 +1,60 @@ +# This workflow is triggered on two events: workflow_dispatch and push. +# +# - The workflow_dispatch event allows you to manually trigger the workflow from GitHub's UI. +# - The push event triggers the workflow whenever there's a push to the master branch, but only +# if the changes include files in the LearnModuleExercises/SampleApps/** directory. +# +# The defaults section sets the default shell for all run commands in the workflow to PowerShell (pwsh). +# +# The workflow consists of a single job named create_zip, which runs on the latest version of Ubuntu. +# +# This job has three steps: +# +# 1. The Checkout step uses the actions/checkout@v4 action to checkout the repository's code onto the +# runner. This is a common first step in most workflows as it allows subsequent steps to operate on +# the codebase. +# +# 2. The Create SampleApps zip step changes the current directory to ./LearnModuleExercises/SampleApps +# and then creates a zip file of all the files in that directory, including those in the .vscode +# subdirectory. The -r option is used to zip directories recursively and the -q option is used to run +# the command quietly without printing a lot of output. The resulting zip file is saved in the +# ../Downloads directory with the name SampleApps.zip. +# +# 3. The Commit and push step uses the Endbug/add-and-commit@v7 action to add the newly created zip file +# to the repository, commit the changes with the message 'Updating Zip for API source files', and then +# push the changes back to the repository. The add input is set to the path of the zip file and the push +# input is set to true to enable pushing. +# +# This workflow is useful for automatically packaging and versioning sample applications whenever changes +# are made to them. +# +name: CreateAZ2007M2SamplesZip +on: + workflow_dispatch: + push: + branches: + - 'main' + paths: + - DownloadableCodeProjects/az-2007-m2-explain-document/** + +defaults: + run: + shell: pwsh + +jobs: + create_zip: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Create AZ2007M2 SampleApps zip + run: | + cd ./DownloadableCodeProjects/az-2007-m2-explain-document + rm -f ../Downloads/AZ2007LabAppM2.zip + zip -r -q ../Downloads/AZ2007LabAppM2.zip $(git ls-files) + - name: Commit and push + uses: Endbug/add-and-commit@v7 + with: + add: '["DownloadableCodeProjects/Downloads/AZ2007LabAppM2.zip"]' + message: 'Updating Zip with sample app source files' + push: true diff --git a/.github/workflows/AZ2007_Zip_Code_M2_py.yml b/.github/workflows/AZ2007_Zip_Code_M2_py.yml new file mode 100644 index 0000000..558e370 --- /dev/null +++ b/.github/workflows/AZ2007_Zip_Code_M2_py.yml @@ -0,0 +1,60 @@ +# This workflow is triggered on two events: workflow_dispatch and push. +# +# - The workflow_dispatch event allows you to manually trigger the workflow from GitHub's UI. +# - The push event triggers the workflow whenever there's a push to the master branch, but only +# if the changes include files in the LearnModuleExercises/SampleApps/** directory. +# +# The defaults section sets the default shell for all run commands in the workflow to PowerShell (pwsh). +# +# The workflow consists of a single job named create_zip, which runs on the latest version of Ubuntu. +# +# This job has three steps: +# +# 1. The Checkout step uses the actions/checkout@v4 action to checkout the repository's code onto the +# runner. This is a common first step in most workflows as it allows subsequent steps to operate on +# the codebase. +# +# 2. The Create SampleApps zip step changes the current directory to ./LearnModuleExercises/SampleApps +# and then creates a zip file of all the files in that directory, including those in the .vscode +# subdirectory. The -r option is used to zip directories recursively and the -q option is used to run +# the command quietly without printing a lot of output. The resulting zip file is saved in the +# ../Downloads directory with the name SampleApps.zip. +# +# 3. The Commit and push step uses the Endbug/add-and-commit@v7 action to add the newly created zip file +# to the repository, commit the changes with the message 'Updating Zip for API source files', and then +# push the changes back to the repository. The add input is set to the path of the zip file and the push +# input is set to true to enable pushing. +# +# This workflow is useful for automatically packaging and versioning sample applications whenever changes +# are made to them. +# +name: CreateAZ2007M2PySamplesZip +on: + workflow_dispatch: + push: + branches: + - 'main' + paths: + - DownloadableCodeProjects/az-2007-m2-explain-document-python/** + +defaults: + run: + shell: pwsh + +jobs: + create_zip: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Create AZ2007M2 PySampleApps zip + run: | + cd ./DownloadableCodeProjects/az-2007-m2-explain-document-python + rm -f ../Downloads/AZ2007LabAppM2Python.zip + zip -r -q ../Downloads/AZ2007LabAppM2Python.zip $(git ls-files) + - name: Commit and push + uses: Endbug/add-and-commit@v7 + with: + add: '["DownloadableCodeProjects/Downloads/AZ2007LabAppM2Python.zip"]' + message: 'Updating Zip with sample app source files' + push: true diff --git a/.github/workflows/AZ2007_Zip_Code_M3.yml b/.github/workflows/AZ2007_Zip_Code_M3.yml new file mode 100644 index 0000000..3cf8a3d --- /dev/null +++ b/.github/workflows/AZ2007_Zip_Code_M3.yml @@ -0,0 +1,60 @@ +# This workflow is triggered on two events: workflow_dispatch and push. +# +# - The workflow_dispatch event allows you to manually trigger the workflow from GitHub's UI. +# - The push event triggers the workflow whenever there's a push to the master branch, but only +# if the changes include files in the LearnModuleExercises/SampleApps/** directory. +# +# The defaults section sets the default shell for all run commands in the workflow to PowerShell (pwsh). +# +# The workflow consists of a single job named create_zip, which runs on the latest version of Ubuntu. +# +# This job has three steps: +# +# 1. The Checkout step uses the actions/checkout@v4 action to checkout the repository's code onto the +# runner. This is a common first step in most workflows as it allows subsequent steps to operate on +# the codebase. +# +# 2. The Create SampleApps zip step changes the current directory to ./LearnModuleExercises/SampleApps +# and then creates a zip file of all the files in that directory, including those in the .vscode +# subdirectory. The -r option is used to zip directories recursively and the -q option is used to run +# the command quietly without printing a lot of output. The resulting zip file is saved in the +# ../Downloads directory with the name SampleApps.zip. +# +# 3. The Commit and push step uses the Endbug/add-and-commit@v7 action to add the newly created zip file +# to the repository, commit the changes with the message 'Updating Zip for API source files', and then +# push the changes back to the repository. The add input is set to the path of the zip file and the push +# input is set to true to enable pushing. +# +# This workflow is useful for automatically packaging and versioning sample applications whenever changes +# are made to them. +# +name: CreateAZ2007M3SamplesZip +on: + workflow_dispatch: + push: + branches: + - 'main' + paths: + - DownloadableCodeProjects/az-2007-m3-develop-code/** + +defaults: + run: + shell: pwsh + +jobs: + create_zip: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Create AZ2007M3 SampleApps zip + run: | + cd ./DownloadableCodeProjects/az-2007-m3-develop-code + rm -f ../Downloads/AZ2007LabAppM3.zip + zip -r -q ../Downloads/AZ2007LabAppM3.zip $(git ls-files) + - name: Commit and push + uses: Endbug/add-and-commit@v7 + with: + add: '["DownloadableCodeProjects/Downloads/AZ2007LabAppM3.zip"]' + message: 'Updating Zip with sample app source files' + push: true diff --git a/.github/workflows/AZ2007_Zip_Code_M3_py.yml b/.github/workflows/AZ2007_Zip_Code_M3_py.yml new file mode 100644 index 0000000..d9592ee --- /dev/null +++ b/.github/workflows/AZ2007_Zip_Code_M3_py.yml @@ -0,0 +1,60 @@ +# This workflow is triggered on two events: workflow_dispatch and push. +# +# - The workflow_dispatch event allows you to manually trigger the workflow from GitHub's UI. +# - The push event triggers the workflow whenever there's a push to the master branch, but only +# if the changes include files in the LearnModuleExercises/SampleApps/** directory. +# +# The defaults section sets the default shell for all run commands in the workflow to PowerShell (pwsh). +# +# The workflow consists of a single job named create_zip, which runs on the latest version of Ubuntu. +# +# This job has three steps: +# +# 1. The Checkout step uses the actions/checkout@v4 action to checkout the repository's code onto the +# runner. This is a common first step in most workflows as it allows subsequent steps to operate on +# the codebase. +# +# 2. The Create SampleApps zip step changes the current directory to ./LearnModuleExercises/SampleApps +# and then creates a zip file of all the files in that directory, including those in the .vscode +# subdirectory. The -r option is used to zip directories recursively and the -q option is used to run +# the command quietly without printing a lot of output. The resulting zip file is saved in the +# ../Downloads directory with the name SampleApps.zip. +# +# 3. The Commit and push step uses the Endbug/add-and-commit@v7 action to add the newly created zip file +# to the repository, commit the changes with the message 'Updating Zip for API source files', and then +# push the changes back to the repository. The add input is set to the path of the zip file and the push +# input is set to true to enable pushing. +# +# This workflow is useful for automatically packaging and versioning sample applications whenever changes +# are made to them. +# +name: CreateAZ2007M3PySamplesZip +on: + workflow_dispatch: + push: + branches: + - 'main' + paths: + - DownloadableCodeProjects/az-2007-m3-develop-code-python/** + +defaults: + run: + shell: pwsh + +jobs: + create_zip: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Create AZ2007M3 PySampleApps zip + run: | + cd ./DownloadableCodeProjects/az-2007-m3-develop-code-python + rm -f ../Downloads/AZ2007LabAppM3Python.zip + zip -r -q ../Downloads/AZ2007LabAppM3Python.zip $(git ls-files) + - name: Commit and push + uses: Endbug/add-and-commit@v7 + with: + add: '["DownloadableCodeProjects/Downloads/AZ2007LabAppM3Python.zip"]' + message: 'Updating Zip with sample app source files' + push: true diff --git a/.github/workflows/AZ2007_Zip_Code_M4.yml b/.github/workflows/AZ2007_Zip_Code_M4.yml new file mode 100644 index 0000000..faee4e2 --- /dev/null +++ b/.github/workflows/AZ2007_Zip_Code_M4.yml @@ -0,0 +1,60 @@ +# This workflow is triggered on two events: workflow_dispatch and push. +# +# - The workflow_dispatch event allows you to manually trigger the workflow from GitHub's UI. +# - The push event triggers the workflow whenever there's a push to the master branch, but only +# if the changes include files in the LearnModuleExercises/SampleApps/** directory. +# +# The defaults section sets the default shell for all run commands in the workflow to PowerShell (pwsh). +# +# The workflow consists of a single job named create_zip, which runs on the latest version of Ubuntu. +# +# This job has three steps: +# +# 1. The Checkout step uses the actions/checkout@v4 action to checkout the repository's code onto the +# runner. This is a common first step in most workflows as it allows subsequent steps to operate on +# the codebase. +# +# 2. The Create SampleApps zip step changes the current directory to ./LearnModuleExercises/SampleApps +# and then creates a zip file of all the files in that directory, including those in the .vscode +# subdirectory. The -r option is used to zip directories recursively and the -q option is used to run +# the command quietly without printing a lot of output. The resulting zip file is saved in the +# ../Downloads directory with the name SampleApps.zip. +# +# 3. The Commit and push step uses the Endbug/add-and-commit@v7 action to add the newly created zip file +# to the repository, commit the changes with the message 'Updating Zip for API source files', and then +# push the changes back to the repository. The add input is set to the path of the zip file and the push +# input is set to true to enable pushing. +# +# This workflow is useful for automatically packaging and versioning sample applications whenever changes +# are made to them. +# +name: CreateAZ2007M4SamplesZip +on: + workflow_dispatch: + push: + branches: + - 'main' + paths: + - DownloadableCodeProjects/az-2007-m4-develop-unit-tests/** + +defaults: + run: + shell: pwsh + +jobs: + create_zip: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Create AZ2007M4 SampleApps zip + run: | + cd ./DownloadableCodeProjects/az-2007-m4-develop-unit-tests + rm -f ../Downloads/AZ2007LabAppM4.zip + zip -r -q ../Downloads/AZ2007LabAppM4.zip $(git ls-files) + - name: Commit and push + uses: Endbug/add-and-commit@v7 + with: + add: '["DownloadableCodeProjects/Downloads/AZ2007LabAppM4.zip"]' + message: 'Updating Zip with sample app source files' + push: true diff --git a/.github/workflows/AZ2007_Zip_Code_M4_py.yml b/.github/workflows/AZ2007_Zip_Code_M4_py.yml new file mode 100644 index 0000000..464a37b --- /dev/null +++ b/.github/workflows/AZ2007_Zip_Code_M4_py.yml @@ -0,0 +1,60 @@ +# This workflow is triggered on two events: workflow_dispatch and push. +# +# - The workflow_dispatch event allows you to manually trigger the workflow from GitHub's UI. +# - The push event triggers the workflow whenever there's a push to the master branch, but only +# if the changes include files in the LearnModuleExercises/SampleApps/** directory. +# +# The defaults section sets the default shell for all run commands in the workflow to PowerShell (pwsh). +# +# The workflow consists of a single job named create_zip, which runs on the latest version of Ubuntu. +# +# This job has three steps: +# +# 1. The Checkout step uses the actions/checkout@v4 action to checkout the repository's code onto the +# runner. This is a common first step in most workflows as it allows subsequent steps to operate on +# the codebase. +# +# 2. The Create SampleApps zip step changes the current directory to ./LearnModuleExercises/SampleApps +# and then creates a zip file of all the files in that directory, including those in the .vscode +# subdirectory. The -r option is used to zip directories recursively and the -q option is used to run +# the command quietly without printing a lot of output. The resulting zip file is saved in the +# ../Downloads directory with the name SampleApps.zip. +# +# 3. The Commit and push step uses the Endbug/add-and-commit@v7 action to add the newly created zip file +# to the repository, commit the changes with the message 'Updating Zip for API source files', and then +# push the changes back to the repository. The add input is set to the path of the zip file and the push +# input is set to true to enable pushing. +# +# This workflow is useful for automatically packaging and versioning sample applications whenever changes +# are made to them. +# +name: CreateAZ2007M4PySamplesZip +on: + workflow_dispatch: + push: + branches: + - 'main' + paths: + - DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/** + +defaults: + run: + shell: pwsh + +jobs: + create_zip: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Create AZ2007M4 PySampleApps zip + run: | + cd ./DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest + rm -f ../Downloads/AZ2007LabAppM4Python.zip + zip -r -q ../Downloads/AZ2007LabAppM4Python.zip $(git ls-files) + - name: Commit and push + uses: Endbug/add-and-commit@v7 + with: + add: '["DownloadableCodeProjects/Downloads/AZ2007LabAppM4Python.zip"]' + message: 'Updating Zip with sample app source files' + push: true diff --git a/.github/workflows/AZ2007_Zip_Code_M5.yml b/.github/workflows/AZ2007_Zip_Code_M5.yml new file mode 100644 index 0000000..5d6be95 --- /dev/null +++ b/.github/workflows/AZ2007_Zip_Code_M5.yml @@ -0,0 +1,60 @@ +# This workflow is triggered on two events: workflow_dispatch and push. +# +# - The workflow_dispatch event allows you to manually trigger the workflow from GitHub's UI. +# - The push event triggers the workflow whenever there's a push to the master branch, but only +# if the changes include files in the LearnModuleExercises/SampleApps/** directory. +# +# The defaults section sets the default shell for all run commands in the workflow to PowerShell (pwsh). +# +# The workflow consists of a single job named create_zip, which runs on the latest version of Ubuntu. +# +# This job has three steps: +# +# 1. The Checkout step uses the actions/checkout@v4 action to checkout the repository's code onto the +# runner. This is a common first step in most workflows as it allows subsequent steps to operate on +# the codebase. +# +# 2. The Create SampleApps zip step changes the current directory to ./LearnModuleExercises/SampleApps +# and then creates a zip file of all the files in that directory, including those in the .vscode +# subdirectory. The -r option is used to zip directories recursively and the -q option is used to run +# the command quietly without printing a lot of output. The resulting zip file is saved in the +# ../Downloads directory with the name SampleApps.zip. +# +# 3. The Commit and push step uses the Endbug/add-and-commit@v7 action to add the newly created zip file +# to the repository, commit the changes with the message 'Updating Zip for API source files', and then +# push the changes back to the repository. The add input is set to the path of the zip file and the push +# input is set to true to enable pushing. +# +# This workflow is useful for automatically packaging and versioning sample applications whenever changes +# are made to them. +# +name: CreateAZ2007M5SamplesZip +on: + workflow_dispatch: + push: + branches: + - 'main' + paths: + - DownloadableCodeProjects/az-2007-m5-refactor-improve-code/** + +defaults: + run: + shell: pwsh + +jobs: + create_zip: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Create AZ2007M5 SampleApps zip + run: | + cd ./DownloadableCodeProjects/az-2007-m5-refactor-improve-code + rm -f ../Downloads/AZ2007LabAppM5.zip + zip -r -q ../Downloads/AZ2007LabAppM5.zip $(git ls-files) + - name: Commit and push + uses: Endbug/add-and-commit@v7 + with: + add: '["DownloadableCodeProjects/Downloads/AZ2007LabAppM5.zip"]' + message: 'Updating Zip with sample app source files' + push: true diff --git a/.github/workflows/AZ2007_Zip_Code_M5_py.yml b/.github/workflows/AZ2007_Zip_Code_M5_py.yml new file mode 100644 index 0000000..3c88ec5 --- /dev/null +++ b/.github/workflows/AZ2007_Zip_Code_M5_py.yml @@ -0,0 +1,60 @@ +# This workflow is triggered on two events: workflow_dispatch and push. +# +# - The workflow_dispatch event allows you to manually trigger the workflow from GitHub's UI. +# - The push event triggers the workflow whenever there's a push to the master branch, but only +# if the changes include files in the LearnModuleExercises/SampleApps/** directory. +# +# The defaults section sets the default shell for all run commands in the workflow to PowerShell (pwsh). +# +# The workflow consists of a single job named create_zip, which runs on the latest version of Ubuntu. +# +# This job has three steps: +# +# 1. The Checkout step uses the actions/checkout@v4 action to checkout the repository's code onto the +# runner. This is a common first step in most workflows as it allows subsequent steps to operate on +# the codebase. +# +# 2. The Create SampleApps zip step changes the current directory to ./LearnModuleExercises/SampleApps +# and then creates a zip file of all the files in that directory, including those in the .vscode +# subdirectory. The -r option is used to zip directories recursively and the -q option is used to run +# the command quietly without printing a lot of output. The resulting zip file is saved in the +# ../Downloads directory with the name SampleApps.zip. +# +# 3. The Commit and push step uses the Endbug/add-and-commit@v7 action to add the newly created zip file +# to the repository, commit the changes with the message 'Updating Zip for API source files', and then +# push the changes back to the repository. The add input is set to the path of the zip file and the push +# input is set to true to enable pushing. +# +# This workflow is useful for automatically packaging and versioning sample applications whenever changes +# are made to them. +# +name: CreateAZ2007M5PySamplesZip +on: + workflow_dispatch: + push: + branches: + - 'main' + paths: + - DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/** + +defaults: + run: + shell: pwsh + +jobs: + create_zip: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Create AZ2007M5 PySampleApps zip + run: | + cd ./DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python + rm -f ../Downloads/AZ2007LabAppM5Python.zip + zip -r -q ../Downloads/AZ2007LabAppM5Python.zip $(git ls-files) + - name: Commit and push + uses: Endbug/add-and-commit@v7 + with: + add: '["DownloadableCodeProjects/Downloads/AZ2007LabAppM5Python.zip"]' + message: 'Updating Zip with sample app source files' + push: true diff --git a/.github/workflows/GHCopilotProjects_Zip_Code_Ex10.yml b/.github/workflows/GHCopilotProjects_Zip_Code_Ex10.yml new file mode 100644 index 0000000..f5cebf4 --- /dev/null +++ b/.github/workflows/GHCopilotProjects_Zip_Code_Ex10.yml @@ -0,0 +1,80 @@ +# This workflow is triggered on two events: workflow_dispatch and push. +# +# - The workflow_dispatch event allows you to manually trigger the workflow from GitHub's UI. +# - The push event triggers the workflow whenever there's a push to the master branch, but only +# if the changes include files in the LearnModuleExercises/SampleApps/** directory. +# +# The defaults section sets the default shell for all run commands in the workflow to PowerShell (pwsh). +# +# The workflow consists of a single job named create_zip, which runs on the latest version of Ubuntu. +# +# This job has three steps: +# +# 1. The Checkout step uses the actions/checkout@v4 action to checkout the repository's code onto the +# runner. This is a common first step in most workflows as it allows subsequent steps to operate on +# the codebase. +# +# 2. The Create SampleApps zip step changes the current directory to ./LearnModuleExercises/SampleApps +# and then creates a zip file of all the files in that directory, including those in the .vscode +# subdirectory. The -r option is used to zip directories recursively and the -q option is used to run +# the command quietly without printing a lot of output. The resulting zip file is saved in the +# ../Downloads directory with the name SampleApps.zip. +# +# 3. The Commit and push step uses the Endbug/add-and-commit@v7 action to add the newly created zip file +# to the repository, commit the changes with the message 'Updating Zip for API source files', and then +# push the changes back to the repository. The add input is set to the path of the zip file and the push +# input is set to true to enable pushing. +# +# This workflow is useful for automatically packaging and versioning sample applications whenever changes +# are made to them. +# +name: CreateGHCopilotEx10SamplesZip +on: + workflow_dispatch: + push: + branches: + - 'main' + paths: + - DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/** + +defaults: + run: + shell: bash + +jobs: + create_zip: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Create GHCopilotEx10 SampleApps zip + run: | + # Ensure Downloads directory exists + mkdir -p ./DownloadableCodeProjects/Downloads + + # Change to source directory + cd ./DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling + + # Check if there are any git-tracked files to zip + if [ -z "$(git ls-files)" ]; then + echo "No git-tracked files found in the source directory" + exit 1 + fi + + # Remove existing zip file and create new one + rm -f ../../Downloads/GHCopilotEx10LabApps.zip + zip -r -q ../../Downloads/GHCopilotEx10LabApps.zip $(git ls-files) + + # Verify zip file was created + if [ ! -f ../../Downloads/GHCopilotEx10LabApps.zip ]; then + echo "Failed to create zip file" + exit 1 + fi + + echo "Successfully created GHCopilotEx10LabApps.zip" + - name: Commit and push + uses: Endbug/add-and-commit@v7 + with: + add: '["DownloadableCodeProjects/Downloads/GHCopilotEx10LabApps.zip"]' + message: 'Updating Zip with sample app source files' + push: true diff --git a/.github/workflows/GHCopilotProjects_Zip_Code_Ex7.yml b/.github/workflows/GHCopilotProjects_Zip_Code_Ex7.yml new file mode 100644 index 0000000..d0d1486 --- /dev/null +++ b/.github/workflows/GHCopilotProjects_Zip_Code_Ex7.yml @@ -0,0 +1,80 @@ +# This workflow is triggered on two events: workflow_dispatch and push. +# +# - The workflow_dispatch event allows you to manually trigger the workflow from GitHub's UI. +# - The push event triggers the workflow whenever there's a push to the master branch, but only +# if the changes include files in the LearnModuleExercises/SampleApps/** directory. +# +# The defaults section sets the default shell for all run commands in the workflow to PowerShell (pwsh). +# +# The workflow consists of a single job named create_zip, which runs on the latest version of Ubuntu. +# +# This job has three steps: +# +# 1. The Checkout step uses the actions/checkout@v4 action to checkout the repository's code onto the +# runner. This is a common first step in most workflows as it allows subsequent steps to operate on +# the codebase. +# +# 2. The Create SampleApps zip step changes the current directory to ./LearnModuleExercises/SampleApps +# and then creates a zip file of all the files in that directory, including those in the .vscode +# subdirectory. The -r option is used to zip directories recursively and the -q option is used to run +# the command quietly without printing a lot of output. The resulting zip file is saved in the +# ../Downloads directory with the name SampleApps.zip. +# +# 3. The Commit and push step uses the Endbug/add-and-commit@v7 action to add the newly created zip file +# to the repository, commit the changes with the message 'Updating Zip for API source files', and then +# push the changes back to the repository. The add input is set to the path of the zip file and the push +# input is set to true to enable pushing. +# +# This workflow is useful for automatically packaging and versioning sample applications whenever changes +# are made to them. +# +name: CreateGHCopilotEx7SamplesZip +on: + workflow_dispatch: + push: + branches: + - 'main' + paths: + - DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/** + +defaults: + run: + shell: bash + +jobs: + create_zip: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Create GHCopilotEx7 SampleApps zip + run: | + # Ensure Downloads directory exists + mkdir -p ./DownloadableCodeProjects/Downloads + + # Change to source directory + cd ./DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code + + # Check if there are any git-tracked files to zip + if [ -z "$(git ls-files)" ]; then + echo "No git-tracked files found in the source directory" + exit 1 + fi + + # Remove existing zip file and create new one + rm -f ../../Downloads/GHCopilotEx7LabApps.zip + zip -r -q ../../Downloads/GHCopilotEx7LabApps.zip $(git ls-files) + + # Verify zip file was created + if [ ! -f ../../Downloads/GHCopilotEx7LabApps.zip ]; then + echo "Failed to create zip file" + exit 1 + fi + + echo "Successfully created GHCopilotEx7LabApps.zip" + - name: Commit and push + uses: Endbug/add-and-commit@v7 + with: + add: '["DownloadableCodeProjects/Downloads/GHCopilotEx7LabApps.zip"]' + message: 'Updating Zip with sample app source files' + push: true diff --git a/.github/workflows/GHCopilotProjects_Zip_Code_Ex8.yml b/.github/workflows/GHCopilotProjects_Zip_Code_Ex8.yml new file mode 100644 index 0000000..104a5f1 --- /dev/null +++ b/.github/workflows/GHCopilotProjects_Zip_Code_Ex8.yml @@ -0,0 +1,80 @@ +# This workflow is triggered on two events: workflow_dispatch and push. +# +# - The workflow_dispatch event allows you to manually trigger the workflow from GitHub's UI. +# - The push event triggers the workflow whenever there's a push to the master branch, but only +# if the changes include files in the LearnModuleExercises/SampleApps/** directory. +# +# The defaults section sets the default shell for all run commands in the workflow to PowerShell (pwsh). +# +# The workflow consists of a single job named create_zip, which runs on the latest version of Ubuntu. +# +# This job has three steps: +# +# 1. The Checkout step uses the actions/checkout@v4 action to checkout the repository's code onto the +# runner. This is a common first step in most workflows as it allows subsequent steps to operate on +# the codebase. +# +# 2. The Create SampleApps zip step changes the current directory to ./LearnModuleExercises/SampleApps +# and then creates a zip file of all the files in that directory, including those in the .vscode +# subdirectory. The -r option is used to zip directories recursively and the -q option is used to run +# the command quietly without printing a lot of output. The resulting zip file is saved in the +# ../Downloads directory with the name SampleApps.zip. +# +# 3. The Commit and push step uses the Endbug/add-and-commit@v7 action to add the newly created zip file +# to the repository, commit the changes with the message 'Updating Zip for API source files', and then +# push the changes back to the repository. The add input is set to the path of the zip file and the push +# input is set to true to enable pushing. +# +# This workflow is useful for automatically packaging and versioning sample applications whenever changes +# are made to them. +# +name: CreateGHCopilotEx8SamplesZip +on: + workflow_dispatch: + push: + branches: + - 'main' + paths: + - DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/** + +defaults: + run: + shell: bash + +jobs: + create_zip: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Create GHCopilotEx8 SampleApps zip + run: | + # Ensure Downloads directory exists + mkdir -p ./DownloadableCodeProjects/Downloads + + # Change to source directory + cd ./DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions + + # Check if there are any git-tracked files to zip + if [ -z "$(git ls-files)" ]; then + echo "No git-tracked files found in the source directory" + exit 1 + fi + + # Remove existing zip file and create new one + rm -f ../../Downloads/GHCopilotEx8LabApps.zip + zip -r -q ../../Downloads/GHCopilotEx8LabApps.zip $(git ls-files) + + # Verify zip file was created + if [ ! -f ../../Downloads/GHCopilotEx8LabApps.zip ]; then + echo "Failed to create zip file" + exit 1 + fi + + echo "Successfully created GHCopilotEx8LabApps.zip" + - name: Commit and push + uses: Endbug/add-and-commit@v7 + with: + add: '["DownloadableCodeProjects/Downloads/GHCopilotEx8LabApps.zip"]' + message: 'Updating Zip with sample app source files' + push: true diff --git a/.github/workflows/GHCopilotProjects_Zip_Code_Ex9.yml b/.github/workflows/GHCopilotProjects_Zip_Code_Ex9.yml new file mode 100644 index 0000000..472fb4c --- /dev/null +++ b/.github/workflows/GHCopilotProjects_Zip_Code_Ex9.yml @@ -0,0 +1,80 @@ +# This workflow is triggered on two events: workflow_dispatch and push. +# +# - The workflow_dispatch event allows you to manually trigger the workflow from GitHub's UI. +# - The push event triggers the workflow whenever there's a push to the master branch, but only +# if the changes include files in the LearnModuleExercises/SampleApps/** directory. +# +# The defaults section sets the default shell for all run commands in the workflow to PowerShell (pwsh). +# +# The workflow consists of a single job named create_zip, which runs on the latest version of Ubuntu. +# +# This job has three steps: +# +# 1. The Checkout step uses the actions/checkout@v4 action to checkout the repository's code onto the +# runner. This is a common first step in most workflows as it allows subsequent steps to operate on +# the codebase. +# +# 2. The Create SampleApps zip step changes the current directory to ./LearnModuleExercises/SampleApps +# and then creates a zip file of all the files in that directory, including those in the .vscode +# subdirectory. The -r option is used to zip directories recursively and the -q option is used to run +# the command quietly without printing a lot of output. The resulting zip file is saved in the +# ../Downloads directory with the name SampleApps.zip. +# +# 3. The Commit and push step uses the Endbug/add-and-commit@v7 action to add the newly created zip file +# to the repository, commit the changes with the message 'Updating Zip for API source files', and then +# push the changes back to the repository. The add input is set to the path of the zip file and the push +# input is set to true to enable pushing. +# +# This workflow is useful for automatically packaging and versioning sample applications whenever changes +# are made to them. +# +name: CreateGHCopilotEx9SamplesZip +on: + workflow_dispatch: + push: + branches: + - 'main' + paths: + - DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/** + +defaults: + run: + shell: bash + +jobs: + create_zip: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Create GHCopilotEx9 SampleApps zip + run: | + # Ensure Downloads directory exists + mkdir -p ./DownloadableCodeProjects/Downloads + + # Change to source directory + cd ./DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals + + # Check if there are any git-tracked files to zip + if [ -z "$(git ls-files)" ]; then + echo "No git-tracked files found in the source directory" + exit 1 + fi + + # Remove existing zip file and create new one + rm -f ../../Downloads/GHCopilotEx9LabApps.zip + zip -r -q ../../Downloads/GHCopilotEx9LabApps.zip $(git ls-files) + + # Verify zip file was created + if [ ! -f ../../Downloads/GHCopilotEx9LabApps.zip ]; then + echo "Failed to create zip file" + exit 1 + fi + + echo "Successfully created GHCopilotEx9LabApps.zip" + - name: Commit and push + uses: Endbug/add-and-commit@v7 + with: + add: '["DownloadableCodeProjects/Downloads/GHCopilotEx9LabApps.zip"]' + message: 'Updating Zip with sample app source files' + push: true diff --git a/.github/workflows/GHSpecKitGetStarted_Zip_Code_Ex13.yml b/.github/workflows/GHSpecKitGetStarted_Zip_Code_Ex13.yml new file mode 100644 index 0000000..909ec04 --- /dev/null +++ b/.github/workflows/GHSpecKitGetStarted_Zip_Code_Ex13.yml @@ -0,0 +1,55 @@ +name: CreateGHSpecKitEx13Zip +on: + workflow_dispatch: + push: + branches: + - '**' + paths: + - DownloadableCodeProjects/standalone-lab-projects/sdd-get-started-rss-feed/**/*.md + pull_request: + branches: + - '**' + paths: + - DownloadableCodeProjects/standalone-lab-projects/sdd-get-started-rss-feed/**/*.md + +defaults: + run: + shell: bash + +jobs: + create_zip: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Create GHSpecKitEx13 Stakeholder Documents zip + run: | + # Ensure Downloads directory exists + mkdir -p DownloadableCodeProjects/Downloads + + # Remove existing zip file + rm -f DownloadableCodeProjects/Downloads/GHSpecKitEx13StakeholderDocuments.zip + + # Zip only the contents of the sdd-get-started-rss-feed folder (not the parent path) + cd DownloadableCodeProjects/standalone-lab-projects/sdd-get-started-rss-feed + CONTENTS=$(ls -A) + if [ -z "$CONTENTS" ]; then + echo "No files to zip in sdd-get-started-rss-feed" + exit 1 + fi + zip -r -q ../../Downloads/GHSpecKitEx13StakeholderDocuments.zip $CONTENTS + cd - + + # Verify zip file was created + if [ ! -f DownloadableCodeProjects/Downloads/GHSpecKitEx13StakeholderDocuments.zip ]; then + echo "Failed to create zip file" + exit 1 + fi + + echo "Successfully created GHSpecKitEx13StakeholderDocuments.zip" + - name: Commit and push + uses: Endbug/add-and-commit@v7 + with: + add: '["DownloadableCodeProjects/Downloads/GHSpecKitEx13StakeholderDocuments.zip"]' + message: 'Updating Zip with sample app source files' + push: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f2a084b --- /dev/null +++ b/.gitignore @@ -0,0 +1,167 @@ +# Build Folders (you can keep bin if you'd like, to store dlls and pdbs) +[Bb]in/ +[Oo]bj/ + +# mstest test results +TestResults + +## VSCode +.vscode/* + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Include dlls if theyfre in the NuGet packages directory +!/packages/*/lib/*.dll +!/packages/*/lib/*/*.dll +# Include dlls if they're in the CommonReferences directory +!*CommonReferences/*.dll + +# Build results +[Dd]ebug/ +[Rr]elease/ +x64/ +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.pyc +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.log +*.vspscc +*.vssscc +*.sln +.builds + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper* + +# NCrunch +*.ncrunch* +.*crunch*.local.xml + +# Installshield output folder +[Ee]xpress + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish + +# Publish Web Output +*.Publish.xml + +# NuGet Packages Directory +# packages + +# Windows Azure Build Output +csx +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +[Bb]in +[Oo]bj +sql +TestResults +[Tt]est[Rr]esult* +*.[Cc]ache +*.editorconfig +ClientBin +[Ss]tyle[Cc]op.* +~$* +*.dbmdl +Generated_Code #added for RIA/Silverlight projects + +# Backup & report files from converting an old project file to a newer +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML + +# Python artifacts +__pycache__/ +*.py[cod] +*.pyo +*.pyd + +# Python virtual environments +.env/ +.venv/ +env/ +venv/ +ENV/ +ENV*/ + +# Python packaging +build/ +dist/ +*.egg-info/ +.eggs/ + +# Test output (pytest, unittest, coverage, etc.) +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.pytest_cache/ +test-results/ +junit-*.xml + +# IPython/Jupyter +.ipynb_checkpoints/ + +# mypy +.mypy_cache/ +.dmypy.json + +# Pyre type checker +.pyre/ + +# Pyright type checker +.pyrightcache/ diff --git a/Allfiles/Demos/01/azuredeploy.json b/Allfiles/Demos/01/azuredeploy.json deleted file mode 100644 index 47c2e20..0000000 --- a/Allfiles/Demos/01/azuredeploy.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - }, - "variables": { - }, - "resources": [ - ], - "outputs": { - } - } \ No newline at end of file diff --git a/Allfiles/Labs/01/Starter/azuredeploy.json b/Allfiles/Labs/01/Starter/azuredeploy.json deleted file mode 100644 index 47c2e20..0000000 --- a/Allfiles/Labs/01/Starter/azuredeploy.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - }, - "variables": { - }, - "resources": [ - ], - "outputs": { - } - } \ No newline at end of file diff --git a/Allfiles/read-me.md b/Allfiles/read-me.md new file mode 100644 index 0000000..c1157af --- /dev/null +++ b/Allfiles/read-me.md @@ -0,0 +1,13 @@ +Add any files that the learner needs to download in order t complete the exercises. +These may include: +- Code starter files +- Data files +- Scripts to setup a "starting point" for an exercises + +If appropriate, organize the files into folders that correspond to the exercises (e.g. 01, 02, etc.) + +In the lab instructions, include the steps that the learner needs to follow to get the files into their lab environment. Typically, this is accomplished by: +- Instructing the learner to clone this repo (assumes they have Git installed and are sufficiently skilled to use it). +- Providing hyperlinks to individual RAW files in this repo. + +Delete this file after you've added your assets to the repo. \ No newline at end of file diff --git a/DownloadableCodeProjects/Downloads/AZ2007LabAppM2.zip b/DownloadableCodeProjects/Downloads/AZ2007LabAppM2.zip new file mode 100644 index 0000000..400fa78 Binary files /dev/null and b/DownloadableCodeProjects/Downloads/AZ2007LabAppM2.zip differ diff --git a/DownloadableCodeProjects/Downloads/AZ2007LabAppM2Python.zip b/DownloadableCodeProjects/Downloads/AZ2007LabAppM2Python.zip new file mode 100644 index 0000000..914bb90 Binary files /dev/null and b/DownloadableCodeProjects/Downloads/AZ2007LabAppM2Python.zip differ diff --git a/DownloadableCodeProjects/Downloads/AZ2007LabAppM3.zip b/DownloadableCodeProjects/Downloads/AZ2007LabAppM3.zip new file mode 100644 index 0000000..4beb3f1 Binary files /dev/null and b/DownloadableCodeProjects/Downloads/AZ2007LabAppM3.zip differ diff --git a/DownloadableCodeProjects/Downloads/AZ2007LabAppM3Python.zip b/DownloadableCodeProjects/Downloads/AZ2007LabAppM3Python.zip new file mode 100644 index 0000000..c749e29 Binary files /dev/null and b/DownloadableCodeProjects/Downloads/AZ2007LabAppM3Python.zip differ diff --git a/DownloadableCodeProjects/Downloads/AZ2007LabAppM4.zip b/DownloadableCodeProjects/Downloads/AZ2007LabAppM4.zip new file mode 100644 index 0000000..e2dd71b Binary files /dev/null and b/DownloadableCodeProjects/Downloads/AZ2007LabAppM4.zip differ diff --git a/DownloadableCodeProjects/Downloads/AZ2007LabAppM4Python.zip b/DownloadableCodeProjects/Downloads/AZ2007LabAppM4Python.zip new file mode 100644 index 0000000..317de86 Binary files /dev/null and b/DownloadableCodeProjects/Downloads/AZ2007LabAppM4Python.zip differ diff --git a/DownloadableCodeProjects/Downloads/AZ2007LabAppM5.zip b/DownloadableCodeProjects/Downloads/AZ2007LabAppM5.zip new file mode 100644 index 0000000..c80cba5 Binary files /dev/null and b/DownloadableCodeProjects/Downloads/AZ2007LabAppM5.zip differ diff --git a/DownloadableCodeProjects/Downloads/AZ2007LabAppM5Python.zip b/DownloadableCodeProjects/Downloads/AZ2007LabAppM5Python.zip new file mode 100644 index 0000000..c98588f Binary files /dev/null and b/DownloadableCodeProjects/Downloads/AZ2007LabAppM5Python.zip differ diff --git a/DownloadableCodeProjects/Downloads/GHCopilotEx10LabApps.zip b/DownloadableCodeProjects/Downloads/GHCopilotEx10LabApps.zip new file mode 100644 index 0000000..ef9f13d Binary files /dev/null and b/DownloadableCodeProjects/Downloads/GHCopilotEx10LabApps.zip differ diff --git a/DownloadableCodeProjects/Downloads/GHCopilotEx7LabApps.zip b/DownloadableCodeProjects/Downloads/GHCopilotEx7LabApps.zip new file mode 100644 index 0000000..9949d74 Binary files /dev/null and b/DownloadableCodeProjects/Downloads/GHCopilotEx7LabApps.zip differ diff --git a/DownloadableCodeProjects/Downloads/GHCopilotEx8LabApps.zip b/DownloadableCodeProjects/Downloads/GHCopilotEx8LabApps.zip new file mode 100644 index 0000000..ea2b78d Binary files /dev/null and b/DownloadableCodeProjects/Downloads/GHCopilotEx8LabApps.zip differ diff --git a/DownloadableCodeProjects/Downloads/GHCopilotEx9LabApps.zip b/DownloadableCodeProjects/Downloads/GHCopilotEx9LabApps.zip new file mode 100644 index 0000000..e0b8b50 Binary files /dev/null and b/DownloadableCodeProjects/Downloads/GHCopilotEx9LabApps.zip differ diff --git a/DownloadableCodeProjects/Downloads/GHSpecKitEx13StakeholderDocuments.zip b/DownloadableCodeProjects/Downloads/GHSpecKitEx13StakeholderDocuments.zip new file mode 100644 index 0000000..28d1ded Binary files /dev/null and b/DownloadableCodeProjects/Downloads/GHSpecKitEx13StakeholderDocuments.zip differ diff --git a/DownloadableCodeProjects/Downloads/readme.txt b/DownloadableCodeProjects/Downloads/readme.txt new file mode 100644 index 0000000..b3a4252 --- /dev/null +++ b/DownloadableCodeProjects/Downloads/readme.txt @@ -0,0 +1 @@ +placeholder \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/.gitignore b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/.gitignore new file mode 100644 index 0000000..dc73868 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/.gitignore @@ -0,0 +1,68 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*.pyo +*.pyd + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +.env/ +.venv/ +env/ +venv/ +ENV/ +ENV*/ + +# PyInstaller +*.manifest +*.spec + +# Unit test / coverage / pytest +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.pytest_cache/ +test-results/ +junit-*.xml + +# Jupyter Notebook +.ipynb_checkpoints/ + +# mypy +.mypy_cache/ +.dmypy.json + +# Pyre type checker +.pyre/ + +# Pyright type checker +.pyrightcache/ + +# VS Code settings +.vscode/ \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/entities/author.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/entities/author.py new file mode 100644 index 0000000..dec0e83 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/entities/author.py @@ -0,0 +1,6 @@ +from dataclasses import dataclass + +@dataclass +class Author: + id: int + name: str diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/entities/book.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/entities/book.py new file mode 100644 index 0000000..50f4e38 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/entities/book.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass +from typing import Optional +from .author import Author + +@dataclass +class Book: + id: int + title: str + author_id: int + genre: str + image_name: str + isbn: str + author: Optional[Author] = None diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/entities/book_item.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/entities/book_item.py new file mode 100644 index 0000000..f5f7fb7 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/entities/book_item.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass +from typing import Optional +from datetime import datetime +from .book import Book + +@dataclass +class BookItem: + id: int + book_id: int + acquisition_date: datetime + condition: Optional[str] = None + book: Optional[Book] = None diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/entities/loan.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/entities/loan.py new file mode 100644 index 0000000..51955ea --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/entities/loan.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass +from typing import Optional +from datetime import datetime +from .patron import Patron +from .book_item import BookItem + +@dataclass +class Loan: + id: int + book_item_id: int + patron_id: int + patron: Optional[Patron] = None + loan_date: datetime = None + due_date: datetime = None + return_date: Optional[datetime] = None + book_item: Optional[BookItem] = None diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/entities/patron.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/entities/patron.py new file mode 100644 index 0000000..98e5096 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/entities/patron.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass, field +from typing import List, Optional +from datetime import datetime +# from .loan import Loan # Use string annotation to avoid circular import + +@dataclass +class Patron: + id: int + name: str + membership_end: datetime + membership_start: datetime + image_name: Optional[str] = None + loans: List['Loan'] = field(default_factory=list) diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/enums/loan_extension_status.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/enums/loan_extension_status.py new file mode 100644 index 0000000..20cf2c5 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/enums/loan_extension_status.py @@ -0,0 +1,9 @@ +from enum import Enum + +class LoanExtensionStatus(Enum): + SUCCESS = 'Book loan extension was successful.' + LOAN_NOT_FOUND = 'Loan not found.' + LOAN_EXPIRED = 'Cannot extend book loan as it already has expired. Return the book instead.' + MEMBERSHIP_EXPIRED = "Cannot extend book loan due to expired patron's membership." + LOAN_RETURNED = 'Cannot extend book loan as the book is already returned.' + ERROR = 'Cannot extend book loan due to an error.' diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/enums/loan_return_status.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/enums/loan_return_status.py new file mode 100644 index 0000000..5f9221a --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/enums/loan_return_status.py @@ -0,0 +1,7 @@ +from enum import Enum + +class LoanReturnStatus(Enum): + SUCCESS = 'Book was successfully returned.' + LOAN_NOT_FOUND = 'Loan not found.' + ALREADY_RETURNED = 'Cannot return book as the book is already returned.' + ERROR = 'Cannot return book due to an error.' diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/enums/membership_renewal_status.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/enums/membership_renewal_status.py new file mode 100644 index 0000000..e36433e --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/enums/membership_renewal_status.py @@ -0,0 +1,8 @@ +from enum import Enum + +class MembershipRenewalStatus(Enum): + SUCCESS = 'Membership renewal was successful.' + PATRON_NOT_FOUND = 'Patron not found.' + TOO_EARLY_TO_RENEW = 'It is too early to renew the membership.' + LOAN_NOT_RETURNED = 'Cannot renew membership due to an outstanding loan.' + ERROR = 'Cannot renew membership due to an error.' diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_repository.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_repository.py new file mode 100644 index 0000000..d02d022 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_repository.py @@ -0,0 +1,12 @@ +from abc import ABC, abstractmethod +from typing import Optional +from ..entities.loan import Loan + +class ILoanRepository(ABC): + @abstractmethod + def get_loan(self, loan_id: int) -> Optional[Loan]: + pass + + @abstractmethod + def update_loan(self, loan: Loan) -> None: + pass diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_service.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_service.py new file mode 100644 index 0000000..a34c633 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_service.py @@ -0,0 +1,12 @@ +from abc import ABC, abstractmethod +from ..enums.loan_return_status import LoanReturnStatus +from ..enums.loan_extension_status import LoanExtensionStatus + +class ILoanService(ABC): + @abstractmethod + def return_loan(self, loan_id: int) -> LoanReturnStatus: + pass + + @abstractmethod + def extend_loan(self, loan_id: int) -> LoanExtensionStatus: + pass diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_repository.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_repository.py new file mode 100644 index 0000000..57305f1 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_repository.py @@ -0,0 +1,16 @@ +from abc import ABC, abstractmethod +from typing import List, Optional +from ..entities.patron import Patron + +class IPatronRepository(ABC): + @abstractmethod + def get_patron(self, patron_id: int) -> Optional[Patron]: + pass + + @abstractmethod + def search_patrons(self, search_input: str) -> List[Patron]: + pass + + @abstractmethod + def update_patron(self, patron: Patron) -> None: + pass diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_service.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_service.py new file mode 100644 index 0000000..e20199f --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_service.py @@ -0,0 +1,7 @@ +from abc import ABC, abstractmethod +from ..enums.membership_renewal_status import MembershipRenewalStatus + +class IPatronService(ABC): + @abstractmethod + def renew_membership(self, patron_id: int) -> MembershipRenewalStatus: + pass diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/services/loan_service.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/services/loan_service.py new file mode 100644 index 0000000..0a10307 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/services/loan_service.py @@ -0,0 +1,41 @@ +from ..interfaces.iloan_service import ILoanService +from ..interfaces.iloan_repository import ILoanRepository +from ..enums.loan_return_status import LoanReturnStatus +from ..enums.loan_extension_status import LoanExtensionStatus +from datetime import datetime, timedelta + +class LoanService(ILoanService): + EXTEND_BY_DAYS = 14 + + def __init__(self, loan_repository: ILoanRepository): + self._loan_repository = loan_repository + + def return_loan(self, loan_id: int) -> LoanReturnStatus: + loan = self._loan_repository.get_loan(loan_id) + if loan is None: + return LoanReturnStatus.LOAN_NOT_FOUND + if loan.return_date is not None: + return LoanReturnStatus.ALREADY_RETURNED + loan.return_date = datetime.now() + try: + self._loan_repository.update_loan(loan) + return LoanReturnStatus.SUCCESS + except Exception: + return LoanReturnStatus.ERROR + + def extend_loan(self, loan_id: int) -> LoanExtensionStatus: + loan = self._loan_repository.get_loan(loan_id) + if loan is None: + return LoanExtensionStatus.LOAN_NOT_FOUND + if loan.patron and loan.patron.membership_end < datetime.now(): + return LoanExtensionStatus.MEMBERSHIP_EXPIRED + if loan.return_date is not None: + return LoanExtensionStatus.LOAN_RETURNED + if loan.due_date < datetime.now(): + return LoanExtensionStatus.LOAN_EXPIRED + try: + loan.due_date = loan.due_date + timedelta(days=self.EXTEND_BY_DAYS) + self._loan_repository.update_loan(loan) + return LoanExtensionStatus.SUCCESS + except Exception: + return LoanExtensionStatus.ERROR diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/services/patron_service.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/services/patron_service.py new file mode 100644 index 0000000..44279e9 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/application_core/services/patron_service.py @@ -0,0 +1,23 @@ +from ..interfaces.ipatron_service import IPatronService +from ..interfaces.ipatron_repository import IPatronRepository +from ..enums.membership_renewal_status import MembershipRenewalStatus +from datetime import datetime, timedelta + +class PatronService(IPatronService): + def __init__(self, patron_repository: IPatronRepository): + self._patron_repository = patron_repository + + def renew_membership(self, patron_id: int) -> MembershipRenewalStatus: + patron = self._patron_repository.get_patron(patron_id) + if patron is None: + return MembershipRenewalStatus.PATRON_NOT_FOUND + if patron.membership_end >= datetime.now() + timedelta(days=30): + return MembershipRenewalStatus.TOO_EARLY_TO_RENEW + if any(l.return_date is None and l.due_date < datetime.now() for l in getattr(patron, 'loans', [])): + return MembershipRenewalStatus.LOAN_NOT_RETURNED + patron.membership_end = patron.membership_end + timedelta(days=365) + try: + self._patron_repository.update_patron(patron) + return MembershipRenewalStatus.SUCCESS + except Exception: + return MembershipRenewalStatus.ERROR diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/console/common_actions.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/console/common_actions.py new file mode 100644 index 0000000..011eda9 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/console/common_actions.py @@ -0,0 +1,10 @@ +from enum import Flag, auto + +class CommonActions(Flag): + REPEAT = 0 + SELECT = auto() + QUIT = auto() + SEARCH_PATRONS = auto() + RENEW_PATRON_MEMBERSHIP = auto() + RETURN_LOANED_BOOK = auto() + EXTEND_LOANED_BOOK = auto() diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/console/console_app.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/console/console_app.py new file mode 100644 index 0000000..c7b97a0 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/console/console_app.py @@ -0,0 +1,161 @@ +from .console_state import ConsoleState +from .common_actions import CommonActions +from application_core.interfaces.ipatron_repository import IPatronRepository +from application_core.interfaces.iloan_repository import ILoanRepository +from application_core.interfaces.iloan_service import ILoanService +from application_core.interfaces.ipatron_service import IPatronService + +class ConsoleApp: + def __init__( + self, + loan_service: ILoanService, + patron_service: IPatronService, + patron_repository: IPatronRepository, + loan_repository: ILoanRepository + ): + self._current_state: ConsoleState = ConsoleState.PATRON_SEARCH + self.matching_patrons = [] + self.selected_patron_details = None + self.selected_loan_details = None + self._patron_repository = patron_repository + self._loan_repository = loan_repository + self._loan_service = loan_service + self._patron_service = patron_service + + def run(self) -> None: + while True: + if self._current_state == ConsoleState.PATRON_SEARCH: + self._current_state = self.patron_search() + elif self._current_state == ConsoleState.PATRON_SEARCH_RESULTS: + self._current_state = self.patron_search_results() + elif self._current_state == ConsoleState.PATRON_DETAILS: + self._current_state = self.patron_details() + elif self._current_state == ConsoleState.LOAN_DETAILS: + self._current_state = self.loan_details() + elif self._current_state == ConsoleState.QUIT: + break + + def patron_search(self) -> ConsoleState: + search_input = input("Enter a string to search for patrons by name: ").strip() + if not search_input: + print("No input provided. Please try again.") + return ConsoleState.PATRON_SEARCH + self.matching_patrons = self._patron_repository.search_patrons(search_input) + if not self.matching_patrons: + print("No matching patrons found.") + return ConsoleState.PATRON_SEARCH + print("Matching Patrons:") + for idx, patron in enumerate(self.matching_patrons, 1): + print(f"{idx}) {patron.name}") + return ConsoleState.PATRON_SEARCH_RESULTS + + def patron_search_results(self) -> ConsoleState: + print("\nInput Options:") + print(" - Type a number to select a patron from the list") + print(" - Type 's' to search again") + print(" - Type 'q' to quit") + selection = input("Enter your choice: ").strip().lower() + if selection == 'q': + return ConsoleState.QUIT + elif selection == 's': + return ConsoleState.PATRON_SEARCH + elif selection.isdigit(): + idx = int(selection) + if 1 <= idx <= len(self.matching_patrons): + self.selected_patron_details = self.matching_patrons[idx - 1] + return ConsoleState.PATRON_DETAILS + else: + print("Invalid selection. Please enter a valid number.") + return ConsoleState.PATRON_SEARCH_RESULTS + else: + print("Invalid input. Please enter a number, 's', or 'q'.") + return ConsoleState.PATRON_SEARCH_RESULTS + + def patron_details(self) -> ConsoleState: + patron = self.selected_patron_details + print(f"\nName: {patron.name}") + print(f"Membership Expiration: {patron.membership_end}") + loans = self._loan_repository.get_loans_by_patron_id(patron.id) + print("\nBook Loans:") + + # Filter and display valid loans + valid_loans = [] + for idx, loan in enumerate(loans, 1): + if not getattr(loan, 'book_item', None) or not getattr(loan.book_item, 'book', None): + print(f"{idx}) [Invalid loan data: missing book information]") + else: + returned = "True" if getattr(loan, 'return_date', None) else "False" + print(f"{idx}) {loan.book_item.book.title} - Due: {loan.due_date} - Returned: {returned}") + valid_loans.append((idx, loan)) + if valid_loans: + print("Type a number to select a loan from the list") + if not valid_loans: + print("No valid loans for this patron.") + print("Input Options:") + print(" - Type 's' to search again") + print(" - Type 'q' to quit") + selection = input("Enter your choice: ").strip().lower() + if selection == 'q': + return ConsoleState.QUIT + elif selection == 's': + return ConsoleState.PATRON_SEARCH + else: + print("Invalid input.") + return ConsoleState.PATRON_DETAILS + else: + print("Input Options:") + print(" - Type 'm' to renew membership") + print(" - Type 's' to search again") + print(" - Type 'q' to quit") + selection = input("Enter your choice: ").strip().lower() + if selection == 'q': + return ConsoleState.QUIT + elif selection == 's': + return ConsoleState.PATRON_SEARCH + elif selection == 'm': + status = self._patron_service.renew_membership(patron.id) + print(status) + self.selected_patron_details = self._patron_repository.get_patron(patron.id) + return ConsoleState.PATRON_DETAILS + elif selection.isdigit(): + idx = int(selection) + if 1 <= idx <= len(valid_loans): + self.selected_loan_details = valid_loans[idx - 1][1] + return ConsoleState.LOAN_DETAILS + print("Invalid selection. Please enter a number shown in the list above.") + return ConsoleState.PATRON_DETAILS + else: + print("Invalid input. Please enter a number, 'm', 's', or 'q'.") + return ConsoleState.PATRON_DETAILS + + def loan_details(self) -> ConsoleState: + loan = self.selected_loan_details + print(f"\nBook title: {loan.book_item.book.title}") + print(f"Book Author: {loan.book_item.book.author.name}") + print(f"Due date: {loan.due_date}") + returned = "True" if getattr(loan, 'return_date', None) else "False" + print(f"Returned: {returned}\n") + print("Input Options:") + print(" - Type 'r' to return book") + print(" - Type 'e' to extend loan") + print(" - Type 's' to search again") + print(" - Type 'q' to quit") + selection = input("Enter your choice: ").strip().lower() + if selection == 'q': + return ConsoleState.QUIT + elif selection == 's': + return ConsoleState.PATRON_SEARCH + elif selection == 'r': + status = self._loan_service.return_loan(loan.id) + print("Book was successfully returned.") + print(status) + self.selected_loan_details = self._loan_repository.get_loan(loan.id) + return ConsoleState.LOAN_DETAILS + elif selection == 'e': + status = self._loan_service.extend_loan(loan.id) + print(status) + self.selected_loan_details = self._loan_repository.get_loan(loan.id) + return ConsoleState.LOAN_DETAILS + else: + print("Invalid input.") + return ConsoleState.LOAN_DETAILS diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/console/console_state.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/console/console_state.py new file mode 100644 index 0000000..714335a --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/console/console_state.py @@ -0,0 +1,8 @@ +from enum import Enum + +class ConsoleState(Enum): + PATRON_SEARCH = 1 + PATRON_SEARCH_RESULTS = 2 + PATRON_DETAILS = 3 + LOAN_DETAILS = 4 + QUIT = 5 diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/console/main.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/console/main.py new file mode 100644 index 0000000..f8ca682 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/console/main.py @@ -0,0 +1,25 @@ +import sys +from pathlib import Path + +# Add the parent directory to sys.path +sys.path.append(str(Path(__file__).resolve().parent.parent)) + +from application_core.services.loan_service import LoanService +from application_core.services.patron_service import PatronService +from infrastructure.json_data import JsonData +from infrastructure.json_loan_repository import JsonLoanRepository +from infrastructure.json_patron_repository import JsonPatronRepository +from console.console_app import ConsoleApp + + +def main(): + json_data = JsonData() + patron_repo = JsonPatronRepository(json_data) + loan_repo = JsonLoanRepository(json_data) + loan_service = LoanService(loan_repo) + patron_service = PatronService(patron_repo) + app = ConsoleApp(loan_service, patron_service, patron_repo, loan_repo) + app.run() + +if __name__ == "__main__": + main() diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/Json/Authors.json b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/Json/Authors.json new file mode 100644 index 0000000..2f61038 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/Json/Authors.json @@ -0,0 +1,22 @@ +[ + {"Id": 1, "Name": "Author One"}, + {"Id": 2, "Name": "Author Two"}, + {"Id": 3, "Name": "Author Three"}, + {"Id": 4, "Name": "Author Four"}, + {"Id": 5, "Name": "Author Five"}, + {"Id": 6, "Name": "Author Six"}, + {"Id": 7, "Name": "Author Seven"}, + {"Id": 8, "Name": "Author Eight"}, + {"Id": 9, "Name": "Author Nine"}, + {"Id": 10, "Name": "Author Ten"}, + {"Id": 11, "Name": "Author Eleven"}, + {"Id": 12, "Name": "Author Twelve"}, + {"Id": 13, "Name": "Author Thirteen"}, + {"Id": 14, "Name": "Author Fourteen"}, + {"Id": 15, "Name": "Author Fifteen"}, + {"Id": 16, "Name": "Author Sixteen"}, + {"Id": 17, "Name": "Author Seventeen"}, + {"Id": 18, "Name": "Author Eighteen"}, + {"Id": 19, "Name": "Author Nineteen"}, + {"Id": 20, "Name": "Author Twenty"} +] diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/Json/BookItems.json b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/Json/BookItems.json new file mode 100644 index 0000000..f5e1d1b --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/Json/BookItems.json @@ -0,0 +1,22 @@ +[ + {"Id": 1, "BookId": 1, "AcquisitionDate": "2023-09-20T00:40:43.1716563", "Condition": "Good"}, + {"Id": 2, "BookId": 2, "AcquisitionDate": "2023-09-20T00:40:43.1717503", "Condition": "Fair"}, + {"Id": 3, "BookId": 3, "AcquisitionDate": "2023-09-20T00:40:43.1717511", "Condition": "Excellent"}, + {"Id": 4, "BookId": 4, "AcquisitionDate": "2023-09-20T00:40:43.1717513", "Condition": "Poor"}, + {"Id": 5, "BookId": 5, "AcquisitionDate": "2023-09-20T00:40:43.1717516", "Condition": "Good"}, + {"Id": 6, "BookId": 6, "AcquisitionDate": "2023-09-20T00:40:43.1717521", "Condition": "Fair"}, + {"Id": 7, "BookId": 7, "AcquisitionDate": "2023-09-20T00:40:43.1717523", "Condition": "Excellent"}, + {"Id": 8, "BookId": 8, "AcquisitionDate": "2023-09-20T00:40:43.1717526", "Condition": "Poor"}, + {"Id": 9, "BookId": 9, "AcquisitionDate": "2023-09-20T00:40:43.171757", "Condition": "Good"}, + {"Id": 10, "BookId": 10, "AcquisitionDate": "2023-09-20T00:40:43.1717574", "Condition": "Fair"}, + {"Id": 11, "BookId": 11, "AcquisitionDate": "2023-09-20T00:40:43.1717576", "Condition": "Excellent"}, + {"Id": 12, "BookId": 12, "AcquisitionDate": "2023-09-20T00:40:43.1717578", "Condition": "Poor"}, + {"Id": 13, "BookId": 13, "AcquisitionDate": "2023-09-20T00:40:43.171758", "Condition": "Good"}, + {"Id": 14, "BookId": 14, "AcquisitionDate": "2023-09-20T00:40:43.1717609", "Condition": "Fair"}, + {"Id": 15, "BookId": 15, "AcquisitionDate": "2023-09-20T00:40:43.1717611", "Condition": "Excellent"}, + {"Id": 16, "BookId": 16, "AcquisitionDate": "2023-09-20T00:40:43.1717613", "Condition": "Poor"}, + {"Id": 17, "BookId": 17, "AcquisitionDate": "2023-09-20T00:40:43.1717616", "Condition": "Good"}, + {"Id": 18, "BookId": 18, "AcquisitionDate": "2023-09-20T00:40:43.1717619", "Condition": "Fair"}, + {"Id": 19, "BookId": 19, "AcquisitionDate": "2023-09-20T00:40:43.1717621", "Condition": "Excellent"}, + {"Id": 20, "BookId": 20, "AcquisitionDate": "2023-09-20T00:40:43.1717626", "Condition": "Poor"} +] diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/Json/Books.json b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/Json/Books.json new file mode 100644 index 0000000..ac80673 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/Json/Books.json @@ -0,0 +1,22 @@ +[ + {"Id": 1, "Title": "Book One", "AuthorId": 1, "Genre": "Dystopian", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524935"}, + {"Id": 2, "Title": "Book Two", "AuthorId": 2, "Genre": "Classic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524936"}, + {"Id": 3, "Title": "Book Three", "AuthorId": 3, "Genre": "Romance", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524937"}, + {"Id": 4, "Title": "Book Four", "AuthorId": 4, "Genre": "Classic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524938"}, + {"Id": 5, "Title": "Book Five", "AuthorId": 5, "Genre": "Coming-of-age", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524939"}, + {"Id": 6, "Title": "Book Six", "AuthorId": 6, "Genre": "Modernist", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524940"}, + {"Id": 7, "Title": "Book Seven", "AuthorId": 7, "Genre": "Adventure", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524941"}, + {"Id": 8, "Title": "Book Eight", "AuthorId": 8, "Genre": "Fantasy", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524942"}, + {"Id": 9, "Title": "Book Nine", "AuthorId": 9, "Genre": "Fantasy", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524943"}, + {"Id": 10, "Title": "Book Ten", "AuthorId": 10, "Genre": "Epic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524944"}, + {"Id": 11, "Title": "Book Eleven", "AuthorId": 11, "Genre": "Psychological", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524945"}, + {"Id": 12, "Title": "Book Twelve", "AuthorId": 12, "Genre": "Psychological", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524946"}, + {"Id": 13, "Title": "Book Thirteen", "AuthorId": 13, "Genre": "Magical realism", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524947"}, + {"Id": 14, "Title": "Book Fourteen", "AuthorId": 14, "Genre": "Classic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524948"}, + {"Id": 15, "Title": "Book Fifteen", "AuthorId": 15, "Genre": "Adventure", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524949"}, + {"Id": 16, "Title": "Book Sixteen", "AuthorId": 16, "Genre": "Historical", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524950"}, + {"Id": 17, "Title": "Book Seventeen", "AuthorId": 17, "Genre": "Gothic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524951"}, + {"Id": 18, "Title": "Book Eighteen", "AuthorId": 18, "Genre": "Dystopian", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524952"}, + {"Id": 19, "Title": "Book Nineteen", "AuthorId": 19, "Genre": "Classic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524953"}, + {"Id": 20, "Title": "Book Twenty", "AuthorId": 20, "Genre": "Adventure", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524954"} +] diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/Json/Loans.json b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/Json/Loans.json new file mode 100644 index 0000000..b0ebd1d --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/Json/Loans.json @@ -0,0 +1,482 @@ +[ + { + "Id": 1, + "BookItemId": 1, + "PatronId": 1, + "LoanDate": "2025-06-10T10:00:00", + "DueDate": "2025-06-24T10:00:00", + "ReturnDate": null + }, + { + "Id": 2, + "BookItemId": 1, + "PatronId": 10, + "LoanDate": "2024-05-01T10:00:00", + "DueDate": "2024-05-15T10:00:00", + "ReturnDate": "2024-05-10T10:00:00" + }, + { + "Id": 3, + "BookItemId": 2, + "PatronId": 2, + "LoanDate": "2025-06-11T10:00:00", + "DueDate": "2025-06-25T10:00:00", + "ReturnDate": null + }, + { + "Id": 4, + "BookItemId": 2, + "PatronId": 11, + "LoanDate": "2024-06-01T10:00:00", + "DueDate": "2024-06-15T10:00:00", + "ReturnDate": "2024-06-10T10:00:00" + }, + { + "Id": 5, + "BookItemId": 3, + "PatronId": 3, + "LoanDate": "2025-06-12T10:00:00", + "DueDate": "2025-06-26T10:00:00", + "ReturnDate": null + }, + { + "Id": 6, + "BookItemId": 3, + "PatronId": 12, + "LoanDate": "2024-07-01T10:00:00", + "DueDate": "2024-07-15T10:00:00", + "ReturnDate": "2024-07-10T10:00:00" + }, + { + "Id": 7, + "BookItemId": 4, + "PatronId": 4, + "LoanDate": "2025-06-13T10:00:00", + "DueDate": "2025-06-27T10:00:00", + "ReturnDate": null + }, + { + "Id": 8, + "BookItemId": 4, + "PatronId": 13, + "LoanDate": "2024-08-01T10:00:00", + "DueDate": "2024-08-15T10:00:00", + "ReturnDate": "2024-08-10T10:00:00" + }, + { + "Id": 9, + "BookItemId": 5, + "PatronId": 5, + "LoanDate": "2025-06-14T10:00:00", + "DueDate": "2025-06-28T10:00:00", + "ReturnDate": null + }, + { + "Id": 10, + "BookItemId": 5, + "PatronId": 14, + "LoanDate": "2024-09-01T10:00:00", + "DueDate": "2024-09-15T10:00:00", + "ReturnDate": "2024-09-10T10:00:00" + }, + { + "Id": 11, + "BookItemId": 6, + "PatronId": 6, + "LoanDate": "2025-06-15T10:00:00", + "DueDate": "2025-06-29T10:00:00", + "ReturnDate": null + }, + { + "Id": 12, + "BookItemId": 6, + "PatronId": 15, + "LoanDate": "2024-10-01T10:00:00", + "DueDate": "2024-10-15T10:00:00", + "ReturnDate": "2024-10-10T10:00:00" + }, + { + "Id": 13, + "BookItemId": 7, + "PatronId": 7, + "LoanDate": "2025-06-16T10:00:00", + "DueDate": "2025-06-30T10:00:00", + "ReturnDate": null + }, + { + "Id": 14, + "BookItemId": 7, + "PatronId": 16, + "LoanDate": "2024-11-01T10:00:00", + "DueDate": "2024-11-15T10:00:00", + "ReturnDate": "2024-11-10T10:00:00" + }, + { + "Id": 15, + "BookItemId": 8, + "PatronId": 8, + "LoanDate": "2025-06-17T10:00:00", + "DueDate": "2025-07-01T10:00:00", + "ReturnDate": null + }, + { + "Id": 16, + "BookItemId": 8, + "PatronId": 17, + "LoanDate": "2024-12-01T10:00:00", + "DueDate": "2024-12-15T10:00:00", + "ReturnDate": "2024-12-10T10:00:00" + }, + { + "Id": 17, + "BookItemId": 9, + "PatronId": 9, + "LoanDate": "2025-06-18T10:00:00", + "DueDate": "2025-07-02T10:00:00", + "ReturnDate": null + }, + { + "Id": 18, + "BookItemId": 9, + "PatronId": 18, + "LoanDate": "2025-01-01T10:00:00", + "DueDate": "2025-01-15T10:00:00", + "ReturnDate": "2025-01-10T10:00:00" + }, + { + "Id": 19, + "BookItemId": 10, + "PatronId": 10, + "LoanDate": "2025-06-19T10:00:00", + "DueDate": "2025-07-03T10:00:00", + "ReturnDate": null + }, + { + "Id": 20, + "BookItemId": 10, + "PatronId": 19, + "LoanDate": "2025-02-01T10:00:00", + "DueDate": "2025-02-15T10:00:00", + "ReturnDate": "2025-02-10T10:00:00" + }, + { + "Id": 21, + "BookItemId": 11, + "PatronId": 11, + "LoanDate": "2025-06-20T10:00:00", + "DueDate": "2025-07-04T10:00:00", + "ReturnDate": null + }, + { + "Id": 22, + "BookItemId": 11, + "PatronId": 20, + "LoanDate": "2025-03-01T10:00:00", + "DueDate": "2025-03-15T10:00:00", + "ReturnDate": "2025-03-10T10:00:00" + }, + { + "Id": 23, + "BookItemId": 12, + "PatronId": 12, + "LoanDate": "2025-06-21T10:00:00", + "DueDate": "2025-07-05T10:00:00", + "ReturnDate": null + }, + { + "Id": 24, + "BookItemId": 12, + "PatronId": 1, + "LoanDate": "2023-01-01T10:00:00", + "DueDate": "2023-01-15T10:00:00", + "ReturnDate": "2023-01-10T10:00:00" + }, + { + "Id": 25, + "BookItemId": 13, + "PatronId": 13, + "LoanDate": "2025-06-22T10:00:00", + "DueDate": "2025-07-06T10:00:00", + "ReturnDate": null + }, + { + "Id": 26, + "BookItemId": 13, + "PatronId": 2, + "LoanDate": "2023-02-01T10:00:00", + "DueDate": "2023-02-15T10:00:00", + "ReturnDate": "2023-02-10T10:00:00" + }, + { + "Id": 27, + "BookItemId": 14, + "PatronId": 14, + "LoanDate": "2025-06-23T10:00:00", + "DueDate": "2025-07-07T10:00:00", + "ReturnDate": null + }, + { + "Id": 28, + "BookItemId": 14, + "PatronId": 3, + "LoanDate": "2023-03-01T10:00:00", + "DueDate": "2023-03-15T10:00:00", + "ReturnDate": "2023-03-10T10:00:00" + }, + { + "Id": 29, + "BookItemId": 15, + "PatronId": 15, + "LoanDate": "2025-06-24T10:00:00", + "DueDate": "2025-07-08T10:00:00", + "ReturnDate": null + }, + { + "Id": 30, + "BookItemId": 15, + "PatronId": 4, + "LoanDate": "2023-04-01T10:00:00", + "DueDate": "2023-04-15T10:00:00", + "ReturnDate": "2023-04-10T10:00:00" + }, + { + "Id": 31, + "BookItemId": 16, + "PatronId": 5, + "LoanDate": "2023-05-01T10:00:00", + "DueDate": "2023-05-15T10:00:00", + "ReturnDate": "2023-05-10T10:00:00" + }, + { + "Id": 32, + "BookItemId": 17, + "PatronId": 6, + "LoanDate": "2023-06-01T10:00:00", + "DueDate": "2023-06-15T10:00:00", + "ReturnDate": "2023-06-10T10:00:00" + }, + { + "Id": 33, + "BookItemId": 18, + "PatronId": 7, + "LoanDate": "2023-07-01T10:00:00", + "DueDate": "2023-07-15T10:00:00", + "ReturnDate": "2023-07-10T10:00:00" + }, + { + "Id": 34, + "BookItemId": 19, + "PatronId": 8, + "LoanDate": "2023-08-01T10:00:00", + "DueDate": "2023-08-15T10:00:00", + "ReturnDate": "2023-08-10T10:00:00" + }, + { + "Id": 35, + "BookItemId": 20, + "PatronId": 9, + "LoanDate": "2023-09-01T10:00:00", + "DueDate": "2023-09-15T10:00:00", + "ReturnDate": "2023-09-10T10:00:00" + }, + { + "Id": 36, + "BookItemId": 16, + "PatronId": 21, + "LoanDate": "2023-10-01T10:00:00", + "DueDate": "2023-10-15T10:00:00", + "ReturnDate": "2023-10-10T10:00:00" + }, + { + "Id": 37, + "BookItemId": 17, + "PatronId": 22, + "LoanDate": "2023-11-01T10:00:00", + "DueDate": "2023-11-15T10:00:00", + "ReturnDate": "2023-11-10T10:00:00" + }, + { + "Id": 38, + "BookItemId": 18, + "PatronId": 23, + "LoanDate": "2023-12-01T10:00:00", + "DueDate": "2023-12-15T10:00:00", + "ReturnDate": "2023-12-10T10:00:00" + }, + { + "Id": 39, + "BookItemId": 19, + "PatronId": 24, + "LoanDate": "2024-01-01T10:00:00", + "DueDate": "2024-01-15T10:00:00", + "ReturnDate": "2024-01-10T10:00:00" + }, + { + "Id": 40, + "BookItemId": 20, + "PatronId": 25, + "LoanDate": "2024-02-01T10:00:00", + "DueDate": "2024-02-15T10:00:00", + "ReturnDate": "2024-02-10T10:00:00" + }, + { + "Id": 41, + "BookItemId": 16, + "PatronId": 26, + "LoanDate": "2024-03-01T10:00:00", + "DueDate": "2024-03-15T10:00:00", + "ReturnDate": "2024-03-10T10:00:00" + }, + { + "Id": 42, + "BookItemId": 17, + "PatronId": 27, + "LoanDate": "2024-04-01T10:00:00", + "DueDate": "2024-04-15T10:00:00", + "ReturnDate": "2024-04-10T10:00:00" + }, + { + "Id": 43, + "BookItemId": 18, + "PatronId": 28, + "LoanDate": "2024-05-01T10:00:00", + "DueDate": "2024-05-15T10:00:00", + "ReturnDate": "2024-05-10T10:00:00" + }, + { + "Id": 44, + "BookItemId": 19, + "PatronId": 29, + "LoanDate": "2024-06-01T10:00:00", + "DueDate": "2024-06-15T10:00:00", + "ReturnDate": "2024-06-10T10:00:00" + }, + { + "Id": 45, + "BookItemId": 20, + "PatronId": 30, + "LoanDate": "2024-07-01T10:00:00", + "DueDate": "2024-07-15T10:00:00", + "ReturnDate": "2024-07-10T10:00:00" + }, + { + "Id": 46, + "BookItemId": 16, + "PatronId": 31, + "LoanDate": "2024-08-01T10:00:00", + "DueDate": "2024-08-15T10:00:00", + "ReturnDate": "2024-08-10T10:00:00" + }, + { + "Id": 47, + "BookItemId": 17, + "PatronId": 32, + "LoanDate": "2024-09-01T10:00:00", + "DueDate": "2024-09-15T10:00:00", + "ReturnDate": "2024-09-10T10:00:00" + }, + { + "Id": 48, + "BookItemId": 18, + "PatronId": 33, + "LoanDate": "2024-10-01T10:00:00", + "DueDate": "2024-10-15T10:00:00", + "ReturnDate": "2024-10-10T10:00:00" + }, + { + "Id": 49, + "BookItemId": 19, + "PatronId": 34, + "LoanDate": "2024-11-01T10:00:00", + "DueDate": "2024-11-15T10:00:00", + "ReturnDate": "2024-11-10T10:00:00" + }, + { + "Id": 50, + "BookItemId": 20, + "PatronId": 35, + "LoanDate": "2024-12-01T10:00:00", + "DueDate": "2024-12-15T10:00:00", + "ReturnDate": "2024-12-10T10:00:00" + }, + { + "Id": 51, + "BookItemId": 16, + "PatronId": 36, + "LoanDate": "2025-01-01T10:00:00", + "DueDate": "2025-01-15T10:00:00", + "ReturnDate": "2025-01-10T10:00:00" + }, + { + "Id": 52, + "BookItemId": 17, + "PatronId": 37, + "LoanDate": "2025-02-01T10:00:00", + "DueDate": "2025-02-15T10:00:00", + "ReturnDate": "2025-02-10T10:00:00" + }, + { + "Id": 53, + "BookItemId": 18, + "PatronId": 38, + "LoanDate": "2025-03-01T10:00:00", + "DueDate": "2025-03-15T10:00:00", + "ReturnDate": "2025-03-10T10:00:00" + }, + { + "Id": 54, + "BookItemId": 19, + "PatronId": 39, + "LoanDate": "2025-04-01T10:00:00", + "DueDate": "2025-04-15T10:00:00", + "ReturnDate": "2025-04-10T10:00:00" + }, + { + "Id": 55, + "BookItemId": 20, + "PatronId": 40, + "LoanDate": "2025-05-01T10:00:00", + "DueDate": "2025-05-15T10:00:00", + "ReturnDate": "2025-05-10T10:00:00" + }, + { + "Id": 56, + "BookItemId": 16, + "PatronId": 41, + "LoanDate": "2025-05-11T10:00:00", + "DueDate": "2025-05-25T10:00:00", + "ReturnDate": "2025-05-20T10:00:00" + }, + { + "Id": 57, + "BookItemId": 17, + "PatronId": 42, + "LoanDate": "2025-05-12T10:00:00", + "DueDate": "2025-05-26T10:00:00", + "ReturnDate": "2025-05-21T10:00:00" + }, + { + "Id": 58, + "BookItemId": 18, + "PatronId": 48, + "LoanDate": "2025-05-13T10:00:00", + "DueDate": "2025-05-27T10:00:00", + "ReturnDate": "2025-05-22T10:00:00" + }, + { + "Id": 59, + "BookItemId": 19, + "PatronId": 49, + "LoanDate": "2025-05-14T10:00:00", + "DueDate": "2025-05-28T10:00:00", + "ReturnDate": "2025-05-23T10:00:00" + }, + { + "Id": 60, + "BookItemId": 20, + "PatronId": 50, + "LoanDate": "2025-05-15T10:00:00", + "DueDate": "2025-05-29T10:00:00", + "ReturnDate": "2025-05-24T10:00:00" + } +] \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/Json/Patrons.json b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/Json/Patrons.json new file mode 100644 index 0000000..7c05687 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/Json/Patrons.json @@ -0,0 +1,52 @@ +[ + {"Id": 1, "Name": "Patron One", "MembershipEnd": "2024-12-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron One.jpg"}, + {"Id": 2, "Name": "Patron Two", "MembershipEnd": "2025-02-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Two.jpg"}, + {"Id": 3, "Name": "Patron Three", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Three.jpg"}, + {"Id": 4, "Name": "Patron Four", "MembershipEnd": "2025-04-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Four.jpg"}, + {"Id": 5, "Name": "Patron Five", "MembershipEnd": "2025-05-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Five.jpg"}, + {"Id": 6, "Name": "Patron Six", "MembershipEnd": "2025-06-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Six.jpg"}, + {"Id": 7, "Name": "Patron Seven", "MembershipEnd": "2025-07-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Seven.jpg"}, + {"Id": 8, "Name": "Patron Eight", "MembershipEnd": "2024-01-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Eight.jpg"}, + {"Id": 9, "Name": "Patron Nine", "MembershipEnd": "2024-02-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Nine.jpg"}, + {"Id": 10, "Name": "Patron Ten", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Ten.jpg"}, + {"Id": 11, "Name": "Patron Eleven", "MembershipEnd": "2024-04-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Eleven.jpg"}, + {"Id": 12, "Name": "Patron Twelve", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twelve.jpg"}, + {"Id": 13, "Name": "Patron Thirteen", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirteen.jpg"}, + {"Id": 14, "Name": "Patron Fourteen", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Fourteen.jpg"}, + {"Id": 15, "Name": "Patron Fifteen", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Fifteen.jpg"}, + {"Id": 16, "Name": "Patron Sixteen", "MembershipEnd": "2024-04-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Sixteen.jpg"}, + {"Id": 17, "Name": "Patron Seventeen", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Seventeen.jpg"}, + {"Id": 18, "Name": "Patron Eighteen", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Eighteen.jpg"}, + {"Id": 19, "Name": "Patron Nineteen", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Nineteen.jpg"}, + {"Id": 20, "Name": "Patron Twenty", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty.jpg"}, + {"Id": 21, "Name": "Patron Twenty-One", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-One.jpg"}, + {"Id": 22, "Name": "Patron Twenty-Two", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Two.jpg"}, + {"Id": 23, "Name": "Patron Twenty-Three", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Three.jpg"}, + {"Id": 24, "Name": "Patron Twenty-Four", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Four.jpg"}, + {"Id": 25, "Name": "Patron Twenty-Five", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Five.jpg"}, + {"Id": 26, "Name": "Patron Twenty-Six", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Six.jpg"}, + {"Id": 27, "Name": "Patron Twenty-Seven", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Seven.jpg"}, + {"Id": 28, "Name": "Patron Twenty-Eight", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Eight.jpg"}, + {"Id": 29, "Name": "Patron Twenty-Nine", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Nine.jpg"}, + {"Id": 30, "Name": "Patron Thirty", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty.jpg"}, + {"Id": 31, "Name": "Patron Thirty-One", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-One.jpg"}, + {"Id": 32, "Name": "Patron Thirty-Two", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Two.jpg"}, + {"Id": 33, "Name": "Patron Thirty-Three", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Three.jpg"}, + {"Id": 34, "Name": "Patron Thirty-Four", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Four.jpg"}, + {"Id": 35, "Name": "Patron Thirty-Five", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Five.jpg"}, + {"Id": 36, "Name": "Patron Thirty-Six", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Six.jpg"}, + {"Id": 37, "Name": "Patron Thirty-Seven", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Seven.jpg"}, + {"Id": 38, "Name": "Patron Thirty-Eight", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Eight.jpg"}, + {"Id": 39, "Name": "Patron Thirty-Nine", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Nine.jpg"}, + {"Id": 40, "Name": "Patron Forty", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty.jpg"}, + {"Id": 41, "Name": "Patron Forty-One", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-One.jpg"}, + {"Id": 42, "Name": "Patron Forty-Two", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Two.jpg"}, + {"Id": 43, "Name": "Patron Forty-Three", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Three.jpg"}, + {"Id": 44, "Name": "Patron Forty-Four", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Four.jpg"}, + {"Id": 45, "Name": "Patron Forty-Five", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Five.jpg"}, + {"Id": 46, "Name": "Patron Forty-Six", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Six.jpg"}, + {"Id": 47, "Name": "Patron Forty-Seven", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Seven.jpg"}, + {"Id": 48, "Name": "Patron Forty-Eight", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Eight.jpg"}, + {"Id": 49, "Name": "Patron Forty-Nine", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Nine.jpg"}, + {"Id": 50, "Name": "Patron Fifty", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Fifty.jpg"} +] diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/json_data.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/json_data.py new file mode 100644 index 0000000..0c4aa54 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/json_data.py @@ -0,0 +1,105 @@ +import json +import os +from pathlib import Path +from application_core.entities.author import Author +from application_core.entities.book import Book +from application_core.entities.book_item import BookItem +from application_core.entities.patron import Patron +from application_core.entities.loan import Loan +from typing import List, Optional +from datetime import datetime + +class JsonData: + def __init__(self): + # Get the absolute path to the project root + self.project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + self.json_dir = os.path.join(self.project_root, "infrastructure", "Json") + self.authors_path = os.path.join(self.json_dir, "Authors.json") + self.books_path = os.path.join(self.json_dir, "Books.json") + self.book_items_path = os.path.join(self.json_dir, "BookItems.json") # <-- Add this line + self.patrons_path = os.path.join(self.json_dir, "Patrons.json") + self.loans_path = os.path.join(self.json_dir, "Loans.json") + self.authors: List[Author] = [] + self.books: List[Book] = [] + self.book_items: List[BookItem] = [] + self.patrons: List[Patron] = [] + self.loans: List[Loan] = [] + self._loaded = False + self.load_data() + + def _parse_datetime(self, value: Optional[str]) -> Optional[datetime]: + if value is None: + return None + return datetime.fromisoformat(value) + + def load_data(self) -> None: + try: + with open(self.authors_path, encoding='utf-8') as f: + authors_data = json.load(f) + self.authors = [Author(id=a['Id'], name=a['Name']) for a in authors_data] + with open(self.books_path, encoding='utf-8') as f: + books_data = json.load(f) + self.books = [Book(id=b['Id'], title=b['Title'], author_id=b['AuthorId'], genre=b['Genre'], image_name=b['ImageName'], isbn=b['ISBN']) for b in books_data] + with open(self.book_items_path, encoding='utf-8') as f: # <-- Fix here + items_data = json.load(f) + self.book_items = [BookItem(id=bi['Id'], book_id=bi['BookId'], acquisition_date=self._parse_datetime(bi['AcquisitionDate']), condition=bi.get('Condition')) for bi in items_data] + with open(self.patrons_path, encoding='utf-8') as f: + patrons_data = json.load(f) + self.patrons = [Patron(id=p['Id'], name=p['Name'], membership_end=self._parse_datetime(p['MembershipEnd']), membership_start=self._parse_datetime(p['MembershipStart']), image_name=p.get('ImageName')) for p in patrons_data] + with open(self.loans_path, encoding='utf-8') as f: + loans_data = json.load(f) + self.loans = [Loan(id=l['Id'], book_item_id=l['BookItemId'], patron_id=l['PatronId'], loan_date=self._parse_datetime(l['LoanDate']), due_date=self._parse_datetime(l['DueDate']), return_date=self._parse_datetime(l['ReturnDate'])) for l in loans_data] + self._loaded = True + + # Build lookup dictionaries for fast access + book_item_dict = {bi.id: bi for bi in self.book_items} + book_dict = {b.id: b for b in self.books} + author_dict = {a.id: a for a in self.authors} + patron_dict = {p.id: p for p in self.patrons} + + # Link book_item and book to each loan + for loan in self.loans: + loan.book_item = book_item_dict.get(loan.book_item_id) + if loan.book_item: + loan.book_item.book = book_dict.get(loan.book_item.book_id) + if loan.book_item.book: + loan.book_item.book.author = author_dict.get(loan.book_item.book.author_id) + loan.patron = patron_dict.get(loan.patron_id) + # Optionally, link loans to patrons + for patron in self.patrons: + patron.loans = [loan for loan in self.loans if loan.patron_id == patron.id] + + except (FileNotFoundError, json.JSONDecodeError) as e: + print(f"Error loading data: {e}") + self._loaded = False + + def save_loans(self, loans: List[Loan]) -> None: + try: + with open(self.loans_path, 'w', encoding='utf-8') as f: + json.dump([ + { + 'Id': l.id, + 'BookItemId': l.book_item_id, + 'PatronId': l.patron_id, + 'LoanDate': l.loan_date.isoformat() if l.loan_date else None, + 'DueDate': l.due_date.isoformat() if l.due_date else None, + 'ReturnDate': l.return_date.isoformat() if l.return_date else None + } for l in loans + ], f, indent=2) + except Exception as e: + print(f"Error saving loans: {e}") + + def save_patrons(self, patrons: List[Patron]) -> None: + try: + with open(self.patrons_path, 'w', encoding='utf-8') as f: + json.dump([ + { + 'Id': p.id, + 'Name': p.name, + 'MembershipEnd': p.membership_end.isoformat() if p.membership_end else None, + 'MembershipStart': p.membership_start.isoformat() if p.membership_start else None, + 'ImageName': p.image_name + } for p in patrons + ], f, indent=2) + except Exception as e: + print(f"Error saving patrons: {e}") diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/json_loan_repository.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/json_loan_repository.py new file mode 100644 index 0000000..7773f22 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/json_loan_repository.py @@ -0,0 +1,24 @@ +from application_core.interfaces.iloan_repository import ILoanRepository +from application_core.entities.loan import Loan +from .json_data import JsonData +from typing import Optional + +class JsonLoanRepository(ILoanRepository): + def __init__(self, json_data: JsonData): + self._json_data = json_data + + def get_loan(self, loan_id: int) -> Optional[Loan]: + for loan in self._json_data.loans: + if loan.id == loan_id: + return loan + return None + + def update_loan(self, loan: Loan) -> None: + for idx, l in enumerate(self._json_data.loans): + if l.id == loan.id: + self._json_data.loans[idx] = loan + self._json_data.save_loans(self._json_data.loans) + return + + def get_loans_by_patron_id(self, patron_id: int): + return [loan for loan in self._json_data.loans if loan.patron_id == patron_id] diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/json_patron_repository.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/json_patron_repository.py new file mode 100644 index 0000000..e9d6e98 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/infrastructure/json_patron_repository.py @@ -0,0 +1,26 @@ +from application_core.interfaces.ipatron_repository import IPatronRepository +from application_core.entities.patron import Patron +from .json_data import JsonData +from typing import List, Optional + +class JsonPatronRepository(IPatronRepository): + def __init__(self, json_data: JsonData): + self._json_data = json_data + + def get_patron(self, patron_id: int) -> Optional[Patron]: + for patron in self._json_data.patrons: + if patron.id == patron_id: + return patron + return None + + def search_patrons(self, search_input: str) -> List[Patron]: + results = [p for p in self._json_data.patrons if search_input.lower() in p.name.lower()] + results.sort(key=lambda p: p.name) + return results + + def update_patron(self, patron: Patron) -> None: + for idx, p in enumerate(self._json_data.patrons): + if p.id == patron.id: + self._json_data.patrons[idx] = patron + self._json_data.save_patrons(self._json_data.patrons) + return diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/tests/test_loan_service.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/tests/test_loan_service.py new file mode 100644 index 0000000..d786b44 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/tests/test_loan_service.py @@ -0,0 +1,32 @@ +import unittest +from unittest.mock import MagicMock +from application_core.services.loan_service import LoanService +from application_core.entities.loan import Loan +from application_core.entities.patron import Patron +from application_core.enums.loan_extension_status import LoanExtensionStatus +from application_core.enums.loan_return_status import LoanReturnStatus +from datetime import datetime, timedelta + +class LoanServiceTest(unittest.TestCase): + def setUp(self): + self.mock_repo = MagicMock() + self.service = LoanService(self.mock_repo) + + def test_extend_loan_success(self): + print("Running test_extend_loan_success...") + patron = Patron(id=1, name="John Doe", membership_end=datetime.now()+timedelta(days=1), membership_start=datetime.now()-timedelta(days=30)) + loan = Loan(id=1, book_item_id=1, patron_id=1, patron=patron, loan_date=datetime.now()-timedelta(days=2), due_date=datetime.now()+timedelta(days=1)) + self.mock_repo.get_loan.return_value = loan + status = self.service.extend_loan(1) + print(f"extend_loan status: {status}") + self.assertEqual(status, LoanExtensionStatus.SUCCESS) + + def test_return_loan_not_found(self): + print("Running test_return_loan_not_found...") + self.mock_repo.get_loan.return_value = None + status = self.service.return_loan(1) + print(f"return_loan status: {status}") + self.assertEqual(status, LoanReturnStatus.LOAN_NOT_FOUND) + +if __name__ == "__main__": + unittest.main() diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/tests/test_patron_service.py b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/tests/test_patron_service.py new file mode 100644 index 0000000..a8a5ba2 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/AccelerateDevGHCopilot/library/tests/test_patron_service.py @@ -0,0 +1,30 @@ +import unittest +from unittest.mock import MagicMock +from application_core.services.patron_service import PatronService +from application_core.entities.patron import Patron +from application_core.enums.membership_renewal_status import MembershipRenewalStatus +from datetime import datetime, timedelta + +class PatronServiceTest(unittest.TestCase): + def setUp(self): + self.mock_repo = MagicMock() + self.service = PatronService(self.mock_repo) + + def test_renew_membership_success(self): + print("Running test_renew_membership_success...") + patron = Patron(id=1, name="John Doe", membership_end=datetime.now()-timedelta(days=1), membership_start=datetime.now()-timedelta(days=365)) + self.mock_repo.get_patron.return_value = patron + self.mock_repo.update_patron.return_value = None + status = self.service.renew_membership(1) + print(f"renew_membership status: {status}") + self.assertEqual(status, MembershipRenewalStatus.SUCCESS) + + def test_renew_membership_patron_not_found(self): + print("Running test_renew_membership_patron_not_found...") + self.mock_repo.get_patron.return_value = None + status = self.service.renew_membership(1) + print(f"renew_membership status: {status}") + self.assertEqual(status, MembershipRenewalStatus.PATRON_NOT_FOUND) + +if __name__ == "__main__": + unittest.main() diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document-python/readme.txt b/DownloadableCodeProjects/az-2007-m2-explain-document-python/readme.txt new file mode 100644 index 0000000..b3a4252 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document-python/readme.txt @@ -0,0 +1 @@ +placeholder \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/.gitignore b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/.gitignore new file mode 100644 index 0000000..9a75e6b --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/.gitignore @@ -0,0 +1,119 @@ +# Build Folders (you can keep bin if you'd like, to store dlls and pdbs) +[Bb]in/ +[Oo]bj/ + +# mstest test results +TestResults + +## VSCode +.vscode/* + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Include dlls if theyfre in the NuGet packages directory +!/packages/*/lib/*.dll +!/packages/*/lib/*/*.dll +# Include dlls if they're in the CommonReferences directory +!*CommonReferences/*.dll + +# Build results +[Dd]ebug/ +[Rr]elease/ +x64/ +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.log +*.vspscc +*.vssscc +*.sln +.builds + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper* + +# NCrunch +*.ncrunch* +.*crunch*.local.xml + +# Installshield output folder +[Ee]xpress + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish + +# Publish Web Output +*.Publish.xml + +# NuGet Packages Directory +# packages + +# Windows Azure Build Output +csx +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +[Bb]in +[Oo]bj +sql +TestResults +[Tt]est[Rr]esult* +*.[Cc]ache +*.editorconfig +ClientBin +[Ss]tyle[Cc]op.* +~$* +*.dbmdl +Generated_Code #added for RIA/Silverlight projects + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Author.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Author.cs new file mode 100644 index 0000000..5d9d1a6 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Author.cs @@ -0,0 +1,7 @@ +namespace Library.ApplicationCore.Entities; + +public class Author +{ + public int Id { get; set; } + public required string Name { get; set; } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Book.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Book.cs new file mode 100644 index 0000000..029b467 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Book.cs @@ -0,0 +1,12 @@ +namespace Library.ApplicationCore.Entities; + +public class Book +{ + public int Id { get; set; } + public required string Title { get; set; } + public int AuthorId { get; set; } + public required string Genre { get; set; } + public required string ImageName { get; set; } + public required string ISBN { get; set; } + public Author? Author { get; set; } +} diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/BookItem.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/BookItem.cs new file mode 100644 index 0000000..5a97332 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/BookItem.cs @@ -0,0 +1,10 @@ +namespace Library.ApplicationCore.Entities; + +public class BookItem +{ + public int Id { get; set; } + public int BookId { get; set; } + public DateTime AcquisitionDate { get; set; } + public string? Condition { get; set; } + public Book? Book { get; set; } +} diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Loan.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Loan.cs new file mode 100644 index 0000000..6d0c33e --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Loan.cs @@ -0,0 +1,13 @@ +namespace Library.ApplicationCore.Entities; + +public class Loan +{ + public int Id { get; set; } + public int BookItemId { get; set; } + public int PatronId { get; set; } + public Patron? Patron { get; set; } + public DateTime LoanDate { get; set; } + public DateTime DueDate { get; set; } + public DateTime? ReturnDate { get; set; } + public BookItem? BookItem { get; set; } +} diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Patron.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Patron.cs new file mode 100644 index 0000000..3a2fd33 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Patron.cs @@ -0,0 +1,11 @@ +namespace Library.ApplicationCore.Entities; + +public class Patron +{ + public int Id { get; set; } + public required string Name { get; set; } + public DateTime MembershipEnd { get; set; } + public DateTime MembershipStart { get; set; } + public string? ImageName { get; set; } + public ICollection Loans { get; set; } = new HashSet(); +} diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/EnumHelper.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/EnumHelper.cs new file mode 100644 index 0000000..5369856 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/EnumHelper.cs @@ -0,0 +1,27 @@ +using System.ComponentModel; +using System.Reflection; + +namespace Library.ApplicationCore.Enums; + +public static class EnumHelper +{ + public static string GetDescription(Enum value) + { + if (value == null) + return string.Empty; + + FieldInfo fieldInfo = value.GetType().GetField(value.ToString())!; + + DescriptionAttribute[] attributes = + (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false); + + if (attributes != null && attributes.Length > 0) + { + return attributes[0].Description; + } + else + { + return value.ToString(); + } + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanExtensionStatus.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanExtensionStatus.cs new file mode 100644 index 0000000..2af2c4a --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanExtensionStatus.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; + +namespace Library.ApplicationCore.Enums; + +public enum LoanExtensionStatus +{ + [Description("Book loan extension was successful.")] + Success, + + [Description("Loan not found.")] + LoanNotFound, + + [Description("Cannot extend book loan as it already has expired. Return the book instead.")] + LoanExpired, + + [Description("Cannot extend book loan due to expired patron's membership.")] + MembershipExpired, + + [Description("Cannot extend book loan as the book is already returned.")] + LoanReturned, + + [Description("Cannot extend book loan due to an error.")] + Error +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanReturnStatus.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanReturnStatus.cs new file mode 100644 index 0000000..61edf46 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanReturnStatus.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; + +namespace Library.ApplicationCore.Enums; + +public enum LoanReturnStatus +{ + [Description("Book was successfully returned.")] + Success, + + [Description("Loan not found.")] + LoanNotFound, + + [Description("Cannot return book as the book is already returned.")] + AlreadyReturned, + + [Description("Cannot return book due to an error.")] + Error +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/MembershipRenewalStatus.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/MembershipRenewalStatus.cs new file mode 100644 index 0000000..1323ae3 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/MembershipRenewalStatus.cs @@ -0,0 +1,21 @@ +using System.ComponentModel; + +namespace Library.ApplicationCore.Enums; + +public enum MembershipRenewalStatus +{ + [Description("Membership renewal was successful.")] + Success, + + [Description("Patron not found.")] + PatronNotFound, + + [Description("It is too early to renew the membership.")] + TooEarlyToRenew, + + [Description("Cannot renew membership due to an outstanding loan.")] + LoanNotReturned, + + [Description("Cannot renew membership due to an error.")] + Error +} diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanRepository.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanRepository.cs new file mode 100644 index 0000000..ab00b02 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanRepository.cs @@ -0,0 +1,8 @@ +using Library.ApplicationCore.Entities; + +namespace Library.ApplicationCore; + +public interface ILoanRepository { + Task GetLoan(int loanId); + Task UpdateLoan(Loan loan); +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanService.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanService.cs new file mode 100644 index 0000000..cb255ce --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanService.cs @@ -0,0 +1,7 @@ +using Library.ApplicationCore.Enums; + +public interface ILoanService +{ + Task ReturnLoan(int loanId); + Task ExtendLoan(int loanId); +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronRepository.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronRepository.cs new file mode 100644 index 0000000..19b97f8 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronRepository.cs @@ -0,0 +1,10 @@ +using Library.ApplicationCore.Entities; + +namespace Library.ApplicationCore; + +public interface IPatronRepository { + Task GetPatron(int patronId); + Task> SearchPatrons(string searchInput); + Task UpdatePatron(Patron patron); +} + diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronService.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronService.cs new file mode 100644 index 0000000..6b5f453 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronService.cs @@ -0,0 +1,6 @@ +using Library.ApplicationCore.Enums; + +public interface IPatronService +{ + Task RenewMembership(int patronId); +} diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Library.ApplicationCore.csproj b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Library.ApplicationCore.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Library.ApplicationCore.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/LoanService.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/LoanService.cs new file mode 100644 index 0000000..0f13d3a --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/LoanService.cs @@ -0,0 +1,70 @@ +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; + +public class LoanService : ILoanService +{ + private ILoanRepository _loanRepository; + + public LoanService(ILoanRepository loanRepository) + { + _loanRepository = loanRepository; + } + + public async Task ReturnLoan(int loanId) + { + Loan? loan = await _loanRepository.GetLoan(loanId); + if (loan == null) + { + return LoanReturnStatus.LoanNotFound; + } + + // check if already returned + if (loan.ReturnDate != null) + { + return LoanReturnStatus.AlreadyReturned; + } + + loan.ReturnDate = DateTime.Now; + try + { + await _loanRepository.UpdateLoan(loan); + return LoanReturnStatus.Success; + } + catch (Exception e) + { + return LoanReturnStatus.Error; + } + } + + public const int ExtendByDays = 14; + + public async Task ExtendLoan(int loanId) + { + var loan = await _loanRepository.GetLoan(loanId); + + if (loan == null) + return LoanExtensionStatus.LoanNotFound; + + // Check if patron's membership is expired + if (loan.Patron!.MembershipEnd < DateTime.Now) + return LoanExtensionStatus.MembershipExpired; + + if (loan.ReturnDate != null) + return LoanExtensionStatus.LoanReturned; + + if (loan.DueDate < DateTime.Now) + return LoanExtensionStatus.LoanExpired; + + loan.DueDate = loan.DueDate.AddDays(ExtendByDays); + try + { + await _loanRepository.UpdateLoan(loan); + return LoanExtensionStatus.Success; + } + catch (Exception e) + { + return LoanExtensionStatus.Error; + } + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/PatronService.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/PatronService.cs new file mode 100644 index 0000000..7ba6d78 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/PatronService.cs @@ -0,0 +1,36 @@ +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; + +public class PatronService : IPatronService +{ + private readonly IPatronRepository _patronRepository; + + public PatronService(IPatronRepository patronRepository) + { + _patronRepository = patronRepository; + } + + public async Task RenewMembership(int patronId) + { + var patron = await _patronRepository.GetPatron(patronId); + if (patron == null) + return MembershipRenewalStatus.PatronNotFound; + + // don't allow to renew till 1 month before expiration + if (patron.MembershipEnd >= DateTime.Now.AddMonths(1)) + return MembershipRenewalStatus.TooEarlyToRenew; + + // don't allow to renew if patron has overdue loans + if (patron.Loans.Any(l => (l.ReturnDate == null) && l.DueDate < DateTime.Now)) + return MembershipRenewalStatus.LoanNotReturned; + + patron.MembershipEnd = patron.MembershipEnd.AddYears(1); + try{ + await _patronRepository.UpdatePatron(patron); + return MembershipRenewalStatus.Success; + } catch (Exception e) { + return MembershipRenewalStatus.Error; + } + } +} diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/CommonActions.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/CommonActions.cs new file mode 100644 index 0000000..681f95c --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/CommonActions.cs @@ -0,0 +1,13 @@ +namespace Library.Console; + +[Flags] +public enum CommonActions +{ + Repeat = 0, + Select = 1, + Quit = 2, + SearchPatrons = 4, + RenewPatronMembership = 8, + ReturnLoanedBook = 16, + ExtendLoanedBook = 32 +} diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs new file mode 100644 index 0000000..9fc9750 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs @@ -0,0 +1,274 @@ +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; +using Library.Console; + +public class ConsoleApp +{ + ConsoleState _currentState = ConsoleState.PatronSearch; + + List matchingPatrons = new List(); + + Patron? selectedPatronDetails = null; + Loan selectedLoanDetails = null!; + + IPatronRepository _patronRepository; + ILoanRepository _loanRepository; + ILoanService _loanService; + IPatronService _patronService; + + public ConsoleApp(ILoanService loanService, IPatronService patronService, IPatronRepository patronRepository, ILoanRepository loanRepository) + { + _patronRepository = patronRepository; + _loanRepository = loanRepository; + _loanService = loanService; + _patronService = patronService; + } + + public async Task Run() + { + while (true) + { + switch (_currentState) + { + case ConsoleState.PatronSearch: + _currentState = await PatronSearch(); + break; + case ConsoleState.PatronSearchResults: + _currentState = await PatronSearchResults(); + break; + case ConsoleState.PatronDetails: + _currentState = await PatronDetails(); + break; + case ConsoleState.LoanDetails: + _currentState = await LoanDetails(); + break; + } + } + } + + async Task PatronSearch() + { + string searchInput = ReadPatronName(); + + matchingPatrons = await _patronRepository.SearchPatrons(searchInput); + + // Guard-style clauses for edge cases + if (matchingPatrons.Count > 20) + { + Console.WriteLine("More than 20 patrons satisfy the search, please provide more specific input..."); + return ConsoleState.PatronSearch; + } + else if (matchingPatrons.Count == 0) + { + Console.WriteLine("No matching patrons found."); + return ConsoleState.PatronSearch; + } + + Console.WriteLine("Matching Patrons:"); + PrintPatronsList(matchingPatrons); + return ConsoleState.PatronSearchResults; + } + + static string ReadPatronName() + { + string? searchInput = null; + while (String.IsNullOrWhiteSpace(searchInput)) + { + Console.Write("Enter a string to search for patrons by name: "); + + searchInput = Console.ReadLine(); + } + return searchInput; + } + + static void PrintPatronsList(List matchingPatrons) + { + int patronNumber = 1; + foreach (Patron patron in matchingPatrons) + { + Console.WriteLine($"{patronNumber}) {patron.Name}"); + patronNumber++; + } + } + + async Task PatronSearchResults() + { + CommonActions options = CommonActions.Select | CommonActions.SearchPatrons | CommonActions.Quit; + CommonActions action = ReadInputOptions(options, out int selectedPatronNumber); + if (action == CommonActions.Select) + { + if (selectedPatronNumber >= 1 && selectedPatronNumber <= matchingPatrons.Count) + { + var selectedPatron = matchingPatrons.ElementAt(selectedPatronNumber - 1); + selectedPatronDetails = await _patronRepository.GetPatron(selectedPatron.Id)!; + return ConsoleState.PatronDetails; + } + else + { + Console.WriteLine("Invalid patron number. Please try again."); + return ConsoleState.PatronSearchResults; + } + } + else if (action == CommonActions.Quit) + { + return ConsoleState.Quit; + } + else if (action == CommonActions.SearchPatrons) + { + return ConsoleState.PatronSearch; + } + + throw new InvalidOperationException("An input option is not handled."); + } + + static CommonActions ReadInputOptions(CommonActions options, out int optionNumber) + { + CommonActions action; + optionNumber = 0; + do + { + Console.WriteLine(); + WriteInputOptions(options); + string? userInput = Console.ReadLine(); + + action = userInput switch + { + "q" when options.HasFlag(CommonActions.Quit) => CommonActions.Quit, + "s" when options.HasFlag(CommonActions.SearchPatrons) => CommonActions.SearchPatrons, + "m" when options.HasFlag(CommonActions.RenewPatronMembership) => CommonActions.RenewPatronMembership, + "e" when options.HasFlag(CommonActions.ExtendLoanedBook) => CommonActions.ExtendLoanedBook, + "r" when options.HasFlag(CommonActions.ReturnLoanedBook) => CommonActions.ReturnLoanedBook, + _ when int.TryParse(userInput, out optionNumber) => CommonActions.Select, + _ => CommonActions.Repeat + }; + + if (action == CommonActions.Repeat) + { + Console.WriteLine("Invalid input. Please try again."); + } + } while (action == CommonActions.Repeat); + return action; + } + + static void WriteInputOptions(CommonActions options) + { + Console.WriteLine("Input Options:"); + if (options.HasFlag(CommonActions.ReturnLoanedBook)) + { + Console.WriteLine(" - \"r\" to mark as returned"); + } + if (options.HasFlag(CommonActions.ExtendLoanedBook)) + { + Console.WriteLine(" - \"e\" to extend the book loan"); + } + if (options.HasFlag(CommonActions.RenewPatronMembership)) + { + Console.WriteLine(" - \"m\" to extend patron's membership"); + } + if (options.HasFlag(CommonActions.SearchPatrons)) + { + Console.WriteLine(" - \"s\" for new search"); + } + if (options.HasFlag(CommonActions.Quit)) + { + Console.WriteLine(" - \"q\" to quit"); + } + if (options.HasFlag(CommonActions.Select)) + { + Console.WriteLine("Or type a number to select a list item."); + } + } + + async Task PatronDetails() + { + Console.WriteLine($"Name: {selectedPatronDetails.Name}"); + Console.WriteLine($"Membership Expiration: {selectedPatronDetails.MembershipEnd}"); + Console.WriteLine(); + Console.WriteLine("Book Loans:"); + int loanNumber = 1; + foreach (Loan loan in selectedPatronDetails.Loans) + { + Console.WriteLine($"{loanNumber}) {loan.BookItem!.Book!.Title} - Due: {loan.DueDate} - Returned: {(loan.ReturnDate != null).ToString()}"); + loanNumber++; + } + + CommonActions options = CommonActions.SearchPatrons | CommonActions.Quit | CommonActions.Select | CommonActions.RenewPatronMembership; + CommonActions action = ReadInputOptions(options, out int selectedLoanNumber); + if (action == CommonActions.Select) + { + if (selectedLoanNumber >= 1 && selectedLoanNumber <= selectedPatronDetails.Loans.Count()) + { + var selectedLoan = selectedPatronDetails.Loans.ElementAt(selectedLoanNumber - 1); + selectedLoanDetails = selectedPatronDetails.Loans.Where(l => l.Id == selectedLoan.Id).Single(); + return ConsoleState.LoanDetails; + } + else + { + Console.WriteLine("Invalid book loan number. Please try again."); + return ConsoleState.PatronDetails; + } + } + else if (action == CommonActions.Quit) + { + return ConsoleState.Quit; + } + else if (action == CommonActions.SearchPatrons) + { + return ConsoleState.PatronSearch; + } + else if (action == CommonActions.RenewPatronMembership) + { + var status = await _patronService.RenewMembership(selectedPatronDetails.Id); + Console.WriteLine(EnumHelper.GetDescription(status)); + // reloading after renewing membership + selectedPatronDetails = (await _patronRepository.GetPatron(selectedPatronDetails.Id))!; + return ConsoleState.PatronDetails; + } + + throw new InvalidOperationException("An input option is not handled."); + } + + async Task LoanDetails() + { + Console.WriteLine($"Book title: {selectedLoanDetails.BookItem!.Book!.Title}"); + Console.WriteLine($"Book Author: {selectedLoanDetails.BookItem!.Book!.Author!.Name}"); + Console.WriteLine($"Due date: {selectedLoanDetails.DueDate}"); + Console.WriteLine($"Returned: {(selectedLoanDetails.ReturnDate != null).ToString()}"); + Console.WriteLine(); + + CommonActions options = CommonActions.SearchPatrons | CommonActions.Quit | CommonActions.ReturnLoanedBook | CommonActions.ExtendLoanedBook; + CommonActions action = ReadInputOptions(options, out int selectedLoanNumber); + + if (action == CommonActions.ExtendLoanedBook) + { + var status = await _loanService.ExtendLoan(selectedLoanDetails.Id); + Console.WriteLine(EnumHelper.GetDescription(status)); + + // reload loan after extending + selectedPatronDetails = (await _patronRepository.GetPatron(selectedPatronDetails.Id))!; + selectedLoanDetails = (await _loanRepository.GetLoan(selectedLoanDetails.Id))!; + return ConsoleState.LoanDetails; + } + else if (action == CommonActions.ReturnLoanedBook) + { + var status = await _loanService.ReturnLoan(selectedLoanDetails.Id); + + Console.WriteLine(EnumHelper.GetDescription(status)); + _currentState = ConsoleState.LoanDetails; + // reload loan after returning + selectedLoanDetails = await _loanRepository.GetLoan(selectedLoanDetails.Id); + return ConsoleState.LoanDetails; + } + else if (action == CommonActions.Quit) + { + return ConsoleState.Quit; + } + else if (action == CommonActions.SearchPatrons) + { + return ConsoleState.PatronSearch; + } + + throw new InvalidOperationException("An input option is not handled."); + } +} diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/ConsoleState.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/ConsoleState.cs new file mode 100644 index 0000000..e9117b6 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/ConsoleState.cs @@ -0,0 +1,10 @@ +namespace Library.Console; + +public enum ConsoleState +{ + PatronSearch, + PatronSearchResults, + PatronDetails, + LoanDetails, + Quit +} diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/Json/Authors.json b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/Json/Authors.json new file mode 100644 index 0000000..1357eb1 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/Json/Authors.json @@ -0,0 +1,82 @@ +[ + { + "Id": 1, + "Name": "Author One" + }, + { + "Id": 2, + "Name": "Author Two" + }, + { + "Id": 3, + "Name": "Author Three" + }, + { + "Id": 4, + "Name": "Author Four" + }, + { + "Id": 5, + "Name": "Author Five" + }, + { + "Id": 6, + "Name": "Author Six" + }, + { + "Id": 7, + "Name": "Author Seven" + }, + { + "Id": 8, + "Name": "Author Eight" + }, + { + "Id": 9, + "Name": "Author Nine" + }, + { + "Id": 10, + "Name": "Author Ten" + }, + { + "Id": 11, + "Name": "Author Eleven" + }, + { + "Id": 12, + "Name": "Author Twelve" + }, + { + "Id": 13, + "Name": "Author Thirteen" + }, + { + "Id": 14, + "Name": "Author Fourteen" + }, + { + "Id": 15, + "Name": "Author Fifteen" + }, + { + "Id": 16, + "Name": "Author Sixteen" + }, + { + "Id": 17, + "Name": "Author Seventeen" + }, + { + "Id": 18, + "Name": "Author Eighteen" + }, + { + "Id": 19, + "Name": "Author Nineteen" + }, + { + "Id": 20, + "Name": "Author Twenty" + } +] \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/Json/BookItems.json b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/Json/BookItems.json new file mode 100644 index 0000000..ed659c5 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/Json/BookItems.json @@ -0,0 +1,122 @@ +[ + { + "Id": 1, + "BookId": 1, + "AcquisitionDate": "2023-09-20T00:40:43.1716563", + "Condition": "Good" + }, + { + "Id": 2, + "BookId": 2, + "AcquisitionDate": "2023-09-20T00:40:43.1717503", + "Condition": "Fair" + }, + { + "Id": 3, + "BookId": 3, + "AcquisitionDate": "2023-09-20T00:40:43.1717511", + "Condition": "Excellent" + }, + { + "Id": 4, + "BookId": 4, + "AcquisitionDate": "2023-09-20T00:40:43.1717513", + "Condition": "Poor" + }, + { + "Id": 5, + "BookId": 5, + "AcquisitionDate": "2023-09-20T00:40:43.1717516", + "Condition": "Good" + }, + { + "Id": 6, + "BookId": 6, + "AcquisitionDate": "2023-09-20T00:40:43.1717521", + "Condition": "Fair" + }, + { + "Id": 7, + "BookId": 7, + "AcquisitionDate": "2023-09-20T00:40:43.1717523", + "Condition": "Excellent" + }, + { + "Id": 8, + "BookId": 8, + "AcquisitionDate": "2023-09-20T00:40:43.1717526", + "Condition": "Poor" + }, + { + "Id": 9, + "BookId": 9, + "AcquisitionDate": "2023-09-20T00:40:43.171757", + "Condition": "Good" + }, + { + "Id": 10, + "BookId": 10, + "AcquisitionDate": "2023-09-20T00:40:43.1717574", + "Condition": "Fair" + }, + { + "Id": 11, + "BookId": 11, + "AcquisitionDate": "2023-09-20T00:40:43.1717576", + "Condition": "Excellent" + }, + { + "Id": 12, + "BookId": 12, + "AcquisitionDate": "2023-09-20T00:40:43.1717578", + "Condition": "Poor" + }, + { + "Id": 13, + "BookId": 13, + "AcquisitionDate": "2023-09-20T00:40:43.171758", + "Condition": "Good" + }, + { + "Id": 14, + "BookId": 14, + "AcquisitionDate": "2023-09-20T00:40:43.1717609", + "Condition": "Fair" + }, + { + "Id": 15, + "BookId": 15, + "AcquisitionDate": "2023-09-20T00:40:43.1717611", + "Condition": "Excellent" + }, + { + "Id": 16, + "BookId": 16, + "AcquisitionDate": "2023-09-20T00:40:43.1717613", + "Condition": "Poor" + }, + { + "Id": 17, + "BookId": 17, + "AcquisitionDate": "2023-09-20T00:40:43.1717616", + "Condition": "Good" + }, + { + "Id": 18, + "BookId": 18, + "AcquisitionDate": "2023-09-20T00:40:43.1717619", + "Condition": "Fair" + }, + { + "Id": 19, + "BookId": 19, + "AcquisitionDate": "2023-09-20T00:40:43.1717621", + "Condition": "Excellent" + }, + { + "Id": 20, + "BookId": 20, + "AcquisitionDate": "2023-09-20T00:40:43.1717626", + "Condition": "Poor" + } +] \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/Json/Books.json b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/Json/Books.json new file mode 100644 index 0000000..51f3339 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/Json/Books.json @@ -0,0 +1,162 @@ +[ + { + "Id": 1, + "Title": "Book One", + "AuthorId": 1, + "Genre": "Dystopian", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524935" + }, + { + "Id": 2, + "Title": "Book Two", + "AuthorId": 2, + "Genre": "Classic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524936" + }, + { + "Id": 3, + "Title": "Book Three", + "AuthorId": 3, + "Genre": "Romance", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524937" + }, + { + "Id": 4, + "Title": "Book Four", + "AuthorId": 4, + "Genre": "Classic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524938" + }, + { + "Id": 5, + "Title": "Book Five", + "AuthorId": 5, + "Genre": "Coming-of-age", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524939" + }, + { + "Id": 6, + "Title": "Book Six", + "AuthorId": 6, + "Genre": "Modernist", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524940" + }, + { + "Id": 7, + "Title": "Book Seven", + "AuthorId": 7, + "Genre": "Adventure", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524941" + }, + { + "Id": 8, + "Title": "Book Eight", + "AuthorId": 8, + "Genre": "Fantasy", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524942" + }, + { + "Id": 9, + "Title": "Book Nine", + "AuthorId": 9, + "Genre": "Fantasy", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524943" + }, + { + "Id": 10, + "Title": "Book Ten", + "AuthorId": 10, + "Genre": "Epic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524944" + }, + { + "Id": 11, + "Title": "Book Eleven", + "AuthorId": 11, + "Genre": "Psychological", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524945" + }, + { + "Id": 12, + "Title": "Book Twelve", + "AuthorId": 12, + "Genre": "Psychological", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524946" + }, + { + "Id": 13, + "Title": "Book Thirteen", + "AuthorId": 13, + "Genre": "Magical realism", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524947" + }, + { + "Id": 14, + "Title": "Book Fourteen", + "AuthorId": 14, + "Genre": "Classic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524948" + }, + { + "Id": 15, + "Title": "Book Fifteen", + "AuthorId": 15, + "Genre": "Adventure", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524949" + }, + { + "Id": 16, + "Title": "Book Sixteen", + "AuthorId": 16, + "Genre": "Historical", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524950" + }, + { + "Id": 17, + "Title": "Book Seventeen", + "AuthorId": 17, + "Genre": "Gothic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524951" + }, + { + "Id": 18, + "Title": "Book Eighteen", + "AuthorId": 18, + "Genre": "Dystopian", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524952" + }, + { + "Id": 19, + "Title": "Book Nineteen", + "AuthorId": 19, + "Genre": "Classic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524953" + }, + { + "Id": 20, + "Title": "Book Twenty", + "AuthorId": 20, + "Genre": "Adventure", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524954" + } +] \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/Json/Loans.json b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/Json/Loans.json new file mode 100644 index 0000000..a84491d --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/Json/Loans.json @@ -0,0 +1,402 @@ +[ + { + "Id": 1, + "BookItemId": 17, + "PatronId": 22, + "LoanDate": "2023-12-08T00:40:43.1808862", + "DueDate": "2023-12-22T00:40:43.1808862", + "ReturnDate": null + }, + { + "Id": 2, + "BookItemId": 6, + "PatronId": 28, + "LoanDate": "2023-12-17T00:40:43.1809243", + "DueDate": "2023-12-31T00:40:43.1809243", + "ReturnDate": null + }, + { + "Id": 3, + "BookItemId": 16, + "PatronId": 4, + "LoanDate": "2023-12-23T00:40:43.1809289", + "DueDate": "2024-01-06T00:40:43.1809289", + "ReturnDate": null + }, + { + "Id": 4, + "BookItemId": 17, + "PatronId": 14, + "LoanDate": "2023-12-22T00:40:43.1809292", + "DueDate": "2024-01-05T00:40:43.1809292", + "ReturnDate": null + }, + { + "Id": 5, + "BookItemId": 6, + "PatronId": 9, + "LoanDate": "2023-12-09T00:40:43.1809295", + "DueDate": "2023-12-23T00:40:43.1809295", + "ReturnDate": null + }, + { + "Id": 6, + "BookItemId": 14, + "PatronId": 25, + "LoanDate": "2023-12-27T00:40:43.18093", + "DueDate": "2024-01-10T00:40:43.18093", + "ReturnDate": null + }, + { + "Id": 7, + "BookItemId": 12, + "PatronId": 50, + "LoanDate": "2023-12-27T00:40:43.1809304", + "DueDate": "2024-01-10T00:40:43.1809304", + "ReturnDate": null + }, + { + "Id": 8, + "BookItemId": 18, + "PatronId": 28, + "LoanDate": "2023-12-26T00:40:43.1809306", + "DueDate": "2024-01-09T00:40:43.1809306", + "ReturnDate": null + }, + { + "Id": 9, + "BookItemId": 8, + "PatronId": 9, + "LoanDate": "2023-12-10T00:40:43.1809309", + "DueDate": "2023-12-24T00:40:43.1809309", + "ReturnDate": null + }, + { + "Id": 10, + "BookItemId": 16, + "PatronId": 3, + "LoanDate": "2023-12-26T00:40:43.1809312", + "DueDate": "2024-01-09T00:40:43.1809312", + "ReturnDate": null + }, + { + "Id": 11, + "BookItemId": 4, + "PatronId": 42, + "LoanDate": "2023-12-15T00:40:43.1809315", + "DueDate": "2023-12-29T00:40:43.1809315", + "ReturnDate": null + }, + { + "Id": 12, + "BookItemId": 17, + "PatronId": 7, + "LoanDate": "2023-12-23T00:40:43.1809331", + "DueDate": "2024-01-06T00:40:43.1809331", + "ReturnDate": null + }, + { + "Id": 13, + "BookItemId": 12, + "PatronId": 5, + "LoanDate": "2023-12-27T00:40:43.1809333", + "DueDate": "2024-01-10T00:40:43.1809333", + "ReturnDate": null + }, + { + "Id": 14, + "BookItemId": 4, + "PatronId": 9, + "LoanDate": "2023-12-10T00:40:43.1809337", + "DueDate": "2023-12-24T00:40:43.1809337", + "ReturnDate": null + }, + { + "Id": 15, + "BookItemId": 7, + "PatronId": 28, + "LoanDate": "2023-12-23T00:40:43.1809339", + "DueDate": "2024-01-06T00:40:43.1809339", + "ReturnDate": null + }, + { + "Id": 16, + "BookItemId": 14, + "PatronId": 3, + "LoanDate": "2023-12-08T00:40:43.1809342", + "DueDate": "2023-12-22T00:40:43.1809342", + "ReturnDate": null + }, + { + "Id": 17, + "BookItemId": 5, + "PatronId": 48, + "LoanDate": "2023-12-16T00:40:43.1809344", + "DueDate": "2023-12-30T00:40:43.1809344", + "ReturnDate": null + }, + { + "Id": 18, + "BookItemId": 4, + "PatronId": 49, + "LoanDate": "2023-12-19T00:40:43.1809348", + "DueDate": "2024-01-02T00:40:43.1809348", + "ReturnDate": null + }, + { + "Id": 19, + "BookItemId": 13, + "PatronId": 33, + "LoanDate": "2023-12-28T00:40:43.180935", + "DueDate": "2024-01-11T00:40:43.180935", + "ReturnDate": null + }, + { + "Id": 20, + "BookItemId": 14, + "PatronId": 48, + "LoanDate": "2023-12-27T00:40:43.1809353", + "DueDate": "2024-01-10T00:40:43.1809353", + "ReturnDate": null + }, + { + "Id": 21, + "BookItemId": 7, + "PatronId": 5, + "LoanDate": "2023-12-12T00:40:43.1809368", + "DueDate": "2023-12-26T00:40:43.1809368", + "ReturnDate": null + }, + { + "Id": 22, + "BookItemId": 9, + "PatronId": 1, + "LoanDate": "2023-12-09T00:40:43.1809371", + "DueDate": "2023-12-23T00:40:43.1809371", + "ReturnDate": null + }, + { + "Id": 23, + "BookItemId": 11, + "PatronId": 33, + "LoanDate": "2023-12-26T00:40:43.1809374", + "DueDate": "2024-01-09T00:40:43.1809374", + "ReturnDate": null + }, + { + "Id": 24, + "BookItemId": 10, + "PatronId": 46, + "LoanDate": "2023-12-28T00:40:43.1809376", + "DueDate": "2024-01-11T00:40:43.1809376", + "ReturnDate": null + }, + { + "Id": 25, + "BookItemId": 20, + "PatronId": 41, + "LoanDate": "2023-12-12T00:40:43.1809379", + "DueDate": "2023-12-26T00:40:43.1809379", + "ReturnDate": null + }, + { + "Id": 26, + "BookItemId": 13, + "PatronId": 15, + "LoanDate": "2023-12-16T00:40:43.1809382", + "DueDate": "2023-12-30T00:40:43.1809382", + "ReturnDate": null + }, + { + "Id": 27, + "BookItemId": 15, + "PatronId": 23, + "LoanDate": "2023-12-18T00:40:43.1809384", + "DueDate": "2024-01-01T00:40:43.1809384", + "ReturnDate": null + }, + { + "Id": 28, + "BookItemId": 15, + "PatronId": 31, + "LoanDate": "2023-12-11T00:40:43.1809387", + "DueDate": "2023-12-25T00:40:43.1809387", + "ReturnDate": null + }, + { + "Id": 29, + "BookItemId": 4, + "PatronId": 10, + "LoanDate": "2023-12-18T00:40:43.1809402", + "DueDate": "2024-01-01T00:40:43.1809402", + "ReturnDate": null + }, + { + "Id": 30, + "BookItemId": 6, + "PatronId": 18, + "LoanDate": "2023-12-12T00:40:43.1809405", + "DueDate": "2023-12-26T00:40:43.1809405", + "ReturnDate": null + }, + { + "Id": 31, + "BookItemId": 11, + "PatronId": 3, + "LoanDate": "2023-12-16T00:40:43.1809408", + "DueDate": "2023-12-30T00:40:43.1809408", + "ReturnDate": null + }, + { + "Id": 32, + "BookItemId": 8, + "PatronId": 20, + "LoanDate": "2023-12-22T00:40:43.1809411", + "DueDate": "2024-01-05T00:40:43.1809411", + "ReturnDate": null + }, + { + "Id": 33, + "BookItemId": 14, + "PatronId": 12, + "LoanDate": "2023-12-28T00:40:43.1809415", + "DueDate": "2024-01-11T00:40:43.1809415", + "ReturnDate": null + }, + { + "Id": 34, + "BookItemId": 19, + "PatronId": 29, + "LoanDate": "2023-12-28T00:40:43.1809458", + "DueDate": "2024-01-11T00:40:43.1809458", + "ReturnDate": "2023-12-29T00:40:54.582495" + }, + { + "Id": 35, + "BookItemId": 7, + "PatronId": 45, + "LoanDate": "2023-12-17T00:40:43.180946", + "DueDate": "2023-12-31T00:40:43.180946", + "ReturnDate": null + }, + { + "Id": 36, + "BookItemId": 11, + "PatronId": 3, + "LoanDate": "2023-12-10T00:40:43.1809463", + "DueDate": "2023-12-24T00:40:43.1809463", + "ReturnDate": null + }, + { + "Id": 37, + "BookItemId": 1, + "PatronId": 5, + "LoanDate": "2023-12-18T00:40:43.1809466", + "DueDate": "2024-01-18T00:40:43.1809466", + "ReturnDate": "2024-01-17T00:40:43.1809466" + }, + { + "Id": 38, + "BookItemId": 15, + "PatronId": 25, + "LoanDate": "2023-12-26T00:40:43.1809481", + "DueDate": "2024-01-09T00:40:43.1809481", + "ReturnDate": null + }, + { + "Id": 39, + "BookItemId": 4, + "PatronId": 33, + "LoanDate": "2023-12-18T00:40:43.1809484", + "DueDate": "2024-01-01T00:40:43.1809484", + "ReturnDate": null + }, + { + "Id": 40, + "BookItemId": 5, + "PatronId": 33, + "LoanDate": "2023-12-25T00:40:43.1809487", + "DueDate": "2024-01-08T00:40:43.1809487", + "ReturnDate": null + }, + { + "Id": 41, + "BookItemId": 14, + "PatronId": 13, + "LoanDate": "2023-12-15T00:40:43.1809489", + "DueDate": "2023-12-29T00:40:43.1809489", + "ReturnDate": null + }, + { + "Id": 42, + "BookItemId": 11, + "PatronId": 10, + "LoanDate": "2023-12-12T00:40:43.1809493", + "DueDate": "2023-12-26T00:40:43.1809493", + "ReturnDate": null + }, + { + "Id": 43, + "BookItemId": 9, + "PatronId": 45, + "LoanDate": "2023-12-14T00:40:43.1809496", + "DueDate": "2023-12-28T00:40:43.1809496", + "ReturnDate": "2023-12-29T00:49:42.3406277" + }, + { + "Id": 44, + "BookItemId": 3, + "PatronId": 46, + "LoanDate": "2023-12-08T00:40:43.1809498", + "DueDate": "2023-12-22T00:40:43.1809498", + "ReturnDate": null + }, + { + "Id": 45, + "BookItemId": 5, + "PatronId": 10, + "LoanDate": "2023-12-24T00:40:43.1809501", + "DueDate": "2024-01-07T00:40:43.1809501", + "ReturnDate": null + }, + { + "Id": 46, + "BookItemId": 1, + "PatronId": 49, + "LoanDate": "2024-07-09T00:40:43.1809503", + "DueDate": "2024-09-09T00:40:43.1809503", + "ReturnDate": null + }, + { + "Id": 47, + "BookItemId": 8, + "PatronId": 36, + "LoanDate": "2023-12-11T00:40:43.1809507", + "DueDate": "2023-12-25T00:40:43.1809507", + "ReturnDate": null + }, + { + "Id": 48, + "BookItemId": 5, + "PatronId": 10, + "LoanDate": "2023-12-18T00:40:43.1809509", + "DueDate": "2024-01-01T00:40:43.1809509", + "ReturnDate": null + }, + { + "Id": 49, + "BookItemId": 20, + "PatronId": 24, + "LoanDate": "2023-12-16T00:40:43.1809512", + "DueDate": "2023-12-30T00:40:43.1809512", + "ReturnDate": null + }, + { + "Id": 50, + "BookItemId": 3, + "PatronId": 45, + "LoanDate": "2023-12-13T00:40:43.1809514", + "DueDate": "2023-12-27T00:40:43.1809514", + "ReturnDate": "2023-12-29T00:49:48.9561798" + } + ] diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/Json/Patrons.json b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/Json/Patrons.json new file mode 100644 index 0000000..5d44d83 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/Json/Patrons.json @@ -0,0 +1,352 @@ +[ + { + "Id": 1, + "Name": "Patron One", + "MembershipEnd": "2024-12-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron One.jpg" + }, + { + "Id": 2, + "Name": "Patron Two", + "MembershipEnd": "2025-02-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Two.jpg" + }, + { + "Id": 3, + "Name": "Patron Three", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Three.jpg" + }, + { + "Id": 4, + "Name": "Patron Four", + "MembershipEnd": "2025-04-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Four.jpg" + }, + { + "Id": 5, + "Name": "Patron Five", + "MembershipEnd": "2025-05-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Five.jpg" + }, + { + "Id": 6, + "Name": "Patron Six", + "MembershipEnd": "2025-06-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Six.jpg" + }, + { + "Id": 7, + "Name": "Patron Seven", + "MembershipEnd": "2025-07-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Seven.jpg" + }, + { + "Id": 8, + "Name": "Patron Eight", + "MembershipEnd": "2024-01-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Eight.jpg" + }, + { + "Id": 9, + "Name": "Patron Nine", + "MembershipEnd": "2024-02-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Nine.jpg" + }, + { + "Id": 10, + "Name": "Patron Ten", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Ten.jpg" + }, + { + "Id": 11, + "Name": "Patron Eleven", + "MembershipEnd": "2024-04-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Eleven.jpg" + }, + { + "Id": 12, + "Name": "Patron Twelve", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twelve.jpg" + }, + { + "Id": 13, + "Name": "Patron Thirteen", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirteen.jpg" + }, + { + "Id": 14, + "Name": "Patron Fourteen", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Fourteen.jpg" + }, + { + "Id": 15, + "Name": "Patron Fifteen", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Fifteen.jpg" + }, + { + "Id": 16, + "Name": "Patron Sixteen", + "MembershipEnd": "2024-04-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Sixteen.jpg" + }, + { + "Id": 17, + "Name": "Patron Seventeen", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Seventeen.jpg" + }, + { + "Id": 18, + "Name": "Patron Eighteen", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Eighteen.jpg" + }, + { + "Id": 19, + "Name": "Patron Nineteen", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Nineteen.jpg" + }, + { + "Id": 20, + "Name": "Patron Twenty", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty.jpg" + }, + { + "Id": 21, + "Name": "Patron Twenty-One", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-One.jpg" + }, + { + "Id": 22, + "Name": "Patron Twenty-Two", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Two.jpg" + }, + { + "Id": 23, + "Name": "Patron Twenty-Three", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Three.jpg" + }, + { + "Id": 24, + "Name": "Patron Twenty-Four", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Four.jpg" + }, + { + "Id": 25, + "Name": "Patron Twenty-Five", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Five.jpg" + }, + { + "Id": 26, + "Name": "Patron Twenty-Six", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Six.jpg" + }, + { + "Id": 27, + "Name": "Patron Twenty-Seven", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Seven.jpg" + }, + { + "Id": 28, + "Name": "Patron Twenty-Eight", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Eight.jpg" + }, + { + "Id": 29, + "Name": "Patron Twenty-Nine", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Nine.jpg" + }, + { + "Id": 30, + "Name": "Patron Thirty", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty.jpg" + }, + { + "Id": 31, + "Name": "Patron Thirty-One", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-One.jpg" + }, + { + "Id": 32, + "Name": "Patron Thirty-Two", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Two.jpg" + }, + { + "Id": 33, + "Name": "Patron Thirty-Three", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Three.jpg" + }, + { + "Id": 34, + "Name": "Patron Thirty-Four", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Four.jpg" + }, + { + "Id": 35, + "Name": "Patron Thirty-Five", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Five.jpg" + }, + { + "Id": 36, + "Name": "Patron Thirty-Six", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Six.jpg" + }, + { + "Id": 37, + "Name": "Patron Thirty-Seven", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Seven.jpg" + }, + { + "Id": 38, + "Name": "Patron Thirty-Eight", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Eight.jpg" + }, + { + "Id": 39, + "Name": "Patron Thirty-Nine", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Nine.jpg" + }, + { + "Id": 40, + "Name": "Patron Forty", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty.jpg" + }, + { + "Id": 41, + "Name": "Patron Forty-One", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-One.jpg" + }, + { + "Id": 42, + "Name": "Patron Forty-Two", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Two.jpg" + }, + { + "Id": 43, + "Name": "Patron Forty-Three", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Three.jpg" + }, + { + "Id": 44, + "Name": "Patron Forty-Four", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Four.jpg" + }, + { + "Id": 45, + "Name": "Patron Forty-Five", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Five.jpg" + }, + { + "Id": 46, + "Name": "Patron Forty-Six", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Six.jpg" + }, + { + "Id": 47, + "Name": "Patron Forty-Seven", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Seven.jpg" + }, + { + "Id": 48, + "Name": "Patron Forty-Eight", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Eight.jpg" + }, + { + "Id": 49, + "Name": "Patron Forty-Nine", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Nine.jpg" + }, + { + "Id": 50, + "Name": "Patron Fifty", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Fifty.jpg" + } +] \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/Library.Console.csproj b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/Library.Console.csproj new file mode 100644 index 0000000..359cee9 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/Library.Console.csproj @@ -0,0 +1,34 @@ + + + + + + + + + Exe + net9.0 + enable + enable + + + + + PreserveNewest + + + + + + PreserveNewest + + + + + + + + + + + diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/Program.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/Program.cs new file mode 100644 index 0000000..1a21671 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/Program.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.DependencyInjection; +using Library.Infrastructure.Data; +using Library.ApplicationCore; +using Microsoft.Extensions.Configuration; + +var services = new ServiceCollection(); + +var configuration = new ConfigurationBuilder() +.SetBasePath(Directory.GetCurrentDirectory()) +.AddJsonFile("appSettings.json") +.Build(); + +services.AddSingleton(configuration); + +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); + +services.AddSingleton(); +services.AddSingleton(); + +var servicesProvider = services.BuildServiceProvider(); + +var consoleApp = servicesProvider.GetRequiredService(); +consoleApp.Run().Wait(); diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/appSettings.json b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/appSettings.json new file mode 100644 index 0000000..3aed751 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Console/appSettings.json @@ -0,0 +1,9 @@ +{ + "JsonPaths": { + "Authors": "Json/Authors.json", + "Books": "Json/Books.json", + "BookItems": "Json/BookItems.json", + "Patrons": "Json/Patrons.json", + "Loans": "Json/Loans.json" + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonData.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonData.cs new file mode 100644 index 0000000..7af26a7 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonData.cs @@ -0,0 +1,212 @@ +using System.Text.Json; +using Library.ApplicationCore.Entities; +using Microsoft.Extensions.Configuration; + +namespace Library.Infrastructure.Data; + +public class JsonData +{ + public List? Authors { get; set; } + public List? Books { get; set; } + public List? BookItems { get; set; } + public List? Patrons { get; set; } + public List? Loans { get; set; } + + private readonly string _authorsPath; + private readonly string _booksPath; + private readonly string _bookItemsPath; + private readonly string _patronsPath; + private readonly string _loansPath; + + public JsonData(IConfiguration configuration) + { + var section = configuration.GetSection("JsonPaths"); + _authorsPath = section["Authors"] ?? Path.Combine("Json", "Authors.json"); + _booksPath = section["Books"] ?? Path.Combine("Json", "Books.json"); + _bookItemsPath = section["BookItems"] ?? Path.Combine("Json", "BookItems.json"); + _patronsPath = section["Patrons"] ?? Path.Combine("Json", "Patrons.json"); + _loansPath = section["Loans"] ?? Path.Combine("Json", "Loans.json"); + } + + public async Task EnsureDataLoaded() + { + if (Patrons == null) + { + await LoadData(); + } + } + + public async Task LoadData() + { + Authors = await LoadJson>(_authorsPath); + Books = await LoadJson>(_booksPath); + BookItems = await LoadJson>(_bookItemsPath); + Patrons = await LoadJson>(_patronsPath); + Loans = await LoadJson>(_loansPath); + } + + public async Task SaveLoans(IEnumerable loans) + { + List loanList = new List(); + foreach (var l in loans) + { + Loan loan = new Loan + { + // making sure only a subset of properties is set and saved + Id = l.Id, + BookItemId = l.BookItemId, + PatronId = l.PatronId, + LoanDate = l.LoanDate, + DueDate = l.DueDate, + ReturnDate = l.ReturnDate + }; + loanList.Add(loan); + } + await SaveJson(_loansPath, loanList); + } + + public async Task SavePatrons(IEnumerable patrons) + { + await SaveJson(_patronsPath, patrons.Select(p => new Patron + { + Id = p.Id, + Name = p.Name, + MembershipStart = p.MembershipStart, + MembershipEnd = p.MembershipEnd, + ImageName = p.ImageName, + }).ToList()); + } + + private async Task SaveJson(string filePath, T data) + { + using (FileStream jsonStream = File.Create(filePath)) + { + await JsonSerializer.SerializeAsync(jsonStream, data); + } + } + + public List GetPopulatedPatrons(IEnumerable patrons) + { + List populated = new List(); + foreach (Patron patron in patrons) + { + populated.Add(GetPopulatedPatron(patron)); + } + return populated; + } + + public Patron GetPopulatedPatron(Patron p) + { + Patron populated = new Patron + { + Id = p.Id, + Name = p.Name, + ImageName = p.ImageName, + MembershipStart = p.MembershipStart, + MembershipEnd = p.MembershipEnd, + Loans = new List() + }; + + foreach (Loan loan in Loans!) + { + if (loan.PatronId == p.Id) + { + populated.Loans.Add(GetPopulatedLoan(loan)); + } + } + + return populated; + } + + public Loan GetPopulatedLoan(Loan l) + { + Loan populated = new Loan + { + Id = l.Id, + BookItemId = l.BookItemId, + PatronId = l.PatronId, + LoanDate = l.LoanDate, + DueDate = l.DueDate, + ReturnDate = l.ReturnDate + }; + + foreach (BookItem bi in BookItems!) + { + if (bi.Id == l.BookItemId) + { + populated.BookItem = GetPopulatedBookItem(bi); + break; + } + } + + foreach (Patron p in Patrons!) + { + if (p.Id == l.PatronId) + { + populated.Patron = p; + break; + } + } + + return populated; + } + + public BookItem GetPopulatedBookItem(BookItem bi) + { + BookItem populated = new BookItem + { + Id = bi.Id, + BookId = bi.BookId, + AcquisitionDate = bi.AcquisitionDate, + Condition = bi.Condition + }; + + foreach (Book b in Books!) + { + if (b.Id == bi.BookId) + { + populated.Book = GetPopulatedBook(b); + break; + } + } + + return populated; + } + + public Book GetPopulatedBook(Book b) + { + Book populated = new Book + { + Id = b.Id, + Title = b.Title, + AuthorId = b.AuthorId, + Genre = b.Genre, + ISBN = b.ISBN, + ImageName = b.ImageName + }; + + foreach (Author a in Authors!) + { + if (a.Id == b.AuthorId) + { + populated.Author = new Author + { + Id = a.Id, + Name = a.Name + }; + break; + } + } + + return populated; + } + + private async Task LoadJson(string filePath) + { + using (FileStream jsonStream = File.OpenRead(filePath)) + { + return await JsonSerializer.DeserializeAsync(jsonStream); + } + } + +} diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonLoanRepository.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonLoanRepository.cs new file mode 100644 index 0000000..2683283 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonLoanRepository.cs @@ -0,0 +1,55 @@ +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; + +namespace Library.Infrastructure.Data; + +public class JsonLoanRepository : ILoanRepository +{ + private readonly JsonData _jsonData; + + public JsonLoanRepository(JsonData jsonData) + { + _jsonData = jsonData; + } + + public async Task GetLoan(int id) + { + await _jsonData.EnsureDataLoaded(); + + foreach (Loan loan in _jsonData.Loans!) + { + if (loan.Id == id) + { + Loan populated = _jsonData.GetPopulatedLoan(loan); + return populated; + } + } + return null; + } + + public async Task UpdateLoan(Loan loan) + { + Loan? existingLoan = null; + foreach (Loan l in _jsonData.Loans!) + { + if (l.Id == loan.Id) + { + existingLoan = l; + break; + } + } + + if (existingLoan != null) + { + existingLoan.BookItemId = loan.BookItemId; + existingLoan.PatronId = loan.PatronId; + existingLoan.LoanDate = loan.LoanDate; + existingLoan.DueDate = loan.DueDate; + existingLoan.ReturnDate = loan.ReturnDate; + + await _jsonData.SaveLoans(_jsonData.Loans!); + + await _jsonData.LoadData(); + } + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonPatronRepository.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonPatronRepository.cs new file mode 100644 index 0000000..efb05f8 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonPatronRepository.cs @@ -0,0 +1,73 @@ +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; + +namespace Library.Infrastructure.Data; + +public class JsonPatronRepository : IPatronRepository +{ + private readonly JsonData _jsonData; + + public JsonPatronRepository(JsonData jsonData) + { + _jsonData = jsonData; + } + + public async Task> SearchPatrons(string searchInput) + { + await _jsonData.EnsureDataLoaded(); + + List searchResults = new List(); + foreach (Patron patron in _jsonData.Patrons) + { + if (patron.Name.Contains(searchInput)) + { + searchResults.Add(patron); + } + } + searchResults.Sort((p1, p2) => String.Compare(p1.Name, p2.Name)); + + searchResults = _jsonData.GetPopulatedPatrons(searchResults); + + return searchResults; + } + + public async Task GetPatron(int id) + { + await _jsonData.EnsureDataLoaded(); + + foreach (Patron patron in _jsonData.Patrons!) + { + if (patron.Id == id) + { + Patron populated = _jsonData.GetPopulatedPatron(patron); + return populated; + } + } + return null; + } + + public async Task UpdatePatron(Patron patron) + { + await _jsonData.EnsureDataLoaded(); + var patrons = _jsonData.Patrons!; + Patron existingPatron = null; + foreach (var p in patrons) + { + if (p.Id == patron.Id) + { + existingPatron = p; + break; + } + } + if (existingPatron != null) + { + existingPatron.Name = patron.Name; + existingPatron.ImageName = patron.ImageName; + existingPatron.MembershipStart = patron.MembershipStart; + existingPatron.MembershipEnd = patron.MembershipEnd; + existingPatron.Loans = patron.Loans; + await _jsonData.SavePatrons(patrons); + await _jsonData.LoadData(); + } + } +} diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Infrastructure/Library.Infrastructure.csproj b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Infrastructure/Library.Infrastructure.csproj new file mode 100644 index 0000000..1a7e6eb --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/src/Library.Infrastructure/Library.Infrastructure.csproj @@ -0,0 +1,17 @@ + + + + + + + + + + + + net9.0 + enable + enable + + + diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ExtendLoan.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ExtendLoan.cs new file mode 100644 index 0000000..d3e695b --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ExtendLoan.cs @@ -0,0 +1,104 @@ +using NSubstitute; +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; + +namespace Library.UnitTests.ApplicationCore.LoanServiceTests; + +public class ExtendLoanTest +{ + private readonly ILoanRepository _mockLoanRepository; + private readonly LoanService _loanService; + + public ExtendLoanTest() + { + _mockLoanRepository = Substitute.For(); + _loanService = new LoanService(_mockLoanRepository); + } + + [Fact(DisplayName = "LoanService.ExtendLoan: Extends the loan successfully")] + public async Task ExtendLoan_ExtendsLoanSuccessfully() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateCurrentLoanForPatron(patron); + var loanDueDate = loan.DueDate; + var loanId = loan.Id; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanExtensionStatus extensionStatus = await _loanService.ExtendLoan(loanId); + + // Assert + Assert.Equal(LoanExtensionStatus.Success, extensionStatus); + Assert.Equal(loanDueDate.AddDays(LoanService.ExtendByDays), loan.DueDate); + } + + [Fact(DisplayName = "LoanService.ExtendLoan: Returns LoanNotFound if loan is not found")] + public async Task ExtendLoan_ReturnsLoanNotFound() + { + // Arrange + var loanId = 1; + _mockLoanRepository.GetLoan(loanId).Returns((Loan?)null); + + // Act + LoanExtensionStatus extensionStatus = await _loanService.ExtendLoan(loanId); + + // Assert + Assert.Equal(LoanExtensionStatus.LoanNotFound, extensionStatus); + } + + [Fact(DisplayName = "LoanService.ExtendLoan: Returns MembershipExpired if patron's membership is expired")] + public async Task ExtendLoan_ReturnsMembershipExpired() + { + // Arrange + var patron = PatronFactory.CreateExpiredPatron(); + var loan = LoanFactory.CreateCurrentLoanForPatron(patron); + var loanId = loan.Id; + var loanDueDate = loan.DueDate; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanExtensionStatus extensionStatus = await _loanService.ExtendLoan(loanId); + + // Assert + Assert.Equal(LoanExtensionStatus.MembershipExpired, extensionStatus); + Assert.Equal(loanDueDate, loan.DueDate); + } + + [Fact(DisplayName = "LoanService.ExtendLoan: Returns LoanReturned if loan is already returned")] + public async Task ExtendLoan_ReturnsLoanReturned() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateReturnedLoanForPatron(patron); + var loanId = loan.Id; + var loanDueDate = loan.DueDate; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanExtensionStatus extensionStatus = await _loanService.ExtendLoan(loanId); + + // Assert + Assert.Equal(LoanExtensionStatus.LoanReturned, extensionStatus); + Assert.Equal(loanDueDate, loan.DueDate); + } + + [Fact(DisplayName = "LoanService.ExtendLoan: Returns LoanExpired if loan is already expired")] + public async Task ExtendLoan_ReturnsLoanExpired() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateExpiredLoanForPatron(patron); + var loanId = loan.Id; + var loanDueDate = loan.DueDate; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanExtensionStatus extensionStatus = await _loanService.ExtendLoan(loanId); + + // Assert + Assert.Equal(LoanExtensionStatus.LoanExpired, extensionStatus); + Assert.Equal(loanDueDate, loan.DueDate); + } +} diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ReturnLoan.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ReturnLoan.cs new file mode 100644 index 0000000..68c3a0d --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ReturnLoan.cs @@ -0,0 +1,99 @@ +using NSubstitute; +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; + +namespace Library.UnitTests.ApplicationCore.LoanServiceTests; + +public class ReturnLoanTest +{ + private readonly ILoanRepository _mockLoanRepository; + private readonly LoanService _loanService; + + public ReturnLoanTest() + { + _mockLoanRepository = Substitute.For(); + _loanService = new LoanService(_mockLoanRepository); + } + + [Fact(DisplayName = "LoanService.ReturnLoan: Returns LoanNotFound if loan is not found")] + public async Task ReturnLoan_ReturnsLoanNotFound() + { + // Arrange + var loanId = 1; + _mockLoanRepository.GetLoan(loanId).Returns((Loan?)null); + + // Act + LoanReturnStatus returnStatus = await _loanService.ReturnLoan(loanId); + + // Assert + Assert.Equal(LoanReturnStatus.LoanNotFound, returnStatus); + } + + [Fact(DisplayName = "LoanService.ReturnLoan: Returns AlreadyReturned if loan is already returned")] + public async Task ReturnLoan_ReturnsAlreadyReturned() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateReturnedLoanForPatron(patron); + var loanId = loan.Id; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanReturnStatus returnStatus = await _loanService.ReturnLoan(loanId); + + // Assert + Assert.Equal(LoanReturnStatus.AlreadyReturned, returnStatus); + } + + [Fact(DisplayName = "LoanService.ReturnLoan: Returns Success and updates return date for a patron with current membership")] + public async Task ReturnLoan_ReturnsSuccessAndUpdateReturnDate() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateCurrentLoanForPatron(patron); + var loanId = loan.Id; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanReturnStatus returnStatus = await _loanService.ReturnLoan(loanId); + + // Assert + Assert.Equal(LoanReturnStatus.Success, returnStatus); + Assert.NotNull(loan.ReturnDate); + } + + [Fact(DisplayName = "LoanService.ReturnLoan: Returns Success and updates return date for an expired loan")] + public async Task ReturnLoan_ReturnsSuccessAndUpdateReturnDateForExpiredLoan() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateExpiredLoanForPatron(patron); + var loanId = loan.Id; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanReturnStatus returnStatus = await _loanService.ReturnLoan(loanId); + + // Assert + Assert.Equal(LoanReturnStatus.Success, returnStatus); + Assert.NotNull(loan.ReturnDate); + } + + [Fact(DisplayName = "LoanService.ReturnLoan: Returns Success and updates return date for a patron with expired membership")] + public async Task ReturnLoan_ReturnsSuccessAndUpdateReturnDateForExpiredPatron() + { + // Arrange + var patron = PatronFactory.CreateExpiredPatron(); + var loan = LoanFactory.CreateCurrentLoanForPatron(patron); + var loanId = loan.Id; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanReturnStatus returnStatus = await _loanService.ReturnLoan(loanId); + + // Assert + Assert.Equal(LoanReturnStatus.Success, returnStatus); + Assert.NotNull(loan.ReturnDate); + } +} diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/PatronService/RenewMembership.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/PatronService/RenewMembership.cs new file mode 100644 index 0000000..ff4d24c --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/PatronService/RenewMembership.cs @@ -0,0 +1,142 @@ +using NSubstitute; +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; + +namespace Library.UnitTests.ApplicationCore.PatronServiceTests; + +public class RenewMembershipTest +{ + private readonly IPatronRepository _mockPatronRepository; + private readonly PatronService _patronService; + + public RenewMembershipTest() + { + _mockPatronRepository = Substitute.For(); + _patronService = new PatronService(_mockPatronRepository); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Renews the membership successfully without loans")] + public async Task RenewMembership_RenewsMembershipSuccessfully() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var membershipEnd = patron.MembershipEnd; + var patronId = patron.Id; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.Success, renewalStatus); + Assert.Equal(membershipEnd.AddYears(1), patron.MembershipEnd); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Renews the membership successfully with expired membership")] + public async Task RenewMembership_RenewsMembershipSuccessfullyWithExpiredMembership() + { + // Arrange + //var membershipEnd = DateTime.Now.AddMonths(-2); + var patron = PatronFactory.CreateExpiredPatron(); + var membershipEnd = patron.MembershipEnd; + var patronId = patron.Id; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.Success, renewalStatus); + Assert.Equal(membershipEnd.AddYears(1), patron.MembershipEnd); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Renews the membership successfully with returned loans")] + public async Task RenewMembership_RenewsMembershipSuccessfullyWithReturnedLoans() + { + // Arrange + var membershipEnd = DateTime.Now.AddDays(1); + var patron = PatronFactory.CreateCurrentPatron(); + patron.MembershipEnd = membershipEnd; + var patronId = patron.Id; + patron.Loans = new List { + LoanFactory.CreateReturnedLoanForPatron(patron) + }; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.Success, renewalStatus); + Assert.Equal(membershipEnd.AddYears(1), patron.MembershipEnd); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Renews the membership successfully with current loans")] + public async Task RenewMembership_RenewsMembershipSuccessfullyWithCurrentLoans() + { + // Arrange + var membershipEnd = DateTime.Now.AddDays(1); + var patron = PatronFactory.CreateCurrentPatron(); + patron.MembershipEnd = membershipEnd; + var patronId = patron.Id; + patron.Loans = new List { + LoanFactory.CreateCurrentLoanForPatron(patron) + }; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.Success, renewalStatus); + Assert.Equal(membershipEnd.AddYears(1), patron.MembershipEnd); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Returns PatronNotFound if patron is not found")] + public async Task RenewMembership_ReturnsPatronNotFound() + { + // Arrange + var patronId = 42; + _mockPatronRepository.GetPatron(patronId).Returns((Patron?)null); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.PatronNotFound, renewalStatus); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Returns TooEarlyToRenew if renewal is not allowed yet")] + public async Task RenewMembership_ReturnsTooEarlyToRenew() + { + // Arrange + var patron = PatronFactory.CreateTooEarlyToRenewPatron(); + var patronId = patron.Id; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.TooEarlyToRenew, renewalStatus); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Returns LoanNotReturned if patron has overdue loans")] + public async Task RenewMembership_ReturnsLoanNotReturned() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var patronId = patron.Id; + patron.Loans = new List { + LoanFactory.CreateExpiredLoanForPatron(patron) + }; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.LoanNotReturned, renewalStatus); + } +} diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/tests/UnitTests/LoanFactory.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/tests/UnitTests/LoanFactory.cs new file mode 100644 index 0000000..251000d --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/tests/UnitTests/LoanFactory.cs @@ -0,0 +1,42 @@ +using Library.ApplicationCore.Entities; + +public static class LoanFactory +{ + public static int loanId = 777; + + public static Loan CreateReturnedLoanForPatron(Patron patron) + { + return new Loan + { + Id = loanId++, + DueDate = DateTime.Now.AddDays(1), + ReturnDate = DateTime.Now.AddDays(-1), + PatronId = patron.Id, + Patron = patron + }; + } + + public static Loan CreateCurrentLoanForPatron(Patron patron) + { + return new Loan + { + Id = loanId++, + DueDate = DateTime.Now.AddDays(1), + ReturnDate = null, + PatronId = patron.Id, + Patron = patron + }; + } + + public static Loan CreateExpiredLoanForPatron(Patron patron) + { + return new Loan + { + Id = loanId++, + DueDate = DateTime.Now.AddDays(-1), + ReturnDate = null, + PatronId = patron.Id, + Patron = patron + }; + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/tests/UnitTests/PatronFactory.cs b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/tests/UnitTests/PatronFactory.cs new file mode 100644 index 0000000..a9c36b7 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/tests/UnitTests/PatronFactory.cs @@ -0,0 +1,39 @@ +using Library.ApplicationCore.Entities; + +public static class PatronFactory +{ + public static int patronId = 42; + + public static Patron CreateCurrentPatron() + { + return new Patron + { + Id = patronId++, + Name = "John Doe", + MembershipEnd = DateTime.Now.AddDays(1), + Loans = new List() + }; + } + + public static Patron CreateTooEarlyToRenewPatron() + { + return new Patron + { + Id = patronId++, + Name = "John Doe", + MembershipEnd = DateTime.Now.AddMonths(2), + Loans = new List() + }; + } + + public static Patron CreateExpiredPatron() + { + return new Patron + { + Id = patronId++, + Name = "John Doe", + MembershipEnd = DateTime.Now.AddMonths(-2), + Loans = new List() + }; + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/tests/UnitTests/UnitTests.csproj b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/tests/UnitTests/UnitTests.csproj new file mode 100644 index 0000000..a156d8f --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/AccelerateDevGHCopilot/tests/UnitTests/UnitTests.csproj @@ -0,0 +1,28 @@ + + + + net9.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + diff --git a/DownloadableCodeProjects/az-2007-m2-explain-document/readme.txt b/DownloadableCodeProjects/az-2007-m2-explain-document/readme.txt new file mode 100644 index 0000000..b3a4252 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m2-explain-document/readme.txt @@ -0,0 +1 @@ +placeholder \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/.gitignore b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/.gitignore new file mode 100644 index 0000000..dc73868 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/.gitignore @@ -0,0 +1,68 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*.pyo +*.pyd + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +.env/ +.venv/ +env/ +venv/ +ENV/ +ENV*/ + +# PyInstaller +*.manifest +*.spec + +# Unit test / coverage / pytest +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.pytest_cache/ +test-results/ +junit-*.xml + +# Jupyter Notebook +.ipynb_checkpoints/ + +# mypy +.mypy_cache/ +.dmypy.json + +# Pyre type checker +.pyre/ + +# Pyright type checker +.pyrightcache/ + +# VS Code settings +.vscode/ \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/entities/author.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/entities/author.py new file mode 100644 index 0000000..dec0e83 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/entities/author.py @@ -0,0 +1,6 @@ +from dataclasses import dataclass + +@dataclass +class Author: + id: int + name: str diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/entities/book.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/entities/book.py new file mode 100644 index 0000000..50f4e38 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/entities/book.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass +from typing import Optional +from .author import Author + +@dataclass +class Book: + id: int + title: str + author_id: int + genre: str + image_name: str + isbn: str + author: Optional[Author] = None diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/entities/book_item.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/entities/book_item.py new file mode 100644 index 0000000..f5f7fb7 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/entities/book_item.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass +from typing import Optional +from datetime import datetime +from .book import Book + +@dataclass +class BookItem: + id: int + book_id: int + acquisition_date: datetime + condition: Optional[str] = None + book: Optional[Book] = None diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/entities/loan.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/entities/loan.py new file mode 100644 index 0000000..51955ea --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/entities/loan.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass +from typing import Optional +from datetime import datetime +from .patron import Patron +from .book_item import BookItem + +@dataclass +class Loan: + id: int + book_item_id: int + patron_id: int + patron: Optional[Patron] = None + loan_date: datetime = None + due_date: datetime = None + return_date: Optional[datetime] = None + book_item: Optional[BookItem] = None diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/entities/patron.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/entities/patron.py new file mode 100644 index 0000000..98e5096 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/entities/patron.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass, field +from typing import List, Optional +from datetime import datetime +# from .loan import Loan # Use string annotation to avoid circular import + +@dataclass +class Patron: + id: int + name: str + membership_end: datetime + membership_start: datetime + image_name: Optional[str] = None + loans: List['Loan'] = field(default_factory=list) diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/enums/loan_extension_status.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/enums/loan_extension_status.py new file mode 100644 index 0000000..20cf2c5 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/enums/loan_extension_status.py @@ -0,0 +1,9 @@ +from enum import Enum + +class LoanExtensionStatus(Enum): + SUCCESS = 'Book loan extension was successful.' + LOAN_NOT_FOUND = 'Loan not found.' + LOAN_EXPIRED = 'Cannot extend book loan as it already has expired. Return the book instead.' + MEMBERSHIP_EXPIRED = "Cannot extend book loan due to expired patron's membership." + LOAN_RETURNED = 'Cannot extend book loan as the book is already returned.' + ERROR = 'Cannot extend book loan due to an error.' diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/enums/loan_return_status.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/enums/loan_return_status.py new file mode 100644 index 0000000..5f9221a --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/enums/loan_return_status.py @@ -0,0 +1,7 @@ +from enum import Enum + +class LoanReturnStatus(Enum): + SUCCESS = 'Book was successfully returned.' + LOAN_NOT_FOUND = 'Loan not found.' + ALREADY_RETURNED = 'Cannot return book as the book is already returned.' + ERROR = 'Cannot return book due to an error.' diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/enums/membership_renewal_status.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/enums/membership_renewal_status.py new file mode 100644 index 0000000..e36433e --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/enums/membership_renewal_status.py @@ -0,0 +1,8 @@ +from enum import Enum + +class MembershipRenewalStatus(Enum): + SUCCESS = 'Membership renewal was successful.' + PATRON_NOT_FOUND = 'Patron not found.' + TOO_EARLY_TO_RENEW = 'It is too early to renew the membership.' + LOAN_NOT_RETURNED = 'Cannot renew membership due to an outstanding loan.' + ERROR = 'Cannot renew membership due to an error.' diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_repository.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_repository.py new file mode 100644 index 0000000..273d78f --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_repository.py @@ -0,0 +1,20 @@ +from abc import ABC, abstractmethod +from typing import Optional +from ..entities.loan import Loan + +class ILoanRepository(ABC): + @abstractmethod + def get_loan(self, loan_id: int) -> Optional[Loan]: + pass + + @abstractmethod + def update_loan(self, loan: Loan) -> None: + pass + + @abstractmethod + def add_loan(self, loan: Loan) -> None: + pass + + @abstractmethod + def get_loans_by_patron_id(self, patron_id: int): + pass diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_service.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_service.py new file mode 100644 index 0000000..866b407 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_service.py @@ -0,0 +1,16 @@ +from abc import ABC, abstractmethod +from ..enums.loan_return_status import LoanReturnStatus +from ..enums.loan_extension_status import LoanExtensionStatus + +class ILoanService(ABC): + @abstractmethod + def return_loan(self, loan_id: int) -> LoanReturnStatus: + pass + + @abstractmethod + def extend_loan(self, loan_id: int) -> LoanExtensionStatus: + pass + + @abstractmethod + def checkout_book(self, patron, book_item, loan_id=None) -> None: + pass diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_repository.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_repository.py new file mode 100644 index 0000000..c116101 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_repository.py @@ -0,0 +1,24 @@ +from abc import ABC, abstractmethod +from typing import List, Optional +from ..entities.patron import Patron + +class IPatronRepository(ABC): + @abstractmethod + def get_patron(self, patron_id: int) -> Optional[Patron]: + pass + + @abstractmethod + def search_patrons(self, search_input: str) -> List[Patron]: + pass + + @abstractmethod + def update_patron(self, patron: Patron) -> None: + pass + + @abstractmethod + def get_all_books(self): + pass + + @abstractmethod + def get_all_book_items(self): + pass diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_service.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_service.py new file mode 100644 index 0000000..e20199f --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_service.py @@ -0,0 +1,7 @@ +from abc import ABC, abstractmethod +from ..enums.membership_renewal_status import MembershipRenewalStatus + +class IPatronService(ABC): + @abstractmethod + def renew_membership(self, patron_id: int) -> MembershipRenewalStatus: + pass diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/services/loan_service.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/services/loan_service.py new file mode 100644 index 0000000..bf554e6 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/services/loan_service.py @@ -0,0 +1,67 @@ +from ..interfaces.iloan_service import ILoanService +from ..interfaces.iloan_repository import ILoanRepository +from ..enums.loan_return_status import LoanReturnStatus +from ..enums.loan_extension_status import LoanExtensionStatus +from datetime import datetime, timedelta + +class LoanService(ILoanService): + EXTEND_BY_DAYS = 14 + + def __init__(self, loan_repository: ILoanRepository): + self._loan_repository = loan_repository + + def return_loan(self, loan_id: int) -> LoanReturnStatus: + loan = self._loan_repository.get_loan(loan_id) + if loan is None: + return LoanReturnStatus.LOAN_NOT_FOUND + if loan.return_date is not None: + return LoanReturnStatus.ALREADY_RETURNED + loan.return_date = datetime.now() + try: + self._loan_repository.update_loan(loan) + return LoanReturnStatus.SUCCESS + except Exception: + return LoanReturnStatus.ERROR + + def extend_loan(self, loan_id: int) -> LoanExtensionStatus: + loan = self._loan_repository.get_loan(loan_id) + if loan is None: + return LoanExtensionStatus.LOAN_NOT_FOUND + if loan.patron and loan.patron.membership_end < datetime.now(): + return LoanExtensionStatus.MEMBERSHIP_EXPIRED + if loan.return_date is not None: + return LoanExtensionStatus.LOAN_RETURNED + if loan.due_date < datetime.now(): + return LoanExtensionStatus.LOAN_EXPIRED + try: + loan.due_date = loan.due_date + timedelta(days=self.EXTEND_BY_DAYS) + self._loan_repository.update_loan(loan) + return LoanExtensionStatus.SUCCESS + except Exception: + return LoanExtensionStatus.ERROR + + def checkout_book(self, patron, book_item, loan_id=None) -> None: + from ..entities.loan import Loan + from datetime import datetime, timedelta + # Generate a new loan ID if not provided + if loan_id is None: + all_loans = getattr(self._loan_repository, 'get_all_loans', lambda: [])() + max_id = 0 + for l in all_loans: + if l.id > max_id: + max_id = l.id + loan_id = max_id + 1 if all_loans else 1 + now = datetime.now() + due = now + timedelta(days=14) + new_loan = Loan( + id=loan_id, + book_item_id=book_item.id, + patron_id=patron.id, + patron=patron, + loan_date=now, + due_date=due, + return_date=None, + book_item=book_item + ) + self._loan_repository.add_loan(new_loan) + return new_loan diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/services/patron_service.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/services/patron_service.py new file mode 100644 index 0000000..7d43a98 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/application_core/services/patron_service.py @@ -0,0 +1,30 @@ +from ..interfaces.ipatron_service import IPatronService +from ..interfaces.ipatron_repository import IPatronRepository +from ..entities.patron import Patron +from ..enums.membership_renewal_status import MembershipRenewalStatus +from datetime import datetime, timedelta + +class PatronService(IPatronService): + EXTEND_BY_DAYS = 365 + + def __init__(self, patron_repository: IPatronRepository): + self._patron_repository = patron_repository + + def renew_membership(self, patron_id: int) -> MembershipRenewalStatus: + patron = self._patron_repository.get_patron(patron_id) + if patron is None: + return MembershipRenewalStatus.PATRON_NOT_FOUND + if patron.membership_end < datetime.now(): + patron.membership_end = datetime.now() + timedelta(days=self.EXTEND_BY_DAYS) + else: + patron.membership_end = patron.membership_end + timedelta(days=self.EXTEND_BY_DAYS) + self._patron_repository.update_patron(patron) + return MembershipRenewalStatus.SUCCESS + + def find_patron_by_name(self, name: str): + results = [] + all_patrons = self._patron_repository.get_all_patrons() + for patron in all_patrons: + if patron.name.lower() == name.lower(): + results.append(patron) + return results diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/console/book_repository.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/console/book_repository.py new file mode 100644 index 0000000..2544e3d --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/console/book_repository.py @@ -0,0 +1,30 @@ +class BookRepository: + def __init__(self, json_data): + self.books = json_data.get('books', []) + self.authors = json_data.get('authors', []) + self.book_items = json_data.get('book_items', []) + + def get_book_by_title(self, title): + for book in self.books: + if book.title.lower() == title.lower(): + return book + return None + +class BookItemRepository: + def __init__(self, json_data): + self.books = json_data.get('books', []) + self.authors = json_data.get('authors', []) + self.book_items = json_data.get('book_items', []) + + def get_book_by_title(self, title): + for book in self.books: + if book.title.lower() == title.lower(): + return book + return None + + def get_items_by_book_id(self, book_id): + items = [] + for item in self.book_items: + if item.book_id == book_id: + items.append(item) + return items \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/console/common_actions.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/console/common_actions.py new file mode 100644 index 0000000..011eda9 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/console/common_actions.py @@ -0,0 +1,10 @@ +from enum import Flag, auto + +class CommonActions(Flag): + REPEAT = 0 + SELECT = auto() + QUIT = auto() + SEARCH_PATRONS = auto() + RENEW_PATRON_MEMBERSHIP = auto() + RETURN_LOANED_BOOK = auto() + EXTEND_LOANED_BOOK = auto() diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/console/console_app.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/console/console_app.py new file mode 100644 index 0000000..02ece3a --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/console/console_app.py @@ -0,0 +1,218 @@ +from .console_state import ConsoleState +from .common_actions import CommonActions +from application_core.interfaces.ipatron_repository import IPatronRepository +from application_core.interfaces.iloan_repository import ILoanRepository +from application_core.interfaces.iloan_service import ILoanService +from application_core.interfaces.ipatron_service import IPatronService + +class ConsoleApp: + def __init__( + self, + loan_service: ILoanService, + patron_service: IPatronService, + patron_repository: IPatronRepository, + loan_repository: ILoanRepository + ): + self._current_state: ConsoleState = ConsoleState.PATRON_SEARCH + self.matching_patrons = [] + self.selected_patron_details = None + self.selected_loan_details = None + self._patron_repository = patron_repository + self._loan_repository = loan_repository + self._loan_service = loan_service + self._patron_service = patron_service + + def write_input_options(self, options): + print("Input Options:") + if options & CommonActions.RETURN_LOANED_BOOK: + print(' - "r" to mark as returned') + if options & CommonActions.EXTEND_LOANED_BOOK: + print(' - "e" to extend the book loan') + if options & CommonActions.RENEW_PATRON_MEMBERSHIP: + print(' - "m" to extend patron\'s membership') + if options & CommonActions.SEARCH_PATRONS: + print(' - "s" for new search') + if options & CommonActions.QUIT: + print(' - "q" to quit') + if options & CommonActions.SELECT: + print(' - type a number to select a list item.') + + def run(self) -> None: + while True: + if self._current_state == ConsoleState.PATRON_SEARCH: + self._current_state = self.patron_search() + elif self._current_state == ConsoleState.PATRON_SEARCH_RESULTS: + self._current_state = self.patron_search_results() + elif self._current_state == ConsoleState.PATRON_DETAILS: + self._current_state = self.patron_details() + elif self._current_state == ConsoleState.LOAN_DETAILS: + self._current_state = self.loan_details() + elif self._current_state == ConsoleState.QUIT: + break + + def patron_search(self) -> ConsoleState: + search_input = input("Enter a string to search for patrons by name: ").strip() + if not search_input: + print("No input provided. Please try again.") + return ConsoleState.PATRON_SEARCH + self.matching_patrons = self._patron_repository.search_patrons(search_input) + if not self.matching_patrons: + print("No matching patrons found.") + return ConsoleState.PATRON_SEARCH + return ConsoleState.PATRON_SEARCH_RESULTS + + def patron_search_results(self) -> ConsoleState: + print("\nMatching Patrons:") + idx = 1 + for patron in self.matching_patrons: + print(f"{idx}) {patron.name}") + idx += 1 + if self.matching_patrons: + self.write_input_options( + CommonActions.SELECT | CommonActions.SEARCH_PATRONS | CommonActions.QUIT + ) + else: + self.write_input_options( + CommonActions.SEARCH_PATRONS | CommonActions.QUIT + ) + selection = input("Enter your choice: ").strip().lower() + if selection == 'q': + return ConsoleState.QUIT + elif selection == 's': + return ConsoleState.PATRON_SEARCH + elif selection.isdigit(): + idx = int(selection) + if 1 <= idx <= len(self.matching_patrons): + self.selected_patron_details = self.matching_patrons[idx - 1] + return ConsoleState.PATRON_DETAILS + else: + print("Invalid selection. Please enter a valid number.") + return ConsoleState.PATRON_SEARCH_RESULTS + else: + print("Invalid input. Please enter a number, 's', or 'q'.") + return ConsoleState.PATRON_SEARCH_RESULTS + + def patron_details(self) -> ConsoleState: + patron = self.selected_patron_details + print(f"\nName: {patron.name}") + print(f"Membership Expiration: {patron.membership_end}") + loans = self._loan_repository.get_loans_by_patron_id(patron.id) + print("\nBook Loans History:") + + valid_loans = self._print_loans(loans) + + if valid_loans: + options = ( + CommonActions.RENEW_PATRON_MEMBERSHIP + | CommonActions.SEARCH_PATRONS + | CommonActions.QUIT + | CommonActions.SELECT + ) + selection = self._get_patron_details_input(options) + return self._handle_patron_details_selection(selection, patron, valid_loans) + else: + print("No valid loans for this patron.") + options = ( + CommonActions.SEARCH_PATRONS + | CommonActions.QUIT + ) + selection = self._get_patron_details_input(options) + return self._handle_no_loans_selection(selection) + + def _print_loans(self, loans): + valid_loans = [] + idx = 1 + for loan in loans: + if not getattr(loan, 'book_item', None) or not getattr(loan.book_item, 'book', None): + print(f"{idx}) [Invalid loan data: missing book information]") + else: + returned = "True" if getattr(loan, 'return_date', None) else "False" + print(f"{idx}) {loan.book_item.book.title} - Due: {loan.due_date} - Returned: {returned}") + valid_loans.append((idx, loan)) + idx += 1 + return valid_loans + + def _get_patron_details_input(self, options): + self.write_input_options(options) + return input("Enter your choice: ").strip().lower() + + def _handle_patron_details_selection(self, selection, patron, valid_loans): + if selection == 'q': + return ConsoleState.QUIT + elif selection == 's': + return ConsoleState.PATRON_SEARCH + elif selection == 'm': + status = self._patron_service.renew_membership(patron.id) + print(status) + self.selected_patron_details = self._patron_repository.get_patron(patron.id) + return ConsoleState.PATRON_DETAILS + elif selection.isdigit(): + idx = int(selection) + if 1 <= idx <= len(valid_loans): + self.selected_loan_details = valid_loans[idx - 1][1] + return ConsoleState.LOAN_DETAILS + print("Invalid selection. Please enter a number shown in the list above.") + return ConsoleState.PATRON_DETAILS + else: + print("Invalid input. Please enter a number, 'm', 's', or 'q'.") + return ConsoleState.PATRON_DETAILS + + def _handle_no_loans_selection(self, selection): + if selection == 'q': + return ConsoleState.QUIT + elif selection == 's': + return ConsoleState.PATRON_SEARCH + else: + print("Invalid input.") + return ConsoleState.PATRON_DETAILS + + def loan_details(self) -> ConsoleState: + loan = self.selected_loan_details + print(f"\nBook title: {loan.book_item.book.title}") + print(f"Book Author: {loan.book_item.book.author.name}") + print(f"Due date: {loan.due_date}") + returned = "True" if getattr(loan, 'return_date', None) else "False" + print(f"Returned: {returned}\n") + options = CommonActions.SEARCH_PATRONS | CommonActions.QUIT + if not getattr(loan, 'return_date', None): + options |= CommonActions.RETURN_LOANED_BOOK | CommonActions.EXTEND_LOANED_BOOK + self.write_input_options(options) + selection = input("Enter your choice: ").strip().lower() + if selection == 'q': + return ConsoleState.QUIT + elif selection == 's': + return ConsoleState.PATRON_SEARCH + elif selection == 'r' and not getattr(loan, 'return_date', None): + status = self._loan_service.return_loan(loan.id) + print("Book was successfully returned.") + print(status) + self.selected_loan_details = self._loan_repository.get_loan(loan.id) + return ConsoleState.LOAN_DETAILS + elif selection == 'e' and not getattr(loan, 'return_date', None): + status = self._loan_service.extend_loan(loan.id) + print(status) + self.selected_loan_details = self._loan_repository.get_loan(loan.id) + return ConsoleState.LOAN_DETAILS + else: + print("Invalid input.") + return ConsoleState.LOAN_DETAILS + +from application_core.services.loan_service import LoanService +from application_core.services.patron_service import PatronService +from infrastructure.json_data import JsonData +from infrastructure.json_loan_repository import JsonLoanRepository +from infrastructure.json_patron_repository import JsonPatronRepository +from console.console_app import ConsoleApp + +def main(): + json_data = JsonData() + patron_repo = JsonPatronRepository(json_data) + loan_repo = JsonLoanRepository(json_data) + loan_service = LoanService(loan_repo) + patron_service = PatronService(patron_repo) + + app = ConsoleApp( + loan_service=loan_service, + patron_service=patron_service + ) + app.run() diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/console/console_state.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/console/console_state.py new file mode 100644 index 0000000..714335a --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/console/console_state.py @@ -0,0 +1,8 @@ +from enum import Enum + +class ConsoleState(Enum): + PATRON_SEARCH = 1 + PATRON_SEARCH_RESULTS = 2 + PATRON_DETAILS = 3 + LOAN_DETAILS = 4 + QUIT = 5 diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/console/main.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/console/main.py new file mode 100644 index 0000000..36de366 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/console/main.py @@ -0,0 +1,32 @@ +import sys +from pathlib import Path + +# Add the parent directory to sys.path +sys.path.append(str(Path(__file__).resolve().parent.parent)) + +from application_core.services.loan_service import LoanService +from application_core.services.patron_service import PatronService +from infrastructure.json_data import JsonData +from infrastructure.json_loan_repository import JsonLoanRepository +from infrastructure.json_patron_repository import JsonPatronRepository +from console.console_app import ConsoleApp + + +def main(): + json_data = JsonData() + patron_repo = JsonPatronRepository(json_data) + loan_repo = JsonLoanRepository(json_data) + loan_service = LoanService(loan_repo) + patron_service = PatronService(patron_repo) + + app = ConsoleApp( + loan_service=loan_service, + patron_service=patron_service, + patron_repository=patron_repo, + loan_repository=loan_repo + ) + app.run() + + +if __name__ == "__main__": + main() diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Authors.json b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Authors.json new file mode 100644 index 0000000..2f61038 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Authors.json @@ -0,0 +1,22 @@ +[ + {"Id": 1, "Name": "Author One"}, + {"Id": 2, "Name": "Author Two"}, + {"Id": 3, "Name": "Author Three"}, + {"Id": 4, "Name": "Author Four"}, + {"Id": 5, "Name": "Author Five"}, + {"Id": 6, "Name": "Author Six"}, + {"Id": 7, "Name": "Author Seven"}, + {"Id": 8, "Name": "Author Eight"}, + {"Id": 9, "Name": "Author Nine"}, + {"Id": 10, "Name": "Author Ten"}, + {"Id": 11, "Name": "Author Eleven"}, + {"Id": 12, "Name": "Author Twelve"}, + {"Id": 13, "Name": "Author Thirteen"}, + {"Id": 14, "Name": "Author Fourteen"}, + {"Id": 15, "Name": "Author Fifteen"}, + {"Id": 16, "Name": "Author Sixteen"}, + {"Id": 17, "Name": "Author Seventeen"}, + {"Id": 18, "Name": "Author Eighteen"}, + {"Id": 19, "Name": "Author Nineteen"}, + {"Id": 20, "Name": "Author Twenty"} +] diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/BookItems.json b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/BookItems.json new file mode 100644 index 0000000..f5e1d1b --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/BookItems.json @@ -0,0 +1,22 @@ +[ + {"Id": 1, "BookId": 1, "AcquisitionDate": "2023-09-20T00:40:43.1716563", "Condition": "Good"}, + {"Id": 2, "BookId": 2, "AcquisitionDate": "2023-09-20T00:40:43.1717503", "Condition": "Fair"}, + {"Id": 3, "BookId": 3, "AcquisitionDate": "2023-09-20T00:40:43.1717511", "Condition": "Excellent"}, + {"Id": 4, "BookId": 4, "AcquisitionDate": "2023-09-20T00:40:43.1717513", "Condition": "Poor"}, + {"Id": 5, "BookId": 5, "AcquisitionDate": "2023-09-20T00:40:43.1717516", "Condition": "Good"}, + {"Id": 6, "BookId": 6, "AcquisitionDate": "2023-09-20T00:40:43.1717521", "Condition": "Fair"}, + {"Id": 7, "BookId": 7, "AcquisitionDate": "2023-09-20T00:40:43.1717523", "Condition": "Excellent"}, + {"Id": 8, "BookId": 8, "AcquisitionDate": "2023-09-20T00:40:43.1717526", "Condition": "Poor"}, + {"Id": 9, "BookId": 9, "AcquisitionDate": "2023-09-20T00:40:43.171757", "Condition": "Good"}, + {"Id": 10, "BookId": 10, "AcquisitionDate": "2023-09-20T00:40:43.1717574", "Condition": "Fair"}, + {"Id": 11, "BookId": 11, "AcquisitionDate": "2023-09-20T00:40:43.1717576", "Condition": "Excellent"}, + {"Id": 12, "BookId": 12, "AcquisitionDate": "2023-09-20T00:40:43.1717578", "Condition": "Poor"}, + {"Id": 13, "BookId": 13, "AcquisitionDate": "2023-09-20T00:40:43.171758", "Condition": "Good"}, + {"Id": 14, "BookId": 14, "AcquisitionDate": "2023-09-20T00:40:43.1717609", "Condition": "Fair"}, + {"Id": 15, "BookId": 15, "AcquisitionDate": "2023-09-20T00:40:43.1717611", "Condition": "Excellent"}, + {"Id": 16, "BookId": 16, "AcquisitionDate": "2023-09-20T00:40:43.1717613", "Condition": "Poor"}, + {"Id": 17, "BookId": 17, "AcquisitionDate": "2023-09-20T00:40:43.1717616", "Condition": "Good"}, + {"Id": 18, "BookId": 18, "AcquisitionDate": "2023-09-20T00:40:43.1717619", "Condition": "Fair"}, + {"Id": 19, "BookId": 19, "AcquisitionDate": "2023-09-20T00:40:43.1717621", "Condition": "Excellent"}, + {"Id": 20, "BookId": 20, "AcquisitionDate": "2023-09-20T00:40:43.1717626", "Condition": "Poor"} +] diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Books.json b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Books.json new file mode 100644 index 0000000..ac80673 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Books.json @@ -0,0 +1,22 @@ +[ + {"Id": 1, "Title": "Book One", "AuthorId": 1, "Genre": "Dystopian", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524935"}, + {"Id": 2, "Title": "Book Two", "AuthorId": 2, "Genre": "Classic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524936"}, + {"Id": 3, "Title": "Book Three", "AuthorId": 3, "Genre": "Romance", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524937"}, + {"Id": 4, "Title": "Book Four", "AuthorId": 4, "Genre": "Classic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524938"}, + {"Id": 5, "Title": "Book Five", "AuthorId": 5, "Genre": "Coming-of-age", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524939"}, + {"Id": 6, "Title": "Book Six", "AuthorId": 6, "Genre": "Modernist", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524940"}, + {"Id": 7, "Title": "Book Seven", "AuthorId": 7, "Genre": "Adventure", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524941"}, + {"Id": 8, "Title": "Book Eight", "AuthorId": 8, "Genre": "Fantasy", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524942"}, + {"Id": 9, "Title": "Book Nine", "AuthorId": 9, "Genre": "Fantasy", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524943"}, + {"Id": 10, "Title": "Book Ten", "AuthorId": 10, "Genre": "Epic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524944"}, + {"Id": 11, "Title": "Book Eleven", "AuthorId": 11, "Genre": "Psychological", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524945"}, + {"Id": 12, "Title": "Book Twelve", "AuthorId": 12, "Genre": "Psychological", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524946"}, + {"Id": 13, "Title": "Book Thirteen", "AuthorId": 13, "Genre": "Magical realism", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524947"}, + {"Id": 14, "Title": "Book Fourteen", "AuthorId": 14, "Genre": "Classic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524948"}, + {"Id": 15, "Title": "Book Fifteen", "AuthorId": 15, "Genre": "Adventure", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524949"}, + {"Id": 16, "Title": "Book Sixteen", "AuthorId": 16, "Genre": "Historical", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524950"}, + {"Id": 17, "Title": "Book Seventeen", "AuthorId": 17, "Genre": "Gothic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524951"}, + {"Id": 18, "Title": "Book Eighteen", "AuthorId": 18, "Genre": "Dystopian", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524952"}, + {"Id": 19, "Title": "Book Nineteen", "AuthorId": 19, "Genre": "Classic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524953"}, + {"Id": 20, "Title": "Book Twenty", "AuthorId": 20, "Genre": "Adventure", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524954"} +] diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Loans.json b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Loans.json new file mode 100644 index 0000000..b0ebd1d --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Loans.json @@ -0,0 +1,482 @@ +[ + { + "Id": 1, + "BookItemId": 1, + "PatronId": 1, + "LoanDate": "2025-06-10T10:00:00", + "DueDate": "2025-06-24T10:00:00", + "ReturnDate": null + }, + { + "Id": 2, + "BookItemId": 1, + "PatronId": 10, + "LoanDate": "2024-05-01T10:00:00", + "DueDate": "2024-05-15T10:00:00", + "ReturnDate": "2024-05-10T10:00:00" + }, + { + "Id": 3, + "BookItemId": 2, + "PatronId": 2, + "LoanDate": "2025-06-11T10:00:00", + "DueDate": "2025-06-25T10:00:00", + "ReturnDate": null + }, + { + "Id": 4, + "BookItemId": 2, + "PatronId": 11, + "LoanDate": "2024-06-01T10:00:00", + "DueDate": "2024-06-15T10:00:00", + "ReturnDate": "2024-06-10T10:00:00" + }, + { + "Id": 5, + "BookItemId": 3, + "PatronId": 3, + "LoanDate": "2025-06-12T10:00:00", + "DueDate": "2025-06-26T10:00:00", + "ReturnDate": null + }, + { + "Id": 6, + "BookItemId": 3, + "PatronId": 12, + "LoanDate": "2024-07-01T10:00:00", + "DueDate": "2024-07-15T10:00:00", + "ReturnDate": "2024-07-10T10:00:00" + }, + { + "Id": 7, + "BookItemId": 4, + "PatronId": 4, + "LoanDate": "2025-06-13T10:00:00", + "DueDate": "2025-06-27T10:00:00", + "ReturnDate": null + }, + { + "Id": 8, + "BookItemId": 4, + "PatronId": 13, + "LoanDate": "2024-08-01T10:00:00", + "DueDate": "2024-08-15T10:00:00", + "ReturnDate": "2024-08-10T10:00:00" + }, + { + "Id": 9, + "BookItemId": 5, + "PatronId": 5, + "LoanDate": "2025-06-14T10:00:00", + "DueDate": "2025-06-28T10:00:00", + "ReturnDate": null + }, + { + "Id": 10, + "BookItemId": 5, + "PatronId": 14, + "LoanDate": "2024-09-01T10:00:00", + "DueDate": "2024-09-15T10:00:00", + "ReturnDate": "2024-09-10T10:00:00" + }, + { + "Id": 11, + "BookItemId": 6, + "PatronId": 6, + "LoanDate": "2025-06-15T10:00:00", + "DueDate": "2025-06-29T10:00:00", + "ReturnDate": null + }, + { + "Id": 12, + "BookItemId": 6, + "PatronId": 15, + "LoanDate": "2024-10-01T10:00:00", + "DueDate": "2024-10-15T10:00:00", + "ReturnDate": "2024-10-10T10:00:00" + }, + { + "Id": 13, + "BookItemId": 7, + "PatronId": 7, + "LoanDate": "2025-06-16T10:00:00", + "DueDate": "2025-06-30T10:00:00", + "ReturnDate": null + }, + { + "Id": 14, + "BookItemId": 7, + "PatronId": 16, + "LoanDate": "2024-11-01T10:00:00", + "DueDate": "2024-11-15T10:00:00", + "ReturnDate": "2024-11-10T10:00:00" + }, + { + "Id": 15, + "BookItemId": 8, + "PatronId": 8, + "LoanDate": "2025-06-17T10:00:00", + "DueDate": "2025-07-01T10:00:00", + "ReturnDate": null + }, + { + "Id": 16, + "BookItemId": 8, + "PatronId": 17, + "LoanDate": "2024-12-01T10:00:00", + "DueDate": "2024-12-15T10:00:00", + "ReturnDate": "2024-12-10T10:00:00" + }, + { + "Id": 17, + "BookItemId": 9, + "PatronId": 9, + "LoanDate": "2025-06-18T10:00:00", + "DueDate": "2025-07-02T10:00:00", + "ReturnDate": null + }, + { + "Id": 18, + "BookItemId": 9, + "PatronId": 18, + "LoanDate": "2025-01-01T10:00:00", + "DueDate": "2025-01-15T10:00:00", + "ReturnDate": "2025-01-10T10:00:00" + }, + { + "Id": 19, + "BookItemId": 10, + "PatronId": 10, + "LoanDate": "2025-06-19T10:00:00", + "DueDate": "2025-07-03T10:00:00", + "ReturnDate": null + }, + { + "Id": 20, + "BookItemId": 10, + "PatronId": 19, + "LoanDate": "2025-02-01T10:00:00", + "DueDate": "2025-02-15T10:00:00", + "ReturnDate": "2025-02-10T10:00:00" + }, + { + "Id": 21, + "BookItemId": 11, + "PatronId": 11, + "LoanDate": "2025-06-20T10:00:00", + "DueDate": "2025-07-04T10:00:00", + "ReturnDate": null + }, + { + "Id": 22, + "BookItemId": 11, + "PatronId": 20, + "LoanDate": "2025-03-01T10:00:00", + "DueDate": "2025-03-15T10:00:00", + "ReturnDate": "2025-03-10T10:00:00" + }, + { + "Id": 23, + "BookItemId": 12, + "PatronId": 12, + "LoanDate": "2025-06-21T10:00:00", + "DueDate": "2025-07-05T10:00:00", + "ReturnDate": null + }, + { + "Id": 24, + "BookItemId": 12, + "PatronId": 1, + "LoanDate": "2023-01-01T10:00:00", + "DueDate": "2023-01-15T10:00:00", + "ReturnDate": "2023-01-10T10:00:00" + }, + { + "Id": 25, + "BookItemId": 13, + "PatronId": 13, + "LoanDate": "2025-06-22T10:00:00", + "DueDate": "2025-07-06T10:00:00", + "ReturnDate": null + }, + { + "Id": 26, + "BookItemId": 13, + "PatronId": 2, + "LoanDate": "2023-02-01T10:00:00", + "DueDate": "2023-02-15T10:00:00", + "ReturnDate": "2023-02-10T10:00:00" + }, + { + "Id": 27, + "BookItemId": 14, + "PatronId": 14, + "LoanDate": "2025-06-23T10:00:00", + "DueDate": "2025-07-07T10:00:00", + "ReturnDate": null + }, + { + "Id": 28, + "BookItemId": 14, + "PatronId": 3, + "LoanDate": "2023-03-01T10:00:00", + "DueDate": "2023-03-15T10:00:00", + "ReturnDate": "2023-03-10T10:00:00" + }, + { + "Id": 29, + "BookItemId": 15, + "PatronId": 15, + "LoanDate": "2025-06-24T10:00:00", + "DueDate": "2025-07-08T10:00:00", + "ReturnDate": null + }, + { + "Id": 30, + "BookItemId": 15, + "PatronId": 4, + "LoanDate": "2023-04-01T10:00:00", + "DueDate": "2023-04-15T10:00:00", + "ReturnDate": "2023-04-10T10:00:00" + }, + { + "Id": 31, + "BookItemId": 16, + "PatronId": 5, + "LoanDate": "2023-05-01T10:00:00", + "DueDate": "2023-05-15T10:00:00", + "ReturnDate": "2023-05-10T10:00:00" + }, + { + "Id": 32, + "BookItemId": 17, + "PatronId": 6, + "LoanDate": "2023-06-01T10:00:00", + "DueDate": "2023-06-15T10:00:00", + "ReturnDate": "2023-06-10T10:00:00" + }, + { + "Id": 33, + "BookItemId": 18, + "PatronId": 7, + "LoanDate": "2023-07-01T10:00:00", + "DueDate": "2023-07-15T10:00:00", + "ReturnDate": "2023-07-10T10:00:00" + }, + { + "Id": 34, + "BookItemId": 19, + "PatronId": 8, + "LoanDate": "2023-08-01T10:00:00", + "DueDate": "2023-08-15T10:00:00", + "ReturnDate": "2023-08-10T10:00:00" + }, + { + "Id": 35, + "BookItemId": 20, + "PatronId": 9, + "LoanDate": "2023-09-01T10:00:00", + "DueDate": "2023-09-15T10:00:00", + "ReturnDate": "2023-09-10T10:00:00" + }, + { + "Id": 36, + "BookItemId": 16, + "PatronId": 21, + "LoanDate": "2023-10-01T10:00:00", + "DueDate": "2023-10-15T10:00:00", + "ReturnDate": "2023-10-10T10:00:00" + }, + { + "Id": 37, + "BookItemId": 17, + "PatronId": 22, + "LoanDate": "2023-11-01T10:00:00", + "DueDate": "2023-11-15T10:00:00", + "ReturnDate": "2023-11-10T10:00:00" + }, + { + "Id": 38, + "BookItemId": 18, + "PatronId": 23, + "LoanDate": "2023-12-01T10:00:00", + "DueDate": "2023-12-15T10:00:00", + "ReturnDate": "2023-12-10T10:00:00" + }, + { + "Id": 39, + "BookItemId": 19, + "PatronId": 24, + "LoanDate": "2024-01-01T10:00:00", + "DueDate": "2024-01-15T10:00:00", + "ReturnDate": "2024-01-10T10:00:00" + }, + { + "Id": 40, + "BookItemId": 20, + "PatronId": 25, + "LoanDate": "2024-02-01T10:00:00", + "DueDate": "2024-02-15T10:00:00", + "ReturnDate": "2024-02-10T10:00:00" + }, + { + "Id": 41, + "BookItemId": 16, + "PatronId": 26, + "LoanDate": "2024-03-01T10:00:00", + "DueDate": "2024-03-15T10:00:00", + "ReturnDate": "2024-03-10T10:00:00" + }, + { + "Id": 42, + "BookItemId": 17, + "PatronId": 27, + "LoanDate": "2024-04-01T10:00:00", + "DueDate": "2024-04-15T10:00:00", + "ReturnDate": "2024-04-10T10:00:00" + }, + { + "Id": 43, + "BookItemId": 18, + "PatronId": 28, + "LoanDate": "2024-05-01T10:00:00", + "DueDate": "2024-05-15T10:00:00", + "ReturnDate": "2024-05-10T10:00:00" + }, + { + "Id": 44, + "BookItemId": 19, + "PatronId": 29, + "LoanDate": "2024-06-01T10:00:00", + "DueDate": "2024-06-15T10:00:00", + "ReturnDate": "2024-06-10T10:00:00" + }, + { + "Id": 45, + "BookItemId": 20, + "PatronId": 30, + "LoanDate": "2024-07-01T10:00:00", + "DueDate": "2024-07-15T10:00:00", + "ReturnDate": "2024-07-10T10:00:00" + }, + { + "Id": 46, + "BookItemId": 16, + "PatronId": 31, + "LoanDate": "2024-08-01T10:00:00", + "DueDate": "2024-08-15T10:00:00", + "ReturnDate": "2024-08-10T10:00:00" + }, + { + "Id": 47, + "BookItemId": 17, + "PatronId": 32, + "LoanDate": "2024-09-01T10:00:00", + "DueDate": "2024-09-15T10:00:00", + "ReturnDate": "2024-09-10T10:00:00" + }, + { + "Id": 48, + "BookItemId": 18, + "PatronId": 33, + "LoanDate": "2024-10-01T10:00:00", + "DueDate": "2024-10-15T10:00:00", + "ReturnDate": "2024-10-10T10:00:00" + }, + { + "Id": 49, + "BookItemId": 19, + "PatronId": 34, + "LoanDate": "2024-11-01T10:00:00", + "DueDate": "2024-11-15T10:00:00", + "ReturnDate": "2024-11-10T10:00:00" + }, + { + "Id": 50, + "BookItemId": 20, + "PatronId": 35, + "LoanDate": "2024-12-01T10:00:00", + "DueDate": "2024-12-15T10:00:00", + "ReturnDate": "2024-12-10T10:00:00" + }, + { + "Id": 51, + "BookItemId": 16, + "PatronId": 36, + "LoanDate": "2025-01-01T10:00:00", + "DueDate": "2025-01-15T10:00:00", + "ReturnDate": "2025-01-10T10:00:00" + }, + { + "Id": 52, + "BookItemId": 17, + "PatronId": 37, + "LoanDate": "2025-02-01T10:00:00", + "DueDate": "2025-02-15T10:00:00", + "ReturnDate": "2025-02-10T10:00:00" + }, + { + "Id": 53, + "BookItemId": 18, + "PatronId": 38, + "LoanDate": "2025-03-01T10:00:00", + "DueDate": "2025-03-15T10:00:00", + "ReturnDate": "2025-03-10T10:00:00" + }, + { + "Id": 54, + "BookItemId": 19, + "PatronId": 39, + "LoanDate": "2025-04-01T10:00:00", + "DueDate": "2025-04-15T10:00:00", + "ReturnDate": "2025-04-10T10:00:00" + }, + { + "Id": 55, + "BookItemId": 20, + "PatronId": 40, + "LoanDate": "2025-05-01T10:00:00", + "DueDate": "2025-05-15T10:00:00", + "ReturnDate": "2025-05-10T10:00:00" + }, + { + "Id": 56, + "BookItemId": 16, + "PatronId": 41, + "LoanDate": "2025-05-11T10:00:00", + "DueDate": "2025-05-25T10:00:00", + "ReturnDate": "2025-05-20T10:00:00" + }, + { + "Id": 57, + "BookItemId": 17, + "PatronId": 42, + "LoanDate": "2025-05-12T10:00:00", + "DueDate": "2025-05-26T10:00:00", + "ReturnDate": "2025-05-21T10:00:00" + }, + { + "Id": 58, + "BookItemId": 18, + "PatronId": 48, + "LoanDate": "2025-05-13T10:00:00", + "DueDate": "2025-05-27T10:00:00", + "ReturnDate": "2025-05-22T10:00:00" + }, + { + "Id": 59, + "BookItemId": 19, + "PatronId": 49, + "LoanDate": "2025-05-14T10:00:00", + "DueDate": "2025-05-28T10:00:00", + "ReturnDate": "2025-05-23T10:00:00" + }, + { + "Id": 60, + "BookItemId": 20, + "PatronId": 50, + "LoanDate": "2025-05-15T10:00:00", + "DueDate": "2025-05-29T10:00:00", + "ReturnDate": "2025-05-24T10:00:00" + } +] \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Patrons.json b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Patrons.json new file mode 100644 index 0000000..7c05687 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Patrons.json @@ -0,0 +1,52 @@ +[ + {"Id": 1, "Name": "Patron One", "MembershipEnd": "2024-12-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron One.jpg"}, + {"Id": 2, "Name": "Patron Two", "MembershipEnd": "2025-02-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Two.jpg"}, + {"Id": 3, "Name": "Patron Three", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Three.jpg"}, + {"Id": 4, "Name": "Patron Four", "MembershipEnd": "2025-04-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Four.jpg"}, + {"Id": 5, "Name": "Patron Five", "MembershipEnd": "2025-05-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Five.jpg"}, + {"Id": 6, "Name": "Patron Six", "MembershipEnd": "2025-06-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Six.jpg"}, + {"Id": 7, "Name": "Patron Seven", "MembershipEnd": "2025-07-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Seven.jpg"}, + {"Id": 8, "Name": "Patron Eight", "MembershipEnd": "2024-01-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Eight.jpg"}, + {"Id": 9, "Name": "Patron Nine", "MembershipEnd": "2024-02-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Nine.jpg"}, + {"Id": 10, "Name": "Patron Ten", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Ten.jpg"}, + {"Id": 11, "Name": "Patron Eleven", "MembershipEnd": "2024-04-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Eleven.jpg"}, + {"Id": 12, "Name": "Patron Twelve", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twelve.jpg"}, + {"Id": 13, "Name": "Patron Thirteen", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirteen.jpg"}, + {"Id": 14, "Name": "Patron Fourteen", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Fourteen.jpg"}, + {"Id": 15, "Name": "Patron Fifteen", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Fifteen.jpg"}, + {"Id": 16, "Name": "Patron Sixteen", "MembershipEnd": "2024-04-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Sixteen.jpg"}, + {"Id": 17, "Name": "Patron Seventeen", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Seventeen.jpg"}, + {"Id": 18, "Name": "Patron Eighteen", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Eighteen.jpg"}, + {"Id": 19, "Name": "Patron Nineteen", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Nineteen.jpg"}, + {"Id": 20, "Name": "Patron Twenty", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty.jpg"}, + {"Id": 21, "Name": "Patron Twenty-One", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-One.jpg"}, + {"Id": 22, "Name": "Patron Twenty-Two", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Two.jpg"}, + {"Id": 23, "Name": "Patron Twenty-Three", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Three.jpg"}, + {"Id": 24, "Name": "Patron Twenty-Four", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Four.jpg"}, + {"Id": 25, "Name": "Patron Twenty-Five", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Five.jpg"}, + {"Id": 26, "Name": "Patron Twenty-Six", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Six.jpg"}, + {"Id": 27, "Name": "Patron Twenty-Seven", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Seven.jpg"}, + {"Id": 28, "Name": "Patron Twenty-Eight", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Eight.jpg"}, + {"Id": 29, "Name": "Patron Twenty-Nine", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Nine.jpg"}, + {"Id": 30, "Name": "Patron Thirty", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty.jpg"}, + {"Id": 31, "Name": "Patron Thirty-One", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-One.jpg"}, + {"Id": 32, "Name": "Patron Thirty-Two", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Two.jpg"}, + {"Id": 33, "Name": "Patron Thirty-Three", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Three.jpg"}, + {"Id": 34, "Name": "Patron Thirty-Four", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Four.jpg"}, + {"Id": 35, "Name": "Patron Thirty-Five", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Five.jpg"}, + {"Id": 36, "Name": "Patron Thirty-Six", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Six.jpg"}, + {"Id": 37, "Name": "Patron Thirty-Seven", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Seven.jpg"}, + {"Id": 38, "Name": "Patron Thirty-Eight", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Eight.jpg"}, + {"Id": 39, "Name": "Patron Thirty-Nine", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Nine.jpg"}, + {"Id": 40, "Name": "Patron Forty", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty.jpg"}, + {"Id": 41, "Name": "Patron Forty-One", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-One.jpg"}, + {"Id": 42, "Name": "Patron Forty-Two", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Two.jpg"}, + {"Id": 43, "Name": "Patron Forty-Three", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Three.jpg"}, + {"Id": 44, "Name": "Patron Forty-Four", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Four.jpg"}, + {"Id": 45, "Name": "Patron Forty-Five", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Five.jpg"}, + {"Id": 46, "Name": "Patron Forty-Six", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Six.jpg"}, + {"Id": 47, "Name": "Patron Forty-Seven", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Seven.jpg"}, + {"Id": 48, "Name": "Patron Forty-Eight", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Eight.jpg"}, + {"Id": 49, "Name": "Patron Forty-Nine", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Nine.jpg"}, + {"Id": 50, "Name": "Patron Fifty", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Fifty.jpg"} +] diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/json_data.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/json_data.py new file mode 100644 index 0000000..0c4aa54 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/json_data.py @@ -0,0 +1,105 @@ +import json +import os +from pathlib import Path +from application_core.entities.author import Author +from application_core.entities.book import Book +from application_core.entities.book_item import BookItem +from application_core.entities.patron import Patron +from application_core.entities.loan import Loan +from typing import List, Optional +from datetime import datetime + +class JsonData: + def __init__(self): + # Get the absolute path to the project root + self.project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + self.json_dir = os.path.join(self.project_root, "infrastructure", "Json") + self.authors_path = os.path.join(self.json_dir, "Authors.json") + self.books_path = os.path.join(self.json_dir, "Books.json") + self.book_items_path = os.path.join(self.json_dir, "BookItems.json") # <-- Add this line + self.patrons_path = os.path.join(self.json_dir, "Patrons.json") + self.loans_path = os.path.join(self.json_dir, "Loans.json") + self.authors: List[Author] = [] + self.books: List[Book] = [] + self.book_items: List[BookItem] = [] + self.patrons: List[Patron] = [] + self.loans: List[Loan] = [] + self._loaded = False + self.load_data() + + def _parse_datetime(self, value: Optional[str]) -> Optional[datetime]: + if value is None: + return None + return datetime.fromisoformat(value) + + def load_data(self) -> None: + try: + with open(self.authors_path, encoding='utf-8') as f: + authors_data = json.load(f) + self.authors = [Author(id=a['Id'], name=a['Name']) for a in authors_data] + with open(self.books_path, encoding='utf-8') as f: + books_data = json.load(f) + self.books = [Book(id=b['Id'], title=b['Title'], author_id=b['AuthorId'], genre=b['Genre'], image_name=b['ImageName'], isbn=b['ISBN']) for b in books_data] + with open(self.book_items_path, encoding='utf-8') as f: # <-- Fix here + items_data = json.load(f) + self.book_items = [BookItem(id=bi['Id'], book_id=bi['BookId'], acquisition_date=self._parse_datetime(bi['AcquisitionDate']), condition=bi.get('Condition')) for bi in items_data] + with open(self.patrons_path, encoding='utf-8') as f: + patrons_data = json.load(f) + self.patrons = [Patron(id=p['Id'], name=p['Name'], membership_end=self._parse_datetime(p['MembershipEnd']), membership_start=self._parse_datetime(p['MembershipStart']), image_name=p.get('ImageName')) for p in patrons_data] + with open(self.loans_path, encoding='utf-8') as f: + loans_data = json.load(f) + self.loans = [Loan(id=l['Id'], book_item_id=l['BookItemId'], patron_id=l['PatronId'], loan_date=self._parse_datetime(l['LoanDate']), due_date=self._parse_datetime(l['DueDate']), return_date=self._parse_datetime(l['ReturnDate'])) for l in loans_data] + self._loaded = True + + # Build lookup dictionaries for fast access + book_item_dict = {bi.id: bi for bi in self.book_items} + book_dict = {b.id: b for b in self.books} + author_dict = {a.id: a for a in self.authors} + patron_dict = {p.id: p for p in self.patrons} + + # Link book_item and book to each loan + for loan in self.loans: + loan.book_item = book_item_dict.get(loan.book_item_id) + if loan.book_item: + loan.book_item.book = book_dict.get(loan.book_item.book_id) + if loan.book_item.book: + loan.book_item.book.author = author_dict.get(loan.book_item.book.author_id) + loan.patron = patron_dict.get(loan.patron_id) + # Optionally, link loans to patrons + for patron in self.patrons: + patron.loans = [loan for loan in self.loans if loan.patron_id == patron.id] + + except (FileNotFoundError, json.JSONDecodeError) as e: + print(f"Error loading data: {e}") + self._loaded = False + + def save_loans(self, loans: List[Loan]) -> None: + try: + with open(self.loans_path, 'w', encoding='utf-8') as f: + json.dump([ + { + 'Id': l.id, + 'BookItemId': l.book_item_id, + 'PatronId': l.patron_id, + 'LoanDate': l.loan_date.isoformat() if l.loan_date else None, + 'DueDate': l.due_date.isoformat() if l.due_date else None, + 'ReturnDate': l.return_date.isoformat() if l.return_date else None + } for l in loans + ], f, indent=2) + except Exception as e: + print(f"Error saving loans: {e}") + + def save_patrons(self, patrons: List[Patron]) -> None: + try: + with open(self.patrons_path, 'w', encoding='utf-8') as f: + json.dump([ + { + 'Id': p.id, + 'Name': p.name, + 'MembershipEnd': p.membership_end.isoformat() if p.membership_end else None, + 'MembershipStart': p.membership_start.isoformat() if p.membership_start else None, + 'ImageName': p.image_name + } for p in patrons + ], f, indent=2) + except Exception as e: + print(f"Error saving patrons: {e}") diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/json_loan_repository.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/json_loan_repository.py new file mode 100644 index 0000000..4bcd087 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/json_loan_repository.py @@ -0,0 +1,54 @@ +import json +from datetime import datetime +from application_core.interfaces.iloan_repository import ILoanRepository +from application_core.entities.loan import Loan +from .json_data import JsonData +from typing import Optional + +class JsonLoanRepository(ILoanRepository): + def __init__(self, json_data: JsonData): + self._json_data = json_data + + def get_loan(self, loan_id: int) -> Optional[Loan]: + for loan in self._json_data.loans: + if loan.id == loan_id: + return loan + return None + + def update_loan(self, loan: Loan) -> None: + for idx in range(len(self._json_data.loans)): + if self._json_data.loans[idx].id == loan.id: + self._json_data.loans[idx] = loan + self._json_data.save_loans(self._json_data.loans) + return + + def add_loan(self, loan: Loan) -> None: + self._json_data.loans.append(loan) + self._json_data.save_loans(self._json_data.loans) + self._json_data.load_data() + + def get_loans_by_patron_id(self, patron_id: int): + result = [] + for loan in self._json_data.loans: + if loan.patron_id == patron_id: + result.append(loan) + return result + + def get_all_loans(self): + return self._json_data.loans + + def get_overdue_loans(self, current_date): + overdue = [] + for loan in self._json_data.loans: + if loan.return_date is None and loan.due_date < current_date: + overdue.append(loan) + return overdue + + def sort_loans_by_due_date(self): + # Manual bubble sort for demonstration + n = len(self._json_data.loans) + for i in range(n): + for j in range(0, n - i - 1): + if self._json_data.loans[j].due_date > self._json_data.loans[j + 1].due_date: + self._json_data.loans[j], self._json_data.loans[j + 1] = self._json_data.loans[j + 1], self._json_data.loans[j] + return self._json_data.loans diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/json_patron_repository.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/json_patron_repository.py new file mode 100644 index 0000000..8b25cac --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/infrastructure/json_patron_repository.py @@ -0,0 +1,55 @@ +import json +from application_core.interfaces.ipatron_repository import IPatronRepository +from application_core.entities.patron import Patron +from .json_data import JsonData +from typing import List, Optional + +class JsonPatronRepository(IPatronRepository): + def __init__(self, json_data: JsonData): + self._json_data = json_data + + def get_patron(self, patron_id: int) -> Optional[Patron]: + for patron in self._json_data.patrons: + if patron.id == patron_id: + return patron + return None + + def search_patrons(self, search_input: str) -> List[Patron]: + results = [] + for p in self._json_data.patrons: + if search_input.lower() in p.name.lower(): + results.append(p) + n = len(results) + for i in range(n): + for j in range(0, n - i - 1): + if results[j].name > results[j + 1].name: + results[j], results[j + 1] = results[j + 1], results[j] + return results + + def update_patron(self, patron: Patron) -> None: + for idx in range(len(self._json_data.patrons)): + if self._json_data.patrons[idx].id == patron.id: + self._json_data.patrons[idx] = patron + self._json_data.save_patrons(self._json_data.patrons) + return + + def add_patron(self, patron: Patron) -> None: + self._json_data.patrons.append(patron) + self._json_data.save_patrons(self._json_data.patrons) + self._json_data.load_data() + + def get_all_patrons(self) -> List[Patron]: + return self._json_data.patrons + + def find_patrons_by_name(self, name: str) -> List[Patron]: + result = [] + for patron in self._json_data.patrons: + if patron.name.lower() == name.lower(): + result.append(patron) + return result + + def get_all_books(self): + return self._json_data.books + + def get_all_book_items(self): + return self._json_data.book_items diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/readme.md b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/readme.md new file mode 100644 index 0000000..b8239b7 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/readme.md @@ -0,0 +1,88 @@ +# Library App + +## Description + +Library App is a modular Python application designed to help library staff manage core operations such as book loans, patron management, and inventory tracking. The project follows a clean architecture, separating domain logic, data access, and user interaction through a console interface. Data is stored in JSON files, making the app easy to set up and run in any environment. + +## Project Structure + +- Library/ + - readme.md + - application_core/ + - entities/ + - author.py + - book_item.py + - book.py + - loan.py + - patron.py + - enums/ + - loan_extension_status.py + - loan_return_status.py + - membership_renewal_status.py + - ... + - interfaces/ + - iloan_repository.py + - iloan_service.py + - ipatron_repository.py + - ipatron_service.py + - ... + - services/ + - loan_service.py + - patron_service.py + - ... + - console/ + - book_repository.py + - common_actions.py + - console_app.py + - console_state.py + - main.py + - infrastructure/ + - json_data.py + - json_loan_repository.py + - json_patron_repository.py + - Json/ + - Authors.json + - Books.json + - BookItems.json + - Loans.json + - Patrons.json + - tests/ + - __init__.py + - test_loan_service.py + - test_patron_service.py + +## Key Classes and Interfaces + +- **Entities (application_core/entities/):** + - `Author`, `Book`, `BookItem`, `Patron`, `Loan`: Represent core library objects. +- **Enums (application_core/enums/):** + - `LoanExtensionStatus`, `LoanReturnStatus`, `MembershipRenewalStatus`: Enumerations for domain-specific statuses. +- **Interfaces (application_core/interfaces/):** + - `ILoanRepository`, `ILoanService`, `IPatronRepository`, `IPatronService`: Define abstractions for repositories and services. +- **Services (application_core/services/):** + - `LoanService`, `PatronService`: Business logic for managing loans, patrons, and books. +- **Console (console/):** + - `ConsoleApp`: Main entry point for the console interface. + - `main.py`: Launches the application. +- **Infrastructure (infrastructure/):** + - `json_data.py`: Utilities for JSON file operations. + - `json_loan_repository.py`, `json_patron_repository.py`: Data access implementations using JSON files. +- **Tests (tests/):** + - Unit tests for core business logic. + +## Usage + +1. **Install Python 3.7+** if not already installed. +2. **Navigate to the Library directory:** + ``` + cd Library + ``` +3. **Run the application:** + ``` + python -m console.main + ``` +4. **Follow the on-screen prompts** to manage books, patrons, and loans. + +## License + +This project is licensed under the MIT License. \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/tests/__init__.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/tests/test_loan_service.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/tests/test_loan_service.py new file mode 100644 index 0000000..04f53fb --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/tests/test_loan_service.py @@ -0,0 +1,26 @@ +import unittest +from unittest.mock import MagicMock +from application_core.services.loan_service import LoanService +from application_core.entities.loan import Loan +from application_core.enums.loan_return_status import LoanReturnStatus +from datetime import datetime, timedelta + +class TestLoanService(unittest.TestCase): + def setUp(self): + self.mock_repo = MagicMock() + self.service = LoanService(self.mock_repo) + + def test_return_loan_success(self): + loan = Loan(id=1, book_item_id=1, patron_id=1, patron=None, loan_date=datetime.now()-timedelta(days=10), due_date=datetime.now()+timedelta(days=10), return_date=None, book_item=None) + self.mock_repo.get_loan.return_value = loan + self.mock_repo.update_loan.return_value = None + status = self.service.return_loan(1) + self.assertEqual(status, LoanReturnStatus.SUCCESS) + + def test_return_loan_not_found(self): + self.mock_repo.get_loan.return_value = None + status = self.service.return_loan(1) + self.assertEqual(status, LoanReturnStatus.LOAN_NOT_FOUND) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/tests/test_patron_service.py b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/tests/test_patron_service.py new file mode 100644 index 0000000..1f6d63f --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/AccelerateDevGHCopilot/library/tests/test_patron_service.py @@ -0,0 +1,26 @@ +import unittest +from unittest.mock import MagicMock +from application_core.services.patron_service import PatronService +from application_core.entities.patron import Patron +from application_core.enums.membership_renewal_status import MembershipRenewalStatus +from datetime import datetime, timedelta + +class PatronServiceTest(unittest.TestCase): + def setUp(self): + self.mock_repo = MagicMock() + self.service = PatronService(self.mock_repo) + + def test_renew_membership_success(self): + patron = Patron(id=1, name="John Doe", membership_end=datetime.now()-timedelta(days=1), membership_start=datetime.now()-timedelta(days=365)) + self.mock_repo.get_patron.return_value = patron + self.mock_repo.update_patron.return_value = None + status = self.service.renew_membership(1) + self.assertEqual(status, MembershipRenewalStatus.SUCCESS) + + def test_renew_membership_patron_not_found(self): + self.mock_repo.get_patron.return_value = None + status = self.service.renew_membership(1) + self.assertEqual(status, MembershipRenewalStatus.PATRON_NOT_FOUND) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code-python/readme.txt b/DownloadableCodeProjects/az-2007-m3-develop-code-python/readme.txt new file mode 100644 index 0000000..b3a4252 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code-python/readme.txt @@ -0,0 +1 @@ +placeholder \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/.gitignore b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/.gitignore new file mode 100644 index 0000000..9a75e6b --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/.gitignore @@ -0,0 +1,119 @@ +# Build Folders (you can keep bin if you'd like, to store dlls and pdbs) +[Bb]in/ +[Oo]bj/ + +# mstest test results +TestResults + +## VSCode +.vscode/* + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Include dlls if theyfre in the NuGet packages directory +!/packages/*/lib/*.dll +!/packages/*/lib/*/*.dll +# Include dlls if they're in the CommonReferences directory +!*CommonReferences/*.dll + +# Build results +[Dd]ebug/ +[Rr]elease/ +x64/ +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.log +*.vspscc +*.vssscc +*.sln +.builds + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper* + +# NCrunch +*.ncrunch* +.*crunch*.local.xml + +# Installshield output folder +[Ee]xpress + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish + +# Publish Web Output +*.Publish.xml + +# NuGet Packages Directory +# packages + +# Windows Azure Build Output +csx +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +[Bb]in +[Oo]bj +sql +TestResults +[Tt]est[Rr]esult* +*.[Cc]ache +*.editorconfig +ClientBin +[Ss]tyle[Cc]op.* +~$* +*.dbmdl +Generated_Code #added for RIA/Silverlight projects + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/README.md b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/README.md new file mode 100644 index 0000000..e47497b --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/README.md @@ -0,0 +1,75 @@ +# Library App + +## Description + +Library App is a modular application designed to manage library operations such as book loans, patron management, and inventory tracking. It is built using .NET and follows a clean architecture approach to ensure scalability and maintainability. + +## Project Structure + +- `AccelerateDevGHCopilot.sln` - Solution file for the project. +- `src/` + - `Library.ApplicationCore/` + - `Entities/` - Contains core domain entities. + - `Enums/` - Defines enumerations used across the application. + - `Interfaces/` - Declares interfaces for core abstractions. + - `Services/` - Implements business logic and domain services. + - `Library.ApplicationCore.csproj` - Project file for the Application Core. + - `Library.Console/` + - `appSettings.json` - Configuration file for the console application. + - `CommonActions.cs` - Contains reusable actions for the console app. + - `ConsoleApp.cs` - Main application logic for the console interface. + - `ConsoleState.cs` - Manages the state of the console application. + - `Program.cs` - Entry point for the console application. + - `Json/` - Contains JSON-related utilities or data. + - `Library.Console.csproj` - Project file for the Console application. + - `Library.Infrastructure/` + - `Data/` - Contains data access implementations. + - `Library.Infrastructure.csproj` - Project file for the Infrastructure layer. +- `tests/` + - `UnitTests/` + - `LoanFactory.cs` - Factory for creating test data related to loans. + - `PatronFactory.cs` - Factory for creating test data related to patrons. + - `ApplicationCore/` - Contains unit tests for the Application Core. + - `UnitTests.csproj` - Project file for unit tests. + +## Key Classes and Interfaces + +- **Entities** + - `Book` - Represents a book in the library. + - `Patron` - Represents a library patron. + - `Loan` - Represents a loan transaction. +- **Interfaces** + - `IBookRepository` - Interface for book-related data operations. + - `IPatronRepository` - Interface for patron-related data operations. + - `ILoanService` - Interface for managing loan operations. +- **Services** + - `LoanService` - Implements loan-related business logic. + - `NotificationService` - Handles notifications for overdue loans. + +## Usage + +1. Clone the repository: + + ```bash + git clone + ``` + +2. Open the solution file `AccelerateDevGHCopilot.sln` in Visual Studio. + +3. Build the solution to restore dependencies and compile the code. + +4. Run the console application: + + ```bash + dotnet run --project src/Library.Console/Library.Console.csproj + ``` + +5. Execute unit tests: + + ```bash + dotnet test tests/UnitTests/UnitTests.csproj + ``` + +## License + +This project is licensed under the MIT License. See the `LICENSE` file for details. diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Author.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Author.cs new file mode 100644 index 0000000..5d9d1a6 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Author.cs @@ -0,0 +1,7 @@ +namespace Library.ApplicationCore.Entities; + +public class Author +{ + public int Id { get; set; } + public required string Name { get; set; } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Book.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Book.cs new file mode 100644 index 0000000..029b467 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Book.cs @@ -0,0 +1,12 @@ +namespace Library.ApplicationCore.Entities; + +public class Book +{ + public int Id { get; set; } + public required string Title { get; set; } + public int AuthorId { get; set; } + public required string Genre { get; set; } + public required string ImageName { get; set; } + public required string ISBN { get; set; } + public Author? Author { get; set; } +} diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/BookItem.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/BookItem.cs new file mode 100644 index 0000000..5a97332 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/BookItem.cs @@ -0,0 +1,10 @@ +namespace Library.ApplicationCore.Entities; + +public class BookItem +{ + public int Id { get; set; } + public int BookId { get; set; } + public DateTime AcquisitionDate { get; set; } + public string? Condition { get; set; } + public Book? Book { get; set; } +} diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Loan.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Loan.cs new file mode 100644 index 0000000..6d0c33e --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Loan.cs @@ -0,0 +1,13 @@ +namespace Library.ApplicationCore.Entities; + +public class Loan +{ + public int Id { get; set; } + public int BookItemId { get; set; } + public int PatronId { get; set; } + public Patron? Patron { get; set; } + public DateTime LoanDate { get; set; } + public DateTime DueDate { get; set; } + public DateTime? ReturnDate { get; set; } + public BookItem? BookItem { get; set; } +} diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Patron.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Patron.cs new file mode 100644 index 0000000..3a2fd33 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Patron.cs @@ -0,0 +1,11 @@ +namespace Library.ApplicationCore.Entities; + +public class Patron +{ + public int Id { get; set; } + public required string Name { get; set; } + public DateTime MembershipEnd { get; set; } + public DateTime MembershipStart { get; set; } + public string? ImageName { get; set; } + public ICollection Loans { get; set; } = new HashSet(); +} diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/EnumHelper.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/EnumHelper.cs new file mode 100644 index 0000000..5369856 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/EnumHelper.cs @@ -0,0 +1,27 @@ +using System.ComponentModel; +using System.Reflection; + +namespace Library.ApplicationCore.Enums; + +public static class EnumHelper +{ + public static string GetDescription(Enum value) + { + if (value == null) + return string.Empty; + + FieldInfo fieldInfo = value.GetType().GetField(value.ToString())!; + + DescriptionAttribute[] attributes = + (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false); + + if (attributes != null && attributes.Length > 0) + { + return attributes[0].Description; + } + else + { + return value.ToString(); + } + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanExtensionStatus.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanExtensionStatus.cs new file mode 100644 index 0000000..2af2c4a --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanExtensionStatus.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; + +namespace Library.ApplicationCore.Enums; + +public enum LoanExtensionStatus +{ + [Description("Book loan extension was successful.")] + Success, + + [Description("Loan not found.")] + LoanNotFound, + + [Description("Cannot extend book loan as it already has expired. Return the book instead.")] + LoanExpired, + + [Description("Cannot extend book loan due to expired patron's membership.")] + MembershipExpired, + + [Description("Cannot extend book loan as the book is already returned.")] + LoanReturned, + + [Description("Cannot extend book loan due to an error.")] + Error +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanReturnStatus.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanReturnStatus.cs new file mode 100644 index 0000000..61edf46 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanReturnStatus.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; + +namespace Library.ApplicationCore.Enums; + +public enum LoanReturnStatus +{ + [Description("Book was successfully returned.")] + Success, + + [Description("Loan not found.")] + LoanNotFound, + + [Description("Cannot return book as the book is already returned.")] + AlreadyReturned, + + [Description("Cannot return book due to an error.")] + Error +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/MembershipRenewalStatus.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/MembershipRenewalStatus.cs new file mode 100644 index 0000000..1323ae3 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/MembershipRenewalStatus.cs @@ -0,0 +1,21 @@ +using System.ComponentModel; + +namespace Library.ApplicationCore.Enums; + +public enum MembershipRenewalStatus +{ + [Description("Membership renewal was successful.")] + Success, + + [Description("Patron not found.")] + PatronNotFound, + + [Description("It is too early to renew the membership.")] + TooEarlyToRenew, + + [Description("Cannot renew membership due to an outstanding loan.")] + LoanNotReturned, + + [Description("Cannot renew membership due to an error.")] + Error +} diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanRepository.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanRepository.cs new file mode 100644 index 0000000..ab00b02 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanRepository.cs @@ -0,0 +1,8 @@ +using Library.ApplicationCore.Entities; + +namespace Library.ApplicationCore; + +public interface ILoanRepository { + Task GetLoan(int loanId); + Task UpdateLoan(Loan loan); +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanService.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanService.cs new file mode 100644 index 0000000..cb255ce --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanService.cs @@ -0,0 +1,7 @@ +using Library.ApplicationCore.Enums; + +public interface ILoanService +{ + Task ReturnLoan(int loanId); + Task ExtendLoan(int loanId); +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronRepository.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronRepository.cs new file mode 100644 index 0000000..19b97f8 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronRepository.cs @@ -0,0 +1,10 @@ +using Library.ApplicationCore.Entities; + +namespace Library.ApplicationCore; + +public interface IPatronRepository { + Task GetPatron(int patronId); + Task> SearchPatrons(string searchInput); + Task UpdatePatron(Patron patron); +} + diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronService.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronService.cs new file mode 100644 index 0000000..6b5f453 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronService.cs @@ -0,0 +1,6 @@ +using Library.ApplicationCore.Enums; + +public interface IPatronService +{ + Task RenewMembership(int patronId); +} diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Library.ApplicationCore.csproj b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Library.ApplicationCore.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Library.ApplicationCore.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/LoanService.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/LoanService.cs new file mode 100644 index 0000000..0f13d3a --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/LoanService.cs @@ -0,0 +1,70 @@ +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; + +public class LoanService : ILoanService +{ + private ILoanRepository _loanRepository; + + public LoanService(ILoanRepository loanRepository) + { + _loanRepository = loanRepository; + } + + public async Task ReturnLoan(int loanId) + { + Loan? loan = await _loanRepository.GetLoan(loanId); + if (loan == null) + { + return LoanReturnStatus.LoanNotFound; + } + + // check if already returned + if (loan.ReturnDate != null) + { + return LoanReturnStatus.AlreadyReturned; + } + + loan.ReturnDate = DateTime.Now; + try + { + await _loanRepository.UpdateLoan(loan); + return LoanReturnStatus.Success; + } + catch (Exception e) + { + return LoanReturnStatus.Error; + } + } + + public const int ExtendByDays = 14; + + public async Task ExtendLoan(int loanId) + { + var loan = await _loanRepository.GetLoan(loanId); + + if (loan == null) + return LoanExtensionStatus.LoanNotFound; + + // Check if patron's membership is expired + if (loan.Patron!.MembershipEnd < DateTime.Now) + return LoanExtensionStatus.MembershipExpired; + + if (loan.ReturnDate != null) + return LoanExtensionStatus.LoanReturned; + + if (loan.DueDate < DateTime.Now) + return LoanExtensionStatus.LoanExpired; + + loan.DueDate = loan.DueDate.AddDays(ExtendByDays); + try + { + await _loanRepository.UpdateLoan(loan); + return LoanExtensionStatus.Success; + } + catch (Exception e) + { + return LoanExtensionStatus.Error; + } + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/PatronService.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/PatronService.cs new file mode 100644 index 0000000..7ba6d78 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/PatronService.cs @@ -0,0 +1,36 @@ +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; + +public class PatronService : IPatronService +{ + private readonly IPatronRepository _patronRepository; + + public PatronService(IPatronRepository patronRepository) + { + _patronRepository = patronRepository; + } + + public async Task RenewMembership(int patronId) + { + var patron = await _patronRepository.GetPatron(patronId); + if (patron == null) + return MembershipRenewalStatus.PatronNotFound; + + // don't allow to renew till 1 month before expiration + if (patron.MembershipEnd >= DateTime.Now.AddMonths(1)) + return MembershipRenewalStatus.TooEarlyToRenew; + + // don't allow to renew if patron has overdue loans + if (patron.Loans.Any(l => (l.ReturnDate == null) && l.DueDate < DateTime.Now)) + return MembershipRenewalStatus.LoanNotReturned; + + patron.MembershipEnd = patron.MembershipEnd.AddYears(1); + try{ + await _patronRepository.UpdatePatron(patron); + return MembershipRenewalStatus.Success; + } catch (Exception e) { + return MembershipRenewalStatus.Error; + } + } +} diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/CommonActions.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/CommonActions.cs new file mode 100644 index 0000000..681f95c --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/CommonActions.cs @@ -0,0 +1,13 @@ +namespace Library.Console; + +[Flags] +public enum CommonActions +{ + Repeat = 0, + Select = 1, + Quit = 2, + SearchPatrons = 4, + RenewPatronMembership = 8, + ReturnLoanedBook = 16, + ExtendLoanedBook = 32 +} diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs new file mode 100644 index 0000000..9fc9750 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs @@ -0,0 +1,274 @@ +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; +using Library.Console; + +public class ConsoleApp +{ + ConsoleState _currentState = ConsoleState.PatronSearch; + + List matchingPatrons = new List(); + + Patron? selectedPatronDetails = null; + Loan selectedLoanDetails = null!; + + IPatronRepository _patronRepository; + ILoanRepository _loanRepository; + ILoanService _loanService; + IPatronService _patronService; + + public ConsoleApp(ILoanService loanService, IPatronService patronService, IPatronRepository patronRepository, ILoanRepository loanRepository) + { + _patronRepository = patronRepository; + _loanRepository = loanRepository; + _loanService = loanService; + _patronService = patronService; + } + + public async Task Run() + { + while (true) + { + switch (_currentState) + { + case ConsoleState.PatronSearch: + _currentState = await PatronSearch(); + break; + case ConsoleState.PatronSearchResults: + _currentState = await PatronSearchResults(); + break; + case ConsoleState.PatronDetails: + _currentState = await PatronDetails(); + break; + case ConsoleState.LoanDetails: + _currentState = await LoanDetails(); + break; + } + } + } + + async Task PatronSearch() + { + string searchInput = ReadPatronName(); + + matchingPatrons = await _patronRepository.SearchPatrons(searchInput); + + // Guard-style clauses for edge cases + if (matchingPatrons.Count > 20) + { + Console.WriteLine("More than 20 patrons satisfy the search, please provide more specific input..."); + return ConsoleState.PatronSearch; + } + else if (matchingPatrons.Count == 0) + { + Console.WriteLine("No matching patrons found."); + return ConsoleState.PatronSearch; + } + + Console.WriteLine("Matching Patrons:"); + PrintPatronsList(matchingPatrons); + return ConsoleState.PatronSearchResults; + } + + static string ReadPatronName() + { + string? searchInput = null; + while (String.IsNullOrWhiteSpace(searchInput)) + { + Console.Write("Enter a string to search for patrons by name: "); + + searchInput = Console.ReadLine(); + } + return searchInput; + } + + static void PrintPatronsList(List matchingPatrons) + { + int patronNumber = 1; + foreach (Patron patron in matchingPatrons) + { + Console.WriteLine($"{patronNumber}) {patron.Name}"); + patronNumber++; + } + } + + async Task PatronSearchResults() + { + CommonActions options = CommonActions.Select | CommonActions.SearchPatrons | CommonActions.Quit; + CommonActions action = ReadInputOptions(options, out int selectedPatronNumber); + if (action == CommonActions.Select) + { + if (selectedPatronNumber >= 1 && selectedPatronNumber <= matchingPatrons.Count) + { + var selectedPatron = matchingPatrons.ElementAt(selectedPatronNumber - 1); + selectedPatronDetails = await _patronRepository.GetPatron(selectedPatron.Id)!; + return ConsoleState.PatronDetails; + } + else + { + Console.WriteLine("Invalid patron number. Please try again."); + return ConsoleState.PatronSearchResults; + } + } + else if (action == CommonActions.Quit) + { + return ConsoleState.Quit; + } + else if (action == CommonActions.SearchPatrons) + { + return ConsoleState.PatronSearch; + } + + throw new InvalidOperationException("An input option is not handled."); + } + + static CommonActions ReadInputOptions(CommonActions options, out int optionNumber) + { + CommonActions action; + optionNumber = 0; + do + { + Console.WriteLine(); + WriteInputOptions(options); + string? userInput = Console.ReadLine(); + + action = userInput switch + { + "q" when options.HasFlag(CommonActions.Quit) => CommonActions.Quit, + "s" when options.HasFlag(CommonActions.SearchPatrons) => CommonActions.SearchPatrons, + "m" when options.HasFlag(CommonActions.RenewPatronMembership) => CommonActions.RenewPatronMembership, + "e" when options.HasFlag(CommonActions.ExtendLoanedBook) => CommonActions.ExtendLoanedBook, + "r" when options.HasFlag(CommonActions.ReturnLoanedBook) => CommonActions.ReturnLoanedBook, + _ when int.TryParse(userInput, out optionNumber) => CommonActions.Select, + _ => CommonActions.Repeat + }; + + if (action == CommonActions.Repeat) + { + Console.WriteLine("Invalid input. Please try again."); + } + } while (action == CommonActions.Repeat); + return action; + } + + static void WriteInputOptions(CommonActions options) + { + Console.WriteLine("Input Options:"); + if (options.HasFlag(CommonActions.ReturnLoanedBook)) + { + Console.WriteLine(" - \"r\" to mark as returned"); + } + if (options.HasFlag(CommonActions.ExtendLoanedBook)) + { + Console.WriteLine(" - \"e\" to extend the book loan"); + } + if (options.HasFlag(CommonActions.RenewPatronMembership)) + { + Console.WriteLine(" - \"m\" to extend patron's membership"); + } + if (options.HasFlag(CommonActions.SearchPatrons)) + { + Console.WriteLine(" - \"s\" for new search"); + } + if (options.HasFlag(CommonActions.Quit)) + { + Console.WriteLine(" - \"q\" to quit"); + } + if (options.HasFlag(CommonActions.Select)) + { + Console.WriteLine("Or type a number to select a list item."); + } + } + + async Task PatronDetails() + { + Console.WriteLine($"Name: {selectedPatronDetails.Name}"); + Console.WriteLine($"Membership Expiration: {selectedPatronDetails.MembershipEnd}"); + Console.WriteLine(); + Console.WriteLine("Book Loans:"); + int loanNumber = 1; + foreach (Loan loan in selectedPatronDetails.Loans) + { + Console.WriteLine($"{loanNumber}) {loan.BookItem!.Book!.Title} - Due: {loan.DueDate} - Returned: {(loan.ReturnDate != null).ToString()}"); + loanNumber++; + } + + CommonActions options = CommonActions.SearchPatrons | CommonActions.Quit | CommonActions.Select | CommonActions.RenewPatronMembership; + CommonActions action = ReadInputOptions(options, out int selectedLoanNumber); + if (action == CommonActions.Select) + { + if (selectedLoanNumber >= 1 && selectedLoanNumber <= selectedPatronDetails.Loans.Count()) + { + var selectedLoan = selectedPatronDetails.Loans.ElementAt(selectedLoanNumber - 1); + selectedLoanDetails = selectedPatronDetails.Loans.Where(l => l.Id == selectedLoan.Id).Single(); + return ConsoleState.LoanDetails; + } + else + { + Console.WriteLine("Invalid book loan number. Please try again."); + return ConsoleState.PatronDetails; + } + } + else if (action == CommonActions.Quit) + { + return ConsoleState.Quit; + } + else if (action == CommonActions.SearchPatrons) + { + return ConsoleState.PatronSearch; + } + else if (action == CommonActions.RenewPatronMembership) + { + var status = await _patronService.RenewMembership(selectedPatronDetails.Id); + Console.WriteLine(EnumHelper.GetDescription(status)); + // reloading after renewing membership + selectedPatronDetails = (await _patronRepository.GetPatron(selectedPatronDetails.Id))!; + return ConsoleState.PatronDetails; + } + + throw new InvalidOperationException("An input option is not handled."); + } + + async Task LoanDetails() + { + Console.WriteLine($"Book title: {selectedLoanDetails.BookItem!.Book!.Title}"); + Console.WriteLine($"Book Author: {selectedLoanDetails.BookItem!.Book!.Author!.Name}"); + Console.WriteLine($"Due date: {selectedLoanDetails.DueDate}"); + Console.WriteLine($"Returned: {(selectedLoanDetails.ReturnDate != null).ToString()}"); + Console.WriteLine(); + + CommonActions options = CommonActions.SearchPatrons | CommonActions.Quit | CommonActions.ReturnLoanedBook | CommonActions.ExtendLoanedBook; + CommonActions action = ReadInputOptions(options, out int selectedLoanNumber); + + if (action == CommonActions.ExtendLoanedBook) + { + var status = await _loanService.ExtendLoan(selectedLoanDetails.Id); + Console.WriteLine(EnumHelper.GetDescription(status)); + + // reload loan after extending + selectedPatronDetails = (await _patronRepository.GetPatron(selectedPatronDetails.Id))!; + selectedLoanDetails = (await _loanRepository.GetLoan(selectedLoanDetails.Id))!; + return ConsoleState.LoanDetails; + } + else if (action == CommonActions.ReturnLoanedBook) + { + var status = await _loanService.ReturnLoan(selectedLoanDetails.Id); + + Console.WriteLine(EnumHelper.GetDescription(status)); + _currentState = ConsoleState.LoanDetails; + // reload loan after returning + selectedLoanDetails = await _loanRepository.GetLoan(selectedLoanDetails.Id); + return ConsoleState.LoanDetails; + } + else if (action == CommonActions.Quit) + { + return ConsoleState.Quit; + } + else if (action == CommonActions.SearchPatrons) + { + return ConsoleState.PatronSearch; + } + + throw new InvalidOperationException("An input option is not handled."); + } +} diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/ConsoleState.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/ConsoleState.cs new file mode 100644 index 0000000..e9117b6 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/ConsoleState.cs @@ -0,0 +1,10 @@ +namespace Library.Console; + +public enum ConsoleState +{ + PatronSearch, + PatronSearchResults, + PatronDetails, + LoanDetails, + Quit +} diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/Json/Authors.json b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/Json/Authors.json new file mode 100644 index 0000000..1357eb1 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/Json/Authors.json @@ -0,0 +1,82 @@ +[ + { + "Id": 1, + "Name": "Author One" + }, + { + "Id": 2, + "Name": "Author Two" + }, + { + "Id": 3, + "Name": "Author Three" + }, + { + "Id": 4, + "Name": "Author Four" + }, + { + "Id": 5, + "Name": "Author Five" + }, + { + "Id": 6, + "Name": "Author Six" + }, + { + "Id": 7, + "Name": "Author Seven" + }, + { + "Id": 8, + "Name": "Author Eight" + }, + { + "Id": 9, + "Name": "Author Nine" + }, + { + "Id": 10, + "Name": "Author Ten" + }, + { + "Id": 11, + "Name": "Author Eleven" + }, + { + "Id": 12, + "Name": "Author Twelve" + }, + { + "Id": 13, + "Name": "Author Thirteen" + }, + { + "Id": 14, + "Name": "Author Fourteen" + }, + { + "Id": 15, + "Name": "Author Fifteen" + }, + { + "Id": 16, + "Name": "Author Sixteen" + }, + { + "Id": 17, + "Name": "Author Seventeen" + }, + { + "Id": 18, + "Name": "Author Eighteen" + }, + { + "Id": 19, + "Name": "Author Nineteen" + }, + { + "Id": 20, + "Name": "Author Twenty" + } +] \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/Json/BookItems.json b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/Json/BookItems.json new file mode 100644 index 0000000..ed659c5 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/Json/BookItems.json @@ -0,0 +1,122 @@ +[ + { + "Id": 1, + "BookId": 1, + "AcquisitionDate": "2023-09-20T00:40:43.1716563", + "Condition": "Good" + }, + { + "Id": 2, + "BookId": 2, + "AcquisitionDate": "2023-09-20T00:40:43.1717503", + "Condition": "Fair" + }, + { + "Id": 3, + "BookId": 3, + "AcquisitionDate": "2023-09-20T00:40:43.1717511", + "Condition": "Excellent" + }, + { + "Id": 4, + "BookId": 4, + "AcquisitionDate": "2023-09-20T00:40:43.1717513", + "Condition": "Poor" + }, + { + "Id": 5, + "BookId": 5, + "AcquisitionDate": "2023-09-20T00:40:43.1717516", + "Condition": "Good" + }, + { + "Id": 6, + "BookId": 6, + "AcquisitionDate": "2023-09-20T00:40:43.1717521", + "Condition": "Fair" + }, + { + "Id": 7, + "BookId": 7, + "AcquisitionDate": "2023-09-20T00:40:43.1717523", + "Condition": "Excellent" + }, + { + "Id": 8, + "BookId": 8, + "AcquisitionDate": "2023-09-20T00:40:43.1717526", + "Condition": "Poor" + }, + { + "Id": 9, + "BookId": 9, + "AcquisitionDate": "2023-09-20T00:40:43.171757", + "Condition": "Good" + }, + { + "Id": 10, + "BookId": 10, + "AcquisitionDate": "2023-09-20T00:40:43.1717574", + "Condition": "Fair" + }, + { + "Id": 11, + "BookId": 11, + "AcquisitionDate": "2023-09-20T00:40:43.1717576", + "Condition": "Excellent" + }, + { + "Id": 12, + "BookId": 12, + "AcquisitionDate": "2023-09-20T00:40:43.1717578", + "Condition": "Poor" + }, + { + "Id": 13, + "BookId": 13, + "AcquisitionDate": "2023-09-20T00:40:43.171758", + "Condition": "Good" + }, + { + "Id": 14, + "BookId": 14, + "AcquisitionDate": "2023-09-20T00:40:43.1717609", + "Condition": "Fair" + }, + { + "Id": 15, + "BookId": 15, + "AcquisitionDate": "2023-09-20T00:40:43.1717611", + "Condition": "Excellent" + }, + { + "Id": 16, + "BookId": 16, + "AcquisitionDate": "2023-09-20T00:40:43.1717613", + "Condition": "Poor" + }, + { + "Id": 17, + "BookId": 17, + "AcquisitionDate": "2023-09-20T00:40:43.1717616", + "Condition": "Good" + }, + { + "Id": 18, + "BookId": 18, + "AcquisitionDate": "2023-09-20T00:40:43.1717619", + "Condition": "Fair" + }, + { + "Id": 19, + "BookId": 19, + "AcquisitionDate": "2023-09-20T00:40:43.1717621", + "Condition": "Excellent" + }, + { + "Id": 20, + "BookId": 20, + "AcquisitionDate": "2023-09-20T00:40:43.1717626", + "Condition": "Poor" + } +] \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/Json/Books.json b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/Json/Books.json new file mode 100644 index 0000000..51f3339 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/Json/Books.json @@ -0,0 +1,162 @@ +[ + { + "Id": 1, + "Title": "Book One", + "AuthorId": 1, + "Genre": "Dystopian", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524935" + }, + { + "Id": 2, + "Title": "Book Two", + "AuthorId": 2, + "Genre": "Classic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524936" + }, + { + "Id": 3, + "Title": "Book Three", + "AuthorId": 3, + "Genre": "Romance", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524937" + }, + { + "Id": 4, + "Title": "Book Four", + "AuthorId": 4, + "Genre": "Classic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524938" + }, + { + "Id": 5, + "Title": "Book Five", + "AuthorId": 5, + "Genre": "Coming-of-age", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524939" + }, + { + "Id": 6, + "Title": "Book Six", + "AuthorId": 6, + "Genre": "Modernist", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524940" + }, + { + "Id": 7, + "Title": "Book Seven", + "AuthorId": 7, + "Genre": "Adventure", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524941" + }, + { + "Id": 8, + "Title": "Book Eight", + "AuthorId": 8, + "Genre": "Fantasy", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524942" + }, + { + "Id": 9, + "Title": "Book Nine", + "AuthorId": 9, + "Genre": "Fantasy", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524943" + }, + { + "Id": 10, + "Title": "Book Ten", + "AuthorId": 10, + "Genre": "Epic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524944" + }, + { + "Id": 11, + "Title": "Book Eleven", + "AuthorId": 11, + "Genre": "Psychological", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524945" + }, + { + "Id": 12, + "Title": "Book Twelve", + "AuthorId": 12, + "Genre": "Psychological", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524946" + }, + { + "Id": 13, + "Title": "Book Thirteen", + "AuthorId": 13, + "Genre": "Magical realism", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524947" + }, + { + "Id": 14, + "Title": "Book Fourteen", + "AuthorId": 14, + "Genre": "Classic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524948" + }, + { + "Id": 15, + "Title": "Book Fifteen", + "AuthorId": 15, + "Genre": "Adventure", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524949" + }, + { + "Id": 16, + "Title": "Book Sixteen", + "AuthorId": 16, + "Genre": "Historical", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524950" + }, + { + "Id": 17, + "Title": "Book Seventeen", + "AuthorId": 17, + "Genre": "Gothic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524951" + }, + { + "Id": 18, + "Title": "Book Eighteen", + "AuthorId": 18, + "Genre": "Dystopian", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524952" + }, + { + "Id": 19, + "Title": "Book Nineteen", + "AuthorId": 19, + "Genre": "Classic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524953" + }, + { + "Id": 20, + "Title": "Book Twenty", + "AuthorId": 20, + "Genre": "Adventure", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524954" + } +] \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/Json/Loans.json b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/Json/Loans.json new file mode 100644 index 0000000..961ad6d --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/Json/Loans.json @@ -0,0 +1,402 @@ +[ + { + "Id": 1, + "BookItemId": 17, + "PatronId": 22, + "LoanDate": "2023-12-08T00:40:43.1808862", + "DueDate": "2023-12-22T00:40:43.1808862", + "ReturnDate": null + }, + { + "Id": 2, + "BookItemId": 6, + "PatronId": 28, + "LoanDate": "2023-12-17T00:40:43.1809243", + "DueDate": "2023-12-31T00:40:43.1809243", + "ReturnDate": null + }, + { + "Id": 3, + "BookItemId": 16, + "PatronId": 4, + "LoanDate": "2023-12-23T00:40:43.1809289", + "DueDate": "2024-01-06T00:40:43.1809289", + "ReturnDate": null + }, + { + "Id": 4, + "BookItemId": 17, + "PatronId": 14, + "LoanDate": "2023-12-22T00:40:43.1809292", + "DueDate": "2024-01-05T00:40:43.1809292", + "ReturnDate": null + }, + { + "Id": 5, + "BookItemId": 6, + "PatronId": 9, + "LoanDate": "2023-12-09T00:40:43.1809295", + "DueDate": "2023-12-23T00:40:43.1809295", + "ReturnDate": null + }, + { + "Id": 6, + "BookItemId": 14, + "PatronId": 25, + "LoanDate": "2023-12-27T00:40:43.18093", + "DueDate": "2024-01-10T00:40:43.18093", + "ReturnDate": null + }, + { + "Id": 7, + "BookItemId": 12, + "PatronId": 50, + "LoanDate": "2023-12-27T00:40:43.1809304", + "DueDate": "2024-01-10T00:40:43.1809304", + "ReturnDate": null + }, + { + "Id": 8, + "BookItemId": 18, + "PatronId": 28, + "LoanDate": "2023-12-26T00:40:43.1809306", + "DueDate": "2024-01-09T00:40:43.1809306", + "ReturnDate": null + }, + { + "Id": 9, + "BookItemId": 8, + "PatronId": 9, + "LoanDate": "2023-12-10T00:40:43.1809309", + "DueDate": "2023-12-24T00:40:43.1809309", + "ReturnDate": null + }, + { + "Id": 10, + "BookItemId": 16, + "PatronId": 3, + "LoanDate": "2023-12-26T00:40:43.1809312", + "DueDate": "2024-01-09T00:40:43.1809312", + "ReturnDate": null + }, + { + "Id": 11, + "BookItemId": 4, + "PatronId": 42, + "LoanDate": "2023-12-15T00:40:43.1809315", + "DueDate": "2023-12-29T00:40:43.1809315", + "ReturnDate": null + }, + { + "Id": 12, + "BookItemId": 17, + "PatronId": 7, + "LoanDate": "2023-12-23T00:40:43.1809331", + "DueDate": "2024-01-06T00:40:43.1809331", + "ReturnDate": null + }, + { + "Id": 13, + "BookItemId": 12, + "PatronId": 5, + "LoanDate": "2023-12-27T00:40:43.1809333", + "DueDate": "2024-01-10T00:40:43.1809333", + "ReturnDate": null + }, + { + "Id": 14, + "BookItemId": 4, + "PatronId": 9, + "LoanDate": "2023-12-10T00:40:43.1809337", + "DueDate": "2023-12-24T00:40:43.1809337", + "ReturnDate": null + }, + { + "Id": 15, + "BookItemId": 7, + "PatronId": 28, + "LoanDate": "2023-12-23T00:40:43.1809339", + "DueDate": "2024-01-06T00:40:43.1809339", + "ReturnDate": null + }, + { + "Id": 16, + "BookItemId": 14, + "PatronId": 3, + "LoanDate": "2023-12-08T00:40:43.1809342", + "DueDate": "2023-12-22T00:40:43.1809342", + "ReturnDate": null + }, + { + "Id": 17, + "BookItemId": 5, + "PatronId": 48, + "LoanDate": "2023-12-16T00:40:43.1809344", + "DueDate": "2023-12-30T00:40:43.1809344", + "ReturnDate": null + }, + { + "Id": 18, + "BookItemId": 4, + "PatronId": 49, + "LoanDate": "2023-12-19T00:40:43.1809348", + "DueDate": "2024-01-02T00:40:43.1809348", + "ReturnDate": null + }, + { + "Id": 19, + "BookItemId": 13, + "PatronId": 33, + "LoanDate": "2023-12-28T00:40:43.180935", + "DueDate": "2024-01-11T00:40:43.180935", + "ReturnDate": null + }, + { + "Id": 20, + "BookItemId": 14, + "PatronId": 48, + "LoanDate": "2023-12-27T00:40:43.1809353", + "DueDate": "2024-01-10T00:40:43.1809353", + "ReturnDate": null + }, + { + "Id": 21, + "BookItemId": 7, + "PatronId": 5, + "LoanDate": "2023-12-12T00:40:43.1809368", + "DueDate": "2023-12-26T00:40:43.1809368", + "ReturnDate": null + }, + { + "Id": 22, + "BookItemId": 9, + "PatronId": 1, + "LoanDate": "2023-12-09T00:40:43.1809371", + "DueDate": "2023-12-23T00:40:43.1809371", + "ReturnDate": null + }, + { + "Id": 23, + "BookItemId": 11, + "PatronId": 33, + "LoanDate": "2023-12-26T00:40:43.1809374", + "DueDate": "2024-01-09T00:40:43.1809374", + "ReturnDate": null + }, + { + "Id": 24, + "BookItemId": 10, + "PatronId": 46, + "LoanDate": "2023-12-28T00:40:43.1809376", + "DueDate": "2024-01-11T00:40:43.1809376", + "ReturnDate": null + }, + { + "Id": 25, + "BookItemId": 20, + "PatronId": 41, + "LoanDate": "2023-12-12T00:40:43.1809379", + "DueDate": "2023-12-26T00:40:43.1809379", + "ReturnDate": null + }, + { + "Id": 26, + "BookItemId": 13, + "PatronId": 15, + "LoanDate": "2023-12-16T00:40:43.1809382", + "DueDate": "2023-12-30T00:40:43.1809382", + "ReturnDate": null + }, + { + "Id": 27, + "BookItemId": 15, + "PatronId": 23, + "LoanDate": "2023-12-18T00:40:43.1809384", + "DueDate": "2024-01-01T00:40:43.1809384", + "ReturnDate": null + }, + { + "Id": 28, + "BookItemId": 15, + "PatronId": 31, + "LoanDate": "2023-12-11T00:40:43.1809387", + "DueDate": "2023-12-25T00:40:43.1809387", + "ReturnDate": null + }, + { + "Id": 29, + "BookItemId": 4, + "PatronId": 10, + "LoanDate": "2023-12-18T00:40:43.1809402", + "DueDate": "2024-01-01T00:40:43.1809402", + "ReturnDate": null + }, + { + "Id": 30, + "BookItemId": 6, + "PatronId": 18, + "LoanDate": "2023-12-12T00:40:43.1809405", + "DueDate": "2023-12-26T00:40:43.1809405", + "ReturnDate": null + }, + { + "Id": 31, + "BookItemId": 11, + "PatronId": 3, + "LoanDate": "2023-12-16T00:40:43.1809408", + "DueDate": "2023-12-30T00:40:43.1809408", + "ReturnDate": null + }, + { + "Id": 32, + "BookItemId": 8, + "PatronId": 20, + "LoanDate": "2023-12-22T00:40:43.1809411", + "DueDate": "2024-01-05T00:40:43.1809411", + "ReturnDate": null + }, + { + "Id": 33, + "BookItemId": 14, + "PatronId": 12, + "LoanDate": "2023-12-28T00:40:43.1809415", + "DueDate": "2024-01-11T00:40:43.1809415", + "ReturnDate": null + }, + { + "Id": 34, + "BookItemId": 19, + "PatronId": 29, + "LoanDate": "2023-12-28T00:40:43.1809458", + "DueDate": "2024-01-11T00:40:43.1809458", + "ReturnDate": "2023-12-29T00:40:54.582495" + }, + { + "Id": 35, + "BookItemId": 7, + "PatronId": 45, + "LoanDate": "2023-12-17T00:40:43.180946", + "DueDate": "2023-12-31T00:40:43.180946", + "ReturnDate": null + }, + { + "Id": 36, + "BookItemId": 11, + "PatronId": 3, + "LoanDate": "2023-12-10T00:40:43.1809463", + "DueDate": "2023-12-24T00:40:43.1809463", + "ReturnDate": null + }, + { + "Id": 37, + "BookItemId": 1, + "PatronId": 5, + "LoanDate": "2023-12-18T00:40:43.1809466", + "DueDate": "2024-01-18T00:40:43.1809466", + "ReturnDate": "2024-01-17T00:40:43.1809466" + }, + { + "Id": 38, + "BookItemId": 15, + "PatronId": 25, + "LoanDate": "2023-12-26T00:40:43.1809481", + "DueDate": "2024-01-09T00:40:43.1809481", + "ReturnDate": null + }, + { + "Id": 39, + "BookItemId": 4, + "PatronId": 33, + "LoanDate": "2023-12-18T00:40:43.1809484", + "DueDate": "2024-01-01T00:40:43.1809484", + "ReturnDate": null + }, + { + "Id": 40, + "BookItemId": 5, + "PatronId": 33, + "LoanDate": "2023-12-25T00:40:43.1809487", + "DueDate": "2024-01-08T00:40:43.1809487", + "ReturnDate": null + }, + { + "Id": 41, + "BookItemId": 14, + "PatronId": 13, + "LoanDate": "2023-12-15T00:40:43.1809489", + "DueDate": "2023-12-29T00:40:43.1809489", + "ReturnDate": null + }, + { + "Id": 42, + "BookItemId": 11, + "PatronId": 10, + "LoanDate": "2023-12-12T00:40:43.1809493", + "DueDate": "2023-12-26T00:40:43.1809493", + "ReturnDate": null + }, + { + "Id": 43, + "BookItemId": 9, + "PatronId": 45, + "LoanDate": "2023-12-14T00:40:43.1809496", + "DueDate": "2023-12-28T00:40:43.1809496", + "ReturnDate": "2023-12-29T00:49:42.3406277" + }, + { + "Id": 44, + "BookItemId": 3, + "PatronId": 46, + "LoanDate": "2023-12-08T00:40:43.1809498", + "DueDate": "2023-12-22T00:40:43.1809498", + "ReturnDate": null + }, + { + "Id": 45, + "BookItemId": 5, + "PatronId": 10, + "LoanDate": "2023-12-24T00:40:43.1809501", + "DueDate": "2024-01-07T00:40:43.1809501", + "ReturnDate": null + }, + { + "Id": 46, + "BookItemId": 1, + "PatronId": 49, + "LoanDate": "2024-07-09T00:40:43.1809503", + "DueDate": "2024-09-09T00:40:43.1809503", + "ReturnDate": null + }, + { + "Id": 47, + "BookItemId": 8, + "PatronId": 36, + "LoanDate": "2023-12-11T00:40:43.1809507", + "DueDate": "2023-12-25T00:40:43.1809507", + "ReturnDate": null + }, + { + "Id": 48, + "BookItemId": 5, + "PatronId": 10, + "LoanDate": "2023-12-18T00:40:43.1809509", + "DueDate": "2024-01-01T00:40:43.1809509", + "ReturnDate": null + }, + { + "Id": 49, + "BookItemId": 20, + "PatronId": 24, + "LoanDate": "2023-12-16T00:40:43.1809512", + "DueDate": "2023-12-30T00:40:43.1809512", + "ReturnDate": null + }, + { + "Id": 50, + "BookItemId": 2, + "PatronId": 45, + "LoanDate": "2023-12-13T00:40:43.1809514", + "DueDate": "2023-12-27T00:40:43.1809514", + "ReturnDate": "2023-12-29T00:49:48.9561798" + } + ] diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/Json/Patrons.json b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/Json/Patrons.json new file mode 100644 index 0000000..5d44d83 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/Json/Patrons.json @@ -0,0 +1,352 @@ +[ + { + "Id": 1, + "Name": "Patron One", + "MembershipEnd": "2024-12-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron One.jpg" + }, + { + "Id": 2, + "Name": "Patron Two", + "MembershipEnd": "2025-02-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Two.jpg" + }, + { + "Id": 3, + "Name": "Patron Three", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Three.jpg" + }, + { + "Id": 4, + "Name": "Patron Four", + "MembershipEnd": "2025-04-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Four.jpg" + }, + { + "Id": 5, + "Name": "Patron Five", + "MembershipEnd": "2025-05-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Five.jpg" + }, + { + "Id": 6, + "Name": "Patron Six", + "MembershipEnd": "2025-06-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Six.jpg" + }, + { + "Id": 7, + "Name": "Patron Seven", + "MembershipEnd": "2025-07-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Seven.jpg" + }, + { + "Id": 8, + "Name": "Patron Eight", + "MembershipEnd": "2024-01-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Eight.jpg" + }, + { + "Id": 9, + "Name": "Patron Nine", + "MembershipEnd": "2024-02-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Nine.jpg" + }, + { + "Id": 10, + "Name": "Patron Ten", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Ten.jpg" + }, + { + "Id": 11, + "Name": "Patron Eleven", + "MembershipEnd": "2024-04-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Eleven.jpg" + }, + { + "Id": 12, + "Name": "Patron Twelve", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twelve.jpg" + }, + { + "Id": 13, + "Name": "Patron Thirteen", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirteen.jpg" + }, + { + "Id": 14, + "Name": "Patron Fourteen", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Fourteen.jpg" + }, + { + "Id": 15, + "Name": "Patron Fifteen", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Fifteen.jpg" + }, + { + "Id": 16, + "Name": "Patron Sixteen", + "MembershipEnd": "2024-04-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Sixteen.jpg" + }, + { + "Id": 17, + "Name": "Patron Seventeen", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Seventeen.jpg" + }, + { + "Id": 18, + "Name": "Patron Eighteen", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Eighteen.jpg" + }, + { + "Id": 19, + "Name": "Patron Nineteen", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Nineteen.jpg" + }, + { + "Id": 20, + "Name": "Patron Twenty", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty.jpg" + }, + { + "Id": 21, + "Name": "Patron Twenty-One", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-One.jpg" + }, + { + "Id": 22, + "Name": "Patron Twenty-Two", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Two.jpg" + }, + { + "Id": 23, + "Name": "Patron Twenty-Three", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Three.jpg" + }, + { + "Id": 24, + "Name": "Patron Twenty-Four", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Four.jpg" + }, + { + "Id": 25, + "Name": "Patron Twenty-Five", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Five.jpg" + }, + { + "Id": 26, + "Name": "Patron Twenty-Six", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Six.jpg" + }, + { + "Id": 27, + "Name": "Patron Twenty-Seven", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Seven.jpg" + }, + { + "Id": 28, + "Name": "Patron Twenty-Eight", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Eight.jpg" + }, + { + "Id": 29, + "Name": "Patron Twenty-Nine", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Nine.jpg" + }, + { + "Id": 30, + "Name": "Patron Thirty", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty.jpg" + }, + { + "Id": 31, + "Name": "Patron Thirty-One", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-One.jpg" + }, + { + "Id": 32, + "Name": "Patron Thirty-Two", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Two.jpg" + }, + { + "Id": 33, + "Name": "Patron Thirty-Three", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Three.jpg" + }, + { + "Id": 34, + "Name": "Patron Thirty-Four", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Four.jpg" + }, + { + "Id": 35, + "Name": "Patron Thirty-Five", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Five.jpg" + }, + { + "Id": 36, + "Name": "Patron Thirty-Six", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Six.jpg" + }, + { + "Id": 37, + "Name": "Patron Thirty-Seven", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Seven.jpg" + }, + { + "Id": 38, + "Name": "Patron Thirty-Eight", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Eight.jpg" + }, + { + "Id": 39, + "Name": "Patron Thirty-Nine", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Nine.jpg" + }, + { + "Id": 40, + "Name": "Patron Forty", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty.jpg" + }, + { + "Id": 41, + "Name": "Patron Forty-One", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-One.jpg" + }, + { + "Id": 42, + "Name": "Patron Forty-Two", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Two.jpg" + }, + { + "Id": 43, + "Name": "Patron Forty-Three", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Three.jpg" + }, + { + "Id": 44, + "Name": "Patron Forty-Four", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Four.jpg" + }, + { + "Id": 45, + "Name": "Patron Forty-Five", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Five.jpg" + }, + { + "Id": 46, + "Name": "Patron Forty-Six", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Six.jpg" + }, + { + "Id": 47, + "Name": "Patron Forty-Seven", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Seven.jpg" + }, + { + "Id": 48, + "Name": "Patron Forty-Eight", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Eight.jpg" + }, + { + "Id": 49, + "Name": "Patron Forty-Nine", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Nine.jpg" + }, + { + "Id": 50, + "Name": "Patron Fifty", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Fifty.jpg" + } +] \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/Library.Console.csproj b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/Library.Console.csproj new file mode 100644 index 0000000..359cee9 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/Library.Console.csproj @@ -0,0 +1,34 @@ + + + + + + + + + Exe + net9.0 + enable + enable + + + + + PreserveNewest + + + + + + PreserveNewest + + + + + + + + + + + diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/Program.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/Program.cs new file mode 100644 index 0000000..1a21671 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/Program.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.DependencyInjection; +using Library.Infrastructure.Data; +using Library.ApplicationCore; +using Microsoft.Extensions.Configuration; + +var services = new ServiceCollection(); + +var configuration = new ConfigurationBuilder() +.SetBasePath(Directory.GetCurrentDirectory()) +.AddJsonFile("appSettings.json") +.Build(); + +services.AddSingleton(configuration); + +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); + +services.AddSingleton(); +services.AddSingleton(); + +var servicesProvider = services.BuildServiceProvider(); + +var consoleApp = servicesProvider.GetRequiredService(); +consoleApp.Run().Wait(); diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/appSettings.json b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/appSettings.json new file mode 100644 index 0000000..3aed751 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Console/appSettings.json @@ -0,0 +1,9 @@ +{ + "JsonPaths": { + "Authors": "Json/Authors.json", + "Books": "Json/Books.json", + "BookItems": "Json/BookItems.json", + "Patrons": "Json/Patrons.json", + "Loans": "Json/Loans.json" + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonData.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonData.cs new file mode 100644 index 0000000..7af26a7 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonData.cs @@ -0,0 +1,212 @@ +using System.Text.Json; +using Library.ApplicationCore.Entities; +using Microsoft.Extensions.Configuration; + +namespace Library.Infrastructure.Data; + +public class JsonData +{ + public List? Authors { get; set; } + public List? Books { get; set; } + public List? BookItems { get; set; } + public List? Patrons { get; set; } + public List? Loans { get; set; } + + private readonly string _authorsPath; + private readonly string _booksPath; + private readonly string _bookItemsPath; + private readonly string _patronsPath; + private readonly string _loansPath; + + public JsonData(IConfiguration configuration) + { + var section = configuration.GetSection("JsonPaths"); + _authorsPath = section["Authors"] ?? Path.Combine("Json", "Authors.json"); + _booksPath = section["Books"] ?? Path.Combine("Json", "Books.json"); + _bookItemsPath = section["BookItems"] ?? Path.Combine("Json", "BookItems.json"); + _patronsPath = section["Patrons"] ?? Path.Combine("Json", "Patrons.json"); + _loansPath = section["Loans"] ?? Path.Combine("Json", "Loans.json"); + } + + public async Task EnsureDataLoaded() + { + if (Patrons == null) + { + await LoadData(); + } + } + + public async Task LoadData() + { + Authors = await LoadJson>(_authorsPath); + Books = await LoadJson>(_booksPath); + BookItems = await LoadJson>(_bookItemsPath); + Patrons = await LoadJson>(_patronsPath); + Loans = await LoadJson>(_loansPath); + } + + public async Task SaveLoans(IEnumerable loans) + { + List loanList = new List(); + foreach (var l in loans) + { + Loan loan = new Loan + { + // making sure only a subset of properties is set and saved + Id = l.Id, + BookItemId = l.BookItemId, + PatronId = l.PatronId, + LoanDate = l.LoanDate, + DueDate = l.DueDate, + ReturnDate = l.ReturnDate + }; + loanList.Add(loan); + } + await SaveJson(_loansPath, loanList); + } + + public async Task SavePatrons(IEnumerable patrons) + { + await SaveJson(_patronsPath, patrons.Select(p => new Patron + { + Id = p.Id, + Name = p.Name, + MembershipStart = p.MembershipStart, + MembershipEnd = p.MembershipEnd, + ImageName = p.ImageName, + }).ToList()); + } + + private async Task SaveJson(string filePath, T data) + { + using (FileStream jsonStream = File.Create(filePath)) + { + await JsonSerializer.SerializeAsync(jsonStream, data); + } + } + + public List GetPopulatedPatrons(IEnumerable patrons) + { + List populated = new List(); + foreach (Patron patron in patrons) + { + populated.Add(GetPopulatedPatron(patron)); + } + return populated; + } + + public Patron GetPopulatedPatron(Patron p) + { + Patron populated = new Patron + { + Id = p.Id, + Name = p.Name, + ImageName = p.ImageName, + MembershipStart = p.MembershipStart, + MembershipEnd = p.MembershipEnd, + Loans = new List() + }; + + foreach (Loan loan in Loans!) + { + if (loan.PatronId == p.Id) + { + populated.Loans.Add(GetPopulatedLoan(loan)); + } + } + + return populated; + } + + public Loan GetPopulatedLoan(Loan l) + { + Loan populated = new Loan + { + Id = l.Id, + BookItemId = l.BookItemId, + PatronId = l.PatronId, + LoanDate = l.LoanDate, + DueDate = l.DueDate, + ReturnDate = l.ReturnDate + }; + + foreach (BookItem bi in BookItems!) + { + if (bi.Id == l.BookItemId) + { + populated.BookItem = GetPopulatedBookItem(bi); + break; + } + } + + foreach (Patron p in Patrons!) + { + if (p.Id == l.PatronId) + { + populated.Patron = p; + break; + } + } + + return populated; + } + + public BookItem GetPopulatedBookItem(BookItem bi) + { + BookItem populated = new BookItem + { + Id = bi.Id, + BookId = bi.BookId, + AcquisitionDate = bi.AcquisitionDate, + Condition = bi.Condition + }; + + foreach (Book b in Books!) + { + if (b.Id == bi.BookId) + { + populated.Book = GetPopulatedBook(b); + break; + } + } + + return populated; + } + + public Book GetPopulatedBook(Book b) + { + Book populated = new Book + { + Id = b.Id, + Title = b.Title, + AuthorId = b.AuthorId, + Genre = b.Genre, + ISBN = b.ISBN, + ImageName = b.ImageName + }; + + foreach (Author a in Authors!) + { + if (a.Id == b.AuthorId) + { + populated.Author = new Author + { + Id = a.Id, + Name = a.Name + }; + break; + } + } + + return populated; + } + + private async Task LoadJson(string filePath) + { + using (FileStream jsonStream = File.OpenRead(filePath)) + { + return await JsonSerializer.DeserializeAsync(jsonStream); + } + } + +} diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonLoanRepository.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonLoanRepository.cs new file mode 100644 index 0000000..2683283 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonLoanRepository.cs @@ -0,0 +1,55 @@ +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; + +namespace Library.Infrastructure.Data; + +public class JsonLoanRepository : ILoanRepository +{ + private readonly JsonData _jsonData; + + public JsonLoanRepository(JsonData jsonData) + { + _jsonData = jsonData; + } + + public async Task GetLoan(int id) + { + await _jsonData.EnsureDataLoaded(); + + foreach (Loan loan in _jsonData.Loans!) + { + if (loan.Id == id) + { + Loan populated = _jsonData.GetPopulatedLoan(loan); + return populated; + } + } + return null; + } + + public async Task UpdateLoan(Loan loan) + { + Loan? existingLoan = null; + foreach (Loan l in _jsonData.Loans!) + { + if (l.Id == loan.Id) + { + existingLoan = l; + break; + } + } + + if (existingLoan != null) + { + existingLoan.BookItemId = loan.BookItemId; + existingLoan.PatronId = loan.PatronId; + existingLoan.LoanDate = loan.LoanDate; + existingLoan.DueDate = loan.DueDate; + existingLoan.ReturnDate = loan.ReturnDate; + + await _jsonData.SaveLoans(_jsonData.Loans!); + + await _jsonData.LoadData(); + } + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonPatronRepository.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonPatronRepository.cs new file mode 100644 index 0000000..efb05f8 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonPatronRepository.cs @@ -0,0 +1,73 @@ +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; + +namespace Library.Infrastructure.Data; + +public class JsonPatronRepository : IPatronRepository +{ + private readonly JsonData _jsonData; + + public JsonPatronRepository(JsonData jsonData) + { + _jsonData = jsonData; + } + + public async Task> SearchPatrons(string searchInput) + { + await _jsonData.EnsureDataLoaded(); + + List searchResults = new List(); + foreach (Patron patron in _jsonData.Patrons) + { + if (patron.Name.Contains(searchInput)) + { + searchResults.Add(patron); + } + } + searchResults.Sort((p1, p2) => String.Compare(p1.Name, p2.Name)); + + searchResults = _jsonData.GetPopulatedPatrons(searchResults); + + return searchResults; + } + + public async Task GetPatron(int id) + { + await _jsonData.EnsureDataLoaded(); + + foreach (Patron patron in _jsonData.Patrons!) + { + if (patron.Id == id) + { + Patron populated = _jsonData.GetPopulatedPatron(patron); + return populated; + } + } + return null; + } + + public async Task UpdatePatron(Patron patron) + { + await _jsonData.EnsureDataLoaded(); + var patrons = _jsonData.Patrons!; + Patron existingPatron = null; + foreach (var p in patrons) + { + if (p.Id == patron.Id) + { + existingPatron = p; + break; + } + } + if (existingPatron != null) + { + existingPatron.Name = patron.Name; + existingPatron.ImageName = patron.ImageName; + existingPatron.MembershipStart = patron.MembershipStart; + existingPatron.MembershipEnd = patron.MembershipEnd; + existingPatron.Loans = patron.Loans; + await _jsonData.SavePatrons(patrons); + await _jsonData.LoadData(); + } + } +} diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Library.Infrastructure.csproj b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Library.Infrastructure.csproj new file mode 100644 index 0000000..1a7e6eb --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Library.Infrastructure.csproj @@ -0,0 +1,17 @@ + + + + + + + + + + + + net9.0 + enable + enable + + + diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ExtendLoan.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ExtendLoan.cs new file mode 100644 index 0000000..d3e695b --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ExtendLoan.cs @@ -0,0 +1,104 @@ +using NSubstitute; +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; + +namespace Library.UnitTests.ApplicationCore.LoanServiceTests; + +public class ExtendLoanTest +{ + private readonly ILoanRepository _mockLoanRepository; + private readonly LoanService _loanService; + + public ExtendLoanTest() + { + _mockLoanRepository = Substitute.For(); + _loanService = new LoanService(_mockLoanRepository); + } + + [Fact(DisplayName = "LoanService.ExtendLoan: Extends the loan successfully")] + public async Task ExtendLoan_ExtendsLoanSuccessfully() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateCurrentLoanForPatron(patron); + var loanDueDate = loan.DueDate; + var loanId = loan.Id; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanExtensionStatus extensionStatus = await _loanService.ExtendLoan(loanId); + + // Assert + Assert.Equal(LoanExtensionStatus.Success, extensionStatus); + Assert.Equal(loanDueDate.AddDays(LoanService.ExtendByDays), loan.DueDate); + } + + [Fact(DisplayName = "LoanService.ExtendLoan: Returns LoanNotFound if loan is not found")] + public async Task ExtendLoan_ReturnsLoanNotFound() + { + // Arrange + var loanId = 1; + _mockLoanRepository.GetLoan(loanId).Returns((Loan?)null); + + // Act + LoanExtensionStatus extensionStatus = await _loanService.ExtendLoan(loanId); + + // Assert + Assert.Equal(LoanExtensionStatus.LoanNotFound, extensionStatus); + } + + [Fact(DisplayName = "LoanService.ExtendLoan: Returns MembershipExpired if patron's membership is expired")] + public async Task ExtendLoan_ReturnsMembershipExpired() + { + // Arrange + var patron = PatronFactory.CreateExpiredPatron(); + var loan = LoanFactory.CreateCurrentLoanForPatron(patron); + var loanId = loan.Id; + var loanDueDate = loan.DueDate; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanExtensionStatus extensionStatus = await _loanService.ExtendLoan(loanId); + + // Assert + Assert.Equal(LoanExtensionStatus.MembershipExpired, extensionStatus); + Assert.Equal(loanDueDate, loan.DueDate); + } + + [Fact(DisplayName = "LoanService.ExtendLoan: Returns LoanReturned if loan is already returned")] + public async Task ExtendLoan_ReturnsLoanReturned() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateReturnedLoanForPatron(patron); + var loanId = loan.Id; + var loanDueDate = loan.DueDate; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanExtensionStatus extensionStatus = await _loanService.ExtendLoan(loanId); + + // Assert + Assert.Equal(LoanExtensionStatus.LoanReturned, extensionStatus); + Assert.Equal(loanDueDate, loan.DueDate); + } + + [Fact(DisplayName = "LoanService.ExtendLoan: Returns LoanExpired if loan is already expired")] + public async Task ExtendLoan_ReturnsLoanExpired() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateExpiredLoanForPatron(patron); + var loanId = loan.Id; + var loanDueDate = loan.DueDate; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanExtensionStatus extensionStatus = await _loanService.ExtendLoan(loanId); + + // Assert + Assert.Equal(LoanExtensionStatus.LoanExpired, extensionStatus); + Assert.Equal(loanDueDate, loan.DueDate); + } +} diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ReturnLoan.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ReturnLoan.cs new file mode 100644 index 0000000..68c3a0d --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ReturnLoan.cs @@ -0,0 +1,99 @@ +using NSubstitute; +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; + +namespace Library.UnitTests.ApplicationCore.LoanServiceTests; + +public class ReturnLoanTest +{ + private readonly ILoanRepository _mockLoanRepository; + private readonly LoanService _loanService; + + public ReturnLoanTest() + { + _mockLoanRepository = Substitute.For(); + _loanService = new LoanService(_mockLoanRepository); + } + + [Fact(DisplayName = "LoanService.ReturnLoan: Returns LoanNotFound if loan is not found")] + public async Task ReturnLoan_ReturnsLoanNotFound() + { + // Arrange + var loanId = 1; + _mockLoanRepository.GetLoan(loanId).Returns((Loan?)null); + + // Act + LoanReturnStatus returnStatus = await _loanService.ReturnLoan(loanId); + + // Assert + Assert.Equal(LoanReturnStatus.LoanNotFound, returnStatus); + } + + [Fact(DisplayName = "LoanService.ReturnLoan: Returns AlreadyReturned if loan is already returned")] + public async Task ReturnLoan_ReturnsAlreadyReturned() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateReturnedLoanForPatron(patron); + var loanId = loan.Id; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanReturnStatus returnStatus = await _loanService.ReturnLoan(loanId); + + // Assert + Assert.Equal(LoanReturnStatus.AlreadyReturned, returnStatus); + } + + [Fact(DisplayName = "LoanService.ReturnLoan: Returns Success and updates return date for a patron with current membership")] + public async Task ReturnLoan_ReturnsSuccessAndUpdateReturnDate() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateCurrentLoanForPatron(patron); + var loanId = loan.Id; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanReturnStatus returnStatus = await _loanService.ReturnLoan(loanId); + + // Assert + Assert.Equal(LoanReturnStatus.Success, returnStatus); + Assert.NotNull(loan.ReturnDate); + } + + [Fact(DisplayName = "LoanService.ReturnLoan: Returns Success and updates return date for an expired loan")] + public async Task ReturnLoan_ReturnsSuccessAndUpdateReturnDateForExpiredLoan() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateExpiredLoanForPatron(patron); + var loanId = loan.Id; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanReturnStatus returnStatus = await _loanService.ReturnLoan(loanId); + + // Assert + Assert.Equal(LoanReturnStatus.Success, returnStatus); + Assert.NotNull(loan.ReturnDate); + } + + [Fact(DisplayName = "LoanService.ReturnLoan: Returns Success and updates return date for a patron with expired membership")] + public async Task ReturnLoan_ReturnsSuccessAndUpdateReturnDateForExpiredPatron() + { + // Arrange + var patron = PatronFactory.CreateExpiredPatron(); + var loan = LoanFactory.CreateCurrentLoanForPatron(patron); + var loanId = loan.Id; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanReturnStatus returnStatus = await _loanService.ReturnLoan(loanId); + + // Assert + Assert.Equal(LoanReturnStatus.Success, returnStatus); + Assert.NotNull(loan.ReturnDate); + } +} diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/PatronService/RenewMembership.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/PatronService/RenewMembership.cs new file mode 100644 index 0000000..ff4d24c --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/PatronService/RenewMembership.cs @@ -0,0 +1,142 @@ +using NSubstitute; +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; + +namespace Library.UnitTests.ApplicationCore.PatronServiceTests; + +public class RenewMembershipTest +{ + private readonly IPatronRepository _mockPatronRepository; + private readonly PatronService _patronService; + + public RenewMembershipTest() + { + _mockPatronRepository = Substitute.For(); + _patronService = new PatronService(_mockPatronRepository); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Renews the membership successfully without loans")] + public async Task RenewMembership_RenewsMembershipSuccessfully() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var membershipEnd = patron.MembershipEnd; + var patronId = patron.Id; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.Success, renewalStatus); + Assert.Equal(membershipEnd.AddYears(1), patron.MembershipEnd); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Renews the membership successfully with expired membership")] + public async Task RenewMembership_RenewsMembershipSuccessfullyWithExpiredMembership() + { + // Arrange + //var membershipEnd = DateTime.Now.AddMonths(-2); + var patron = PatronFactory.CreateExpiredPatron(); + var membershipEnd = patron.MembershipEnd; + var patronId = patron.Id; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.Success, renewalStatus); + Assert.Equal(membershipEnd.AddYears(1), patron.MembershipEnd); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Renews the membership successfully with returned loans")] + public async Task RenewMembership_RenewsMembershipSuccessfullyWithReturnedLoans() + { + // Arrange + var membershipEnd = DateTime.Now.AddDays(1); + var patron = PatronFactory.CreateCurrentPatron(); + patron.MembershipEnd = membershipEnd; + var patronId = patron.Id; + patron.Loans = new List { + LoanFactory.CreateReturnedLoanForPatron(patron) + }; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.Success, renewalStatus); + Assert.Equal(membershipEnd.AddYears(1), patron.MembershipEnd); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Renews the membership successfully with current loans")] + public async Task RenewMembership_RenewsMembershipSuccessfullyWithCurrentLoans() + { + // Arrange + var membershipEnd = DateTime.Now.AddDays(1); + var patron = PatronFactory.CreateCurrentPatron(); + patron.MembershipEnd = membershipEnd; + var patronId = patron.Id; + patron.Loans = new List { + LoanFactory.CreateCurrentLoanForPatron(patron) + }; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.Success, renewalStatus); + Assert.Equal(membershipEnd.AddYears(1), patron.MembershipEnd); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Returns PatronNotFound if patron is not found")] + public async Task RenewMembership_ReturnsPatronNotFound() + { + // Arrange + var patronId = 42; + _mockPatronRepository.GetPatron(patronId).Returns((Patron?)null); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.PatronNotFound, renewalStatus); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Returns TooEarlyToRenew if renewal is not allowed yet")] + public async Task RenewMembership_ReturnsTooEarlyToRenew() + { + // Arrange + var patron = PatronFactory.CreateTooEarlyToRenewPatron(); + var patronId = patron.Id; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.TooEarlyToRenew, renewalStatus); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Returns LoanNotReturned if patron has overdue loans")] + public async Task RenewMembership_ReturnsLoanNotReturned() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var patronId = patron.Id; + patron.Loans = new List { + LoanFactory.CreateExpiredLoanForPatron(patron) + }; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.LoanNotReturned, renewalStatus); + } +} diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/tests/UnitTests/LoanFactory.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/tests/UnitTests/LoanFactory.cs new file mode 100644 index 0000000..251000d --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/tests/UnitTests/LoanFactory.cs @@ -0,0 +1,42 @@ +using Library.ApplicationCore.Entities; + +public static class LoanFactory +{ + public static int loanId = 777; + + public static Loan CreateReturnedLoanForPatron(Patron patron) + { + return new Loan + { + Id = loanId++, + DueDate = DateTime.Now.AddDays(1), + ReturnDate = DateTime.Now.AddDays(-1), + PatronId = patron.Id, + Patron = patron + }; + } + + public static Loan CreateCurrentLoanForPatron(Patron patron) + { + return new Loan + { + Id = loanId++, + DueDate = DateTime.Now.AddDays(1), + ReturnDate = null, + PatronId = patron.Id, + Patron = patron + }; + } + + public static Loan CreateExpiredLoanForPatron(Patron patron) + { + return new Loan + { + Id = loanId++, + DueDate = DateTime.Now.AddDays(-1), + ReturnDate = null, + PatronId = patron.Id, + Patron = patron + }; + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/tests/UnitTests/PatronFactory.cs b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/tests/UnitTests/PatronFactory.cs new file mode 100644 index 0000000..a9c36b7 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/tests/UnitTests/PatronFactory.cs @@ -0,0 +1,39 @@ +using Library.ApplicationCore.Entities; + +public static class PatronFactory +{ + public static int patronId = 42; + + public static Patron CreateCurrentPatron() + { + return new Patron + { + Id = patronId++, + Name = "John Doe", + MembershipEnd = DateTime.Now.AddDays(1), + Loans = new List() + }; + } + + public static Patron CreateTooEarlyToRenewPatron() + { + return new Patron + { + Id = patronId++, + Name = "John Doe", + MembershipEnd = DateTime.Now.AddMonths(2), + Loans = new List() + }; + } + + public static Patron CreateExpiredPatron() + { + return new Patron + { + Id = patronId++, + Name = "John Doe", + MembershipEnd = DateTime.Now.AddMonths(-2), + Loans = new List() + }; + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/tests/UnitTests/UnitTests.csproj b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/tests/UnitTests/UnitTests.csproj new file mode 100644 index 0000000..a156d8f --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/AccelerateDevGHCopilot/tests/UnitTests/UnitTests.csproj @@ -0,0 +1,28 @@ + + + + net9.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + diff --git a/DownloadableCodeProjects/az-2007-m3-develop-code/readme.txt b/DownloadableCodeProjects/az-2007-m3-develop-code/readme.txt new file mode 100644 index 0000000..b3a4252 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m3-develop-code/readme.txt @@ -0,0 +1 @@ +placeholder \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/.gitignore b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/.gitignore new file mode 100644 index 0000000..dc73868 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/.gitignore @@ -0,0 +1,68 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*.pyo +*.pyd + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +.env/ +.venv/ +env/ +venv/ +ENV/ +ENV*/ + +# PyInstaller +*.manifest +*.spec + +# Unit test / coverage / pytest +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.pytest_cache/ +test-results/ +junit-*.xml + +# Jupyter Notebook +.ipynb_checkpoints/ + +# mypy +.mypy_cache/ +.dmypy.json + +# Pyre type checker +.pyre/ + +# Pyright type checker +.pyrightcache/ + +# VS Code settings +.vscode/ \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/entities/author.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/entities/author.py new file mode 100644 index 0000000..dec0e83 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/entities/author.py @@ -0,0 +1,6 @@ +from dataclasses import dataclass + +@dataclass +class Author: + id: int + name: str diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/entities/book.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/entities/book.py new file mode 100644 index 0000000..50f4e38 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/entities/book.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass +from typing import Optional +from .author import Author + +@dataclass +class Book: + id: int + title: str + author_id: int + genre: str + image_name: str + isbn: str + author: Optional[Author] = None diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/entities/book_item.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/entities/book_item.py new file mode 100644 index 0000000..f5f7fb7 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/entities/book_item.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass +from typing import Optional +from datetime import datetime +from .book import Book + +@dataclass +class BookItem: + id: int + book_id: int + acquisition_date: datetime + condition: Optional[str] = None + book: Optional[Book] = None diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/entities/loan.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/entities/loan.py new file mode 100644 index 0000000..51955ea --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/entities/loan.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass +from typing import Optional +from datetime import datetime +from .patron import Patron +from .book_item import BookItem + +@dataclass +class Loan: + id: int + book_item_id: int + patron_id: int + patron: Optional[Patron] = None + loan_date: datetime = None + due_date: datetime = None + return_date: Optional[datetime] = None + book_item: Optional[BookItem] = None diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/entities/patron.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/entities/patron.py new file mode 100644 index 0000000..98e5096 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/entities/patron.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass, field +from typing import List, Optional +from datetime import datetime +# from .loan import Loan # Use string annotation to avoid circular import + +@dataclass +class Patron: + id: int + name: str + membership_end: datetime + membership_start: datetime + image_name: Optional[str] = None + loans: List['Loan'] = field(default_factory=list) diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/enums/loan_extension_status.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/enums/loan_extension_status.py new file mode 100644 index 0000000..20cf2c5 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/enums/loan_extension_status.py @@ -0,0 +1,9 @@ +from enum import Enum + +class LoanExtensionStatus(Enum): + SUCCESS = 'Book loan extension was successful.' + LOAN_NOT_FOUND = 'Loan not found.' + LOAN_EXPIRED = 'Cannot extend book loan as it already has expired. Return the book instead.' + MEMBERSHIP_EXPIRED = "Cannot extend book loan due to expired patron's membership." + LOAN_RETURNED = 'Cannot extend book loan as the book is already returned.' + ERROR = 'Cannot extend book loan due to an error.' diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/enums/loan_return_status.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/enums/loan_return_status.py new file mode 100644 index 0000000..5f9221a --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/enums/loan_return_status.py @@ -0,0 +1,7 @@ +from enum import Enum + +class LoanReturnStatus(Enum): + SUCCESS = 'Book was successfully returned.' + LOAN_NOT_FOUND = 'Loan not found.' + ALREADY_RETURNED = 'Cannot return book as the book is already returned.' + ERROR = 'Cannot return book due to an error.' diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/enums/membership_renewal_status.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/enums/membership_renewal_status.py new file mode 100644 index 0000000..e36433e --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/enums/membership_renewal_status.py @@ -0,0 +1,8 @@ +from enum import Enum + +class MembershipRenewalStatus(Enum): + SUCCESS = 'Membership renewal was successful.' + PATRON_NOT_FOUND = 'Patron not found.' + TOO_EARLY_TO_RENEW = 'It is too early to renew the membership.' + LOAN_NOT_RETURNED = 'Cannot renew membership due to an outstanding loan.' + ERROR = 'Cannot renew membership due to an error.' diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_repository.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_repository.py new file mode 100644 index 0000000..273d78f --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_repository.py @@ -0,0 +1,20 @@ +from abc import ABC, abstractmethod +from typing import Optional +from ..entities.loan import Loan + +class ILoanRepository(ABC): + @abstractmethod + def get_loan(self, loan_id: int) -> Optional[Loan]: + pass + + @abstractmethod + def update_loan(self, loan: Loan) -> None: + pass + + @abstractmethod + def add_loan(self, loan: Loan) -> None: + pass + + @abstractmethod + def get_loans_by_patron_id(self, patron_id: int): + pass diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_service.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_service.py new file mode 100644 index 0000000..866b407 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_service.py @@ -0,0 +1,16 @@ +from abc import ABC, abstractmethod +from ..enums.loan_return_status import LoanReturnStatus +from ..enums.loan_extension_status import LoanExtensionStatus + +class ILoanService(ABC): + @abstractmethod + def return_loan(self, loan_id: int) -> LoanReturnStatus: + pass + + @abstractmethod + def extend_loan(self, loan_id: int) -> LoanExtensionStatus: + pass + + @abstractmethod + def checkout_book(self, patron, book_item, loan_id=None) -> None: + pass diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_repository.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_repository.py new file mode 100644 index 0000000..c116101 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_repository.py @@ -0,0 +1,24 @@ +from abc import ABC, abstractmethod +from typing import List, Optional +from ..entities.patron import Patron + +class IPatronRepository(ABC): + @abstractmethod + def get_patron(self, patron_id: int) -> Optional[Patron]: + pass + + @abstractmethod + def search_patrons(self, search_input: str) -> List[Patron]: + pass + + @abstractmethod + def update_patron(self, patron: Patron) -> None: + pass + + @abstractmethod + def get_all_books(self): + pass + + @abstractmethod + def get_all_book_items(self): + pass diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_service.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_service.py new file mode 100644 index 0000000..e20199f --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_service.py @@ -0,0 +1,7 @@ +from abc import ABC, abstractmethod +from ..enums.membership_renewal_status import MembershipRenewalStatus + +class IPatronService(ABC): + @abstractmethod + def renew_membership(self, patron_id: int) -> MembershipRenewalStatus: + pass diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/services/loan_service.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/services/loan_service.py new file mode 100644 index 0000000..bf554e6 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/services/loan_service.py @@ -0,0 +1,67 @@ +from ..interfaces.iloan_service import ILoanService +from ..interfaces.iloan_repository import ILoanRepository +from ..enums.loan_return_status import LoanReturnStatus +from ..enums.loan_extension_status import LoanExtensionStatus +from datetime import datetime, timedelta + +class LoanService(ILoanService): + EXTEND_BY_DAYS = 14 + + def __init__(self, loan_repository: ILoanRepository): + self._loan_repository = loan_repository + + def return_loan(self, loan_id: int) -> LoanReturnStatus: + loan = self._loan_repository.get_loan(loan_id) + if loan is None: + return LoanReturnStatus.LOAN_NOT_FOUND + if loan.return_date is not None: + return LoanReturnStatus.ALREADY_RETURNED + loan.return_date = datetime.now() + try: + self._loan_repository.update_loan(loan) + return LoanReturnStatus.SUCCESS + except Exception: + return LoanReturnStatus.ERROR + + def extend_loan(self, loan_id: int) -> LoanExtensionStatus: + loan = self._loan_repository.get_loan(loan_id) + if loan is None: + return LoanExtensionStatus.LOAN_NOT_FOUND + if loan.patron and loan.patron.membership_end < datetime.now(): + return LoanExtensionStatus.MEMBERSHIP_EXPIRED + if loan.return_date is not None: + return LoanExtensionStatus.LOAN_RETURNED + if loan.due_date < datetime.now(): + return LoanExtensionStatus.LOAN_EXPIRED + try: + loan.due_date = loan.due_date + timedelta(days=self.EXTEND_BY_DAYS) + self._loan_repository.update_loan(loan) + return LoanExtensionStatus.SUCCESS + except Exception: + return LoanExtensionStatus.ERROR + + def checkout_book(self, patron, book_item, loan_id=None) -> None: + from ..entities.loan import Loan + from datetime import datetime, timedelta + # Generate a new loan ID if not provided + if loan_id is None: + all_loans = getattr(self._loan_repository, 'get_all_loans', lambda: [])() + max_id = 0 + for l in all_loans: + if l.id > max_id: + max_id = l.id + loan_id = max_id + 1 if all_loans else 1 + now = datetime.now() + due = now + timedelta(days=14) + new_loan = Loan( + id=loan_id, + book_item_id=book_item.id, + patron_id=patron.id, + patron=patron, + loan_date=now, + due_date=due, + return_date=None, + book_item=book_item + ) + self._loan_repository.add_loan(new_loan) + return new_loan diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/services/patron_service.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/services/patron_service.py new file mode 100644 index 0000000..7d43a98 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/application_core/services/patron_service.py @@ -0,0 +1,30 @@ +from ..interfaces.ipatron_service import IPatronService +from ..interfaces.ipatron_repository import IPatronRepository +from ..entities.patron import Patron +from ..enums.membership_renewal_status import MembershipRenewalStatus +from datetime import datetime, timedelta + +class PatronService(IPatronService): + EXTEND_BY_DAYS = 365 + + def __init__(self, patron_repository: IPatronRepository): + self._patron_repository = patron_repository + + def renew_membership(self, patron_id: int) -> MembershipRenewalStatus: + patron = self._patron_repository.get_patron(patron_id) + if patron is None: + return MembershipRenewalStatus.PATRON_NOT_FOUND + if patron.membership_end < datetime.now(): + patron.membership_end = datetime.now() + timedelta(days=self.EXTEND_BY_DAYS) + else: + patron.membership_end = patron.membership_end + timedelta(days=self.EXTEND_BY_DAYS) + self._patron_repository.update_patron(patron) + return MembershipRenewalStatus.SUCCESS + + def find_patron_by_name(self, name: str): + results = [] + all_patrons = self._patron_repository.get_all_patrons() + for patron in all_patrons: + if patron.name.lower() == name.lower(): + results.append(patron) + return results diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/console/book_repository.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/console/book_repository.py new file mode 100644 index 0000000..2544e3d --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/console/book_repository.py @@ -0,0 +1,30 @@ +class BookRepository: + def __init__(self, json_data): + self.books = json_data.get('books', []) + self.authors = json_data.get('authors', []) + self.book_items = json_data.get('book_items', []) + + def get_book_by_title(self, title): + for book in self.books: + if book.title.lower() == title.lower(): + return book + return None + +class BookItemRepository: + def __init__(self, json_data): + self.books = json_data.get('books', []) + self.authors = json_data.get('authors', []) + self.book_items = json_data.get('book_items', []) + + def get_book_by_title(self, title): + for book in self.books: + if book.title.lower() == title.lower(): + return book + return None + + def get_items_by_book_id(self, book_id): + items = [] + for item in self.book_items: + if item.book_id == book_id: + items.append(item) + return items \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/console/common_actions.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/console/common_actions.py new file mode 100644 index 0000000..20fe63e --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/console/common_actions.py @@ -0,0 +1,11 @@ +from enum import Flag, auto + +class CommonActions(Flag): + REPEAT = 0 + SELECT = auto() + QUIT = auto() + SEARCH_PATRONS = auto() + SEARCH_BOOKS = auto() + RENEW_PATRON_MEMBERSHIP = auto() + RETURN_LOANED_BOOK = auto() + EXTEND_LOANED_BOOK = auto() diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/console/console_app.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/console/console_app.py new file mode 100644 index 0000000..c11e458 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/console/console_app.py @@ -0,0 +1,318 @@ +from .console_state import ConsoleState +from .common_actions import CommonActions +from application_core.interfaces.ipatron_repository import IPatronRepository +from application_core.interfaces.iloan_repository import ILoanRepository +from application_core.interfaces.iloan_service import ILoanService +from application_core.interfaces.ipatron_service import IPatronService +from typing import Optional + +class ConsoleApp: + def __init__( + self, + loan_service: ILoanService, + patron_service: IPatronService, + patron_repository: IPatronRepository, + loan_repository: ILoanRepository, + json_data # <-- add json_data parameter + ): + self._current_state: ConsoleState = ConsoleState.PATRON_SEARCH + self.matching_patrons = [] + self.selected_patron_details = None + self.selected_loan_details = None + self._patron_repository = patron_repository + self._loan_repository = loan_repository + self._loan_service = loan_service + self._patron_service = patron_service + self._json_data = json_data # <-- store json_data + + def write_input_options(self, options): + print("Input Options:") + if options & CommonActions.RETURN_LOANED_BOOK: + print(' - "r" to mark as returned') + if options & CommonActions.EXTEND_LOANED_BOOK: + print(' - "e" to extend the book loan') + if options & CommonActions.RENEW_PATRON_MEMBERSHIP: + print(' - "m" to extend patron\'s membership') + if options & CommonActions.SEARCH_PATRONS: + print(' - "s" for new search') + if options & CommonActions.SEARCH_BOOKS: + print(' - "b" to check for book availability') + if options & CommonActions.QUIT: + print(' - "q" to quit') + if options & CommonActions.SELECT: + print(' - type a number to select a list item.') + + def run(self) -> None: + while True: + if self._current_state == ConsoleState.PATRON_SEARCH: + self._current_state = self.patron_search() + elif self._current_state == ConsoleState.PATRON_SEARCH_RESULTS: + self._current_state = self.patron_search_results() + elif self._current_state == ConsoleState.PATRON_DETAILS: + self._current_state = self.patron_details() + elif self._current_state == ConsoleState.LOAN_DETAILS: + self._current_state = self.loan_details() + elif self._current_state == ConsoleState.QUIT: + break + + def patron_search(self) -> ConsoleState: + search_input = input("Enter a string to search for patrons by name: ").strip() + if not search_input: + print("No input provided. Please try again.") + return ConsoleState.PATRON_SEARCH + self.matching_patrons = self._patron_repository.search_patrons(search_input) + if not self.matching_patrons: + print("No matching patrons found.") + return ConsoleState.PATRON_SEARCH + return ConsoleState.PATRON_SEARCH_RESULTS + + def patron_search_results(self) -> ConsoleState: + print("\nMatching Patrons:") + idx = 1 + for patron in self.matching_patrons: + print(f"{idx}) {patron.name}") + idx += 1 + if self.matching_patrons: + self.write_input_options( + CommonActions.SELECT | CommonActions.SEARCH_PATRONS | CommonActions.QUIT + ) + else: + self.write_input_options( + CommonActions.SEARCH_PATRONS | CommonActions.QUIT + ) + selection = input("Enter your choice: ").strip().lower() + if selection == 'q': + return ConsoleState.QUIT + elif selection == 's': + return ConsoleState.PATRON_SEARCH + elif selection.isdigit(): + idx = int(selection) + if 1 <= idx <= len(self.matching_patrons): + self.selected_patron_details = self.matching_patrons[idx - 1] + return ConsoleState.PATRON_DETAILS + else: + print("Invalid selection. Please enter a valid number.") + return ConsoleState.PATRON_SEARCH_RESULTS + else: + print("Invalid input. Please enter a number, 's', or 'q'.") + return ConsoleState.PATRON_SEARCH_RESULTS + + def patron_details(self) -> ConsoleState: + patron = self.selected_patron_details + print(f"\nName: {patron.name}") + print(f"Membership Expiration: {patron.membership_end}") + loans = self._loan_repository.get_loans_by_patron_id(patron.id) + print("\nBook Loans History:") + + valid_loans = self._print_loans(loans) + + if valid_loans: + options = ( + CommonActions.RENEW_PATRON_MEMBERSHIP + | CommonActions.SEARCH_PATRONS + | CommonActions.QUIT + | CommonActions.SELECT + | CommonActions.SEARCH_BOOKS # Added SEARCH_BOOKS to options + ) + selection = self._get_patron_details_input(options) + return self._handle_patron_details_selection(selection, patron, valid_loans) + else: + print("No valid loans for this patron.") + options = ( + CommonActions.SEARCH_PATRONS + | CommonActions.QUIT + | CommonActions.SEARCH_BOOKS # Added SEARCH_BOOKS to options + ) + selection = self._get_patron_details_input(options) + return self._handle_no_loans_selection(selection) + + def _print_loans(self, loans): + valid_loans = [] + idx = 1 + for loan in loans: + if not getattr(loan, 'book_item', None) or not getattr(loan.book_item, 'book', None): + print(f"{idx}) [Invalid loan data: missing book information]") + else: + returned = "True" if getattr(loan, 'return_date', None) else "False" + print(f"{idx}) {loan.book_item.book.title} - Due: {loan.due_date} - Returned: {returned}") + valid_loans.append((idx, loan)) + idx += 1 + return valid_loans + + def _get_patron_details_input(self, options): + self.write_input_options(options) + return input("Enter your choice: ").strip().lower() + + def _handle_patron_details_selection(self, selection, patron, valid_loans): + if selection == 'q': + return ConsoleState.QUIT + elif selection == 's': + return ConsoleState.PATRON_SEARCH + elif selection == 'm': + status = self._patron_service.renew_membership(patron.id) + print(status) + self.selected_patron_details = self._patron_repository.get_patron(patron.id) + return ConsoleState.PATRON_DETAILS + elif selection == 'b': + return self.search_books() # Call the new search_books method + elif selection.isdigit(): + idx = int(selection) + if 1 <= idx <= len(valid_loans): + self.selected_loan_details = valid_loans[idx - 1][1] + return ConsoleState.LOAN_DETAILS + print("Invalid selection. Please enter a number shown in the list above.") + return ConsoleState.PATRON_DETAILS + else: + print("Invalid input. Please enter a number, 'm', 'b', 's', or 'q'.") + return ConsoleState.PATRON_DETAILS + + def _handle_no_loans_selection(self, selection): + if selection == 'q': + return ConsoleState.QUIT + elif selection == 's': + return ConsoleState.PATRON_SEARCH + elif selection == 'b': + return self.search_books() # Handle SEARCH_BOOKS when no loans + else: + print("Invalid input.") + return ConsoleState.PATRON_DETAILS + + def search_books(self) -> ConsoleState: + while True: + book_title = input("Enter a book title to search for: ").strip() + if not book_title: + print("No book title provided. Please try again.") + continue + + # Case-insensitive, partial or exact match + books = self._json_data.books + matches = [b for b in books if book_title.lower() in b.title.lower()] + + if not matches: + print("No matching books found.") + again = input("Search again? (y/n): ").strip().lower() + if again == 'y': + continue + else: + return ConsoleState.PATRON_DETAILS + + if len(matches) == 1: + book = matches[0] + else: + print("\nMultiple books found:") + for idx, b in enumerate(matches, 1): + print(f"{idx}) {b.title}") + selection = input("Select a book by number or 'r' to refine search: ").strip().lower() + if selection == 'r': + continue + if not selection.isdigit() or not (1 <= int(selection) <= len(matches)): + print("Invalid selection.") + continue + book = matches[int(selection) - 1] + + # Find all book items (copies) for this book + book_items = [bi for bi in self._json_data.book_items if bi.book_id == book.id] + if not book_items: + print("No copies of this book are in the library.") + again = input("Search again? (y/n): ").strip().lower() + if again == 'y': + continue + else: + return ConsoleState.PATRON_DETAILS + + # Find all loans for these book items + loans = self._json_data.loans + on_loan = [] + available = [] + for item in book_items: + # Find latest loan for this item (if any) + item_loans = [l for l in loans if l.book_item_id == item.id] + if item_loans: + # Get the most recent loan (by LoanDate) + latest_loan = max(item_loans, key=lambda l: l.loan_date or l.due_date or l.return_date or 0) + if latest_loan.return_date is None: + on_loan.append(latest_loan) + else: + available.append(item) + else: + available.append(item) + + if available: + print(f"Book '{book.title}' is available for loan.") + # Prompt for checkout + checkout = input("Would you like to check out this book? (y/n): ").strip().lower() + if checkout == 'y': + if not self.selected_patron_details: + print("No patron selected. Please select a patron first.") + return ConsoleState.PATRON_SEARCH + # Use the first available copy + book_item = available[0] + loan = self._loan_service.checkout_book(self.selected_patron_details, book_item) + print(f"Book '{book.title}' checked out successfully. Due date: {loan.due_date}") + return ConsoleState.PATRON_DETAILS + else: + # All copies are on loan, show due dates + due_dates = [l.due_date for l in on_loan if l.due_date] + if due_dates: + next_due = min(due_dates) + print(f"All copies of '{book.title}' are currently on loan. Next due date: {next_due}") + else: + print(f"All copies of '{book.title}' are currently on loan.") + + again = input("Search for another book? (y/n): ").strip().lower() + if again == 'y': + continue + else: + return ConsoleState.PATRON_DETAILS + + def loan_details(self) -> ConsoleState: + loan = self.selected_loan_details + print(f"\nBook title: {loan.book_item.book.title}") + print(f"Book Author: {loan.book_item.book.author.name}") + print(f"Due date: {loan.due_date}") + returned = "True" if getattr(loan, 'return_date', None) else "False" + print(f"Returned: {returned}\n") + options = CommonActions.SEARCH_PATRONS | CommonActions.QUIT + if not getattr(loan, 'return_date', None): + options |= CommonActions.RETURN_LOANED_BOOK | CommonActions.EXTEND_LOANED_BOOK + self.write_input_options(options) + selection = input("Enter your choice: ").strip().lower() + if selection == 'q': + return ConsoleState.QUIT + elif selection == 's': + return ConsoleState.PATRON_SEARCH + elif selection == 'r' and not getattr(loan, 'return_date', None): + status = self._loan_service.return_loan(loan.id) + print("Book was successfully returned.") + print(status) + self.selected_loan_details = self._loan_repository.get_loan(loan.id) + return ConsoleState.LOAN_DETAILS + elif selection == 'e' and not getattr(loan, 'return_date', None): + status = self._loan_service.extend_loan(loan.id) + print(status) + self.selected_loan_details = self._loan_repository.get_loan(loan.id) + return ConsoleState.LOAN_DETAILS + else: + print("Invalid input.") + return ConsoleState.LOAN_DETAILS + +from application_core.services.loan_service import LoanService +from application_core.services.patron_service import PatronService +from infrastructure.json_data import JsonData +from infrastructure.json_loan_repository import JsonLoanRepository +from infrastructure.json_patron_repository import JsonPatronRepository +from console.console_app import ConsoleApp + +def main(): + json_data = JsonData() + patron_repo = JsonPatronRepository(json_data) + loan_repo = JsonLoanRepository(json_data) + loan_service = LoanService(loan_repo) + patron_service = PatronService(patron_repo) + + app = ConsoleApp( + loan_service=loan_service, + patron_service=patron_service, + json_data=json_data # <-- pass json_data to ConsoleApp + ) + app.run() diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/console/console_state.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/console/console_state.py new file mode 100644 index 0000000..714335a --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/console/console_state.py @@ -0,0 +1,8 @@ +from enum import Enum + +class ConsoleState(Enum): + PATRON_SEARCH = 1 + PATRON_SEARCH_RESULTS = 2 + PATRON_DETAILS = 3 + LOAN_DETAILS = 4 + QUIT = 5 diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/console/main.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/console/main.py new file mode 100644 index 0000000..9bb9b8a --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/console/main.py @@ -0,0 +1,33 @@ +import sys +from pathlib import Path + +# Add the parent directory to sys.path +sys.path.append(str(Path(__file__).resolve().parent.parent)) + +from application_core.services.loan_service import LoanService +from application_core.services.patron_service import PatronService +from infrastructure.json_data import JsonData +from infrastructure.json_loan_repository import JsonLoanRepository +from infrastructure.json_patron_repository import JsonPatronRepository +from console.console_app import ConsoleApp + + +def main(): + json_data = JsonData() + patron_repo = JsonPatronRepository(json_data) + loan_repo = JsonLoanRepository(json_data) + loan_service = LoanService(loan_repo) + patron_service = PatronService(patron_repo) + + app = ConsoleApp( + loan_service=loan_service, + patron_service=patron_service, + patron_repository=patron_repo, + loan_repository=loan_repo, + json_data=json_data # <-- pass json_data here + ) + app.run() + + +if __name__ == "__main__": + main() diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/Json/Authors.json b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/Json/Authors.json new file mode 100644 index 0000000..2f61038 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/Json/Authors.json @@ -0,0 +1,22 @@ +[ + {"Id": 1, "Name": "Author One"}, + {"Id": 2, "Name": "Author Two"}, + {"Id": 3, "Name": "Author Three"}, + {"Id": 4, "Name": "Author Four"}, + {"Id": 5, "Name": "Author Five"}, + {"Id": 6, "Name": "Author Six"}, + {"Id": 7, "Name": "Author Seven"}, + {"Id": 8, "Name": "Author Eight"}, + {"Id": 9, "Name": "Author Nine"}, + {"Id": 10, "Name": "Author Ten"}, + {"Id": 11, "Name": "Author Eleven"}, + {"Id": 12, "Name": "Author Twelve"}, + {"Id": 13, "Name": "Author Thirteen"}, + {"Id": 14, "Name": "Author Fourteen"}, + {"Id": 15, "Name": "Author Fifteen"}, + {"Id": 16, "Name": "Author Sixteen"}, + {"Id": 17, "Name": "Author Seventeen"}, + {"Id": 18, "Name": "Author Eighteen"}, + {"Id": 19, "Name": "Author Nineteen"}, + {"Id": 20, "Name": "Author Twenty"} +] diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/Json/BookItems.json b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/Json/BookItems.json new file mode 100644 index 0000000..f5e1d1b --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/Json/BookItems.json @@ -0,0 +1,22 @@ +[ + {"Id": 1, "BookId": 1, "AcquisitionDate": "2023-09-20T00:40:43.1716563", "Condition": "Good"}, + {"Id": 2, "BookId": 2, "AcquisitionDate": "2023-09-20T00:40:43.1717503", "Condition": "Fair"}, + {"Id": 3, "BookId": 3, "AcquisitionDate": "2023-09-20T00:40:43.1717511", "Condition": "Excellent"}, + {"Id": 4, "BookId": 4, "AcquisitionDate": "2023-09-20T00:40:43.1717513", "Condition": "Poor"}, + {"Id": 5, "BookId": 5, "AcquisitionDate": "2023-09-20T00:40:43.1717516", "Condition": "Good"}, + {"Id": 6, "BookId": 6, "AcquisitionDate": "2023-09-20T00:40:43.1717521", "Condition": "Fair"}, + {"Id": 7, "BookId": 7, "AcquisitionDate": "2023-09-20T00:40:43.1717523", "Condition": "Excellent"}, + {"Id": 8, "BookId": 8, "AcquisitionDate": "2023-09-20T00:40:43.1717526", "Condition": "Poor"}, + {"Id": 9, "BookId": 9, "AcquisitionDate": "2023-09-20T00:40:43.171757", "Condition": "Good"}, + {"Id": 10, "BookId": 10, "AcquisitionDate": "2023-09-20T00:40:43.1717574", "Condition": "Fair"}, + {"Id": 11, "BookId": 11, "AcquisitionDate": "2023-09-20T00:40:43.1717576", "Condition": "Excellent"}, + {"Id": 12, "BookId": 12, "AcquisitionDate": "2023-09-20T00:40:43.1717578", "Condition": "Poor"}, + {"Id": 13, "BookId": 13, "AcquisitionDate": "2023-09-20T00:40:43.171758", "Condition": "Good"}, + {"Id": 14, "BookId": 14, "AcquisitionDate": "2023-09-20T00:40:43.1717609", "Condition": "Fair"}, + {"Id": 15, "BookId": 15, "AcquisitionDate": "2023-09-20T00:40:43.1717611", "Condition": "Excellent"}, + {"Id": 16, "BookId": 16, "AcquisitionDate": "2023-09-20T00:40:43.1717613", "Condition": "Poor"}, + {"Id": 17, "BookId": 17, "AcquisitionDate": "2023-09-20T00:40:43.1717616", "Condition": "Good"}, + {"Id": 18, "BookId": 18, "AcquisitionDate": "2023-09-20T00:40:43.1717619", "Condition": "Fair"}, + {"Id": 19, "BookId": 19, "AcquisitionDate": "2023-09-20T00:40:43.1717621", "Condition": "Excellent"}, + {"Id": 20, "BookId": 20, "AcquisitionDate": "2023-09-20T00:40:43.1717626", "Condition": "Poor"} +] diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/Json/Books.json b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/Json/Books.json new file mode 100644 index 0000000..ac80673 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/Json/Books.json @@ -0,0 +1,22 @@ +[ + {"Id": 1, "Title": "Book One", "AuthorId": 1, "Genre": "Dystopian", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524935"}, + {"Id": 2, "Title": "Book Two", "AuthorId": 2, "Genre": "Classic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524936"}, + {"Id": 3, "Title": "Book Three", "AuthorId": 3, "Genre": "Romance", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524937"}, + {"Id": 4, "Title": "Book Four", "AuthorId": 4, "Genre": "Classic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524938"}, + {"Id": 5, "Title": "Book Five", "AuthorId": 5, "Genre": "Coming-of-age", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524939"}, + {"Id": 6, "Title": "Book Six", "AuthorId": 6, "Genre": "Modernist", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524940"}, + {"Id": 7, "Title": "Book Seven", "AuthorId": 7, "Genre": "Adventure", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524941"}, + {"Id": 8, "Title": "Book Eight", "AuthorId": 8, "Genre": "Fantasy", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524942"}, + {"Id": 9, "Title": "Book Nine", "AuthorId": 9, "Genre": "Fantasy", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524943"}, + {"Id": 10, "Title": "Book Ten", "AuthorId": 10, "Genre": "Epic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524944"}, + {"Id": 11, "Title": "Book Eleven", "AuthorId": 11, "Genre": "Psychological", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524945"}, + {"Id": 12, "Title": "Book Twelve", "AuthorId": 12, "Genre": "Psychological", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524946"}, + {"Id": 13, "Title": "Book Thirteen", "AuthorId": 13, "Genre": "Magical realism", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524947"}, + {"Id": 14, "Title": "Book Fourteen", "AuthorId": 14, "Genre": "Classic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524948"}, + {"Id": 15, "Title": "Book Fifteen", "AuthorId": 15, "Genre": "Adventure", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524949"}, + {"Id": 16, "Title": "Book Sixteen", "AuthorId": 16, "Genre": "Historical", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524950"}, + {"Id": 17, "Title": "Book Seventeen", "AuthorId": 17, "Genre": "Gothic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524951"}, + {"Id": 18, "Title": "Book Eighteen", "AuthorId": 18, "Genre": "Dystopian", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524952"}, + {"Id": 19, "Title": "Book Nineteen", "AuthorId": 19, "Genre": "Classic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524953"}, + {"Id": 20, "Title": "Book Twenty", "AuthorId": 20, "Genre": "Adventure", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524954"} +] diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/Json/Loans.json b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/Json/Loans.json new file mode 100644 index 0000000..b0ebd1d --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/Json/Loans.json @@ -0,0 +1,482 @@ +[ + { + "Id": 1, + "BookItemId": 1, + "PatronId": 1, + "LoanDate": "2025-06-10T10:00:00", + "DueDate": "2025-06-24T10:00:00", + "ReturnDate": null + }, + { + "Id": 2, + "BookItemId": 1, + "PatronId": 10, + "LoanDate": "2024-05-01T10:00:00", + "DueDate": "2024-05-15T10:00:00", + "ReturnDate": "2024-05-10T10:00:00" + }, + { + "Id": 3, + "BookItemId": 2, + "PatronId": 2, + "LoanDate": "2025-06-11T10:00:00", + "DueDate": "2025-06-25T10:00:00", + "ReturnDate": null + }, + { + "Id": 4, + "BookItemId": 2, + "PatronId": 11, + "LoanDate": "2024-06-01T10:00:00", + "DueDate": "2024-06-15T10:00:00", + "ReturnDate": "2024-06-10T10:00:00" + }, + { + "Id": 5, + "BookItemId": 3, + "PatronId": 3, + "LoanDate": "2025-06-12T10:00:00", + "DueDate": "2025-06-26T10:00:00", + "ReturnDate": null + }, + { + "Id": 6, + "BookItemId": 3, + "PatronId": 12, + "LoanDate": "2024-07-01T10:00:00", + "DueDate": "2024-07-15T10:00:00", + "ReturnDate": "2024-07-10T10:00:00" + }, + { + "Id": 7, + "BookItemId": 4, + "PatronId": 4, + "LoanDate": "2025-06-13T10:00:00", + "DueDate": "2025-06-27T10:00:00", + "ReturnDate": null + }, + { + "Id": 8, + "BookItemId": 4, + "PatronId": 13, + "LoanDate": "2024-08-01T10:00:00", + "DueDate": "2024-08-15T10:00:00", + "ReturnDate": "2024-08-10T10:00:00" + }, + { + "Id": 9, + "BookItemId": 5, + "PatronId": 5, + "LoanDate": "2025-06-14T10:00:00", + "DueDate": "2025-06-28T10:00:00", + "ReturnDate": null + }, + { + "Id": 10, + "BookItemId": 5, + "PatronId": 14, + "LoanDate": "2024-09-01T10:00:00", + "DueDate": "2024-09-15T10:00:00", + "ReturnDate": "2024-09-10T10:00:00" + }, + { + "Id": 11, + "BookItemId": 6, + "PatronId": 6, + "LoanDate": "2025-06-15T10:00:00", + "DueDate": "2025-06-29T10:00:00", + "ReturnDate": null + }, + { + "Id": 12, + "BookItemId": 6, + "PatronId": 15, + "LoanDate": "2024-10-01T10:00:00", + "DueDate": "2024-10-15T10:00:00", + "ReturnDate": "2024-10-10T10:00:00" + }, + { + "Id": 13, + "BookItemId": 7, + "PatronId": 7, + "LoanDate": "2025-06-16T10:00:00", + "DueDate": "2025-06-30T10:00:00", + "ReturnDate": null + }, + { + "Id": 14, + "BookItemId": 7, + "PatronId": 16, + "LoanDate": "2024-11-01T10:00:00", + "DueDate": "2024-11-15T10:00:00", + "ReturnDate": "2024-11-10T10:00:00" + }, + { + "Id": 15, + "BookItemId": 8, + "PatronId": 8, + "LoanDate": "2025-06-17T10:00:00", + "DueDate": "2025-07-01T10:00:00", + "ReturnDate": null + }, + { + "Id": 16, + "BookItemId": 8, + "PatronId": 17, + "LoanDate": "2024-12-01T10:00:00", + "DueDate": "2024-12-15T10:00:00", + "ReturnDate": "2024-12-10T10:00:00" + }, + { + "Id": 17, + "BookItemId": 9, + "PatronId": 9, + "LoanDate": "2025-06-18T10:00:00", + "DueDate": "2025-07-02T10:00:00", + "ReturnDate": null + }, + { + "Id": 18, + "BookItemId": 9, + "PatronId": 18, + "LoanDate": "2025-01-01T10:00:00", + "DueDate": "2025-01-15T10:00:00", + "ReturnDate": "2025-01-10T10:00:00" + }, + { + "Id": 19, + "BookItemId": 10, + "PatronId": 10, + "LoanDate": "2025-06-19T10:00:00", + "DueDate": "2025-07-03T10:00:00", + "ReturnDate": null + }, + { + "Id": 20, + "BookItemId": 10, + "PatronId": 19, + "LoanDate": "2025-02-01T10:00:00", + "DueDate": "2025-02-15T10:00:00", + "ReturnDate": "2025-02-10T10:00:00" + }, + { + "Id": 21, + "BookItemId": 11, + "PatronId": 11, + "LoanDate": "2025-06-20T10:00:00", + "DueDate": "2025-07-04T10:00:00", + "ReturnDate": null + }, + { + "Id": 22, + "BookItemId": 11, + "PatronId": 20, + "LoanDate": "2025-03-01T10:00:00", + "DueDate": "2025-03-15T10:00:00", + "ReturnDate": "2025-03-10T10:00:00" + }, + { + "Id": 23, + "BookItemId": 12, + "PatronId": 12, + "LoanDate": "2025-06-21T10:00:00", + "DueDate": "2025-07-05T10:00:00", + "ReturnDate": null + }, + { + "Id": 24, + "BookItemId": 12, + "PatronId": 1, + "LoanDate": "2023-01-01T10:00:00", + "DueDate": "2023-01-15T10:00:00", + "ReturnDate": "2023-01-10T10:00:00" + }, + { + "Id": 25, + "BookItemId": 13, + "PatronId": 13, + "LoanDate": "2025-06-22T10:00:00", + "DueDate": "2025-07-06T10:00:00", + "ReturnDate": null + }, + { + "Id": 26, + "BookItemId": 13, + "PatronId": 2, + "LoanDate": "2023-02-01T10:00:00", + "DueDate": "2023-02-15T10:00:00", + "ReturnDate": "2023-02-10T10:00:00" + }, + { + "Id": 27, + "BookItemId": 14, + "PatronId": 14, + "LoanDate": "2025-06-23T10:00:00", + "DueDate": "2025-07-07T10:00:00", + "ReturnDate": null + }, + { + "Id": 28, + "BookItemId": 14, + "PatronId": 3, + "LoanDate": "2023-03-01T10:00:00", + "DueDate": "2023-03-15T10:00:00", + "ReturnDate": "2023-03-10T10:00:00" + }, + { + "Id": 29, + "BookItemId": 15, + "PatronId": 15, + "LoanDate": "2025-06-24T10:00:00", + "DueDate": "2025-07-08T10:00:00", + "ReturnDate": null + }, + { + "Id": 30, + "BookItemId": 15, + "PatronId": 4, + "LoanDate": "2023-04-01T10:00:00", + "DueDate": "2023-04-15T10:00:00", + "ReturnDate": "2023-04-10T10:00:00" + }, + { + "Id": 31, + "BookItemId": 16, + "PatronId": 5, + "LoanDate": "2023-05-01T10:00:00", + "DueDate": "2023-05-15T10:00:00", + "ReturnDate": "2023-05-10T10:00:00" + }, + { + "Id": 32, + "BookItemId": 17, + "PatronId": 6, + "LoanDate": "2023-06-01T10:00:00", + "DueDate": "2023-06-15T10:00:00", + "ReturnDate": "2023-06-10T10:00:00" + }, + { + "Id": 33, + "BookItemId": 18, + "PatronId": 7, + "LoanDate": "2023-07-01T10:00:00", + "DueDate": "2023-07-15T10:00:00", + "ReturnDate": "2023-07-10T10:00:00" + }, + { + "Id": 34, + "BookItemId": 19, + "PatronId": 8, + "LoanDate": "2023-08-01T10:00:00", + "DueDate": "2023-08-15T10:00:00", + "ReturnDate": "2023-08-10T10:00:00" + }, + { + "Id": 35, + "BookItemId": 20, + "PatronId": 9, + "LoanDate": "2023-09-01T10:00:00", + "DueDate": "2023-09-15T10:00:00", + "ReturnDate": "2023-09-10T10:00:00" + }, + { + "Id": 36, + "BookItemId": 16, + "PatronId": 21, + "LoanDate": "2023-10-01T10:00:00", + "DueDate": "2023-10-15T10:00:00", + "ReturnDate": "2023-10-10T10:00:00" + }, + { + "Id": 37, + "BookItemId": 17, + "PatronId": 22, + "LoanDate": "2023-11-01T10:00:00", + "DueDate": "2023-11-15T10:00:00", + "ReturnDate": "2023-11-10T10:00:00" + }, + { + "Id": 38, + "BookItemId": 18, + "PatronId": 23, + "LoanDate": "2023-12-01T10:00:00", + "DueDate": "2023-12-15T10:00:00", + "ReturnDate": "2023-12-10T10:00:00" + }, + { + "Id": 39, + "BookItemId": 19, + "PatronId": 24, + "LoanDate": "2024-01-01T10:00:00", + "DueDate": "2024-01-15T10:00:00", + "ReturnDate": "2024-01-10T10:00:00" + }, + { + "Id": 40, + "BookItemId": 20, + "PatronId": 25, + "LoanDate": "2024-02-01T10:00:00", + "DueDate": "2024-02-15T10:00:00", + "ReturnDate": "2024-02-10T10:00:00" + }, + { + "Id": 41, + "BookItemId": 16, + "PatronId": 26, + "LoanDate": "2024-03-01T10:00:00", + "DueDate": "2024-03-15T10:00:00", + "ReturnDate": "2024-03-10T10:00:00" + }, + { + "Id": 42, + "BookItemId": 17, + "PatronId": 27, + "LoanDate": "2024-04-01T10:00:00", + "DueDate": "2024-04-15T10:00:00", + "ReturnDate": "2024-04-10T10:00:00" + }, + { + "Id": 43, + "BookItemId": 18, + "PatronId": 28, + "LoanDate": "2024-05-01T10:00:00", + "DueDate": "2024-05-15T10:00:00", + "ReturnDate": "2024-05-10T10:00:00" + }, + { + "Id": 44, + "BookItemId": 19, + "PatronId": 29, + "LoanDate": "2024-06-01T10:00:00", + "DueDate": "2024-06-15T10:00:00", + "ReturnDate": "2024-06-10T10:00:00" + }, + { + "Id": 45, + "BookItemId": 20, + "PatronId": 30, + "LoanDate": "2024-07-01T10:00:00", + "DueDate": "2024-07-15T10:00:00", + "ReturnDate": "2024-07-10T10:00:00" + }, + { + "Id": 46, + "BookItemId": 16, + "PatronId": 31, + "LoanDate": "2024-08-01T10:00:00", + "DueDate": "2024-08-15T10:00:00", + "ReturnDate": "2024-08-10T10:00:00" + }, + { + "Id": 47, + "BookItemId": 17, + "PatronId": 32, + "LoanDate": "2024-09-01T10:00:00", + "DueDate": "2024-09-15T10:00:00", + "ReturnDate": "2024-09-10T10:00:00" + }, + { + "Id": 48, + "BookItemId": 18, + "PatronId": 33, + "LoanDate": "2024-10-01T10:00:00", + "DueDate": "2024-10-15T10:00:00", + "ReturnDate": "2024-10-10T10:00:00" + }, + { + "Id": 49, + "BookItemId": 19, + "PatronId": 34, + "LoanDate": "2024-11-01T10:00:00", + "DueDate": "2024-11-15T10:00:00", + "ReturnDate": "2024-11-10T10:00:00" + }, + { + "Id": 50, + "BookItemId": 20, + "PatronId": 35, + "LoanDate": "2024-12-01T10:00:00", + "DueDate": "2024-12-15T10:00:00", + "ReturnDate": "2024-12-10T10:00:00" + }, + { + "Id": 51, + "BookItemId": 16, + "PatronId": 36, + "LoanDate": "2025-01-01T10:00:00", + "DueDate": "2025-01-15T10:00:00", + "ReturnDate": "2025-01-10T10:00:00" + }, + { + "Id": 52, + "BookItemId": 17, + "PatronId": 37, + "LoanDate": "2025-02-01T10:00:00", + "DueDate": "2025-02-15T10:00:00", + "ReturnDate": "2025-02-10T10:00:00" + }, + { + "Id": 53, + "BookItemId": 18, + "PatronId": 38, + "LoanDate": "2025-03-01T10:00:00", + "DueDate": "2025-03-15T10:00:00", + "ReturnDate": "2025-03-10T10:00:00" + }, + { + "Id": 54, + "BookItemId": 19, + "PatronId": 39, + "LoanDate": "2025-04-01T10:00:00", + "DueDate": "2025-04-15T10:00:00", + "ReturnDate": "2025-04-10T10:00:00" + }, + { + "Id": 55, + "BookItemId": 20, + "PatronId": 40, + "LoanDate": "2025-05-01T10:00:00", + "DueDate": "2025-05-15T10:00:00", + "ReturnDate": "2025-05-10T10:00:00" + }, + { + "Id": 56, + "BookItemId": 16, + "PatronId": 41, + "LoanDate": "2025-05-11T10:00:00", + "DueDate": "2025-05-25T10:00:00", + "ReturnDate": "2025-05-20T10:00:00" + }, + { + "Id": 57, + "BookItemId": 17, + "PatronId": 42, + "LoanDate": "2025-05-12T10:00:00", + "DueDate": "2025-05-26T10:00:00", + "ReturnDate": "2025-05-21T10:00:00" + }, + { + "Id": 58, + "BookItemId": 18, + "PatronId": 48, + "LoanDate": "2025-05-13T10:00:00", + "DueDate": "2025-05-27T10:00:00", + "ReturnDate": "2025-05-22T10:00:00" + }, + { + "Id": 59, + "BookItemId": 19, + "PatronId": 49, + "LoanDate": "2025-05-14T10:00:00", + "DueDate": "2025-05-28T10:00:00", + "ReturnDate": "2025-05-23T10:00:00" + }, + { + "Id": 60, + "BookItemId": 20, + "PatronId": 50, + "LoanDate": "2025-05-15T10:00:00", + "DueDate": "2025-05-29T10:00:00", + "ReturnDate": "2025-05-24T10:00:00" + } +] \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/Json/Patrons.json b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/Json/Patrons.json new file mode 100644 index 0000000..7c05687 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/Json/Patrons.json @@ -0,0 +1,52 @@ +[ + {"Id": 1, "Name": "Patron One", "MembershipEnd": "2024-12-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron One.jpg"}, + {"Id": 2, "Name": "Patron Two", "MembershipEnd": "2025-02-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Two.jpg"}, + {"Id": 3, "Name": "Patron Three", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Three.jpg"}, + {"Id": 4, "Name": "Patron Four", "MembershipEnd": "2025-04-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Four.jpg"}, + {"Id": 5, "Name": "Patron Five", "MembershipEnd": "2025-05-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Five.jpg"}, + {"Id": 6, "Name": "Patron Six", "MembershipEnd": "2025-06-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Six.jpg"}, + {"Id": 7, "Name": "Patron Seven", "MembershipEnd": "2025-07-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Seven.jpg"}, + {"Id": 8, "Name": "Patron Eight", "MembershipEnd": "2024-01-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Eight.jpg"}, + {"Id": 9, "Name": "Patron Nine", "MembershipEnd": "2024-02-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Nine.jpg"}, + {"Id": 10, "Name": "Patron Ten", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Ten.jpg"}, + {"Id": 11, "Name": "Patron Eleven", "MembershipEnd": "2024-04-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Eleven.jpg"}, + {"Id": 12, "Name": "Patron Twelve", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twelve.jpg"}, + {"Id": 13, "Name": "Patron Thirteen", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirteen.jpg"}, + {"Id": 14, "Name": "Patron Fourteen", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Fourteen.jpg"}, + {"Id": 15, "Name": "Patron Fifteen", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Fifteen.jpg"}, + {"Id": 16, "Name": "Patron Sixteen", "MembershipEnd": "2024-04-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Sixteen.jpg"}, + {"Id": 17, "Name": "Patron Seventeen", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Seventeen.jpg"}, + {"Id": 18, "Name": "Patron Eighteen", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Eighteen.jpg"}, + {"Id": 19, "Name": "Patron Nineteen", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Nineteen.jpg"}, + {"Id": 20, "Name": "Patron Twenty", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty.jpg"}, + {"Id": 21, "Name": "Patron Twenty-One", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-One.jpg"}, + {"Id": 22, "Name": "Patron Twenty-Two", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Two.jpg"}, + {"Id": 23, "Name": "Patron Twenty-Three", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Three.jpg"}, + {"Id": 24, "Name": "Patron Twenty-Four", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Four.jpg"}, + {"Id": 25, "Name": "Patron Twenty-Five", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Five.jpg"}, + {"Id": 26, "Name": "Patron Twenty-Six", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Six.jpg"}, + {"Id": 27, "Name": "Patron Twenty-Seven", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Seven.jpg"}, + {"Id": 28, "Name": "Patron Twenty-Eight", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Eight.jpg"}, + {"Id": 29, "Name": "Patron Twenty-Nine", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Nine.jpg"}, + {"Id": 30, "Name": "Patron Thirty", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty.jpg"}, + {"Id": 31, "Name": "Patron Thirty-One", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-One.jpg"}, + {"Id": 32, "Name": "Patron Thirty-Two", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Two.jpg"}, + {"Id": 33, "Name": "Patron Thirty-Three", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Three.jpg"}, + {"Id": 34, "Name": "Patron Thirty-Four", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Four.jpg"}, + {"Id": 35, "Name": "Patron Thirty-Five", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Five.jpg"}, + {"Id": 36, "Name": "Patron Thirty-Six", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Six.jpg"}, + {"Id": 37, "Name": "Patron Thirty-Seven", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Seven.jpg"}, + {"Id": 38, "Name": "Patron Thirty-Eight", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Eight.jpg"}, + {"Id": 39, "Name": "Patron Thirty-Nine", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Nine.jpg"}, + {"Id": 40, "Name": "Patron Forty", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty.jpg"}, + {"Id": 41, "Name": "Patron Forty-One", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-One.jpg"}, + {"Id": 42, "Name": "Patron Forty-Two", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Two.jpg"}, + {"Id": 43, "Name": "Patron Forty-Three", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Three.jpg"}, + {"Id": 44, "Name": "Patron Forty-Four", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Four.jpg"}, + {"Id": 45, "Name": "Patron Forty-Five", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Five.jpg"}, + {"Id": 46, "Name": "Patron Forty-Six", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Six.jpg"}, + {"Id": 47, "Name": "Patron Forty-Seven", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Seven.jpg"}, + {"Id": 48, "Name": "Patron Forty-Eight", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Eight.jpg"}, + {"Id": 49, "Name": "Patron Forty-Nine", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Nine.jpg"}, + {"Id": 50, "Name": "Patron Fifty", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Fifty.jpg"} +] diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/json_data.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/json_data.py new file mode 100644 index 0000000..0c4aa54 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/json_data.py @@ -0,0 +1,105 @@ +import json +import os +from pathlib import Path +from application_core.entities.author import Author +from application_core.entities.book import Book +from application_core.entities.book_item import BookItem +from application_core.entities.patron import Patron +from application_core.entities.loan import Loan +from typing import List, Optional +from datetime import datetime + +class JsonData: + def __init__(self): + # Get the absolute path to the project root + self.project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + self.json_dir = os.path.join(self.project_root, "infrastructure", "Json") + self.authors_path = os.path.join(self.json_dir, "Authors.json") + self.books_path = os.path.join(self.json_dir, "Books.json") + self.book_items_path = os.path.join(self.json_dir, "BookItems.json") # <-- Add this line + self.patrons_path = os.path.join(self.json_dir, "Patrons.json") + self.loans_path = os.path.join(self.json_dir, "Loans.json") + self.authors: List[Author] = [] + self.books: List[Book] = [] + self.book_items: List[BookItem] = [] + self.patrons: List[Patron] = [] + self.loans: List[Loan] = [] + self._loaded = False + self.load_data() + + def _parse_datetime(self, value: Optional[str]) -> Optional[datetime]: + if value is None: + return None + return datetime.fromisoformat(value) + + def load_data(self) -> None: + try: + with open(self.authors_path, encoding='utf-8') as f: + authors_data = json.load(f) + self.authors = [Author(id=a['Id'], name=a['Name']) for a in authors_data] + with open(self.books_path, encoding='utf-8') as f: + books_data = json.load(f) + self.books = [Book(id=b['Id'], title=b['Title'], author_id=b['AuthorId'], genre=b['Genre'], image_name=b['ImageName'], isbn=b['ISBN']) for b in books_data] + with open(self.book_items_path, encoding='utf-8') as f: # <-- Fix here + items_data = json.load(f) + self.book_items = [BookItem(id=bi['Id'], book_id=bi['BookId'], acquisition_date=self._parse_datetime(bi['AcquisitionDate']), condition=bi.get('Condition')) for bi in items_data] + with open(self.patrons_path, encoding='utf-8') as f: + patrons_data = json.load(f) + self.patrons = [Patron(id=p['Id'], name=p['Name'], membership_end=self._parse_datetime(p['MembershipEnd']), membership_start=self._parse_datetime(p['MembershipStart']), image_name=p.get('ImageName')) for p in patrons_data] + with open(self.loans_path, encoding='utf-8') as f: + loans_data = json.load(f) + self.loans = [Loan(id=l['Id'], book_item_id=l['BookItemId'], patron_id=l['PatronId'], loan_date=self._parse_datetime(l['LoanDate']), due_date=self._parse_datetime(l['DueDate']), return_date=self._parse_datetime(l['ReturnDate'])) for l in loans_data] + self._loaded = True + + # Build lookup dictionaries for fast access + book_item_dict = {bi.id: bi for bi in self.book_items} + book_dict = {b.id: b for b in self.books} + author_dict = {a.id: a for a in self.authors} + patron_dict = {p.id: p for p in self.patrons} + + # Link book_item and book to each loan + for loan in self.loans: + loan.book_item = book_item_dict.get(loan.book_item_id) + if loan.book_item: + loan.book_item.book = book_dict.get(loan.book_item.book_id) + if loan.book_item.book: + loan.book_item.book.author = author_dict.get(loan.book_item.book.author_id) + loan.patron = patron_dict.get(loan.patron_id) + # Optionally, link loans to patrons + for patron in self.patrons: + patron.loans = [loan for loan in self.loans if loan.patron_id == patron.id] + + except (FileNotFoundError, json.JSONDecodeError) as e: + print(f"Error loading data: {e}") + self._loaded = False + + def save_loans(self, loans: List[Loan]) -> None: + try: + with open(self.loans_path, 'w', encoding='utf-8') as f: + json.dump([ + { + 'Id': l.id, + 'BookItemId': l.book_item_id, + 'PatronId': l.patron_id, + 'LoanDate': l.loan_date.isoformat() if l.loan_date else None, + 'DueDate': l.due_date.isoformat() if l.due_date else None, + 'ReturnDate': l.return_date.isoformat() if l.return_date else None + } for l in loans + ], f, indent=2) + except Exception as e: + print(f"Error saving loans: {e}") + + def save_patrons(self, patrons: List[Patron]) -> None: + try: + with open(self.patrons_path, 'w', encoding='utf-8') as f: + json.dump([ + { + 'Id': p.id, + 'Name': p.name, + 'MembershipEnd': p.membership_end.isoformat() if p.membership_end else None, + 'MembershipStart': p.membership_start.isoformat() if p.membership_start else None, + 'ImageName': p.image_name + } for p in patrons + ], f, indent=2) + except Exception as e: + print(f"Error saving patrons: {e}") diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/json_loan_repository.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/json_loan_repository.py new file mode 100644 index 0000000..4bcd087 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/json_loan_repository.py @@ -0,0 +1,54 @@ +import json +from datetime import datetime +from application_core.interfaces.iloan_repository import ILoanRepository +from application_core.entities.loan import Loan +from .json_data import JsonData +from typing import Optional + +class JsonLoanRepository(ILoanRepository): + def __init__(self, json_data: JsonData): + self._json_data = json_data + + def get_loan(self, loan_id: int) -> Optional[Loan]: + for loan in self._json_data.loans: + if loan.id == loan_id: + return loan + return None + + def update_loan(self, loan: Loan) -> None: + for idx in range(len(self._json_data.loans)): + if self._json_data.loans[idx].id == loan.id: + self._json_data.loans[idx] = loan + self._json_data.save_loans(self._json_data.loans) + return + + def add_loan(self, loan: Loan) -> None: + self._json_data.loans.append(loan) + self._json_data.save_loans(self._json_data.loans) + self._json_data.load_data() + + def get_loans_by_patron_id(self, patron_id: int): + result = [] + for loan in self._json_data.loans: + if loan.patron_id == patron_id: + result.append(loan) + return result + + def get_all_loans(self): + return self._json_data.loans + + def get_overdue_loans(self, current_date): + overdue = [] + for loan in self._json_data.loans: + if loan.return_date is None and loan.due_date < current_date: + overdue.append(loan) + return overdue + + def sort_loans_by_due_date(self): + # Manual bubble sort for demonstration + n = len(self._json_data.loans) + for i in range(n): + for j in range(0, n - i - 1): + if self._json_data.loans[j].due_date > self._json_data.loans[j + 1].due_date: + self._json_data.loans[j], self._json_data.loans[j + 1] = self._json_data.loans[j + 1], self._json_data.loans[j] + return self._json_data.loans diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/json_patron_repository.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/json_patron_repository.py new file mode 100644 index 0000000..8b25cac --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/infrastructure/json_patron_repository.py @@ -0,0 +1,55 @@ +import json +from application_core.interfaces.ipatron_repository import IPatronRepository +from application_core.entities.patron import Patron +from .json_data import JsonData +from typing import List, Optional + +class JsonPatronRepository(IPatronRepository): + def __init__(self, json_data: JsonData): + self._json_data = json_data + + def get_patron(self, patron_id: int) -> Optional[Patron]: + for patron in self._json_data.patrons: + if patron.id == patron_id: + return patron + return None + + def search_patrons(self, search_input: str) -> List[Patron]: + results = [] + for p in self._json_data.patrons: + if search_input.lower() in p.name.lower(): + results.append(p) + n = len(results) + for i in range(n): + for j in range(0, n - i - 1): + if results[j].name > results[j + 1].name: + results[j], results[j + 1] = results[j + 1], results[j] + return results + + def update_patron(self, patron: Patron) -> None: + for idx in range(len(self._json_data.patrons)): + if self._json_data.patrons[idx].id == patron.id: + self._json_data.patrons[idx] = patron + self._json_data.save_patrons(self._json_data.patrons) + return + + def add_patron(self, patron: Patron) -> None: + self._json_data.patrons.append(patron) + self._json_data.save_patrons(self._json_data.patrons) + self._json_data.load_data() + + def get_all_patrons(self) -> List[Patron]: + return self._json_data.patrons + + def find_patrons_by_name(self, name: str) -> List[Patron]: + result = [] + for patron in self._json_data.patrons: + if patron.name.lower() == name.lower(): + result.append(patron) + return result + + def get_all_books(self): + return self._json_data.books + + def get_all_book_items(self): + return self._json_data.book_items diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/readme.md b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/readme.md new file mode 100644 index 0000000..f084bfd --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/readme.md @@ -0,0 +1,88 @@ +# Library App + +## Description + +Library App is a Python-based console application for managing a library's books, patrons, and loans. It supports searching for patrons and books, checking out and returning books, extending loans, and renewing patron memberships. Data is persisted in JSON files, and the application is structured using a clean separation of entities, repositories, services, and console UI. + +## Project Structure + +- application_core/ + - entities/ + - author.py + - book.py + - book_item.py + - loan.py + - patron.py + - enums/ + - loan_extension_status.py + - loan_return_status.py + - membership_renewal_status.py + - interfaces/ + - iloan_repository.py + - iloan_service.py + - ipatron_repository.py + - ipatron_service.py + - services/ + - loan_service.py + - patron_service.py +- console/ + - book_repository.py + - common_actions.py + - console_app.py + - console_state.py + - main.py +- infrastructure/ + - json_data.py + - json_loan_repository.py + - json_patron_repository.py + - Json/ + - Authors.json + - Books.json + - BookItems.json + - Loans.json + - Patrons.json +- tests/ + - test_loan_service.py + - test_patron_service.py + - __init__.py +- readme.md + +## Key Classes and Interfaces + +- **Entities** + - `Author`, `Book`, `BookItem`, `Loan`, `Patron`: Data models for library domain objects. +- **Enums** + - `LoanExtensionStatus`, `LoanReturnStatus`, `MembershipRenewalStatus`: Status codes for operations. +- **Interfaces** + - `ILoanRepository`, `ILoanService`, `IPatronRepository`, `IPatronService`: Abstract base classes defining contracts for repositories and services. +- **Services** + - `LoanService`: Handles loan operations (checkout, return, extend). + - `PatronService`: Handles patron operations (renew membership, search). +- **Repositories** + - `JsonLoanRepository`, `JsonPatronRepository`: Implement data access using JSON files. + - `JsonData`: Loads and saves all data from/to JSON files. +- **Console UI** + - `ConsoleApp`: Main application loop and user interaction. + - `common_actions.py`, `console_state.py`: Define UI actions and states. + +## Usage + +1. **Install Requirements** + No external dependencies are required beyond Python 3.7+. + +2. **Run the Application** + From the `console` directory (or project root), run: + ``` + python -m console.main + ``` + Follow the on-screen prompts to search for patrons, manage loans, and check book availability. + +3. **Run Tests** + From the project root, run: + ``` + python -m unittest discover tests + ``` + +## License + +This project is licensed under the MIT License. \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/tests/__init__.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/tests/test_loan_service.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/tests/test_loan_service.py new file mode 100644 index 0000000..04f53fb --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/tests/test_loan_service.py @@ -0,0 +1,26 @@ +import unittest +from unittest.mock import MagicMock +from application_core.services.loan_service import LoanService +from application_core.entities.loan import Loan +from application_core.enums.loan_return_status import LoanReturnStatus +from datetime import datetime, timedelta + +class TestLoanService(unittest.TestCase): + def setUp(self): + self.mock_repo = MagicMock() + self.service = LoanService(self.mock_repo) + + def test_return_loan_success(self): + loan = Loan(id=1, book_item_id=1, patron_id=1, patron=None, loan_date=datetime.now()-timedelta(days=10), due_date=datetime.now()+timedelta(days=10), return_date=None, book_item=None) + self.mock_repo.get_loan.return_value = loan + self.mock_repo.update_loan.return_value = None + status = self.service.return_loan(1) + self.assertEqual(status, LoanReturnStatus.SUCCESS) + + def test_return_loan_not_found(self): + self.mock_repo.get_loan.return_value = None + status = self.service.return_loan(1) + self.assertEqual(status, LoanReturnStatus.LOAN_NOT_FOUND) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/tests/test_patron_service.py b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/tests/test_patron_service.py new file mode 100644 index 0000000..1f6d63f --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/AccelerateDevGHCopilot/library/tests/test_patron_service.py @@ -0,0 +1,26 @@ +import unittest +from unittest.mock import MagicMock +from application_core.services.patron_service import PatronService +from application_core.entities.patron import Patron +from application_core.enums.membership_renewal_status import MembershipRenewalStatus +from datetime import datetime, timedelta + +class PatronServiceTest(unittest.TestCase): + def setUp(self): + self.mock_repo = MagicMock() + self.service = PatronService(self.mock_repo) + + def test_renew_membership_success(self): + patron = Patron(id=1, name="John Doe", membership_end=datetime.now()-timedelta(days=1), membership_start=datetime.now()-timedelta(days=365)) + self.mock_repo.get_patron.return_value = patron + self.mock_repo.update_patron.return_value = None + status = self.service.renew_membership(1) + self.assertEqual(status, MembershipRenewalStatus.SUCCESS) + + def test_renew_membership_patron_not_found(self): + self.mock_repo.get_patron.return_value = None + status = self.service.renew_membership(1) + self.assertEqual(status, MembershipRenewalStatus.PATRON_NOT_FOUND) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/readme.txt b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/readme.txt new file mode 100644 index 0000000..7cbbb83 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests-pytest/readme.txt @@ -0,0 +1 @@ +placeholder \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/.gitignore b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/.gitignore new file mode 100644 index 0000000..9a75e6b --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/.gitignore @@ -0,0 +1,119 @@ +# Build Folders (you can keep bin if you'd like, to store dlls and pdbs) +[Bb]in/ +[Oo]bj/ + +# mstest test results +TestResults + +## VSCode +.vscode/* + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Include dlls if theyfre in the NuGet packages directory +!/packages/*/lib/*.dll +!/packages/*/lib/*/*.dll +# Include dlls if they're in the CommonReferences directory +!*CommonReferences/*.dll + +# Build results +[Dd]ebug/ +[Rr]elease/ +x64/ +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.log +*.vspscc +*.vssscc +*.sln +.builds + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper* + +# NCrunch +*.ncrunch* +.*crunch*.local.xml + +# Installshield output folder +[Ee]xpress + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish + +# Publish Web Output +*.Publish.xml + +# NuGet Packages Directory +# packages + +# Windows Azure Build Output +csx +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +[Bb]in +[Oo]bj +sql +TestResults +[Tt]est[Rr]esult* +*.[Cc]ache +*.editorconfig +ClientBin +[Ss]tyle[Cc]op.* +~$* +*.dbmdl +Generated_Code #added for RIA/Silverlight projects + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/README.md b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/README.md new file mode 100644 index 0000000..6fc0e23 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/README.md @@ -0,0 +1,76 @@ +# Library App + +## Description + +Library App is a modular application designed to manage library operations such as book loans, patron management, and inventory tracking. It is built using .NET and follows a clean architecture approach to ensure scalability and maintainability. + +## Project Structure + +- `AccelerateDevGHCopilot.sln` - Solution file for the project. +- `src/` + - `Library.ApplicationCore/` + - `Entities/` - Contains core domain entities. + - `Enums/` - Defines enumerations used across the application. + - `Interfaces/` - Declares interfaces for core abstractions. + - `Services/` - Implements business logic and domain services. + - `Library.ApplicationCore.csproj` - Project file for the Application Core. + - `Library.Console/` + - `appSettings.json` - Configuration file for the console application. + - `CommonActions.cs` - Contains reusable actions for the console app. + - `ConsoleApp.cs` - Main application logic for the console interface. + - `ConsoleState.cs` - Manages the state of the console application. + - `Program.cs` - Entry point for the console application. + - `Json/` - Contains JSON-related utilities or data. + - `Library.Console.csproj` - Project file for the Console application. + - `Library.Infrastructure/` + - `Data/` - Contains data access implementations. + - `Library.Infrastructure.csproj` - Project file for the Infrastructure layer. +- `tests/` + - `UnitTests/` + - `LoanFactory.cs` - Factory for creating test data related to loans. + - `PatronFactory.cs` - Factory for creating test data related to patrons. + - `ApplicationCore/` - Contains unit tests for the Application Core. + - `UnitTests.csproj` - Project file for unit tests. + +## Key Classes and Interfaces + +- **Entities** + - `Book` - Represents a book in the library. + - `Patron` - Represents a library patron. + - `Loan` - Represents a loan transaction. +- **Interfaces** + - `IBookRepository` - Interface for book-related data operations. + - `IPatronRepository` - Interface for patron-related data operations. + - `ILoanService` - Interface for managing loan operations. +- **Services** + - `LoanService` - Implements loan-related business logic. + - `NotificationService` - Handles notifications for overdue loans. + +## Usage + +1. Clone the repository: + + ```bash + git clone + ``` + +2. Open the solution file `AccelerateDevGHCopilot.sln` in Visual Studio. + +3. Build the solution to restore dependencies and compile the code. + +4. Run the console application: + + ```bash + dotnet run --project src/Library.Console/Library.Console.csproj + ``` + +5. Execute unit tests: + + ```bash + dotnet test tests/UnitTests/UnitTests.csproj + ``` + +## License + +This project is licensed under the MIT License. See the `LICENSE` file for details. + diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Author.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Author.cs new file mode 100644 index 0000000..5d9d1a6 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Author.cs @@ -0,0 +1,7 @@ +namespace Library.ApplicationCore.Entities; + +public class Author +{ + public int Id { get; set; } + public required string Name { get; set; } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Book.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Book.cs new file mode 100644 index 0000000..029b467 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Book.cs @@ -0,0 +1,12 @@ +namespace Library.ApplicationCore.Entities; + +public class Book +{ + public int Id { get; set; } + public required string Title { get; set; } + public int AuthorId { get; set; } + public required string Genre { get; set; } + public required string ImageName { get; set; } + public required string ISBN { get; set; } + public Author? Author { get; set; } +} diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/BookItem.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/BookItem.cs new file mode 100644 index 0000000..5a97332 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/BookItem.cs @@ -0,0 +1,10 @@ +namespace Library.ApplicationCore.Entities; + +public class BookItem +{ + public int Id { get; set; } + public int BookId { get; set; } + public DateTime AcquisitionDate { get; set; } + public string? Condition { get; set; } + public Book? Book { get; set; } +} diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Loan.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Loan.cs new file mode 100644 index 0000000..6d0c33e --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Loan.cs @@ -0,0 +1,13 @@ +namespace Library.ApplicationCore.Entities; + +public class Loan +{ + public int Id { get; set; } + public int BookItemId { get; set; } + public int PatronId { get; set; } + public Patron? Patron { get; set; } + public DateTime LoanDate { get; set; } + public DateTime DueDate { get; set; } + public DateTime? ReturnDate { get; set; } + public BookItem? BookItem { get; set; } +} diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Patron.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Patron.cs new file mode 100644 index 0000000..3a2fd33 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Patron.cs @@ -0,0 +1,11 @@ +namespace Library.ApplicationCore.Entities; + +public class Patron +{ + public int Id { get; set; } + public required string Name { get; set; } + public DateTime MembershipEnd { get; set; } + public DateTime MembershipStart { get; set; } + public string? ImageName { get; set; } + public ICollection Loans { get; set; } = new HashSet(); +} diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/EnumHelper.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/EnumHelper.cs new file mode 100644 index 0000000..5369856 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/EnumHelper.cs @@ -0,0 +1,27 @@ +using System.ComponentModel; +using System.Reflection; + +namespace Library.ApplicationCore.Enums; + +public static class EnumHelper +{ + public static string GetDescription(Enum value) + { + if (value == null) + return string.Empty; + + FieldInfo fieldInfo = value.GetType().GetField(value.ToString())!; + + DescriptionAttribute[] attributes = + (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false); + + if (attributes != null && attributes.Length > 0) + { + return attributes[0].Description; + } + else + { + return value.ToString(); + } + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanExtensionStatus.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanExtensionStatus.cs new file mode 100644 index 0000000..2af2c4a --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanExtensionStatus.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; + +namespace Library.ApplicationCore.Enums; + +public enum LoanExtensionStatus +{ + [Description("Book loan extension was successful.")] + Success, + + [Description("Loan not found.")] + LoanNotFound, + + [Description("Cannot extend book loan as it already has expired. Return the book instead.")] + LoanExpired, + + [Description("Cannot extend book loan due to expired patron's membership.")] + MembershipExpired, + + [Description("Cannot extend book loan as the book is already returned.")] + LoanReturned, + + [Description("Cannot extend book loan due to an error.")] + Error +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanReturnStatus.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanReturnStatus.cs new file mode 100644 index 0000000..61edf46 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanReturnStatus.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; + +namespace Library.ApplicationCore.Enums; + +public enum LoanReturnStatus +{ + [Description("Book was successfully returned.")] + Success, + + [Description("Loan not found.")] + LoanNotFound, + + [Description("Cannot return book as the book is already returned.")] + AlreadyReturned, + + [Description("Cannot return book due to an error.")] + Error +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/MembershipRenewalStatus.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/MembershipRenewalStatus.cs new file mode 100644 index 0000000..1323ae3 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/MembershipRenewalStatus.cs @@ -0,0 +1,21 @@ +using System.ComponentModel; + +namespace Library.ApplicationCore.Enums; + +public enum MembershipRenewalStatus +{ + [Description("Membership renewal was successful.")] + Success, + + [Description("Patron not found.")] + PatronNotFound, + + [Description("It is too early to renew the membership.")] + TooEarlyToRenew, + + [Description("Cannot renew membership due to an outstanding loan.")] + LoanNotReturned, + + [Description("Cannot renew membership due to an error.")] + Error +} diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanRepository.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanRepository.cs new file mode 100644 index 0000000..ab00b02 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanRepository.cs @@ -0,0 +1,8 @@ +using Library.ApplicationCore.Entities; + +namespace Library.ApplicationCore; + +public interface ILoanRepository { + Task GetLoan(int loanId); + Task UpdateLoan(Loan loan); +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanService.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanService.cs new file mode 100644 index 0000000..cb255ce --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanService.cs @@ -0,0 +1,7 @@ +using Library.ApplicationCore.Enums; + +public interface ILoanService +{ + Task ReturnLoan(int loanId); + Task ExtendLoan(int loanId); +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronRepository.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronRepository.cs new file mode 100644 index 0000000..19b97f8 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronRepository.cs @@ -0,0 +1,10 @@ +using Library.ApplicationCore.Entities; + +namespace Library.ApplicationCore; + +public interface IPatronRepository { + Task GetPatron(int patronId); + Task> SearchPatrons(string searchInput); + Task UpdatePatron(Patron patron); +} + diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronService.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronService.cs new file mode 100644 index 0000000..6b5f453 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronService.cs @@ -0,0 +1,6 @@ +using Library.ApplicationCore.Enums; + +public interface IPatronService +{ + Task RenewMembership(int patronId); +} diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Library.ApplicationCore.csproj b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Library.ApplicationCore.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Library.ApplicationCore.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/LoanService.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/LoanService.cs new file mode 100644 index 0000000..0f13d3a --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/LoanService.cs @@ -0,0 +1,70 @@ +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; + +public class LoanService : ILoanService +{ + private ILoanRepository _loanRepository; + + public LoanService(ILoanRepository loanRepository) + { + _loanRepository = loanRepository; + } + + public async Task ReturnLoan(int loanId) + { + Loan? loan = await _loanRepository.GetLoan(loanId); + if (loan == null) + { + return LoanReturnStatus.LoanNotFound; + } + + // check if already returned + if (loan.ReturnDate != null) + { + return LoanReturnStatus.AlreadyReturned; + } + + loan.ReturnDate = DateTime.Now; + try + { + await _loanRepository.UpdateLoan(loan); + return LoanReturnStatus.Success; + } + catch (Exception e) + { + return LoanReturnStatus.Error; + } + } + + public const int ExtendByDays = 14; + + public async Task ExtendLoan(int loanId) + { + var loan = await _loanRepository.GetLoan(loanId); + + if (loan == null) + return LoanExtensionStatus.LoanNotFound; + + // Check if patron's membership is expired + if (loan.Patron!.MembershipEnd < DateTime.Now) + return LoanExtensionStatus.MembershipExpired; + + if (loan.ReturnDate != null) + return LoanExtensionStatus.LoanReturned; + + if (loan.DueDate < DateTime.Now) + return LoanExtensionStatus.LoanExpired; + + loan.DueDate = loan.DueDate.AddDays(ExtendByDays); + try + { + await _loanRepository.UpdateLoan(loan); + return LoanExtensionStatus.Success; + } + catch (Exception e) + { + return LoanExtensionStatus.Error; + } + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/PatronService.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/PatronService.cs new file mode 100644 index 0000000..7ba6d78 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/PatronService.cs @@ -0,0 +1,36 @@ +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; + +public class PatronService : IPatronService +{ + private readonly IPatronRepository _patronRepository; + + public PatronService(IPatronRepository patronRepository) + { + _patronRepository = patronRepository; + } + + public async Task RenewMembership(int patronId) + { + var patron = await _patronRepository.GetPatron(patronId); + if (patron == null) + return MembershipRenewalStatus.PatronNotFound; + + // don't allow to renew till 1 month before expiration + if (patron.MembershipEnd >= DateTime.Now.AddMonths(1)) + return MembershipRenewalStatus.TooEarlyToRenew; + + // don't allow to renew if patron has overdue loans + if (patron.Loans.Any(l => (l.ReturnDate == null) && l.DueDate < DateTime.Now)) + return MembershipRenewalStatus.LoanNotReturned; + + patron.MembershipEnd = patron.MembershipEnd.AddYears(1); + try{ + await _patronRepository.UpdatePatron(patron); + return MembershipRenewalStatus.Success; + } catch (Exception e) { + return MembershipRenewalStatus.Error; + } + } +} diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/CommonActions.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/CommonActions.cs new file mode 100644 index 0000000..a002da6 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/CommonActions.cs @@ -0,0 +1,14 @@ +namespace Library.Console; + +[Flags] +public enum CommonActions +{ + Repeat = 0, + Select = 1, + Quit = 2, + SearchPatrons = 4, + RenewPatronMembership = 8, + ReturnLoanedBook = 16, + ExtendLoanedBook = 32, + SearchBooks = 64 +} diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs new file mode 100644 index 0000000..64d0bb4 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs @@ -0,0 +1,326 @@ +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; +using Library.Console; +using System.Globalization; +using Library.Infrastructure.Data; + +public class ConsoleApp +{ + ConsoleState _currentState = ConsoleState.PatronSearch; + + List matchingPatrons = new List(); + + Patron? selectedPatronDetails = null; + Loan selectedLoanDetails = null!; + + IPatronRepository _patronRepository; + ILoanRepository _loanRepository; + ILoanService _loanService; + IPatronService _patronService; + JsonData _jsonData; + + public ConsoleApp(ILoanService loanService, IPatronService patronService, IPatronRepository patronRepository, ILoanRepository loanRepository, JsonData jsonData) + { + _patronRepository = patronRepository; + _loanRepository = loanRepository; + _loanService = loanService; + _patronService = patronService; + _jsonData = jsonData; + } + + public async Task Run() + { + while (true) + { + switch (_currentState) + { + case ConsoleState.PatronSearch: + _currentState = await PatronSearch(); + break; + case ConsoleState.PatronSearchResults: + _currentState = await PatronSearchResults(); + break; + case ConsoleState.PatronDetails: + _currentState = await PatronDetails(); + break; + case ConsoleState.LoanDetails: + _currentState = await LoanDetails(); + break; + } + } + } + + async Task PatronSearch() + { + string searchInput = ReadPatronName(); + + matchingPatrons = await _patronRepository.SearchPatrons(searchInput); + + // Guard-style clauses for edge cases + if (matchingPatrons.Count > 20) + { + Console.WriteLine("More than 20 patrons satisfy the search, please provide more specific input..."); + return ConsoleState.PatronSearch; + } + else if (matchingPatrons.Count == 0) + { + Console.WriteLine("No matching patrons found."); + return ConsoleState.PatronSearch; + } + + Console.WriteLine("Matching Patrons:"); + PrintPatronsList(matchingPatrons); + return ConsoleState.PatronSearchResults; + } + + static string ReadPatronName() + { + string? searchInput = null; + while (String.IsNullOrWhiteSpace(searchInput)) + { + Console.Write("Enter a string to search for patrons by name: "); + + searchInput = Console.ReadLine(); + } + return searchInput; + } + + static void PrintPatronsList(List matchingPatrons) + { + int patronNumber = 1; + foreach (Patron patron in matchingPatrons) + { + Console.WriteLine($"{patronNumber}) {patron.Name}"); + patronNumber++; + } + } + + async Task PatronSearchResults() + { + CommonActions options = CommonActions.Select | CommonActions.SearchPatrons | CommonActions.Quit; + CommonActions action = ReadInputOptions(options, out int selectedPatronNumber); + if (action == CommonActions.Select) + { + if (selectedPatronNumber >= 1 && selectedPatronNumber <= matchingPatrons.Count) + { + var selectedPatron = matchingPatrons.ElementAt(selectedPatronNumber - 1); + selectedPatronDetails = await _patronRepository.GetPatron(selectedPatron.Id)!; + return ConsoleState.PatronDetails; + } + else + { + Console.WriteLine("Invalid patron number. Please try again."); + return ConsoleState.PatronSearchResults; + } + } + else if (action == CommonActions.Quit) + { + return ConsoleState.Quit; + } + else if (action == CommonActions.SearchPatrons) + { + return ConsoleState.PatronSearch; + } + + throw new InvalidOperationException("An input option is not handled."); + } + + static CommonActions ReadInputOptions(CommonActions options, out int optionNumber) + { + CommonActions action; + optionNumber = 0; + do + { + Console.WriteLine(); + WriteInputOptions(options); + string? userInput = Console.ReadLine(); + + action = userInput switch + { + "q" when options.HasFlag(CommonActions.Quit) => CommonActions.Quit, + "s" when options.HasFlag(CommonActions.SearchPatrons) => CommonActions.SearchPatrons, + "m" when options.HasFlag(CommonActions.RenewPatronMembership) => CommonActions.RenewPatronMembership, + "e" when options.HasFlag(CommonActions.ExtendLoanedBook) => CommonActions.ExtendLoanedBook, + "r" when options.HasFlag(CommonActions.ReturnLoanedBook) => CommonActions.ReturnLoanedBook, + "b" when options.HasFlag(CommonActions.SearchBooks) => CommonActions.SearchBooks, + _ when int.TryParse(userInput, out optionNumber) => CommonActions.Select, + _ => CommonActions.Repeat + }; + + if (action == CommonActions.Repeat) + { + Console.WriteLine("Invalid input. Please try again."); + } + } while (action == CommonActions.Repeat); + return action; + } + + static void WriteInputOptions(CommonActions options) + { + Console.WriteLine("Input Options:"); + if (options.HasFlag(CommonActions.ReturnLoanedBook)) + { + Console.WriteLine(" - \"r\" to mark as returned"); + } + if (options.HasFlag(CommonActions.ExtendLoanedBook)) + { + Console.WriteLine(" - \"e\" to extend the book loan"); + } + if (options.HasFlag(CommonActions.RenewPatronMembership)) + { + Console.WriteLine(" - \"m\" to extend patron's membership"); + } + if (options.HasFlag(CommonActions.SearchPatrons)) + { + Console.WriteLine(" - \"s\" for new search"); + } + if (options.HasFlag(CommonActions.SearchBooks)) + { + Console.WriteLine(" - \"b\" to check for book availability"); + } + if (options.HasFlag(CommonActions.Quit)) + { + Console.WriteLine(" - \"q\" to quit"); + } + if (options.HasFlag(CommonActions.Select)) + { + Console.WriteLine("Or type a number to select a list item."); + } + } + + async Task PatronDetails() + { + Console.WriteLine($"Name: {selectedPatronDetails.Name}"); + Console.WriteLine($"Membership Expiration: {selectedPatronDetails.MembershipEnd}"); + Console.WriteLine(); + Console.WriteLine("Book Loans:"); + int loanNumber = 1; + foreach (Loan loan in selectedPatronDetails.Loans) + { + Console.WriteLine($"{loanNumber}) {loan.BookItem!.Book!.Title} - Due: {loan.DueDate} - Returned: {(loan.ReturnDate != null).ToString()}"); + loanNumber++; + } + + CommonActions options = CommonActions.SearchPatrons | CommonActions.Quit | CommonActions.Select | CommonActions.RenewPatronMembership | CommonActions.SearchBooks; + CommonActions action = ReadInputOptions(options, out int selectedLoanNumber); + if (action == CommonActions.Select) + { + if (selectedLoanNumber >= 1 && selectedLoanNumber <= selectedPatronDetails.Loans.Count()) + { + var selectedLoan = selectedPatronDetails.Loans.ElementAt(selectedLoanNumber - 1); + selectedLoanDetails = selectedPatronDetails.Loans.Where(l => l.Id == selectedLoan.Id).Single(); + return ConsoleState.LoanDetails; + } + else + { + Console.WriteLine("Invalid book loan number. Please try again."); + return ConsoleState.PatronDetails; + } + } + else if (action == CommonActions.Quit) + { + return ConsoleState.Quit; + } + else if (action == CommonActions.SearchPatrons) + { + return ConsoleState.PatronSearch; + } + else if (action == CommonActions.RenewPatronMembership) + { + var status = await _patronService.RenewMembership(selectedPatronDetails.Id); + Console.WriteLine(EnumHelper.GetDescription(status)); + // reloading after renewing membership + selectedPatronDetails = (await _patronRepository.GetPatron(selectedPatronDetails.Id))!; + return ConsoleState.PatronDetails; + } + else if (action == CommonActions.SearchBooks) + { + await SearchBooks(); + return ConsoleState.PatronDetails; + } + + throw new InvalidOperationException("An input option is not handled."); + } + + async Task SearchBooks() + { + string? bookTitle = null; + while (string.IsNullOrWhiteSpace(bookTitle)) + { + Console.Write("Enter a book title to search for: "); + bookTitle = Console.ReadLine(); + } + + await _jsonData.EnsureDataLoaded(); + + var book = _jsonData.Books!.FirstOrDefault(b => string.Equals(b.Title, bookTitle, StringComparison.OrdinalIgnoreCase)); + if (book == null) + { + Console.WriteLine($"No book found with the title \"{bookTitle}\"."); + return ConsoleState.PatronDetails; + } + + var bookItem = _jsonData.BookItems!.FirstOrDefault(bi => bi.BookId == book.Id); + if (bookItem == null) + { + Console.WriteLine($"No book item found for the title \"{book.Title}\"."); + return ConsoleState.PatronDetails; + } + + var loan = _jsonData.Loans!.FirstOrDefault(l => l.BookItemId == bookItem.Id && l.ReturnDate == null); + if (loan == null) + { + Console.WriteLine($"\"{book.Title}\" is available for loan."); + } + else + { + Console.WriteLine($"\"{book.Title}\" is on loan to another patron. The return due date is {loan.DueDate.ToString("d", CultureInfo.InvariantCulture)}."); + } + + return ConsoleState.PatronDetails; + } + + async Task LoanDetails() + { + Console.WriteLine($"Book title: {selectedLoanDetails.BookItem!.Book!.Title}"); + Console.WriteLine($"Book Author: {selectedLoanDetails.BookItem!.Book!.Author!.Name}"); + Console.WriteLine($"Due date: {selectedLoanDetails.DueDate}"); + Console.WriteLine($"Returned: {(selectedLoanDetails.ReturnDate != null).ToString()}"); + Console.WriteLine(); + + CommonActions options = CommonActions.SearchPatrons | CommonActions.Quit | CommonActions.ReturnLoanedBook | CommonActions.ExtendLoanedBook; + CommonActions action = ReadInputOptions(options, out int selectedLoanNumber); + + if (action == CommonActions.ExtendLoanedBook) + { + var status = await _loanService.ExtendLoan(selectedLoanDetails.Id); + Console.WriteLine(EnumHelper.GetDescription(status)); + + // reload loan after extending + selectedPatronDetails = (await _patronRepository.GetPatron(selectedPatronDetails.Id))!; + selectedLoanDetails = (await _loanRepository.GetLoan(selectedLoanDetails.Id))!; + return ConsoleState.LoanDetails; + } + else if (action == CommonActions.ReturnLoanedBook) + { + var status = await _loanService.ReturnLoan(selectedLoanDetails.Id); + + Console.WriteLine(EnumHelper.GetDescription(status)); + _currentState = ConsoleState.LoanDetails; + // reload loan after returning + selectedLoanDetails = await _loanRepository.GetLoan(selectedLoanDetails.Id); + return ConsoleState.LoanDetails; + } + else if (action == CommonActions.Quit) + { + return ConsoleState.Quit; + } + else if (action == CommonActions.SearchPatrons) + { + return ConsoleState.PatronSearch; + } + + throw new InvalidOperationException("An input option is not handled."); + } +} diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/ConsoleState.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/ConsoleState.cs new file mode 100644 index 0000000..e9117b6 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/ConsoleState.cs @@ -0,0 +1,10 @@ +namespace Library.Console; + +public enum ConsoleState +{ + PatronSearch, + PatronSearchResults, + PatronDetails, + LoanDetails, + Quit +} diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/Json/Authors.json b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/Json/Authors.json new file mode 100644 index 0000000..1357eb1 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/Json/Authors.json @@ -0,0 +1,82 @@ +[ + { + "Id": 1, + "Name": "Author One" + }, + { + "Id": 2, + "Name": "Author Two" + }, + { + "Id": 3, + "Name": "Author Three" + }, + { + "Id": 4, + "Name": "Author Four" + }, + { + "Id": 5, + "Name": "Author Five" + }, + { + "Id": 6, + "Name": "Author Six" + }, + { + "Id": 7, + "Name": "Author Seven" + }, + { + "Id": 8, + "Name": "Author Eight" + }, + { + "Id": 9, + "Name": "Author Nine" + }, + { + "Id": 10, + "Name": "Author Ten" + }, + { + "Id": 11, + "Name": "Author Eleven" + }, + { + "Id": 12, + "Name": "Author Twelve" + }, + { + "Id": 13, + "Name": "Author Thirteen" + }, + { + "Id": 14, + "Name": "Author Fourteen" + }, + { + "Id": 15, + "Name": "Author Fifteen" + }, + { + "Id": 16, + "Name": "Author Sixteen" + }, + { + "Id": 17, + "Name": "Author Seventeen" + }, + { + "Id": 18, + "Name": "Author Eighteen" + }, + { + "Id": 19, + "Name": "Author Nineteen" + }, + { + "Id": 20, + "Name": "Author Twenty" + } +] \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/Json/BookItems.json b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/Json/BookItems.json new file mode 100644 index 0000000..ed659c5 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/Json/BookItems.json @@ -0,0 +1,122 @@ +[ + { + "Id": 1, + "BookId": 1, + "AcquisitionDate": "2023-09-20T00:40:43.1716563", + "Condition": "Good" + }, + { + "Id": 2, + "BookId": 2, + "AcquisitionDate": "2023-09-20T00:40:43.1717503", + "Condition": "Fair" + }, + { + "Id": 3, + "BookId": 3, + "AcquisitionDate": "2023-09-20T00:40:43.1717511", + "Condition": "Excellent" + }, + { + "Id": 4, + "BookId": 4, + "AcquisitionDate": "2023-09-20T00:40:43.1717513", + "Condition": "Poor" + }, + { + "Id": 5, + "BookId": 5, + "AcquisitionDate": "2023-09-20T00:40:43.1717516", + "Condition": "Good" + }, + { + "Id": 6, + "BookId": 6, + "AcquisitionDate": "2023-09-20T00:40:43.1717521", + "Condition": "Fair" + }, + { + "Id": 7, + "BookId": 7, + "AcquisitionDate": "2023-09-20T00:40:43.1717523", + "Condition": "Excellent" + }, + { + "Id": 8, + "BookId": 8, + "AcquisitionDate": "2023-09-20T00:40:43.1717526", + "Condition": "Poor" + }, + { + "Id": 9, + "BookId": 9, + "AcquisitionDate": "2023-09-20T00:40:43.171757", + "Condition": "Good" + }, + { + "Id": 10, + "BookId": 10, + "AcquisitionDate": "2023-09-20T00:40:43.1717574", + "Condition": "Fair" + }, + { + "Id": 11, + "BookId": 11, + "AcquisitionDate": "2023-09-20T00:40:43.1717576", + "Condition": "Excellent" + }, + { + "Id": 12, + "BookId": 12, + "AcquisitionDate": "2023-09-20T00:40:43.1717578", + "Condition": "Poor" + }, + { + "Id": 13, + "BookId": 13, + "AcquisitionDate": "2023-09-20T00:40:43.171758", + "Condition": "Good" + }, + { + "Id": 14, + "BookId": 14, + "AcquisitionDate": "2023-09-20T00:40:43.1717609", + "Condition": "Fair" + }, + { + "Id": 15, + "BookId": 15, + "AcquisitionDate": "2023-09-20T00:40:43.1717611", + "Condition": "Excellent" + }, + { + "Id": 16, + "BookId": 16, + "AcquisitionDate": "2023-09-20T00:40:43.1717613", + "Condition": "Poor" + }, + { + "Id": 17, + "BookId": 17, + "AcquisitionDate": "2023-09-20T00:40:43.1717616", + "Condition": "Good" + }, + { + "Id": 18, + "BookId": 18, + "AcquisitionDate": "2023-09-20T00:40:43.1717619", + "Condition": "Fair" + }, + { + "Id": 19, + "BookId": 19, + "AcquisitionDate": "2023-09-20T00:40:43.1717621", + "Condition": "Excellent" + }, + { + "Id": 20, + "BookId": 20, + "AcquisitionDate": "2023-09-20T00:40:43.1717626", + "Condition": "Poor" + } +] \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/Json/Books.json b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/Json/Books.json new file mode 100644 index 0000000..51f3339 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/Json/Books.json @@ -0,0 +1,162 @@ +[ + { + "Id": 1, + "Title": "Book One", + "AuthorId": 1, + "Genre": "Dystopian", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524935" + }, + { + "Id": 2, + "Title": "Book Two", + "AuthorId": 2, + "Genre": "Classic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524936" + }, + { + "Id": 3, + "Title": "Book Three", + "AuthorId": 3, + "Genre": "Romance", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524937" + }, + { + "Id": 4, + "Title": "Book Four", + "AuthorId": 4, + "Genre": "Classic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524938" + }, + { + "Id": 5, + "Title": "Book Five", + "AuthorId": 5, + "Genre": "Coming-of-age", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524939" + }, + { + "Id": 6, + "Title": "Book Six", + "AuthorId": 6, + "Genre": "Modernist", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524940" + }, + { + "Id": 7, + "Title": "Book Seven", + "AuthorId": 7, + "Genre": "Adventure", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524941" + }, + { + "Id": 8, + "Title": "Book Eight", + "AuthorId": 8, + "Genre": "Fantasy", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524942" + }, + { + "Id": 9, + "Title": "Book Nine", + "AuthorId": 9, + "Genre": "Fantasy", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524943" + }, + { + "Id": 10, + "Title": "Book Ten", + "AuthorId": 10, + "Genre": "Epic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524944" + }, + { + "Id": 11, + "Title": "Book Eleven", + "AuthorId": 11, + "Genre": "Psychological", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524945" + }, + { + "Id": 12, + "Title": "Book Twelve", + "AuthorId": 12, + "Genre": "Psychological", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524946" + }, + { + "Id": 13, + "Title": "Book Thirteen", + "AuthorId": 13, + "Genre": "Magical realism", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524947" + }, + { + "Id": 14, + "Title": "Book Fourteen", + "AuthorId": 14, + "Genre": "Classic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524948" + }, + { + "Id": 15, + "Title": "Book Fifteen", + "AuthorId": 15, + "Genre": "Adventure", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524949" + }, + { + "Id": 16, + "Title": "Book Sixteen", + "AuthorId": 16, + "Genre": "Historical", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524950" + }, + { + "Id": 17, + "Title": "Book Seventeen", + "AuthorId": 17, + "Genre": "Gothic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524951" + }, + { + "Id": 18, + "Title": "Book Eighteen", + "AuthorId": 18, + "Genre": "Dystopian", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524952" + }, + { + "Id": 19, + "Title": "Book Nineteen", + "AuthorId": 19, + "Genre": "Classic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524953" + }, + { + "Id": 20, + "Title": "Book Twenty", + "AuthorId": 20, + "Genre": "Adventure", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524954" + } +] \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/Json/Loans.json b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/Json/Loans.json new file mode 100644 index 0000000..a84491d --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/Json/Loans.json @@ -0,0 +1,402 @@ +[ + { + "Id": 1, + "BookItemId": 17, + "PatronId": 22, + "LoanDate": "2023-12-08T00:40:43.1808862", + "DueDate": "2023-12-22T00:40:43.1808862", + "ReturnDate": null + }, + { + "Id": 2, + "BookItemId": 6, + "PatronId": 28, + "LoanDate": "2023-12-17T00:40:43.1809243", + "DueDate": "2023-12-31T00:40:43.1809243", + "ReturnDate": null + }, + { + "Id": 3, + "BookItemId": 16, + "PatronId": 4, + "LoanDate": "2023-12-23T00:40:43.1809289", + "DueDate": "2024-01-06T00:40:43.1809289", + "ReturnDate": null + }, + { + "Id": 4, + "BookItemId": 17, + "PatronId": 14, + "LoanDate": "2023-12-22T00:40:43.1809292", + "DueDate": "2024-01-05T00:40:43.1809292", + "ReturnDate": null + }, + { + "Id": 5, + "BookItemId": 6, + "PatronId": 9, + "LoanDate": "2023-12-09T00:40:43.1809295", + "DueDate": "2023-12-23T00:40:43.1809295", + "ReturnDate": null + }, + { + "Id": 6, + "BookItemId": 14, + "PatronId": 25, + "LoanDate": "2023-12-27T00:40:43.18093", + "DueDate": "2024-01-10T00:40:43.18093", + "ReturnDate": null + }, + { + "Id": 7, + "BookItemId": 12, + "PatronId": 50, + "LoanDate": "2023-12-27T00:40:43.1809304", + "DueDate": "2024-01-10T00:40:43.1809304", + "ReturnDate": null + }, + { + "Id": 8, + "BookItemId": 18, + "PatronId": 28, + "LoanDate": "2023-12-26T00:40:43.1809306", + "DueDate": "2024-01-09T00:40:43.1809306", + "ReturnDate": null + }, + { + "Id": 9, + "BookItemId": 8, + "PatronId": 9, + "LoanDate": "2023-12-10T00:40:43.1809309", + "DueDate": "2023-12-24T00:40:43.1809309", + "ReturnDate": null + }, + { + "Id": 10, + "BookItemId": 16, + "PatronId": 3, + "LoanDate": "2023-12-26T00:40:43.1809312", + "DueDate": "2024-01-09T00:40:43.1809312", + "ReturnDate": null + }, + { + "Id": 11, + "BookItemId": 4, + "PatronId": 42, + "LoanDate": "2023-12-15T00:40:43.1809315", + "DueDate": "2023-12-29T00:40:43.1809315", + "ReturnDate": null + }, + { + "Id": 12, + "BookItemId": 17, + "PatronId": 7, + "LoanDate": "2023-12-23T00:40:43.1809331", + "DueDate": "2024-01-06T00:40:43.1809331", + "ReturnDate": null + }, + { + "Id": 13, + "BookItemId": 12, + "PatronId": 5, + "LoanDate": "2023-12-27T00:40:43.1809333", + "DueDate": "2024-01-10T00:40:43.1809333", + "ReturnDate": null + }, + { + "Id": 14, + "BookItemId": 4, + "PatronId": 9, + "LoanDate": "2023-12-10T00:40:43.1809337", + "DueDate": "2023-12-24T00:40:43.1809337", + "ReturnDate": null + }, + { + "Id": 15, + "BookItemId": 7, + "PatronId": 28, + "LoanDate": "2023-12-23T00:40:43.1809339", + "DueDate": "2024-01-06T00:40:43.1809339", + "ReturnDate": null + }, + { + "Id": 16, + "BookItemId": 14, + "PatronId": 3, + "LoanDate": "2023-12-08T00:40:43.1809342", + "DueDate": "2023-12-22T00:40:43.1809342", + "ReturnDate": null + }, + { + "Id": 17, + "BookItemId": 5, + "PatronId": 48, + "LoanDate": "2023-12-16T00:40:43.1809344", + "DueDate": "2023-12-30T00:40:43.1809344", + "ReturnDate": null + }, + { + "Id": 18, + "BookItemId": 4, + "PatronId": 49, + "LoanDate": "2023-12-19T00:40:43.1809348", + "DueDate": "2024-01-02T00:40:43.1809348", + "ReturnDate": null + }, + { + "Id": 19, + "BookItemId": 13, + "PatronId": 33, + "LoanDate": "2023-12-28T00:40:43.180935", + "DueDate": "2024-01-11T00:40:43.180935", + "ReturnDate": null + }, + { + "Id": 20, + "BookItemId": 14, + "PatronId": 48, + "LoanDate": "2023-12-27T00:40:43.1809353", + "DueDate": "2024-01-10T00:40:43.1809353", + "ReturnDate": null + }, + { + "Id": 21, + "BookItemId": 7, + "PatronId": 5, + "LoanDate": "2023-12-12T00:40:43.1809368", + "DueDate": "2023-12-26T00:40:43.1809368", + "ReturnDate": null + }, + { + "Id": 22, + "BookItemId": 9, + "PatronId": 1, + "LoanDate": "2023-12-09T00:40:43.1809371", + "DueDate": "2023-12-23T00:40:43.1809371", + "ReturnDate": null + }, + { + "Id": 23, + "BookItemId": 11, + "PatronId": 33, + "LoanDate": "2023-12-26T00:40:43.1809374", + "DueDate": "2024-01-09T00:40:43.1809374", + "ReturnDate": null + }, + { + "Id": 24, + "BookItemId": 10, + "PatronId": 46, + "LoanDate": "2023-12-28T00:40:43.1809376", + "DueDate": "2024-01-11T00:40:43.1809376", + "ReturnDate": null + }, + { + "Id": 25, + "BookItemId": 20, + "PatronId": 41, + "LoanDate": "2023-12-12T00:40:43.1809379", + "DueDate": "2023-12-26T00:40:43.1809379", + "ReturnDate": null + }, + { + "Id": 26, + "BookItemId": 13, + "PatronId": 15, + "LoanDate": "2023-12-16T00:40:43.1809382", + "DueDate": "2023-12-30T00:40:43.1809382", + "ReturnDate": null + }, + { + "Id": 27, + "BookItemId": 15, + "PatronId": 23, + "LoanDate": "2023-12-18T00:40:43.1809384", + "DueDate": "2024-01-01T00:40:43.1809384", + "ReturnDate": null + }, + { + "Id": 28, + "BookItemId": 15, + "PatronId": 31, + "LoanDate": "2023-12-11T00:40:43.1809387", + "DueDate": "2023-12-25T00:40:43.1809387", + "ReturnDate": null + }, + { + "Id": 29, + "BookItemId": 4, + "PatronId": 10, + "LoanDate": "2023-12-18T00:40:43.1809402", + "DueDate": "2024-01-01T00:40:43.1809402", + "ReturnDate": null + }, + { + "Id": 30, + "BookItemId": 6, + "PatronId": 18, + "LoanDate": "2023-12-12T00:40:43.1809405", + "DueDate": "2023-12-26T00:40:43.1809405", + "ReturnDate": null + }, + { + "Id": 31, + "BookItemId": 11, + "PatronId": 3, + "LoanDate": "2023-12-16T00:40:43.1809408", + "DueDate": "2023-12-30T00:40:43.1809408", + "ReturnDate": null + }, + { + "Id": 32, + "BookItemId": 8, + "PatronId": 20, + "LoanDate": "2023-12-22T00:40:43.1809411", + "DueDate": "2024-01-05T00:40:43.1809411", + "ReturnDate": null + }, + { + "Id": 33, + "BookItemId": 14, + "PatronId": 12, + "LoanDate": "2023-12-28T00:40:43.1809415", + "DueDate": "2024-01-11T00:40:43.1809415", + "ReturnDate": null + }, + { + "Id": 34, + "BookItemId": 19, + "PatronId": 29, + "LoanDate": "2023-12-28T00:40:43.1809458", + "DueDate": "2024-01-11T00:40:43.1809458", + "ReturnDate": "2023-12-29T00:40:54.582495" + }, + { + "Id": 35, + "BookItemId": 7, + "PatronId": 45, + "LoanDate": "2023-12-17T00:40:43.180946", + "DueDate": "2023-12-31T00:40:43.180946", + "ReturnDate": null + }, + { + "Id": 36, + "BookItemId": 11, + "PatronId": 3, + "LoanDate": "2023-12-10T00:40:43.1809463", + "DueDate": "2023-12-24T00:40:43.1809463", + "ReturnDate": null + }, + { + "Id": 37, + "BookItemId": 1, + "PatronId": 5, + "LoanDate": "2023-12-18T00:40:43.1809466", + "DueDate": "2024-01-18T00:40:43.1809466", + "ReturnDate": "2024-01-17T00:40:43.1809466" + }, + { + "Id": 38, + "BookItemId": 15, + "PatronId": 25, + "LoanDate": "2023-12-26T00:40:43.1809481", + "DueDate": "2024-01-09T00:40:43.1809481", + "ReturnDate": null + }, + { + "Id": 39, + "BookItemId": 4, + "PatronId": 33, + "LoanDate": "2023-12-18T00:40:43.1809484", + "DueDate": "2024-01-01T00:40:43.1809484", + "ReturnDate": null + }, + { + "Id": 40, + "BookItemId": 5, + "PatronId": 33, + "LoanDate": "2023-12-25T00:40:43.1809487", + "DueDate": "2024-01-08T00:40:43.1809487", + "ReturnDate": null + }, + { + "Id": 41, + "BookItemId": 14, + "PatronId": 13, + "LoanDate": "2023-12-15T00:40:43.1809489", + "DueDate": "2023-12-29T00:40:43.1809489", + "ReturnDate": null + }, + { + "Id": 42, + "BookItemId": 11, + "PatronId": 10, + "LoanDate": "2023-12-12T00:40:43.1809493", + "DueDate": "2023-12-26T00:40:43.1809493", + "ReturnDate": null + }, + { + "Id": 43, + "BookItemId": 9, + "PatronId": 45, + "LoanDate": "2023-12-14T00:40:43.1809496", + "DueDate": "2023-12-28T00:40:43.1809496", + "ReturnDate": "2023-12-29T00:49:42.3406277" + }, + { + "Id": 44, + "BookItemId": 3, + "PatronId": 46, + "LoanDate": "2023-12-08T00:40:43.1809498", + "DueDate": "2023-12-22T00:40:43.1809498", + "ReturnDate": null + }, + { + "Id": 45, + "BookItemId": 5, + "PatronId": 10, + "LoanDate": "2023-12-24T00:40:43.1809501", + "DueDate": "2024-01-07T00:40:43.1809501", + "ReturnDate": null + }, + { + "Id": 46, + "BookItemId": 1, + "PatronId": 49, + "LoanDate": "2024-07-09T00:40:43.1809503", + "DueDate": "2024-09-09T00:40:43.1809503", + "ReturnDate": null + }, + { + "Id": 47, + "BookItemId": 8, + "PatronId": 36, + "LoanDate": "2023-12-11T00:40:43.1809507", + "DueDate": "2023-12-25T00:40:43.1809507", + "ReturnDate": null + }, + { + "Id": 48, + "BookItemId": 5, + "PatronId": 10, + "LoanDate": "2023-12-18T00:40:43.1809509", + "DueDate": "2024-01-01T00:40:43.1809509", + "ReturnDate": null + }, + { + "Id": 49, + "BookItemId": 20, + "PatronId": 24, + "LoanDate": "2023-12-16T00:40:43.1809512", + "DueDate": "2023-12-30T00:40:43.1809512", + "ReturnDate": null + }, + { + "Id": 50, + "BookItemId": 3, + "PatronId": 45, + "LoanDate": "2023-12-13T00:40:43.1809514", + "DueDate": "2023-12-27T00:40:43.1809514", + "ReturnDate": "2023-12-29T00:49:48.9561798" + } + ] diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/Json/Patrons.json b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/Json/Patrons.json new file mode 100644 index 0000000..5d44d83 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/Json/Patrons.json @@ -0,0 +1,352 @@ +[ + { + "Id": 1, + "Name": "Patron One", + "MembershipEnd": "2024-12-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron One.jpg" + }, + { + "Id": 2, + "Name": "Patron Two", + "MembershipEnd": "2025-02-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Two.jpg" + }, + { + "Id": 3, + "Name": "Patron Three", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Three.jpg" + }, + { + "Id": 4, + "Name": "Patron Four", + "MembershipEnd": "2025-04-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Four.jpg" + }, + { + "Id": 5, + "Name": "Patron Five", + "MembershipEnd": "2025-05-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Five.jpg" + }, + { + "Id": 6, + "Name": "Patron Six", + "MembershipEnd": "2025-06-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Six.jpg" + }, + { + "Id": 7, + "Name": "Patron Seven", + "MembershipEnd": "2025-07-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Seven.jpg" + }, + { + "Id": 8, + "Name": "Patron Eight", + "MembershipEnd": "2024-01-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Eight.jpg" + }, + { + "Id": 9, + "Name": "Patron Nine", + "MembershipEnd": "2024-02-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Nine.jpg" + }, + { + "Id": 10, + "Name": "Patron Ten", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Ten.jpg" + }, + { + "Id": 11, + "Name": "Patron Eleven", + "MembershipEnd": "2024-04-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Eleven.jpg" + }, + { + "Id": 12, + "Name": "Patron Twelve", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twelve.jpg" + }, + { + "Id": 13, + "Name": "Patron Thirteen", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirteen.jpg" + }, + { + "Id": 14, + "Name": "Patron Fourteen", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Fourteen.jpg" + }, + { + "Id": 15, + "Name": "Patron Fifteen", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Fifteen.jpg" + }, + { + "Id": 16, + "Name": "Patron Sixteen", + "MembershipEnd": "2024-04-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Sixteen.jpg" + }, + { + "Id": 17, + "Name": "Patron Seventeen", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Seventeen.jpg" + }, + { + "Id": 18, + "Name": "Patron Eighteen", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Eighteen.jpg" + }, + { + "Id": 19, + "Name": "Patron Nineteen", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Nineteen.jpg" + }, + { + "Id": 20, + "Name": "Patron Twenty", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty.jpg" + }, + { + "Id": 21, + "Name": "Patron Twenty-One", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-One.jpg" + }, + { + "Id": 22, + "Name": "Patron Twenty-Two", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Two.jpg" + }, + { + "Id": 23, + "Name": "Patron Twenty-Three", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Three.jpg" + }, + { + "Id": 24, + "Name": "Patron Twenty-Four", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Four.jpg" + }, + { + "Id": 25, + "Name": "Patron Twenty-Five", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Five.jpg" + }, + { + "Id": 26, + "Name": "Patron Twenty-Six", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Six.jpg" + }, + { + "Id": 27, + "Name": "Patron Twenty-Seven", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Seven.jpg" + }, + { + "Id": 28, + "Name": "Patron Twenty-Eight", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Eight.jpg" + }, + { + "Id": 29, + "Name": "Patron Twenty-Nine", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Nine.jpg" + }, + { + "Id": 30, + "Name": "Patron Thirty", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty.jpg" + }, + { + "Id": 31, + "Name": "Patron Thirty-One", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-One.jpg" + }, + { + "Id": 32, + "Name": "Patron Thirty-Two", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Two.jpg" + }, + { + "Id": 33, + "Name": "Patron Thirty-Three", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Three.jpg" + }, + { + "Id": 34, + "Name": "Patron Thirty-Four", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Four.jpg" + }, + { + "Id": 35, + "Name": "Patron Thirty-Five", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Five.jpg" + }, + { + "Id": 36, + "Name": "Patron Thirty-Six", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Six.jpg" + }, + { + "Id": 37, + "Name": "Patron Thirty-Seven", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Seven.jpg" + }, + { + "Id": 38, + "Name": "Patron Thirty-Eight", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Eight.jpg" + }, + { + "Id": 39, + "Name": "Patron Thirty-Nine", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Nine.jpg" + }, + { + "Id": 40, + "Name": "Patron Forty", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty.jpg" + }, + { + "Id": 41, + "Name": "Patron Forty-One", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-One.jpg" + }, + { + "Id": 42, + "Name": "Patron Forty-Two", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Two.jpg" + }, + { + "Id": 43, + "Name": "Patron Forty-Three", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Three.jpg" + }, + { + "Id": 44, + "Name": "Patron Forty-Four", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Four.jpg" + }, + { + "Id": 45, + "Name": "Patron Forty-Five", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Five.jpg" + }, + { + "Id": 46, + "Name": "Patron Forty-Six", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Six.jpg" + }, + { + "Id": 47, + "Name": "Patron Forty-Seven", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Seven.jpg" + }, + { + "Id": 48, + "Name": "Patron Forty-Eight", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Eight.jpg" + }, + { + "Id": 49, + "Name": "Patron Forty-Nine", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Nine.jpg" + }, + { + "Id": 50, + "Name": "Patron Fifty", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Fifty.jpg" + } +] \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/Library.Console.csproj b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/Library.Console.csproj new file mode 100644 index 0000000..359cee9 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/Library.Console.csproj @@ -0,0 +1,34 @@ + + + + + + + + + Exe + net9.0 + enable + enable + + + + + PreserveNewest + + + + + + PreserveNewest + + + + + + + + + + + diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/Program.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/Program.cs new file mode 100644 index 0000000..1a21671 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/Program.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.DependencyInjection; +using Library.Infrastructure.Data; +using Library.ApplicationCore; +using Microsoft.Extensions.Configuration; + +var services = new ServiceCollection(); + +var configuration = new ConfigurationBuilder() +.SetBasePath(Directory.GetCurrentDirectory()) +.AddJsonFile("appSettings.json") +.Build(); + +services.AddSingleton(configuration); + +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); + +services.AddSingleton(); +services.AddSingleton(); + +var servicesProvider = services.BuildServiceProvider(); + +var consoleApp = servicesProvider.GetRequiredService(); +consoleApp.Run().Wait(); diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/appSettings.json b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/appSettings.json new file mode 100644 index 0000000..3aed751 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Console/appSettings.json @@ -0,0 +1,9 @@ +{ + "JsonPaths": { + "Authors": "Json/Authors.json", + "Books": "Json/Books.json", + "BookItems": "Json/BookItems.json", + "Patrons": "Json/Patrons.json", + "Loans": "Json/Loans.json" + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonData.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonData.cs new file mode 100644 index 0000000..7af26a7 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonData.cs @@ -0,0 +1,212 @@ +using System.Text.Json; +using Library.ApplicationCore.Entities; +using Microsoft.Extensions.Configuration; + +namespace Library.Infrastructure.Data; + +public class JsonData +{ + public List? Authors { get; set; } + public List? Books { get; set; } + public List? BookItems { get; set; } + public List? Patrons { get; set; } + public List? Loans { get; set; } + + private readonly string _authorsPath; + private readonly string _booksPath; + private readonly string _bookItemsPath; + private readonly string _patronsPath; + private readonly string _loansPath; + + public JsonData(IConfiguration configuration) + { + var section = configuration.GetSection("JsonPaths"); + _authorsPath = section["Authors"] ?? Path.Combine("Json", "Authors.json"); + _booksPath = section["Books"] ?? Path.Combine("Json", "Books.json"); + _bookItemsPath = section["BookItems"] ?? Path.Combine("Json", "BookItems.json"); + _patronsPath = section["Patrons"] ?? Path.Combine("Json", "Patrons.json"); + _loansPath = section["Loans"] ?? Path.Combine("Json", "Loans.json"); + } + + public async Task EnsureDataLoaded() + { + if (Patrons == null) + { + await LoadData(); + } + } + + public async Task LoadData() + { + Authors = await LoadJson>(_authorsPath); + Books = await LoadJson>(_booksPath); + BookItems = await LoadJson>(_bookItemsPath); + Patrons = await LoadJson>(_patronsPath); + Loans = await LoadJson>(_loansPath); + } + + public async Task SaveLoans(IEnumerable loans) + { + List loanList = new List(); + foreach (var l in loans) + { + Loan loan = new Loan + { + // making sure only a subset of properties is set and saved + Id = l.Id, + BookItemId = l.BookItemId, + PatronId = l.PatronId, + LoanDate = l.LoanDate, + DueDate = l.DueDate, + ReturnDate = l.ReturnDate + }; + loanList.Add(loan); + } + await SaveJson(_loansPath, loanList); + } + + public async Task SavePatrons(IEnumerable patrons) + { + await SaveJson(_patronsPath, patrons.Select(p => new Patron + { + Id = p.Id, + Name = p.Name, + MembershipStart = p.MembershipStart, + MembershipEnd = p.MembershipEnd, + ImageName = p.ImageName, + }).ToList()); + } + + private async Task SaveJson(string filePath, T data) + { + using (FileStream jsonStream = File.Create(filePath)) + { + await JsonSerializer.SerializeAsync(jsonStream, data); + } + } + + public List GetPopulatedPatrons(IEnumerable patrons) + { + List populated = new List(); + foreach (Patron patron in patrons) + { + populated.Add(GetPopulatedPatron(patron)); + } + return populated; + } + + public Patron GetPopulatedPatron(Patron p) + { + Patron populated = new Patron + { + Id = p.Id, + Name = p.Name, + ImageName = p.ImageName, + MembershipStart = p.MembershipStart, + MembershipEnd = p.MembershipEnd, + Loans = new List() + }; + + foreach (Loan loan in Loans!) + { + if (loan.PatronId == p.Id) + { + populated.Loans.Add(GetPopulatedLoan(loan)); + } + } + + return populated; + } + + public Loan GetPopulatedLoan(Loan l) + { + Loan populated = new Loan + { + Id = l.Id, + BookItemId = l.BookItemId, + PatronId = l.PatronId, + LoanDate = l.LoanDate, + DueDate = l.DueDate, + ReturnDate = l.ReturnDate + }; + + foreach (BookItem bi in BookItems!) + { + if (bi.Id == l.BookItemId) + { + populated.BookItem = GetPopulatedBookItem(bi); + break; + } + } + + foreach (Patron p in Patrons!) + { + if (p.Id == l.PatronId) + { + populated.Patron = p; + break; + } + } + + return populated; + } + + public BookItem GetPopulatedBookItem(BookItem bi) + { + BookItem populated = new BookItem + { + Id = bi.Id, + BookId = bi.BookId, + AcquisitionDate = bi.AcquisitionDate, + Condition = bi.Condition + }; + + foreach (Book b in Books!) + { + if (b.Id == bi.BookId) + { + populated.Book = GetPopulatedBook(b); + break; + } + } + + return populated; + } + + public Book GetPopulatedBook(Book b) + { + Book populated = new Book + { + Id = b.Id, + Title = b.Title, + AuthorId = b.AuthorId, + Genre = b.Genre, + ISBN = b.ISBN, + ImageName = b.ImageName + }; + + foreach (Author a in Authors!) + { + if (a.Id == b.AuthorId) + { + populated.Author = new Author + { + Id = a.Id, + Name = a.Name + }; + break; + } + } + + return populated; + } + + private async Task LoadJson(string filePath) + { + using (FileStream jsonStream = File.OpenRead(filePath)) + { + return await JsonSerializer.DeserializeAsync(jsonStream); + } + } + +} diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonLoanRepository.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonLoanRepository.cs new file mode 100644 index 0000000..2683283 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonLoanRepository.cs @@ -0,0 +1,55 @@ +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; + +namespace Library.Infrastructure.Data; + +public class JsonLoanRepository : ILoanRepository +{ + private readonly JsonData _jsonData; + + public JsonLoanRepository(JsonData jsonData) + { + _jsonData = jsonData; + } + + public async Task GetLoan(int id) + { + await _jsonData.EnsureDataLoaded(); + + foreach (Loan loan in _jsonData.Loans!) + { + if (loan.Id == id) + { + Loan populated = _jsonData.GetPopulatedLoan(loan); + return populated; + } + } + return null; + } + + public async Task UpdateLoan(Loan loan) + { + Loan? existingLoan = null; + foreach (Loan l in _jsonData.Loans!) + { + if (l.Id == loan.Id) + { + existingLoan = l; + break; + } + } + + if (existingLoan != null) + { + existingLoan.BookItemId = loan.BookItemId; + existingLoan.PatronId = loan.PatronId; + existingLoan.LoanDate = loan.LoanDate; + existingLoan.DueDate = loan.DueDate; + existingLoan.ReturnDate = loan.ReturnDate; + + await _jsonData.SaveLoans(_jsonData.Loans!); + + await _jsonData.LoadData(); + } + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonPatronRepository.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonPatronRepository.cs new file mode 100644 index 0000000..efb05f8 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonPatronRepository.cs @@ -0,0 +1,73 @@ +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; + +namespace Library.Infrastructure.Data; + +public class JsonPatronRepository : IPatronRepository +{ + private readonly JsonData _jsonData; + + public JsonPatronRepository(JsonData jsonData) + { + _jsonData = jsonData; + } + + public async Task> SearchPatrons(string searchInput) + { + await _jsonData.EnsureDataLoaded(); + + List searchResults = new List(); + foreach (Patron patron in _jsonData.Patrons) + { + if (patron.Name.Contains(searchInput)) + { + searchResults.Add(patron); + } + } + searchResults.Sort((p1, p2) => String.Compare(p1.Name, p2.Name)); + + searchResults = _jsonData.GetPopulatedPatrons(searchResults); + + return searchResults; + } + + public async Task GetPatron(int id) + { + await _jsonData.EnsureDataLoaded(); + + foreach (Patron patron in _jsonData.Patrons!) + { + if (patron.Id == id) + { + Patron populated = _jsonData.GetPopulatedPatron(patron); + return populated; + } + } + return null; + } + + public async Task UpdatePatron(Patron patron) + { + await _jsonData.EnsureDataLoaded(); + var patrons = _jsonData.Patrons!; + Patron existingPatron = null; + foreach (var p in patrons) + { + if (p.Id == patron.Id) + { + existingPatron = p; + break; + } + } + if (existingPatron != null) + { + existingPatron.Name = patron.Name; + existingPatron.ImageName = patron.ImageName; + existingPatron.MembershipStart = patron.MembershipStart; + existingPatron.MembershipEnd = patron.MembershipEnd; + existingPatron.Loans = patron.Loans; + await _jsonData.SavePatrons(patrons); + await _jsonData.LoadData(); + } + } +} diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Infrastructure/Library.Infrastructure.csproj b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Infrastructure/Library.Infrastructure.csproj new file mode 100644 index 0000000..1a7e6eb --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/src/Library.Infrastructure/Library.Infrastructure.csproj @@ -0,0 +1,17 @@ + + + + + + + + + + + + net9.0 + enable + enable + + + diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ExtendLoan.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ExtendLoan.cs new file mode 100644 index 0000000..d3e695b --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ExtendLoan.cs @@ -0,0 +1,104 @@ +using NSubstitute; +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; + +namespace Library.UnitTests.ApplicationCore.LoanServiceTests; + +public class ExtendLoanTest +{ + private readonly ILoanRepository _mockLoanRepository; + private readonly LoanService _loanService; + + public ExtendLoanTest() + { + _mockLoanRepository = Substitute.For(); + _loanService = new LoanService(_mockLoanRepository); + } + + [Fact(DisplayName = "LoanService.ExtendLoan: Extends the loan successfully")] + public async Task ExtendLoan_ExtendsLoanSuccessfully() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateCurrentLoanForPatron(patron); + var loanDueDate = loan.DueDate; + var loanId = loan.Id; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanExtensionStatus extensionStatus = await _loanService.ExtendLoan(loanId); + + // Assert + Assert.Equal(LoanExtensionStatus.Success, extensionStatus); + Assert.Equal(loanDueDate.AddDays(LoanService.ExtendByDays), loan.DueDate); + } + + [Fact(DisplayName = "LoanService.ExtendLoan: Returns LoanNotFound if loan is not found")] + public async Task ExtendLoan_ReturnsLoanNotFound() + { + // Arrange + var loanId = 1; + _mockLoanRepository.GetLoan(loanId).Returns((Loan?)null); + + // Act + LoanExtensionStatus extensionStatus = await _loanService.ExtendLoan(loanId); + + // Assert + Assert.Equal(LoanExtensionStatus.LoanNotFound, extensionStatus); + } + + [Fact(DisplayName = "LoanService.ExtendLoan: Returns MembershipExpired if patron's membership is expired")] + public async Task ExtendLoan_ReturnsMembershipExpired() + { + // Arrange + var patron = PatronFactory.CreateExpiredPatron(); + var loan = LoanFactory.CreateCurrentLoanForPatron(patron); + var loanId = loan.Id; + var loanDueDate = loan.DueDate; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanExtensionStatus extensionStatus = await _loanService.ExtendLoan(loanId); + + // Assert + Assert.Equal(LoanExtensionStatus.MembershipExpired, extensionStatus); + Assert.Equal(loanDueDate, loan.DueDate); + } + + [Fact(DisplayName = "LoanService.ExtendLoan: Returns LoanReturned if loan is already returned")] + public async Task ExtendLoan_ReturnsLoanReturned() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateReturnedLoanForPatron(patron); + var loanId = loan.Id; + var loanDueDate = loan.DueDate; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanExtensionStatus extensionStatus = await _loanService.ExtendLoan(loanId); + + // Assert + Assert.Equal(LoanExtensionStatus.LoanReturned, extensionStatus); + Assert.Equal(loanDueDate, loan.DueDate); + } + + [Fact(DisplayName = "LoanService.ExtendLoan: Returns LoanExpired if loan is already expired")] + public async Task ExtendLoan_ReturnsLoanExpired() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateExpiredLoanForPatron(patron); + var loanId = loan.Id; + var loanDueDate = loan.DueDate; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanExtensionStatus extensionStatus = await _loanService.ExtendLoan(loanId); + + // Assert + Assert.Equal(LoanExtensionStatus.LoanExpired, extensionStatus); + Assert.Equal(loanDueDate, loan.DueDate); + } +} diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ReturnLoan.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ReturnLoan.cs new file mode 100644 index 0000000..68c3a0d --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ReturnLoan.cs @@ -0,0 +1,99 @@ +using NSubstitute; +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; + +namespace Library.UnitTests.ApplicationCore.LoanServiceTests; + +public class ReturnLoanTest +{ + private readonly ILoanRepository _mockLoanRepository; + private readonly LoanService _loanService; + + public ReturnLoanTest() + { + _mockLoanRepository = Substitute.For(); + _loanService = new LoanService(_mockLoanRepository); + } + + [Fact(DisplayName = "LoanService.ReturnLoan: Returns LoanNotFound if loan is not found")] + public async Task ReturnLoan_ReturnsLoanNotFound() + { + // Arrange + var loanId = 1; + _mockLoanRepository.GetLoan(loanId).Returns((Loan?)null); + + // Act + LoanReturnStatus returnStatus = await _loanService.ReturnLoan(loanId); + + // Assert + Assert.Equal(LoanReturnStatus.LoanNotFound, returnStatus); + } + + [Fact(DisplayName = "LoanService.ReturnLoan: Returns AlreadyReturned if loan is already returned")] + public async Task ReturnLoan_ReturnsAlreadyReturned() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateReturnedLoanForPatron(patron); + var loanId = loan.Id; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanReturnStatus returnStatus = await _loanService.ReturnLoan(loanId); + + // Assert + Assert.Equal(LoanReturnStatus.AlreadyReturned, returnStatus); + } + + [Fact(DisplayName = "LoanService.ReturnLoan: Returns Success and updates return date for a patron with current membership")] + public async Task ReturnLoan_ReturnsSuccessAndUpdateReturnDate() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateCurrentLoanForPatron(patron); + var loanId = loan.Id; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanReturnStatus returnStatus = await _loanService.ReturnLoan(loanId); + + // Assert + Assert.Equal(LoanReturnStatus.Success, returnStatus); + Assert.NotNull(loan.ReturnDate); + } + + [Fact(DisplayName = "LoanService.ReturnLoan: Returns Success and updates return date for an expired loan")] + public async Task ReturnLoan_ReturnsSuccessAndUpdateReturnDateForExpiredLoan() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateExpiredLoanForPatron(patron); + var loanId = loan.Id; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanReturnStatus returnStatus = await _loanService.ReturnLoan(loanId); + + // Assert + Assert.Equal(LoanReturnStatus.Success, returnStatus); + Assert.NotNull(loan.ReturnDate); + } + + [Fact(DisplayName = "LoanService.ReturnLoan: Returns Success and updates return date for a patron with expired membership")] + public async Task ReturnLoan_ReturnsSuccessAndUpdateReturnDateForExpiredPatron() + { + // Arrange + var patron = PatronFactory.CreateExpiredPatron(); + var loan = LoanFactory.CreateCurrentLoanForPatron(patron); + var loanId = loan.Id; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanReturnStatus returnStatus = await _loanService.ReturnLoan(loanId); + + // Assert + Assert.Equal(LoanReturnStatus.Success, returnStatus); + Assert.NotNull(loan.ReturnDate); + } +} diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/PatronService/RenewMembership.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/PatronService/RenewMembership.cs new file mode 100644 index 0000000..ff4d24c --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/PatronService/RenewMembership.cs @@ -0,0 +1,142 @@ +using NSubstitute; +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; + +namespace Library.UnitTests.ApplicationCore.PatronServiceTests; + +public class RenewMembershipTest +{ + private readonly IPatronRepository _mockPatronRepository; + private readonly PatronService _patronService; + + public RenewMembershipTest() + { + _mockPatronRepository = Substitute.For(); + _patronService = new PatronService(_mockPatronRepository); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Renews the membership successfully without loans")] + public async Task RenewMembership_RenewsMembershipSuccessfully() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var membershipEnd = patron.MembershipEnd; + var patronId = patron.Id; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.Success, renewalStatus); + Assert.Equal(membershipEnd.AddYears(1), patron.MembershipEnd); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Renews the membership successfully with expired membership")] + public async Task RenewMembership_RenewsMembershipSuccessfullyWithExpiredMembership() + { + // Arrange + //var membershipEnd = DateTime.Now.AddMonths(-2); + var patron = PatronFactory.CreateExpiredPatron(); + var membershipEnd = patron.MembershipEnd; + var patronId = patron.Id; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.Success, renewalStatus); + Assert.Equal(membershipEnd.AddYears(1), patron.MembershipEnd); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Renews the membership successfully with returned loans")] + public async Task RenewMembership_RenewsMembershipSuccessfullyWithReturnedLoans() + { + // Arrange + var membershipEnd = DateTime.Now.AddDays(1); + var patron = PatronFactory.CreateCurrentPatron(); + patron.MembershipEnd = membershipEnd; + var patronId = patron.Id; + patron.Loans = new List { + LoanFactory.CreateReturnedLoanForPatron(patron) + }; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.Success, renewalStatus); + Assert.Equal(membershipEnd.AddYears(1), patron.MembershipEnd); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Renews the membership successfully with current loans")] + public async Task RenewMembership_RenewsMembershipSuccessfullyWithCurrentLoans() + { + // Arrange + var membershipEnd = DateTime.Now.AddDays(1); + var patron = PatronFactory.CreateCurrentPatron(); + patron.MembershipEnd = membershipEnd; + var patronId = patron.Id; + patron.Loans = new List { + LoanFactory.CreateCurrentLoanForPatron(patron) + }; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.Success, renewalStatus); + Assert.Equal(membershipEnd.AddYears(1), patron.MembershipEnd); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Returns PatronNotFound if patron is not found")] + public async Task RenewMembership_ReturnsPatronNotFound() + { + // Arrange + var patronId = 42; + _mockPatronRepository.GetPatron(patronId).Returns((Patron?)null); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.PatronNotFound, renewalStatus); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Returns TooEarlyToRenew if renewal is not allowed yet")] + public async Task RenewMembership_ReturnsTooEarlyToRenew() + { + // Arrange + var patron = PatronFactory.CreateTooEarlyToRenewPatron(); + var patronId = patron.Id; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.TooEarlyToRenew, renewalStatus); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Returns LoanNotReturned if patron has overdue loans")] + public async Task RenewMembership_ReturnsLoanNotReturned() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var patronId = patron.Id; + patron.Loans = new List { + LoanFactory.CreateExpiredLoanForPatron(patron) + }; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.LoanNotReturned, renewalStatus); + } +} diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/tests/UnitTests/LoanFactory.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/tests/UnitTests/LoanFactory.cs new file mode 100644 index 0000000..251000d --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/tests/UnitTests/LoanFactory.cs @@ -0,0 +1,42 @@ +using Library.ApplicationCore.Entities; + +public static class LoanFactory +{ + public static int loanId = 777; + + public static Loan CreateReturnedLoanForPatron(Patron patron) + { + return new Loan + { + Id = loanId++, + DueDate = DateTime.Now.AddDays(1), + ReturnDate = DateTime.Now.AddDays(-1), + PatronId = patron.Id, + Patron = patron + }; + } + + public static Loan CreateCurrentLoanForPatron(Patron patron) + { + return new Loan + { + Id = loanId++, + DueDate = DateTime.Now.AddDays(1), + ReturnDate = null, + PatronId = patron.Id, + Patron = patron + }; + } + + public static Loan CreateExpiredLoanForPatron(Patron patron) + { + return new Loan + { + Id = loanId++, + DueDate = DateTime.Now.AddDays(-1), + ReturnDate = null, + PatronId = patron.Id, + Patron = patron + }; + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/tests/UnitTests/PatronFactory.cs b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/tests/UnitTests/PatronFactory.cs new file mode 100644 index 0000000..a9c36b7 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/tests/UnitTests/PatronFactory.cs @@ -0,0 +1,39 @@ +using Library.ApplicationCore.Entities; + +public static class PatronFactory +{ + public static int patronId = 42; + + public static Patron CreateCurrentPatron() + { + return new Patron + { + Id = patronId++, + Name = "John Doe", + MembershipEnd = DateTime.Now.AddDays(1), + Loans = new List() + }; + } + + public static Patron CreateTooEarlyToRenewPatron() + { + return new Patron + { + Id = patronId++, + Name = "John Doe", + MembershipEnd = DateTime.Now.AddMonths(2), + Loans = new List() + }; + } + + public static Patron CreateExpiredPatron() + { + return new Patron + { + Id = patronId++, + Name = "John Doe", + MembershipEnd = DateTime.Now.AddMonths(-2), + Loans = new List() + }; + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/tests/UnitTests/UnitTests.csproj b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/tests/UnitTests/UnitTests.csproj new file mode 100644 index 0000000..a156d8f --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/AccelerateDevGHCopilot/tests/UnitTests/UnitTests.csproj @@ -0,0 +1,28 @@ + + + + net9.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + diff --git a/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/readme.txt b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/readme.txt new file mode 100644 index 0000000..b3a4252 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m4-develop-unit-tests/readme.txt @@ -0,0 +1 @@ +placeholder \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/.gitignore b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/.gitignore new file mode 100644 index 0000000..dc73868 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/.gitignore @@ -0,0 +1,68 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*.pyo +*.pyd + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +.env/ +.venv/ +env/ +venv/ +ENV/ +ENV*/ + +# PyInstaller +*.manifest +*.spec + +# Unit test / coverage / pytest +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.pytest_cache/ +test-results/ +junit-*.xml + +# Jupyter Notebook +.ipynb_checkpoints/ + +# mypy +.mypy_cache/ +.dmypy.json + +# Pyre type checker +.pyre/ + +# Pyright type checker +.pyrightcache/ + +# VS Code settings +.vscode/ \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/entities/author.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/entities/author.py new file mode 100644 index 0000000..dec0e83 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/entities/author.py @@ -0,0 +1,6 @@ +from dataclasses import dataclass + +@dataclass +class Author: + id: int + name: str diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/entities/book.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/entities/book.py new file mode 100644 index 0000000..50f4e38 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/entities/book.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass +from typing import Optional +from .author import Author + +@dataclass +class Book: + id: int + title: str + author_id: int + genre: str + image_name: str + isbn: str + author: Optional[Author] = None diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/entities/book_item.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/entities/book_item.py new file mode 100644 index 0000000..f5f7fb7 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/entities/book_item.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass +from typing import Optional +from datetime import datetime +from .book import Book + +@dataclass +class BookItem: + id: int + book_id: int + acquisition_date: datetime + condition: Optional[str] = None + book: Optional[Book] = None diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/entities/loan.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/entities/loan.py new file mode 100644 index 0000000..51955ea --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/entities/loan.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass +from typing import Optional +from datetime import datetime +from .patron import Patron +from .book_item import BookItem + +@dataclass +class Loan: + id: int + book_item_id: int + patron_id: int + patron: Optional[Patron] = None + loan_date: datetime = None + due_date: datetime = None + return_date: Optional[datetime] = None + book_item: Optional[BookItem] = None diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/entities/patron.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/entities/patron.py new file mode 100644 index 0000000..98e5096 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/entities/patron.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass, field +from typing import List, Optional +from datetime import datetime +# from .loan import Loan # Use string annotation to avoid circular import + +@dataclass +class Patron: + id: int + name: str + membership_end: datetime + membership_start: datetime + image_name: Optional[str] = None + loans: List['Loan'] = field(default_factory=list) diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/enums/loan_extension_status.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/enums/loan_extension_status.py new file mode 100644 index 0000000..20cf2c5 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/enums/loan_extension_status.py @@ -0,0 +1,9 @@ +from enum import Enum + +class LoanExtensionStatus(Enum): + SUCCESS = 'Book loan extension was successful.' + LOAN_NOT_FOUND = 'Loan not found.' + LOAN_EXPIRED = 'Cannot extend book loan as it already has expired. Return the book instead.' + MEMBERSHIP_EXPIRED = "Cannot extend book loan due to expired patron's membership." + LOAN_RETURNED = 'Cannot extend book loan as the book is already returned.' + ERROR = 'Cannot extend book loan due to an error.' diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/enums/loan_return_status.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/enums/loan_return_status.py new file mode 100644 index 0000000..5f9221a --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/enums/loan_return_status.py @@ -0,0 +1,7 @@ +from enum import Enum + +class LoanReturnStatus(Enum): + SUCCESS = 'Book was successfully returned.' + LOAN_NOT_FOUND = 'Loan not found.' + ALREADY_RETURNED = 'Cannot return book as the book is already returned.' + ERROR = 'Cannot return book due to an error.' diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/enums/membership_renewal_status.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/enums/membership_renewal_status.py new file mode 100644 index 0000000..e36433e --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/enums/membership_renewal_status.py @@ -0,0 +1,8 @@ +from enum import Enum + +class MembershipRenewalStatus(Enum): + SUCCESS = 'Membership renewal was successful.' + PATRON_NOT_FOUND = 'Patron not found.' + TOO_EARLY_TO_RENEW = 'It is too early to renew the membership.' + LOAN_NOT_RETURNED = 'Cannot renew membership due to an outstanding loan.' + ERROR = 'Cannot renew membership due to an error.' diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_repository.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_repository.py new file mode 100644 index 0000000..273d78f --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_repository.py @@ -0,0 +1,20 @@ +from abc import ABC, abstractmethod +from typing import Optional +from ..entities.loan import Loan + +class ILoanRepository(ABC): + @abstractmethod + def get_loan(self, loan_id: int) -> Optional[Loan]: + pass + + @abstractmethod + def update_loan(self, loan: Loan) -> None: + pass + + @abstractmethod + def add_loan(self, loan: Loan) -> None: + pass + + @abstractmethod + def get_loans_by_patron_id(self, patron_id: int): + pass diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_service.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_service.py new file mode 100644 index 0000000..866b407 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/iloan_service.py @@ -0,0 +1,16 @@ +from abc import ABC, abstractmethod +from ..enums.loan_return_status import LoanReturnStatus +from ..enums.loan_extension_status import LoanExtensionStatus + +class ILoanService(ABC): + @abstractmethod + def return_loan(self, loan_id: int) -> LoanReturnStatus: + pass + + @abstractmethod + def extend_loan(self, loan_id: int) -> LoanExtensionStatus: + pass + + @abstractmethod + def checkout_book(self, patron, book_item, loan_id=None) -> None: + pass diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_repository.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_repository.py new file mode 100644 index 0000000..c116101 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_repository.py @@ -0,0 +1,24 @@ +from abc import ABC, abstractmethod +from typing import List, Optional +from ..entities.patron import Patron + +class IPatronRepository(ABC): + @abstractmethod + def get_patron(self, patron_id: int) -> Optional[Patron]: + pass + + @abstractmethod + def search_patrons(self, search_input: str) -> List[Patron]: + pass + + @abstractmethod + def update_patron(self, patron: Patron) -> None: + pass + + @abstractmethod + def get_all_books(self): + pass + + @abstractmethod + def get_all_book_items(self): + pass diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_service.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_service.py new file mode 100644 index 0000000..e20199f --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/interfaces/ipatron_service.py @@ -0,0 +1,7 @@ +from abc import ABC, abstractmethod +from ..enums.membership_renewal_status import MembershipRenewalStatus + +class IPatronService(ABC): + @abstractmethod + def renew_membership(self, patron_id: int) -> MembershipRenewalStatus: + pass diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/services/loan_service.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/services/loan_service.py new file mode 100644 index 0000000..bf554e6 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/services/loan_service.py @@ -0,0 +1,67 @@ +from ..interfaces.iloan_service import ILoanService +from ..interfaces.iloan_repository import ILoanRepository +from ..enums.loan_return_status import LoanReturnStatus +from ..enums.loan_extension_status import LoanExtensionStatus +from datetime import datetime, timedelta + +class LoanService(ILoanService): + EXTEND_BY_DAYS = 14 + + def __init__(self, loan_repository: ILoanRepository): + self._loan_repository = loan_repository + + def return_loan(self, loan_id: int) -> LoanReturnStatus: + loan = self._loan_repository.get_loan(loan_id) + if loan is None: + return LoanReturnStatus.LOAN_NOT_FOUND + if loan.return_date is not None: + return LoanReturnStatus.ALREADY_RETURNED + loan.return_date = datetime.now() + try: + self._loan_repository.update_loan(loan) + return LoanReturnStatus.SUCCESS + except Exception: + return LoanReturnStatus.ERROR + + def extend_loan(self, loan_id: int) -> LoanExtensionStatus: + loan = self._loan_repository.get_loan(loan_id) + if loan is None: + return LoanExtensionStatus.LOAN_NOT_FOUND + if loan.patron and loan.patron.membership_end < datetime.now(): + return LoanExtensionStatus.MEMBERSHIP_EXPIRED + if loan.return_date is not None: + return LoanExtensionStatus.LOAN_RETURNED + if loan.due_date < datetime.now(): + return LoanExtensionStatus.LOAN_EXPIRED + try: + loan.due_date = loan.due_date + timedelta(days=self.EXTEND_BY_DAYS) + self._loan_repository.update_loan(loan) + return LoanExtensionStatus.SUCCESS + except Exception: + return LoanExtensionStatus.ERROR + + def checkout_book(self, patron, book_item, loan_id=None) -> None: + from ..entities.loan import Loan + from datetime import datetime, timedelta + # Generate a new loan ID if not provided + if loan_id is None: + all_loans = getattr(self._loan_repository, 'get_all_loans', lambda: [])() + max_id = 0 + for l in all_loans: + if l.id > max_id: + max_id = l.id + loan_id = max_id + 1 if all_loans else 1 + now = datetime.now() + due = now + timedelta(days=14) + new_loan = Loan( + id=loan_id, + book_item_id=book_item.id, + patron_id=patron.id, + patron=patron, + loan_date=now, + due_date=due, + return_date=None, + book_item=book_item + ) + self._loan_repository.add_loan(new_loan) + return new_loan diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/services/patron_service.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/services/patron_service.py new file mode 100644 index 0000000..7d43a98 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/application_core/services/patron_service.py @@ -0,0 +1,30 @@ +from ..interfaces.ipatron_service import IPatronService +from ..interfaces.ipatron_repository import IPatronRepository +from ..entities.patron import Patron +from ..enums.membership_renewal_status import MembershipRenewalStatus +from datetime import datetime, timedelta + +class PatronService(IPatronService): + EXTEND_BY_DAYS = 365 + + def __init__(self, patron_repository: IPatronRepository): + self._patron_repository = patron_repository + + def renew_membership(self, patron_id: int) -> MembershipRenewalStatus: + patron = self._patron_repository.get_patron(patron_id) + if patron is None: + return MembershipRenewalStatus.PATRON_NOT_FOUND + if patron.membership_end < datetime.now(): + patron.membership_end = datetime.now() + timedelta(days=self.EXTEND_BY_DAYS) + else: + patron.membership_end = patron.membership_end + timedelta(days=self.EXTEND_BY_DAYS) + self._patron_repository.update_patron(patron) + return MembershipRenewalStatus.SUCCESS + + def find_patron_by_name(self, name: str): + results = [] + all_patrons = self._patron_repository.get_all_patrons() + for patron in all_patrons: + if patron.name.lower() == name.lower(): + results.append(patron) + return results diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/console/book_repository.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/console/book_repository.py new file mode 100644 index 0000000..2544e3d --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/console/book_repository.py @@ -0,0 +1,30 @@ +class BookRepository: + def __init__(self, json_data): + self.books = json_data.get('books', []) + self.authors = json_data.get('authors', []) + self.book_items = json_data.get('book_items', []) + + def get_book_by_title(self, title): + for book in self.books: + if book.title.lower() == title.lower(): + return book + return None + +class BookItemRepository: + def __init__(self, json_data): + self.books = json_data.get('books', []) + self.authors = json_data.get('authors', []) + self.book_items = json_data.get('book_items', []) + + def get_book_by_title(self, title): + for book in self.books: + if book.title.lower() == title.lower(): + return book + return None + + def get_items_by_book_id(self, book_id): + items = [] + for item in self.book_items: + if item.book_id == book_id: + items.append(item) + return items \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/console/common_actions.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/console/common_actions.py new file mode 100644 index 0000000..20fe63e --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/console/common_actions.py @@ -0,0 +1,11 @@ +from enum import Flag, auto + +class CommonActions(Flag): + REPEAT = 0 + SELECT = auto() + QUIT = auto() + SEARCH_PATRONS = auto() + SEARCH_BOOKS = auto() + RENEW_PATRON_MEMBERSHIP = auto() + RETURN_LOANED_BOOK = auto() + EXTEND_LOANED_BOOK = auto() diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/console/console_app.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/console/console_app.py new file mode 100644 index 0000000..c11e458 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/console/console_app.py @@ -0,0 +1,318 @@ +from .console_state import ConsoleState +from .common_actions import CommonActions +from application_core.interfaces.ipatron_repository import IPatronRepository +from application_core.interfaces.iloan_repository import ILoanRepository +from application_core.interfaces.iloan_service import ILoanService +from application_core.interfaces.ipatron_service import IPatronService +from typing import Optional + +class ConsoleApp: + def __init__( + self, + loan_service: ILoanService, + patron_service: IPatronService, + patron_repository: IPatronRepository, + loan_repository: ILoanRepository, + json_data # <-- add json_data parameter + ): + self._current_state: ConsoleState = ConsoleState.PATRON_SEARCH + self.matching_patrons = [] + self.selected_patron_details = None + self.selected_loan_details = None + self._patron_repository = patron_repository + self._loan_repository = loan_repository + self._loan_service = loan_service + self._patron_service = patron_service + self._json_data = json_data # <-- store json_data + + def write_input_options(self, options): + print("Input Options:") + if options & CommonActions.RETURN_LOANED_BOOK: + print(' - "r" to mark as returned') + if options & CommonActions.EXTEND_LOANED_BOOK: + print(' - "e" to extend the book loan') + if options & CommonActions.RENEW_PATRON_MEMBERSHIP: + print(' - "m" to extend patron\'s membership') + if options & CommonActions.SEARCH_PATRONS: + print(' - "s" for new search') + if options & CommonActions.SEARCH_BOOKS: + print(' - "b" to check for book availability') + if options & CommonActions.QUIT: + print(' - "q" to quit') + if options & CommonActions.SELECT: + print(' - type a number to select a list item.') + + def run(self) -> None: + while True: + if self._current_state == ConsoleState.PATRON_SEARCH: + self._current_state = self.patron_search() + elif self._current_state == ConsoleState.PATRON_SEARCH_RESULTS: + self._current_state = self.patron_search_results() + elif self._current_state == ConsoleState.PATRON_DETAILS: + self._current_state = self.patron_details() + elif self._current_state == ConsoleState.LOAN_DETAILS: + self._current_state = self.loan_details() + elif self._current_state == ConsoleState.QUIT: + break + + def patron_search(self) -> ConsoleState: + search_input = input("Enter a string to search for patrons by name: ").strip() + if not search_input: + print("No input provided. Please try again.") + return ConsoleState.PATRON_SEARCH + self.matching_patrons = self._patron_repository.search_patrons(search_input) + if not self.matching_patrons: + print("No matching patrons found.") + return ConsoleState.PATRON_SEARCH + return ConsoleState.PATRON_SEARCH_RESULTS + + def patron_search_results(self) -> ConsoleState: + print("\nMatching Patrons:") + idx = 1 + for patron in self.matching_patrons: + print(f"{idx}) {patron.name}") + idx += 1 + if self.matching_patrons: + self.write_input_options( + CommonActions.SELECT | CommonActions.SEARCH_PATRONS | CommonActions.QUIT + ) + else: + self.write_input_options( + CommonActions.SEARCH_PATRONS | CommonActions.QUIT + ) + selection = input("Enter your choice: ").strip().lower() + if selection == 'q': + return ConsoleState.QUIT + elif selection == 's': + return ConsoleState.PATRON_SEARCH + elif selection.isdigit(): + idx = int(selection) + if 1 <= idx <= len(self.matching_patrons): + self.selected_patron_details = self.matching_patrons[idx - 1] + return ConsoleState.PATRON_DETAILS + else: + print("Invalid selection. Please enter a valid number.") + return ConsoleState.PATRON_SEARCH_RESULTS + else: + print("Invalid input. Please enter a number, 's', or 'q'.") + return ConsoleState.PATRON_SEARCH_RESULTS + + def patron_details(self) -> ConsoleState: + patron = self.selected_patron_details + print(f"\nName: {patron.name}") + print(f"Membership Expiration: {patron.membership_end}") + loans = self._loan_repository.get_loans_by_patron_id(patron.id) + print("\nBook Loans History:") + + valid_loans = self._print_loans(loans) + + if valid_loans: + options = ( + CommonActions.RENEW_PATRON_MEMBERSHIP + | CommonActions.SEARCH_PATRONS + | CommonActions.QUIT + | CommonActions.SELECT + | CommonActions.SEARCH_BOOKS # Added SEARCH_BOOKS to options + ) + selection = self._get_patron_details_input(options) + return self._handle_patron_details_selection(selection, patron, valid_loans) + else: + print("No valid loans for this patron.") + options = ( + CommonActions.SEARCH_PATRONS + | CommonActions.QUIT + | CommonActions.SEARCH_BOOKS # Added SEARCH_BOOKS to options + ) + selection = self._get_patron_details_input(options) + return self._handle_no_loans_selection(selection) + + def _print_loans(self, loans): + valid_loans = [] + idx = 1 + for loan in loans: + if not getattr(loan, 'book_item', None) or not getattr(loan.book_item, 'book', None): + print(f"{idx}) [Invalid loan data: missing book information]") + else: + returned = "True" if getattr(loan, 'return_date', None) else "False" + print(f"{idx}) {loan.book_item.book.title} - Due: {loan.due_date} - Returned: {returned}") + valid_loans.append((idx, loan)) + idx += 1 + return valid_loans + + def _get_patron_details_input(self, options): + self.write_input_options(options) + return input("Enter your choice: ").strip().lower() + + def _handle_patron_details_selection(self, selection, patron, valid_loans): + if selection == 'q': + return ConsoleState.QUIT + elif selection == 's': + return ConsoleState.PATRON_SEARCH + elif selection == 'm': + status = self._patron_service.renew_membership(patron.id) + print(status) + self.selected_patron_details = self._patron_repository.get_patron(patron.id) + return ConsoleState.PATRON_DETAILS + elif selection == 'b': + return self.search_books() # Call the new search_books method + elif selection.isdigit(): + idx = int(selection) + if 1 <= idx <= len(valid_loans): + self.selected_loan_details = valid_loans[idx - 1][1] + return ConsoleState.LOAN_DETAILS + print("Invalid selection. Please enter a number shown in the list above.") + return ConsoleState.PATRON_DETAILS + else: + print("Invalid input. Please enter a number, 'm', 'b', 's', or 'q'.") + return ConsoleState.PATRON_DETAILS + + def _handle_no_loans_selection(self, selection): + if selection == 'q': + return ConsoleState.QUIT + elif selection == 's': + return ConsoleState.PATRON_SEARCH + elif selection == 'b': + return self.search_books() # Handle SEARCH_BOOKS when no loans + else: + print("Invalid input.") + return ConsoleState.PATRON_DETAILS + + def search_books(self) -> ConsoleState: + while True: + book_title = input("Enter a book title to search for: ").strip() + if not book_title: + print("No book title provided. Please try again.") + continue + + # Case-insensitive, partial or exact match + books = self._json_data.books + matches = [b for b in books if book_title.lower() in b.title.lower()] + + if not matches: + print("No matching books found.") + again = input("Search again? (y/n): ").strip().lower() + if again == 'y': + continue + else: + return ConsoleState.PATRON_DETAILS + + if len(matches) == 1: + book = matches[0] + else: + print("\nMultiple books found:") + for idx, b in enumerate(matches, 1): + print(f"{idx}) {b.title}") + selection = input("Select a book by number or 'r' to refine search: ").strip().lower() + if selection == 'r': + continue + if not selection.isdigit() or not (1 <= int(selection) <= len(matches)): + print("Invalid selection.") + continue + book = matches[int(selection) - 1] + + # Find all book items (copies) for this book + book_items = [bi for bi in self._json_data.book_items if bi.book_id == book.id] + if not book_items: + print("No copies of this book are in the library.") + again = input("Search again? (y/n): ").strip().lower() + if again == 'y': + continue + else: + return ConsoleState.PATRON_DETAILS + + # Find all loans for these book items + loans = self._json_data.loans + on_loan = [] + available = [] + for item in book_items: + # Find latest loan for this item (if any) + item_loans = [l for l in loans if l.book_item_id == item.id] + if item_loans: + # Get the most recent loan (by LoanDate) + latest_loan = max(item_loans, key=lambda l: l.loan_date or l.due_date or l.return_date or 0) + if latest_loan.return_date is None: + on_loan.append(latest_loan) + else: + available.append(item) + else: + available.append(item) + + if available: + print(f"Book '{book.title}' is available for loan.") + # Prompt for checkout + checkout = input("Would you like to check out this book? (y/n): ").strip().lower() + if checkout == 'y': + if not self.selected_patron_details: + print("No patron selected. Please select a patron first.") + return ConsoleState.PATRON_SEARCH + # Use the first available copy + book_item = available[0] + loan = self._loan_service.checkout_book(self.selected_patron_details, book_item) + print(f"Book '{book.title}' checked out successfully. Due date: {loan.due_date}") + return ConsoleState.PATRON_DETAILS + else: + # All copies are on loan, show due dates + due_dates = [l.due_date for l in on_loan if l.due_date] + if due_dates: + next_due = min(due_dates) + print(f"All copies of '{book.title}' are currently on loan. Next due date: {next_due}") + else: + print(f"All copies of '{book.title}' are currently on loan.") + + again = input("Search for another book? (y/n): ").strip().lower() + if again == 'y': + continue + else: + return ConsoleState.PATRON_DETAILS + + def loan_details(self) -> ConsoleState: + loan = self.selected_loan_details + print(f"\nBook title: {loan.book_item.book.title}") + print(f"Book Author: {loan.book_item.book.author.name}") + print(f"Due date: {loan.due_date}") + returned = "True" if getattr(loan, 'return_date', None) else "False" + print(f"Returned: {returned}\n") + options = CommonActions.SEARCH_PATRONS | CommonActions.QUIT + if not getattr(loan, 'return_date', None): + options |= CommonActions.RETURN_LOANED_BOOK | CommonActions.EXTEND_LOANED_BOOK + self.write_input_options(options) + selection = input("Enter your choice: ").strip().lower() + if selection == 'q': + return ConsoleState.QUIT + elif selection == 's': + return ConsoleState.PATRON_SEARCH + elif selection == 'r' and not getattr(loan, 'return_date', None): + status = self._loan_service.return_loan(loan.id) + print("Book was successfully returned.") + print(status) + self.selected_loan_details = self._loan_repository.get_loan(loan.id) + return ConsoleState.LOAN_DETAILS + elif selection == 'e' and not getattr(loan, 'return_date', None): + status = self._loan_service.extend_loan(loan.id) + print(status) + self.selected_loan_details = self._loan_repository.get_loan(loan.id) + return ConsoleState.LOAN_DETAILS + else: + print("Invalid input.") + return ConsoleState.LOAN_DETAILS + +from application_core.services.loan_service import LoanService +from application_core.services.patron_service import PatronService +from infrastructure.json_data import JsonData +from infrastructure.json_loan_repository import JsonLoanRepository +from infrastructure.json_patron_repository import JsonPatronRepository +from console.console_app import ConsoleApp + +def main(): + json_data = JsonData() + patron_repo = JsonPatronRepository(json_data) + loan_repo = JsonLoanRepository(json_data) + loan_service = LoanService(loan_repo) + patron_service = PatronService(patron_repo) + + app = ConsoleApp( + loan_service=loan_service, + patron_service=patron_service, + json_data=json_data # <-- pass json_data to ConsoleApp + ) + app.run() diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/console/console_state.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/console/console_state.py new file mode 100644 index 0000000..714335a --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/console/console_state.py @@ -0,0 +1,8 @@ +from enum import Enum + +class ConsoleState(Enum): + PATRON_SEARCH = 1 + PATRON_SEARCH_RESULTS = 2 + PATRON_DETAILS = 3 + LOAN_DETAILS = 4 + QUIT = 5 diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/console/main.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/console/main.py new file mode 100644 index 0000000..9bb9b8a --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/console/main.py @@ -0,0 +1,33 @@ +import sys +from pathlib import Path + +# Add the parent directory to sys.path +sys.path.append(str(Path(__file__).resolve().parent.parent)) + +from application_core.services.loan_service import LoanService +from application_core.services.patron_service import PatronService +from infrastructure.json_data import JsonData +from infrastructure.json_loan_repository import JsonLoanRepository +from infrastructure.json_patron_repository import JsonPatronRepository +from console.console_app import ConsoleApp + + +def main(): + json_data = JsonData() + patron_repo = JsonPatronRepository(json_data) + loan_repo = JsonLoanRepository(json_data) + loan_service = LoanService(loan_repo) + patron_service = PatronService(patron_repo) + + app = ConsoleApp( + loan_service=loan_service, + patron_service=patron_service, + patron_repository=patron_repo, + loan_repository=loan_repo, + json_data=json_data # <-- pass json_data here + ) + app.run() + + +if __name__ == "__main__": + main() diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Authors.json b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Authors.json new file mode 100644 index 0000000..2f61038 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Authors.json @@ -0,0 +1,22 @@ +[ + {"Id": 1, "Name": "Author One"}, + {"Id": 2, "Name": "Author Two"}, + {"Id": 3, "Name": "Author Three"}, + {"Id": 4, "Name": "Author Four"}, + {"Id": 5, "Name": "Author Five"}, + {"Id": 6, "Name": "Author Six"}, + {"Id": 7, "Name": "Author Seven"}, + {"Id": 8, "Name": "Author Eight"}, + {"Id": 9, "Name": "Author Nine"}, + {"Id": 10, "Name": "Author Ten"}, + {"Id": 11, "Name": "Author Eleven"}, + {"Id": 12, "Name": "Author Twelve"}, + {"Id": 13, "Name": "Author Thirteen"}, + {"Id": 14, "Name": "Author Fourteen"}, + {"Id": 15, "Name": "Author Fifteen"}, + {"Id": 16, "Name": "Author Sixteen"}, + {"Id": 17, "Name": "Author Seventeen"}, + {"Id": 18, "Name": "Author Eighteen"}, + {"Id": 19, "Name": "Author Nineteen"}, + {"Id": 20, "Name": "Author Twenty"} +] diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/BookItems.json b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/BookItems.json new file mode 100644 index 0000000..f5e1d1b --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/BookItems.json @@ -0,0 +1,22 @@ +[ + {"Id": 1, "BookId": 1, "AcquisitionDate": "2023-09-20T00:40:43.1716563", "Condition": "Good"}, + {"Id": 2, "BookId": 2, "AcquisitionDate": "2023-09-20T00:40:43.1717503", "Condition": "Fair"}, + {"Id": 3, "BookId": 3, "AcquisitionDate": "2023-09-20T00:40:43.1717511", "Condition": "Excellent"}, + {"Id": 4, "BookId": 4, "AcquisitionDate": "2023-09-20T00:40:43.1717513", "Condition": "Poor"}, + {"Id": 5, "BookId": 5, "AcquisitionDate": "2023-09-20T00:40:43.1717516", "Condition": "Good"}, + {"Id": 6, "BookId": 6, "AcquisitionDate": "2023-09-20T00:40:43.1717521", "Condition": "Fair"}, + {"Id": 7, "BookId": 7, "AcquisitionDate": "2023-09-20T00:40:43.1717523", "Condition": "Excellent"}, + {"Id": 8, "BookId": 8, "AcquisitionDate": "2023-09-20T00:40:43.1717526", "Condition": "Poor"}, + {"Id": 9, "BookId": 9, "AcquisitionDate": "2023-09-20T00:40:43.171757", "Condition": "Good"}, + {"Id": 10, "BookId": 10, "AcquisitionDate": "2023-09-20T00:40:43.1717574", "Condition": "Fair"}, + {"Id": 11, "BookId": 11, "AcquisitionDate": "2023-09-20T00:40:43.1717576", "Condition": "Excellent"}, + {"Id": 12, "BookId": 12, "AcquisitionDate": "2023-09-20T00:40:43.1717578", "Condition": "Poor"}, + {"Id": 13, "BookId": 13, "AcquisitionDate": "2023-09-20T00:40:43.171758", "Condition": "Good"}, + {"Id": 14, "BookId": 14, "AcquisitionDate": "2023-09-20T00:40:43.1717609", "Condition": "Fair"}, + {"Id": 15, "BookId": 15, "AcquisitionDate": "2023-09-20T00:40:43.1717611", "Condition": "Excellent"}, + {"Id": 16, "BookId": 16, "AcquisitionDate": "2023-09-20T00:40:43.1717613", "Condition": "Poor"}, + {"Id": 17, "BookId": 17, "AcquisitionDate": "2023-09-20T00:40:43.1717616", "Condition": "Good"}, + {"Id": 18, "BookId": 18, "AcquisitionDate": "2023-09-20T00:40:43.1717619", "Condition": "Fair"}, + {"Id": 19, "BookId": 19, "AcquisitionDate": "2023-09-20T00:40:43.1717621", "Condition": "Excellent"}, + {"Id": 20, "BookId": 20, "AcquisitionDate": "2023-09-20T00:40:43.1717626", "Condition": "Poor"} +] diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Books.json b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Books.json new file mode 100644 index 0000000..ac80673 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Books.json @@ -0,0 +1,22 @@ +[ + {"Id": 1, "Title": "Book One", "AuthorId": 1, "Genre": "Dystopian", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524935"}, + {"Id": 2, "Title": "Book Two", "AuthorId": 2, "Genre": "Classic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524936"}, + {"Id": 3, "Title": "Book Three", "AuthorId": 3, "Genre": "Romance", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524937"}, + {"Id": 4, "Title": "Book Four", "AuthorId": 4, "Genre": "Classic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524938"}, + {"Id": 5, "Title": "Book Five", "AuthorId": 5, "Genre": "Coming-of-age", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524939"}, + {"Id": 6, "Title": "Book Six", "AuthorId": 6, "Genre": "Modernist", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524940"}, + {"Id": 7, "Title": "Book Seven", "AuthorId": 7, "Genre": "Adventure", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524941"}, + {"Id": 8, "Title": "Book Eight", "AuthorId": 8, "Genre": "Fantasy", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524942"}, + {"Id": 9, "Title": "Book Nine", "AuthorId": 9, "Genre": "Fantasy", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524943"}, + {"Id": 10, "Title": "Book Ten", "AuthorId": 10, "Genre": "Epic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524944"}, + {"Id": 11, "Title": "Book Eleven", "AuthorId": 11, "Genre": "Psychological", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524945"}, + {"Id": 12, "Title": "Book Twelve", "AuthorId": 12, "Genre": "Psychological", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524946"}, + {"Id": 13, "Title": "Book Thirteen", "AuthorId": 13, "Genre": "Magical realism", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524947"}, + {"Id": 14, "Title": "Book Fourteen", "AuthorId": 14, "Genre": "Classic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524948"}, + {"Id": 15, "Title": "Book Fifteen", "AuthorId": 15, "Genre": "Adventure", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524949"}, + {"Id": 16, "Title": "Book Sixteen", "AuthorId": 16, "Genre": "Historical", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524950"}, + {"Id": 17, "Title": "Book Seventeen", "AuthorId": 17, "Genre": "Gothic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524951"}, + {"Id": 18, "Title": "Book Eighteen", "AuthorId": 18, "Genre": "Dystopian", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524952"}, + {"Id": 19, "Title": "Book Nineteen", "AuthorId": 19, "Genre": "Classic", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524953"}, + {"Id": 20, "Title": "Book Twenty", "AuthorId": 20, "Genre": "Adventure", "ImageName": "BookCover01.jpg", "ISBN": "978-0451524954"} +] diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Loans.json b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Loans.json new file mode 100644 index 0000000..b0ebd1d --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Loans.json @@ -0,0 +1,482 @@ +[ + { + "Id": 1, + "BookItemId": 1, + "PatronId": 1, + "LoanDate": "2025-06-10T10:00:00", + "DueDate": "2025-06-24T10:00:00", + "ReturnDate": null + }, + { + "Id": 2, + "BookItemId": 1, + "PatronId": 10, + "LoanDate": "2024-05-01T10:00:00", + "DueDate": "2024-05-15T10:00:00", + "ReturnDate": "2024-05-10T10:00:00" + }, + { + "Id": 3, + "BookItemId": 2, + "PatronId": 2, + "LoanDate": "2025-06-11T10:00:00", + "DueDate": "2025-06-25T10:00:00", + "ReturnDate": null + }, + { + "Id": 4, + "BookItemId": 2, + "PatronId": 11, + "LoanDate": "2024-06-01T10:00:00", + "DueDate": "2024-06-15T10:00:00", + "ReturnDate": "2024-06-10T10:00:00" + }, + { + "Id": 5, + "BookItemId": 3, + "PatronId": 3, + "LoanDate": "2025-06-12T10:00:00", + "DueDate": "2025-06-26T10:00:00", + "ReturnDate": null + }, + { + "Id": 6, + "BookItemId": 3, + "PatronId": 12, + "LoanDate": "2024-07-01T10:00:00", + "DueDate": "2024-07-15T10:00:00", + "ReturnDate": "2024-07-10T10:00:00" + }, + { + "Id": 7, + "BookItemId": 4, + "PatronId": 4, + "LoanDate": "2025-06-13T10:00:00", + "DueDate": "2025-06-27T10:00:00", + "ReturnDate": null + }, + { + "Id": 8, + "BookItemId": 4, + "PatronId": 13, + "LoanDate": "2024-08-01T10:00:00", + "DueDate": "2024-08-15T10:00:00", + "ReturnDate": "2024-08-10T10:00:00" + }, + { + "Id": 9, + "BookItemId": 5, + "PatronId": 5, + "LoanDate": "2025-06-14T10:00:00", + "DueDate": "2025-06-28T10:00:00", + "ReturnDate": null + }, + { + "Id": 10, + "BookItemId": 5, + "PatronId": 14, + "LoanDate": "2024-09-01T10:00:00", + "DueDate": "2024-09-15T10:00:00", + "ReturnDate": "2024-09-10T10:00:00" + }, + { + "Id": 11, + "BookItemId": 6, + "PatronId": 6, + "LoanDate": "2025-06-15T10:00:00", + "DueDate": "2025-06-29T10:00:00", + "ReturnDate": null + }, + { + "Id": 12, + "BookItemId": 6, + "PatronId": 15, + "LoanDate": "2024-10-01T10:00:00", + "DueDate": "2024-10-15T10:00:00", + "ReturnDate": "2024-10-10T10:00:00" + }, + { + "Id": 13, + "BookItemId": 7, + "PatronId": 7, + "LoanDate": "2025-06-16T10:00:00", + "DueDate": "2025-06-30T10:00:00", + "ReturnDate": null + }, + { + "Id": 14, + "BookItemId": 7, + "PatronId": 16, + "LoanDate": "2024-11-01T10:00:00", + "DueDate": "2024-11-15T10:00:00", + "ReturnDate": "2024-11-10T10:00:00" + }, + { + "Id": 15, + "BookItemId": 8, + "PatronId": 8, + "LoanDate": "2025-06-17T10:00:00", + "DueDate": "2025-07-01T10:00:00", + "ReturnDate": null + }, + { + "Id": 16, + "BookItemId": 8, + "PatronId": 17, + "LoanDate": "2024-12-01T10:00:00", + "DueDate": "2024-12-15T10:00:00", + "ReturnDate": "2024-12-10T10:00:00" + }, + { + "Id": 17, + "BookItemId": 9, + "PatronId": 9, + "LoanDate": "2025-06-18T10:00:00", + "DueDate": "2025-07-02T10:00:00", + "ReturnDate": null + }, + { + "Id": 18, + "BookItemId": 9, + "PatronId": 18, + "LoanDate": "2025-01-01T10:00:00", + "DueDate": "2025-01-15T10:00:00", + "ReturnDate": "2025-01-10T10:00:00" + }, + { + "Id": 19, + "BookItemId": 10, + "PatronId": 10, + "LoanDate": "2025-06-19T10:00:00", + "DueDate": "2025-07-03T10:00:00", + "ReturnDate": null + }, + { + "Id": 20, + "BookItemId": 10, + "PatronId": 19, + "LoanDate": "2025-02-01T10:00:00", + "DueDate": "2025-02-15T10:00:00", + "ReturnDate": "2025-02-10T10:00:00" + }, + { + "Id": 21, + "BookItemId": 11, + "PatronId": 11, + "LoanDate": "2025-06-20T10:00:00", + "DueDate": "2025-07-04T10:00:00", + "ReturnDate": null + }, + { + "Id": 22, + "BookItemId": 11, + "PatronId": 20, + "LoanDate": "2025-03-01T10:00:00", + "DueDate": "2025-03-15T10:00:00", + "ReturnDate": "2025-03-10T10:00:00" + }, + { + "Id": 23, + "BookItemId": 12, + "PatronId": 12, + "LoanDate": "2025-06-21T10:00:00", + "DueDate": "2025-07-05T10:00:00", + "ReturnDate": null + }, + { + "Id": 24, + "BookItemId": 12, + "PatronId": 1, + "LoanDate": "2023-01-01T10:00:00", + "DueDate": "2023-01-15T10:00:00", + "ReturnDate": "2023-01-10T10:00:00" + }, + { + "Id": 25, + "BookItemId": 13, + "PatronId": 13, + "LoanDate": "2025-06-22T10:00:00", + "DueDate": "2025-07-06T10:00:00", + "ReturnDate": null + }, + { + "Id": 26, + "BookItemId": 13, + "PatronId": 2, + "LoanDate": "2023-02-01T10:00:00", + "DueDate": "2023-02-15T10:00:00", + "ReturnDate": "2023-02-10T10:00:00" + }, + { + "Id": 27, + "BookItemId": 14, + "PatronId": 14, + "LoanDate": "2025-06-23T10:00:00", + "DueDate": "2025-07-07T10:00:00", + "ReturnDate": null + }, + { + "Id": 28, + "BookItemId": 14, + "PatronId": 3, + "LoanDate": "2023-03-01T10:00:00", + "DueDate": "2023-03-15T10:00:00", + "ReturnDate": "2023-03-10T10:00:00" + }, + { + "Id": 29, + "BookItemId": 15, + "PatronId": 15, + "LoanDate": "2025-06-24T10:00:00", + "DueDate": "2025-07-08T10:00:00", + "ReturnDate": null + }, + { + "Id": 30, + "BookItemId": 15, + "PatronId": 4, + "LoanDate": "2023-04-01T10:00:00", + "DueDate": "2023-04-15T10:00:00", + "ReturnDate": "2023-04-10T10:00:00" + }, + { + "Id": 31, + "BookItemId": 16, + "PatronId": 5, + "LoanDate": "2023-05-01T10:00:00", + "DueDate": "2023-05-15T10:00:00", + "ReturnDate": "2023-05-10T10:00:00" + }, + { + "Id": 32, + "BookItemId": 17, + "PatronId": 6, + "LoanDate": "2023-06-01T10:00:00", + "DueDate": "2023-06-15T10:00:00", + "ReturnDate": "2023-06-10T10:00:00" + }, + { + "Id": 33, + "BookItemId": 18, + "PatronId": 7, + "LoanDate": "2023-07-01T10:00:00", + "DueDate": "2023-07-15T10:00:00", + "ReturnDate": "2023-07-10T10:00:00" + }, + { + "Id": 34, + "BookItemId": 19, + "PatronId": 8, + "LoanDate": "2023-08-01T10:00:00", + "DueDate": "2023-08-15T10:00:00", + "ReturnDate": "2023-08-10T10:00:00" + }, + { + "Id": 35, + "BookItemId": 20, + "PatronId": 9, + "LoanDate": "2023-09-01T10:00:00", + "DueDate": "2023-09-15T10:00:00", + "ReturnDate": "2023-09-10T10:00:00" + }, + { + "Id": 36, + "BookItemId": 16, + "PatronId": 21, + "LoanDate": "2023-10-01T10:00:00", + "DueDate": "2023-10-15T10:00:00", + "ReturnDate": "2023-10-10T10:00:00" + }, + { + "Id": 37, + "BookItemId": 17, + "PatronId": 22, + "LoanDate": "2023-11-01T10:00:00", + "DueDate": "2023-11-15T10:00:00", + "ReturnDate": "2023-11-10T10:00:00" + }, + { + "Id": 38, + "BookItemId": 18, + "PatronId": 23, + "LoanDate": "2023-12-01T10:00:00", + "DueDate": "2023-12-15T10:00:00", + "ReturnDate": "2023-12-10T10:00:00" + }, + { + "Id": 39, + "BookItemId": 19, + "PatronId": 24, + "LoanDate": "2024-01-01T10:00:00", + "DueDate": "2024-01-15T10:00:00", + "ReturnDate": "2024-01-10T10:00:00" + }, + { + "Id": 40, + "BookItemId": 20, + "PatronId": 25, + "LoanDate": "2024-02-01T10:00:00", + "DueDate": "2024-02-15T10:00:00", + "ReturnDate": "2024-02-10T10:00:00" + }, + { + "Id": 41, + "BookItemId": 16, + "PatronId": 26, + "LoanDate": "2024-03-01T10:00:00", + "DueDate": "2024-03-15T10:00:00", + "ReturnDate": "2024-03-10T10:00:00" + }, + { + "Id": 42, + "BookItemId": 17, + "PatronId": 27, + "LoanDate": "2024-04-01T10:00:00", + "DueDate": "2024-04-15T10:00:00", + "ReturnDate": "2024-04-10T10:00:00" + }, + { + "Id": 43, + "BookItemId": 18, + "PatronId": 28, + "LoanDate": "2024-05-01T10:00:00", + "DueDate": "2024-05-15T10:00:00", + "ReturnDate": "2024-05-10T10:00:00" + }, + { + "Id": 44, + "BookItemId": 19, + "PatronId": 29, + "LoanDate": "2024-06-01T10:00:00", + "DueDate": "2024-06-15T10:00:00", + "ReturnDate": "2024-06-10T10:00:00" + }, + { + "Id": 45, + "BookItemId": 20, + "PatronId": 30, + "LoanDate": "2024-07-01T10:00:00", + "DueDate": "2024-07-15T10:00:00", + "ReturnDate": "2024-07-10T10:00:00" + }, + { + "Id": 46, + "BookItemId": 16, + "PatronId": 31, + "LoanDate": "2024-08-01T10:00:00", + "DueDate": "2024-08-15T10:00:00", + "ReturnDate": "2024-08-10T10:00:00" + }, + { + "Id": 47, + "BookItemId": 17, + "PatronId": 32, + "LoanDate": "2024-09-01T10:00:00", + "DueDate": "2024-09-15T10:00:00", + "ReturnDate": "2024-09-10T10:00:00" + }, + { + "Id": 48, + "BookItemId": 18, + "PatronId": 33, + "LoanDate": "2024-10-01T10:00:00", + "DueDate": "2024-10-15T10:00:00", + "ReturnDate": "2024-10-10T10:00:00" + }, + { + "Id": 49, + "BookItemId": 19, + "PatronId": 34, + "LoanDate": "2024-11-01T10:00:00", + "DueDate": "2024-11-15T10:00:00", + "ReturnDate": "2024-11-10T10:00:00" + }, + { + "Id": 50, + "BookItemId": 20, + "PatronId": 35, + "LoanDate": "2024-12-01T10:00:00", + "DueDate": "2024-12-15T10:00:00", + "ReturnDate": "2024-12-10T10:00:00" + }, + { + "Id": 51, + "BookItemId": 16, + "PatronId": 36, + "LoanDate": "2025-01-01T10:00:00", + "DueDate": "2025-01-15T10:00:00", + "ReturnDate": "2025-01-10T10:00:00" + }, + { + "Id": 52, + "BookItemId": 17, + "PatronId": 37, + "LoanDate": "2025-02-01T10:00:00", + "DueDate": "2025-02-15T10:00:00", + "ReturnDate": "2025-02-10T10:00:00" + }, + { + "Id": 53, + "BookItemId": 18, + "PatronId": 38, + "LoanDate": "2025-03-01T10:00:00", + "DueDate": "2025-03-15T10:00:00", + "ReturnDate": "2025-03-10T10:00:00" + }, + { + "Id": 54, + "BookItemId": 19, + "PatronId": 39, + "LoanDate": "2025-04-01T10:00:00", + "DueDate": "2025-04-15T10:00:00", + "ReturnDate": "2025-04-10T10:00:00" + }, + { + "Id": 55, + "BookItemId": 20, + "PatronId": 40, + "LoanDate": "2025-05-01T10:00:00", + "DueDate": "2025-05-15T10:00:00", + "ReturnDate": "2025-05-10T10:00:00" + }, + { + "Id": 56, + "BookItemId": 16, + "PatronId": 41, + "LoanDate": "2025-05-11T10:00:00", + "DueDate": "2025-05-25T10:00:00", + "ReturnDate": "2025-05-20T10:00:00" + }, + { + "Id": 57, + "BookItemId": 17, + "PatronId": 42, + "LoanDate": "2025-05-12T10:00:00", + "DueDate": "2025-05-26T10:00:00", + "ReturnDate": "2025-05-21T10:00:00" + }, + { + "Id": 58, + "BookItemId": 18, + "PatronId": 48, + "LoanDate": "2025-05-13T10:00:00", + "DueDate": "2025-05-27T10:00:00", + "ReturnDate": "2025-05-22T10:00:00" + }, + { + "Id": 59, + "BookItemId": 19, + "PatronId": 49, + "LoanDate": "2025-05-14T10:00:00", + "DueDate": "2025-05-28T10:00:00", + "ReturnDate": "2025-05-23T10:00:00" + }, + { + "Id": 60, + "BookItemId": 20, + "PatronId": 50, + "LoanDate": "2025-05-15T10:00:00", + "DueDate": "2025-05-29T10:00:00", + "ReturnDate": "2025-05-24T10:00:00" + } +] \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Patrons.json b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Patrons.json new file mode 100644 index 0000000..7c05687 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/Json/Patrons.json @@ -0,0 +1,52 @@ +[ + {"Id": 1, "Name": "Patron One", "MembershipEnd": "2024-12-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron One.jpg"}, + {"Id": 2, "Name": "Patron Two", "MembershipEnd": "2025-02-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Two.jpg"}, + {"Id": 3, "Name": "Patron Three", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Three.jpg"}, + {"Id": 4, "Name": "Patron Four", "MembershipEnd": "2025-04-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Four.jpg"}, + {"Id": 5, "Name": "Patron Five", "MembershipEnd": "2025-05-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Five.jpg"}, + {"Id": 6, "Name": "Patron Six", "MembershipEnd": "2025-06-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Six.jpg"}, + {"Id": 7, "Name": "Patron Seven", "MembershipEnd": "2025-07-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Seven.jpg"}, + {"Id": 8, "Name": "Patron Eight", "MembershipEnd": "2024-01-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Eight.jpg"}, + {"Id": 9, "Name": "Patron Nine", "MembershipEnd": "2024-02-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Nine.jpg"}, + {"Id": 10, "Name": "Patron Ten", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Ten.jpg"}, + {"Id": 11, "Name": "Patron Eleven", "MembershipEnd": "2024-04-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Eleven.jpg"}, + {"Id": 12, "Name": "Patron Twelve", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twelve.jpg"}, + {"Id": 13, "Name": "Patron Thirteen", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirteen.jpg"}, + {"Id": 14, "Name": "Patron Fourteen", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Fourteen.jpg"}, + {"Id": 15, "Name": "Patron Fifteen", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Fifteen.jpg"}, + {"Id": 16, "Name": "Patron Sixteen", "MembershipEnd": "2024-04-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Sixteen.jpg"}, + {"Id": 17, "Name": "Patron Seventeen", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Seventeen.jpg"}, + {"Id": 18, "Name": "Patron Eighteen", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Eighteen.jpg"}, + {"Id": 19, "Name": "Patron Nineteen", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Nineteen.jpg"}, + {"Id": 20, "Name": "Patron Twenty", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty.jpg"}, + {"Id": 21, "Name": "Patron Twenty-One", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-One.jpg"}, + {"Id": 22, "Name": "Patron Twenty-Two", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Two.jpg"}, + {"Id": 23, "Name": "Patron Twenty-Three", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Three.jpg"}, + {"Id": 24, "Name": "Patron Twenty-Four", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Four.jpg"}, + {"Id": 25, "Name": "Patron Twenty-Five", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Five.jpg"}, + {"Id": 26, "Name": "Patron Twenty-Six", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Six.jpg"}, + {"Id": 27, "Name": "Patron Twenty-Seven", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Seven.jpg"}, + {"Id": 28, "Name": "Patron Twenty-Eight", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Eight.jpg"}, + {"Id": 29, "Name": "Patron Twenty-Nine", "MembershipEnd": "2024-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Twenty-Nine.jpg"}, + {"Id": 30, "Name": "Patron Thirty", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty.jpg"}, + {"Id": 31, "Name": "Patron Thirty-One", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-One.jpg"}, + {"Id": 32, "Name": "Patron Thirty-Two", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Two.jpg"}, + {"Id": 33, "Name": "Patron Thirty-Three", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Three.jpg"}, + {"Id": 34, "Name": "Patron Thirty-Four", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Four.jpg"}, + {"Id": 35, "Name": "Patron Thirty-Five", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Five.jpg"}, + {"Id": 36, "Name": "Patron Thirty-Six", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Six.jpg"}, + {"Id": 37, "Name": "Patron Thirty-Seven", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Seven.jpg"}, + {"Id": 38, "Name": "Patron Thirty-Eight", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Eight.jpg"}, + {"Id": 39, "Name": "Patron Thirty-Nine", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Thirty-Nine.jpg"}, + {"Id": 40, "Name": "Patron Forty", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty.jpg"}, + {"Id": 41, "Name": "Patron Forty-One", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-One.jpg"}, + {"Id": 42, "Name": "Patron Forty-Two", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Two.jpg"}, + {"Id": 43, "Name": "Patron Forty-Three", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Three.jpg"}, + {"Id": 44, "Name": "Patron Forty-Four", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Four.jpg"}, + {"Id": 45, "Name": "Patron Forty-Five", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Five.jpg"}, + {"Id": 46, "Name": "Patron Forty-Six", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Six.jpg"}, + {"Id": 47, "Name": "Patron Forty-Seven", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Seven.jpg"}, + {"Id": 48, "Name": "Patron Forty-Eight", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Eight.jpg"}, + {"Id": 49, "Name": "Patron Forty-Nine", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Forty-Nine.jpg"}, + {"Id": 50, "Name": "Patron Fifty", "MembershipEnd": "2025-03-01T00:40:43.1589724", "MembershipStart": "2001-01-01T00:40:43.1589724", "ImageName": "Patron Fifty.jpg"} +] diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/json_data.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/json_data.py new file mode 100644 index 0000000..0c4aa54 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/json_data.py @@ -0,0 +1,105 @@ +import json +import os +from pathlib import Path +from application_core.entities.author import Author +from application_core.entities.book import Book +from application_core.entities.book_item import BookItem +from application_core.entities.patron import Patron +from application_core.entities.loan import Loan +from typing import List, Optional +from datetime import datetime + +class JsonData: + def __init__(self): + # Get the absolute path to the project root + self.project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + self.json_dir = os.path.join(self.project_root, "infrastructure", "Json") + self.authors_path = os.path.join(self.json_dir, "Authors.json") + self.books_path = os.path.join(self.json_dir, "Books.json") + self.book_items_path = os.path.join(self.json_dir, "BookItems.json") # <-- Add this line + self.patrons_path = os.path.join(self.json_dir, "Patrons.json") + self.loans_path = os.path.join(self.json_dir, "Loans.json") + self.authors: List[Author] = [] + self.books: List[Book] = [] + self.book_items: List[BookItem] = [] + self.patrons: List[Patron] = [] + self.loans: List[Loan] = [] + self._loaded = False + self.load_data() + + def _parse_datetime(self, value: Optional[str]) -> Optional[datetime]: + if value is None: + return None + return datetime.fromisoformat(value) + + def load_data(self) -> None: + try: + with open(self.authors_path, encoding='utf-8') as f: + authors_data = json.load(f) + self.authors = [Author(id=a['Id'], name=a['Name']) for a in authors_data] + with open(self.books_path, encoding='utf-8') as f: + books_data = json.load(f) + self.books = [Book(id=b['Id'], title=b['Title'], author_id=b['AuthorId'], genre=b['Genre'], image_name=b['ImageName'], isbn=b['ISBN']) for b in books_data] + with open(self.book_items_path, encoding='utf-8') as f: # <-- Fix here + items_data = json.load(f) + self.book_items = [BookItem(id=bi['Id'], book_id=bi['BookId'], acquisition_date=self._parse_datetime(bi['AcquisitionDate']), condition=bi.get('Condition')) for bi in items_data] + with open(self.patrons_path, encoding='utf-8') as f: + patrons_data = json.load(f) + self.patrons = [Patron(id=p['Id'], name=p['Name'], membership_end=self._parse_datetime(p['MembershipEnd']), membership_start=self._parse_datetime(p['MembershipStart']), image_name=p.get('ImageName')) for p in patrons_data] + with open(self.loans_path, encoding='utf-8') as f: + loans_data = json.load(f) + self.loans = [Loan(id=l['Id'], book_item_id=l['BookItemId'], patron_id=l['PatronId'], loan_date=self._parse_datetime(l['LoanDate']), due_date=self._parse_datetime(l['DueDate']), return_date=self._parse_datetime(l['ReturnDate'])) for l in loans_data] + self._loaded = True + + # Build lookup dictionaries for fast access + book_item_dict = {bi.id: bi for bi in self.book_items} + book_dict = {b.id: b for b in self.books} + author_dict = {a.id: a for a in self.authors} + patron_dict = {p.id: p for p in self.patrons} + + # Link book_item and book to each loan + for loan in self.loans: + loan.book_item = book_item_dict.get(loan.book_item_id) + if loan.book_item: + loan.book_item.book = book_dict.get(loan.book_item.book_id) + if loan.book_item.book: + loan.book_item.book.author = author_dict.get(loan.book_item.book.author_id) + loan.patron = patron_dict.get(loan.patron_id) + # Optionally, link loans to patrons + for patron in self.patrons: + patron.loans = [loan for loan in self.loans if loan.patron_id == patron.id] + + except (FileNotFoundError, json.JSONDecodeError) as e: + print(f"Error loading data: {e}") + self._loaded = False + + def save_loans(self, loans: List[Loan]) -> None: + try: + with open(self.loans_path, 'w', encoding='utf-8') as f: + json.dump([ + { + 'Id': l.id, + 'BookItemId': l.book_item_id, + 'PatronId': l.patron_id, + 'LoanDate': l.loan_date.isoformat() if l.loan_date else None, + 'DueDate': l.due_date.isoformat() if l.due_date else None, + 'ReturnDate': l.return_date.isoformat() if l.return_date else None + } for l in loans + ], f, indent=2) + except Exception as e: + print(f"Error saving loans: {e}") + + def save_patrons(self, patrons: List[Patron]) -> None: + try: + with open(self.patrons_path, 'w', encoding='utf-8') as f: + json.dump([ + { + 'Id': p.id, + 'Name': p.name, + 'MembershipEnd': p.membership_end.isoformat() if p.membership_end else None, + 'MembershipStart': p.membership_start.isoformat() if p.membership_start else None, + 'ImageName': p.image_name + } for p in patrons + ], f, indent=2) + except Exception as e: + print(f"Error saving patrons: {e}") diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/json_loan_repository.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/json_loan_repository.py new file mode 100644 index 0000000..4bcd087 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/json_loan_repository.py @@ -0,0 +1,54 @@ +import json +from datetime import datetime +from application_core.interfaces.iloan_repository import ILoanRepository +from application_core.entities.loan import Loan +from .json_data import JsonData +from typing import Optional + +class JsonLoanRepository(ILoanRepository): + def __init__(self, json_data: JsonData): + self._json_data = json_data + + def get_loan(self, loan_id: int) -> Optional[Loan]: + for loan in self._json_data.loans: + if loan.id == loan_id: + return loan + return None + + def update_loan(self, loan: Loan) -> None: + for idx in range(len(self._json_data.loans)): + if self._json_data.loans[idx].id == loan.id: + self._json_data.loans[idx] = loan + self._json_data.save_loans(self._json_data.loans) + return + + def add_loan(self, loan: Loan) -> None: + self._json_data.loans.append(loan) + self._json_data.save_loans(self._json_data.loans) + self._json_data.load_data() + + def get_loans_by_patron_id(self, patron_id: int): + result = [] + for loan in self._json_data.loans: + if loan.patron_id == patron_id: + result.append(loan) + return result + + def get_all_loans(self): + return self._json_data.loans + + def get_overdue_loans(self, current_date): + overdue = [] + for loan in self._json_data.loans: + if loan.return_date is None and loan.due_date < current_date: + overdue.append(loan) + return overdue + + def sort_loans_by_due_date(self): + # Manual bubble sort for demonstration + n = len(self._json_data.loans) + for i in range(n): + for j in range(0, n - i - 1): + if self._json_data.loans[j].due_date > self._json_data.loans[j + 1].due_date: + self._json_data.loans[j], self._json_data.loans[j + 1] = self._json_data.loans[j + 1], self._json_data.loans[j] + return self._json_data.loans diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/json_patron_repository.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/json_patron_repository.py new file mode 100644 index 0000000..8b25cac --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/infrastructure/json_patron_repository.py @@ -0,0 +1,55 @@ +import json +from application_core.interfaces.ipatron_repository import IPatronRepository +from application_core.entities.patron import Patron +from .json_data import JsonData +from typing import List, Optional + +class JsonPatronRepository(IPatronRepository): + def __init__(self, json_data: JsonData): + self._json_data = json_data + + def get_patron(self, patron_id: int) -> Optional[Patron]: + for patron in self._json_data.patrons: + if patron.id == patron_id: + return patron + return None + + def search_patrons(self, search_input: str) -> List[Patron]: + results = [] + for p in self._json_data.patrons: + if search_input.lower() in p.name.lower(): + results.append(p) + n = len(results) + for i in range(n): + for j in range(0, n - i - 1): + if results[j].name > results[j + 1].name: + results[j], results[j + 1] = results[j + 1], results[j] + return results + + def update_patron(self, patron: Patron) -> None: + for idx in range(len(self._json_data.patrons)): + if self._json_data.patrons[idx].id == patron.id: + self._json_data.patrons[idx] = patron + self._json_data.save_patrons(self._json_data.patrons) + return + + def add_patron(self, patron: Patron) -> None: + self._json_data.patrons.append(patron) + self._json_data.save_patrons(self._json_data.patrons) + self._json_data.load_data() + + def get_all_patrons(self) -> List[Patron]: + return self._json_data.patrons + + def find_patrons_by_name(self, name: str) -> List[Patron]: + result = [] + for patron in self._json_data.patrons: + if patron.name.lower() == name.lower(): + result.append(patron) + return result + + def get_all_books(self): + return self._json_data.books + + def get_all_book_items(self): + return self._json_data.book_items diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/readme.md b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/readme.md new file mode 100644 index 0000000..e853c73 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/readme.md @@ -0,0 +1,88 @@ +# Library App + +## Description + +Library App is a Python-based console application for managing a library's books, patrons, and loans. It supports searching for patrons and books, checking out and returning books, extending loans, and renewing patron memberships. Data is persisted in JSON files, and the application is structured using a clean separation of entities, repositories, services, and console UI. + +## Project Structure + +- application_core/ + - entities/ + - author.py + - book.py + - book_item.py + - loan.py + - patron.py + - enums/ + - loan_extension_status.py + - loan_return_status.py + - membership_renewal_status.py + - interfaces/ + - iloan_repository.py + - iloan_service.py + - ipatron_repository.py + - ipatron_service.py + - services/ + - loan_service.py + - patron_service.py +- console/ + - book_repository.py + - common_actions.py + - console_app.py + - console_state.py + - main.py +- infrastructure/ + - json_data.py + - json_loan_repository.py + - json_patron_repository.py + - Json/ + - Authors.json + - Books.json + - BookItems.json + - Loans.json + - Patrons.json +- tests/ + - test_loan_service.py + - test_patron_service.py + - __init__.py +- readme.md + +## Key Classes and Interfaces + +- **Entities** + - `Author`, `Book`, `BookItem`, `Loan`, `Patron`: Data models for library domain objects. +- **Enums** + - `LoanExtensionStatus`, `LoanReturnStatus`, `MembershipRenewalStatus`: Status codes for operations. +- **Interfaces** + - `ILoanRepository`, `ILoanService`, `IPatronRepository`, `IPatronService`: Abstract base classes defining contracts for repositories and services. +- **Services** + - `LoanService`: Handles loan operations (checkout, return, extend). + - `PatronService`: Handles patron operations (renew membership, search). +- **Repositories** + - `JsonLoanRepository`, `JsonPatronRepository`: Implement data access using JSON files. + - `JsonData`: Loads and saves all data from/to JSON files. +- **Console UI** + - `ConsoleApp`: Main application loop and user interaction. + - `common_actions.py`, `console_state.py`: Define UI actions and states. + +## Usage + +1. **Install Requirements** + No external dependencies are required beyond Python 3.7+. + +2. **Run the Application** + From the `console` directory (or project root), run: + ``` + python -m console.main + ``` + Follow the on-screen prompts to search for patrons, manage loans, and check book availability. + +3. **Run Tests** + From the project root, run: + ``` + python -m unittest discover tests + ``` + +## License + +This project is licensed under the MIT \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/tests/__init__.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/tests/test_json_loan_repository.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/tests/test_json_loan_repository.py new file mode 100644 index 0000000..0f91dbf --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/tests/test_json_loan_repository.py @@ -0,0 +1,123 @@ +import sys +import unittest +from pathlib import Path + +# Add the parent directory to sys.path for imports +sys.path.append(str(Path(__file__).resolve().parent.parent)) + +from infrastructure.json_loan_repository import JsonLoanRepository +from application_core.entities.loan import Loan +from application_core.entities.book_item import BookItem +from application_core.entities.patron import Patron +from datetime import datetime, timedelta + +class DummyJsonData: + def __init__(self): + self.loans = [] + self.save_loans_called = False + + def save_loans(self, loans): + self.save_loans_called = True + + def load_data(self): + pass + +class TestJsonLoanRepository(unittest.TestCase): + def setUp(self): + self._json_data = DummyJsonData() + test_patron = Patron(id=1, name="Test Patron", membership_end=datetime.now()+timedelta(days=30), membership_start=datetime.now()-timedelta(days=365)) + test_book_item = BookItem(id=1, book_id=1, acquisition_date=datetime.now()-timedelta(days=100)) + test_loan = Loan(id=1, book_item_id=1, patron_id=1, patron=test_patron, loan_date=datetime.now()-timedelta(days=10), due_date=datetime.now()+timedelta(days=4), return_date=None, book_item=test_book_item) + self._json_data.loans = [test_loan] + self._json_loan_repository = JsonLoanRepository(self._json_data) + + def test_get_loan(self): + loan = self._json_loan_repository.get_loan(1) + self.assertIsNotNone(loan) + self.assertEqual(loan.id, 1) + + def test_get_loan_not_found(self): + loan = self._json_loan_repository.get_loan(999) + self.assertIsNone(loan) + + def test_get_loan_found(self): + # Test case where loan with id=1 exists + found_loan = self._json_loan_repository.get_loan(1) + self.assertIsNotNone(found_loan) + self.assertEqual(found_loan.id, 1) + + def test_get_loan_not_found_again(self): + # Test case where loan with id=2 does not exist + not_found_loan = self._json_loan_repository.get_loan(2) + self.assertIsNone(not_found_loan) + +if __name__ == "__main__": + unittest.main() + +import pytest + +@pytest.fixture +def dummy_json_data_with_loans(): + from application_core.entities.loan import Loan + from application_core.entities.book_item import BookItem + from application_core.entities.patron import Patron + from datetime import datetime, timedelta + + class DummyJsonData: + def __init__(self): + self.loans = [] + self.save_loans_called = False + def save_loans(self, loans): + self.save_loans_called = True + def load_data(self): + pass + + json_data = DummyJsonData() + test_patron = Patron(id=1, name="Test Patron", membership_end=datetime.now()+timedelta(days=30), membership_start=datetime.now()-timedelta(days=365)) + test_book_item = BookItem(id=1, book_id=1, acquisition_date=datetime.now()-timedelta(days=100)) + test_loan = Loan(id=1, book_item_id=1, patron_id=1, patron=test_patron, loan_date=datetime.now()-timedelta(days=10), due_date=datetime.now()+timedelta(days=4), return_date=None, book_item=test_book_item) + json_data.loans = [test_loan] + return json_data + +@pytest.mark.parametrize("loan_id,expected", [ + (1, True), + (999, False), +]) +def test_get_loan_param(dummy_json_data_with_loans, loan_id, expected): + from infrastructure.json_loan_repository import JsonLoanRepository + repo = JsonLoanRepository(dummy_json_data_with_loans) + loan = repo.get_loan(loan_id) + assert (loan is not None) == expected + +def test_update_loan_raises_on_missing(dummy_json_data_with_loans): + from infrastructure.json_loan_repository import JsonLoanRepository + from application_core.entities.loan import Loan + repo = JsonLoanRepository(dummy_json_data_with_loans) + missing_loan = Loan(id=999, book_item_id=1, patron_id=1) + # Should not raise, but let's assert update does not call save_loans + repo.update_loan(missing_loan) + assert not dummy_json_data_with_loans.save_loans_called + +def test_add_loan_and_save(dummy_json_data_with_loans): + from infrastructure.json_loan_repository import JsonLoanRepository + from application_core.entities.loan import Loan + from datetime import datetime + repo = JsonLoanRepository(dummy_json_data_with_loans) + new_loan = Loan(id=2, book_item_id=2, patron_id=2, loan_date=datetime.now(), due_date=datetime.now()) + repo.add_loan(new_loan) + assert dummy_json_data_with_loans.save_loans_called + assert any(l.id == 2 for l in dummy_json_data_with_loans.loans) + +def test_get_loans_by_patron_id(dummy_json_data_with_loans): + from infrastructure.json_loan_repository import JsonLoanRepository + repo = JsonLoanRepository(dummy_json_data_with_loans) + result = repo.get_loans_by_patron_id(1) + assert len(result) == 1 + assert result[0].patron_id == 1 + +def test_get_loan_assertion(dummy_json_data_with_loans): + from infrastructure.json_loan_repository import JsonLoanRepository + repo = JsonLoanRepository(dummy_json_data_with_loans) + with pytest.raises(AssertionError): + # This will fail because we assert False + assert repo.get_loan(999) is not None diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/tests/test_loan_service.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/tests/test_loan_service.py new file mode 100644 index 0000000..4ea0fb7 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/tests/test_loan_service.py @@ -0,0 +1,87 @@ +import unittest +from unittest.mock import MagicMock +from application_core.services.loan_service import LoanService +from application_core.entities.loan import Loan +from application_core.enums.loan_return_status import LoanReturnStatus +from datetime import datetime, timedelta + +class TestLoanService(unittest.TestCase): + def setUp(self): + self.mock_repo = MagicMock() + self.service = LoanService(self.mock_repo) + + def test_return_loan_success(self): + loan = Loan(id=1, book_item_id=1, patron_id=1, patron=None, loan_date=datetime.now()-timedelta(days=10), due_date=datetime.now()+timedelta(days=10), return_date=None, book_item=None) + self.mock_repo.get_loan.return_value = loan + self.mock_repo.update_loan.return_value = None + status = self.service.return_loan(1) + self.assertEqual(status, LoanReturnStatus.SUCCESS) + + def test_return_loan_not_found(self): + self.mock_repo.get_loan.return_value = None + status = self.service.return_loan(1) + self.assertEqual(status, LoanReturnStatus.LOAN_NOT_FOUND) + +if __name__ == "__main__": + unittest.main() + +import pytest + +@pytest.fixture +def mock_loan_repo(): + class MockRepo: + def __init__(self): + self.loans = {} + self.updated = False + def get_loan(self, loan_id): + return self.loans.get(loan_id) + def update_loan(self, loan): + self.updated = True + def add_loan(self, loan): + self.loans[loan.id] = loan + def get_all_loans(self): + return list(self.loans.values()) + return MockRepo() + +@pytest.mark.parametrize("loan_id,exists,expected_status", [ + (1, True, LoanReturnStatus.SUCCESS), + (2, False, LoanReturnStatus.LOAN_NOT_FOUND), +]) +def test_return_loan_param(mock_loan_repo, loan_id, exists, expected_status): + from application_core.services.loan_service import LoanService + from application_core.entities.loan import Loan + from datetime import datetime, timedelta + repo = mock_loan_repo + if exists: + repo.loans[loan_id] = Loan(id=loan_id, book_item_id=1, patron_id=1, loan_date=datetime.now()-timedelta(days=2), due_date=datetime.now()+timedelta(days=2), return_date=None) + service = LoanService(repo) + status = service.return_loan(loan_id) + assert status == expected_status + +def test_extend_loan_membership_expired(mock_loan_repo): + from application_core.services.loan_service import LoanService + from application_core.entities.loan import Loan + from application_core.entities.patron import Patron + from application_core.enums.loan_extension_status import LoanExtensionStatus + from datetime import datetime, timedelta + patron = Patron(id=1, name="Expired", membership_end=datetime.now()-timedelta(days=1), membership_start=datetime.now()-timedelta(days=365)) + loan = Loan(id=1, book_item_id=1, patron_id=1, patron=patron, loan_date=datetime.now()-timedelta(days=2), due_date=datetime.now()+timedelta(days=2), return_date=None) + mock_loan_repo.loans[1] = loan + service = LoanService(mock_loan_repo) + status = service.extend_loan(1) + assert status == LoanExtensionStatus.MEMBERSHIP_EXPIRED + +def test_checkout_book_raises_on_missing_patron(mock_loan_repo): + from application_core.services.loan_service import LoanService + from application_core.entities.book_item import BookItem + service = LoanService(mock_loan_repo) + with pytest.raises(AttributeError): + # Patron is None, should raise when accessing patron.id + service.checkout_book(None, BookItem(id=1, book_id=1, acquisition_date=datetime.now())) + +def test_return_loan_assertion(mock_loan_repo): + from application_core.services.loan_service import LoanService + service = LoanService(mock_loan_repo) + with pytest.raises(AssertionError): + # This will fail because we assert False + assert service.return_loan(999) == LoanReturnStatus.SUCCESS \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/tests/test_patron_service.py b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/tests/test_patron_service.py new file mode 100644 index 0000000..3eaec51 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/AccelerateDevGHCopilot/library/tests/test_patron_service.py @@ -0,0 +1,75 @@ +import unittest +from unittest.mock import MagicMock +from application_core.services.patron_service import PatronService +from application_core.entities.patron import Patron +from application_core.enums.membership_renewal_status import MembershipRenewalStatus +from datetime import datetime, timedelta + +class PatronServiceTest(unittest.TestCase): + def setUp(self): + self.mock_repo = MagicMock() + self.service = PatronService(self.mock_repo) + + def test_renew_membership_success(self): + patron = Patron(id=1, name="John Doe", membership_end=datetime.now()-timedelta(days=1), membership_start=datetime.now()-timedelta(days=365)) + self.mock_repo.get_patron.return_value = patron + self.mock_repo.update_patron.return_value = None + status = self.service.renew_membership(1) + self.assertEqual(status, MembershipRenewalStatus.SUCCESS) + + def test_renew_membership_patron_not_found(self): + self.mock_repo.get_patron.return_value = None + status = self.service.renew_membership(1) + self.assertEqual(status, MembershipRenewalStatus.PATRON_NOT_FOUND) + +if __name__ == "__main__": + unittest.main() + +import pytest + +@pytest.fixture +def mock_patron_repo(): + class MockRepo: + def __init__(self): + self.patrons = {} + self.updated = False + def get_patron(self, patron_id): + return self.patrons.get(patron_id) + def update_patron(self, patron): + self.updated = True + def get_all_patrons(self): + return list(self.patrons.values()) + return MockRepo() + +@pytest.mark.parametrize("patron_id,exists,expected_status", [ + (1, True, MembershipRenewalStatus.SUCCESS), + (2, False, MembershipRenewalStatus.PATRON_NOT_FOUND), +]) +def test_renew_membership_param(mock_patron_repo, patron_id, exists, expected_status): + from application_core.services.patron_service import PatronService + from application_core.entities.patron import Patron + from datetime import datetime, timedelta + repo = mock_patron_repo + if exists: + repo.patrons[patron_id] = Patron(id=patron_id, name="Test", membership_end=datetime.now()-timedelta(days=1), membership_start=datetime.now()-timedelta(days=365)) + service = PatronService(repo) + status = service.renew_membership(patron_id) + assert status == expected_status + +def test_find_patron_by_name(mock_patron_repo): + from application_core.services.patron_service import PatronService + from application_core.entities.patron import Patron + repo = mock_patron_repo + repo.patrons[1] = Patron(id=1, name="Alice", membership_end=datetime.now(), membership_start=datetime.now()) + repo.patrons[2] = Patron(id=2, name="Bob", membership_end=datetime.now(), membership_start=datetime.now()) + service = PatronService(repo) + results = service.find_patron_by_name("Alice") + assert len(results) == 1 + assert results[0].name == "Alice" + +def test_renew_membership_assertion(mock_patron_repo): + from application_core.services.patron_service import PatronService + service = PatronService(mock_patron_repo) + with pytest.raises(AssertionError): + # This will fail because we assert False + assert service.renew_membership(999) == MembershipRenewalStatus.SUCCESS \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/readme.txt b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/readme.txt new file mode 100644 index 0000000..b3a4252 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code-python/readme.txt @@ -0,0 +1 @@ +placeholder \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/.gitignore b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/.gitignore new file mode 100644 index 0000000..9a75e6b --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/.gitignore @@ -0,0 +1,119 @@ +# Build Folders (you can keep bin if you'd like, to store dlls and pdbs) +[Bb]in/ +[Oo]bj/ + +# mstest test results +TestResults + +## VSCode +.vscode/* + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Include dlls if theyfre in the NuGet packages directory +!/packages/*/lib/*.dll +!/packages/*/lib/*/*.dll +# Include dlls if they're in the CommonReferences directory +!*CommonReferences/*.dll + +# Build results +[Dd]ebug/ +[Rr]elease/ +x64/ +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.log +*.vspscc +*.vssscc +*.sln +.builds + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper* + +# NCrunch +*.ncrunch* +.*crunch*.local.xml + +# Installshield output folder +[Ee]xpress + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish + +# Publish Web Output +*.Publish.xml + +# NuGet Packages Directory +# packages + +# Windows Azure Build Output +csx +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +[Bb]in +[Oo]bj +sql +TestResults +[Tt]est[Rr]esult* +*.[Cc]ache +*.editorconfig +ClientBin +[Ss]tyle[Cc]op.* +~$* +*.dbmdl +Generated_Code #added for RIA/Silverlight projects + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/README.md b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/README.md new file mode 100644 index 0000000..6fc0e23 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/README.md @@ -0,0 +1,76 @@ +# Library App + +## Description + +Library App is a modular application designed to manage library operations such as book loans, patron management, and inventory tracking. It is built using .NET and follows a clean architecture approach to ensure scalability and maintainability. + +## Project Structure + +- `AccelerateDevGHCopilot.sln` - Solution file for the project. +- `src/` + - `Library.ApplicationCore/` + - `Entities/` - Contains core domain entities. + - `Enums/` - Defines enumerations used across the application. + - `Interfaces/` - Declares interfaces for core abstractions. + - `Services/` - Implements business logic and domain services. + - `Library.ApplicationCore.csproj` - Project file for the Application Core. + - `Library.Console/` + - `appSettings.json` - Configuration file for the console application. + - `CommonActions.cs` - Contains reusable actions for the console app. + - `ConsoleApp.cs` - Main application logic for the console interface. + - `ConsoleState.cs` - Manages the state of the console application. + - `Program.cs` - Entry point for the console application. + - `Json/` - Contains JSON-related utilities or data. + - `Library.Console.csproj` - Project file for the Console application. + - `Library.Infrastructure/` + - `Data/` - Contains data access implementations. + - `Library.Infrastructure.csproj` - Project file for the Infrastructure layer. +- `tests/` + - `UnitTests/` + - `LoanFactory.cs` - Factory for creating test data related to loans. + - `PatronFactory.cs` - Factory for creating test data related to patrons. + - `ApplicationCore/` - Contains unit tests for the Application Core. + - `UnitTests.csproj` - Project file for unit tests. + +## Key Classes and Interfaces + +- **Entities** + - `Book` - Represents a book in the library. + - `Patron` - Represents a library patron. + - `Loan` - Represents a loan transaction. +- **Interfaces** + - `IBookRepository` - Interface for book-related data operations. + - `IPatronRepository` - Interface for patron-related data operations. + - `ILoanService` - Interface for managing loan operations. +- **Services** + - `LoanService` - Implements loan-related business logic. + - `NotificationService` - Handles notifications for overdue loans. + +## Usage + +1. Clone the repository: + + ```bash + git clone + ``` + +2. Open the solution file `AccelerateDevGHCopilot.sln` in Visual Studio. + +3. Build the solution to restore dependencies and compile the code. + +4. Run the console application: + + ```bash + dotnet run --project src/Library.Console/Library.Console.csproj + ``` + +5. Execute unit tests: + + ```bash + dotnet test tests/UnitTests/UnitTests.csproj + ``` + +## License + +This project is licensed under the MIT License. See the `LICENSE` file for details. + diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Author.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Author.cs new file mode 100644 index 0000000..5d9d1a6 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Author.cs @@ -0,0 +1,7 @@ +namespace Library.ApplicationCore.Entities; + +public class Author +{ + public int Id { get; set; } + public required string Name { get; set; } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Book.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Book.cs new file mode 100644 index 0000000..029b467 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Book.cs @@ -0,0 +1,12 @@ +namespace Library.ApplicationCore.Entities; + +public class Book +{ + public int Id { get; set; } + public required string Title { get; set; } + public int AuthorId { get; set; } + public required string Genre { get; set; } + public required string ImageName { get; set; } + public required string ISBN { get; set; } + public Author? Author { get; set; } +} diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/BookItem.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/BookItem.cs new file mode 100644 index 0000000..5a97332 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/BookItem.cs @@ -0,0 +1,10 @@ +namespace Library.ApplicationCore.Entities; + +public class BookItem +{ + public int Id { get; set; } + public int BookId { get; set; } + public DateTime AcquisitionDate { get; set; } + public string? Condition { get; set; } + public Book? Book { get; set; } +} diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Loan.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Loan.cs new file mode 100644 index 0000000..6d0c33e --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Loan.cs @@ -0,0 +1,13 @@ +namespace Library.ApplicationCore.Entities; + +public class Loan +{ + public int Id { get; set; } + public int BookItemId { get; set; } + public int PatronId { get; set; } + public Patron? Patron { get; set; } + public DateTime LoanDate { get; set; } + public DateTime DueDate { get; set; } + public DateTime? ReturnDate { get; set; } + public BookItem? BookItem { get; set; } +} diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Patron.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Patron.cs new file mode 100644 index 0000000..3a2fd33 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Entities/Patron.cs @@ -0,0 +1,11 @@ +namespace Library.ApplicationCore.Entities; + +public class Patron +{ + public int Id { get; set; } + public required string Name { get; set; } + public DateTime MembershipEnd { get; set; } + public DateTime MembershipStart { get; set; } + public string? ImageName { get; set; } + public ICollection Loans { get; set; } = new HashSet(); +} diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/EnumHelper.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/EnumHelper.cs new file mode 100644 index 0000000..5369856 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/EnumHelper.cs @@ -0,0 +1,27 @@ +using System.ComponentModel; +using System.Reflection; + +namespace Library.ApplicationCore.Enums; + +public static class EnumHelper +{ + public static string GetDescription(Enum value) + { + if (value == null) + return string.Empty; + + FieldInfo fieldInfo = value.GetType().GetField(value.ToString())!; + + DescriptionAttribute[] attributes = + (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false); + + if (attributes != null && attributes.Length > 0) + { + return attributes[0].Description; + } + else + { + return value.ToString(); + } + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanExtensionStatus.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanExtensionStatus.cs new file mode 100644 index 0000000..2af2c4a --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanExtensionStatus.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; + +namespace Library.ApplicationCore.Enums; + +public enum LoanExtensionStatus +{ + [Description("Book loan extension was successful.")] + Success, + + [Description("Loan not found.")] + LoanNotFound, + + [Description("Cannot extend book loan as it already has expired. Return the book instead.")] + LoanExpired, + + [Description("Cannot extend book loan due to expired patron's membership.")] + MembershipExpired, + + [Description("Cannot extend book loan as the book is already returned.")] + LoanReturned, + + [Description("Cannot extend book loan due to an error.")] + Error +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanReturnStatus.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanReturnStatus.cs new file mode 100644 index 0000000..61edf46 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/LoanReturnStatus.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; + +namespace Library.ApplicationCore.Enums; + +public enum LoanReturnStatus +{ + [Description("Book was successfully returned.")] + Success, + + [Description("Loan not found.")] + LoanNotFound, + + [Description("Cannot return book as the book is already returned.")] + AlreadyReturned, + + [Description("Cannot return book due to an error.")] + Error +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/MembershipRenewalStatus.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/MembershipRenewalStatus.cs new file mode 100644 index 0000000..1323ae3 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/MembershipRenewalStatus.cs @@ -0,0 +1,21 @@ +using System.ComponentModel; + +namespace Library.ApplicationCore.Enums; + +public enum MembershipRenewalStatus +{ + [Description("Membership renewal was successful.")] + Success, + + [Description("Patron not found.")] + PatronNotFound, + + [Description("It is too early to renew the membership.")] + TooEarlyToRenew, + + [Description("Cannot renew membership due to an outstanding loan.")] + LoanNotReturned, + + [Description("Cannot renew membership due to an error.")] + Error +} diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanRepository.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanRepository.cs new file mode 100644 index 0000000..ab00b02 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanRepository.cs @@ -0,0 +1,8 @@ +using Library.ApplicationCore.Entities; + +namespace Library.ApplicationCore; + +public interface ILoanRepository { + Task GetLoan(int loanId); + Task UpdateLoan(Loan loan); +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanService.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanService.cs new file mode 100644 index 0000000..cb255ce --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/ILoanService.cs @@ -0,0 +1,7 @@ +using Library.ApplicationCore.Enums; + +public interface ILoanService +{ + Task ReturnLoan(int loanId); + Task ExtendLoan(int loanId); +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronRepository.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronRepository.cs new file mode 100644 index 0000000..19b97f8 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronRepository.cs @@ -0,0 +1,10 @@ +using Library.ApplicationCore.Entities; + +namespace Library.ApplicationCore; + +public interface IPatronRepository { + Task GetPatron(int patronId); + Task> SearchPatrons(string searchInput); + Task UpdatePatron(Patron patron); +} + diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronService.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronService.cs new file mode 100644 index 0000000..6b5f453 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Interfaces/IPatronService.cs @@ -0,0 +1,6 @@ +using Library.ApplicationCore.Enums; + +public interface IPatronService +{ + Task RenewMembership(int patronId); +} diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Library.ApplicationCore.csproj b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Library.ApplicationCore.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Library.ApplicationCore.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/LoanService.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/LoanService.cs new file mode 100644 index 0000000..0f13d3a --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/LoanService.cs @@ -0,0 +1,70 @@ +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; + +public class LoanService : ILoanService +{ + private ILoanRepository _loanRepository; + + public LoanService(ILoanRepository loanRepository) + { + _loanRepository = loanRepository; + } + + public async Task ReturnLoan(int loanId) + { + Loan? loan = await _loanRepository.GetLoan(loanId); + if (loan == null) + { + return LoanReturnStatus.LoanNotFound; + } + + // check if already returned + if (loan.ReturnDate != null) + { + return LoanReturnStatus.AlreadyReturned; + } + + loan.ReturnDate = DateTime.Now; + try + { + await _loanRepository.UpdateLoan(loan); + return LoanReturnStatus.Success; + } + catch (Exception e) + { + return LoanReturnStatus.Error; + } + } + + public const int ExtendByDays = 14; + + public async Task ExtendLoan(int loanId) + { + var loan = await _loanRepository.GetLoan(loanId); + + if (loan == null) + return LoanExtensionStatus.LoanNotFound; + + // Check if patron's membership is expired + if (loan.Patron!.MembershipEnd < DateTime.Now) + return LoanExtensionStatus.MembershipExpired; + + if (loan.ReturnDate != null) + return LoanExtensionStatus.LoanReturned; + + if (loan.DueDate < DateTime.Now) + return LoanExtensionStatus.LoanExpired; + + loan.DueDate = loan.DueDate.AddDays(ExtendByDays); + try + { + await _loanRepository.UpdateLoan(loan); + return LoanExtensionStatus.Success; + } + catch (Exception e) + { + return LoanExtensionStatus.Error; + } + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/PatronService.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/PatronService.cs new file mode 100644 index 0000000..7ba6d78 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Services/PatronService.cs @@ -0,0 +1,36 @@ +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; + +public class PatronService : IPatronService +{ + private readonly IPatronRepository _patronRepository; + + public PatronService(IPatronRepository patronRepository) + { + _patronRepository = patronRepository; + } + + public async Task RenewMembership(int patronId) + { + var patron = await _patronRepository.GetPatron(patronId); + if (patron == null) + return MembershipRenewalStatus.PatronNotFound; + + // don't allow to renew till 1 month before expiration + if (patron.MembershipEnd >= DateTime.Now.AddMonths(1)) + return MembershipRenewalStatus.TooEarlyToRenew; + + // don't allow to renew if patron has overdue loans + if (patron.Loans.Any(l => (l.ReturnDate == null) && l.DueDate < DateTime.Now)) + return MembershipRenewalStatus.LoanNotReturned; + + patron.MembershipEnd = patron.MembershipEnd.AddYears(1); + try{ + await _patronRepository.UpdatePatron(patron); + return MembershipRenewalStatus.Success; + } catch (Exception e) { + return MembershipRenewalStatus.Error; + } + } +} diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/CommonActions.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/CommonActions.cs new file mode 100644 index 0000000..a002da6 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/CommonActions.cs @@ -0,0 +1,14 @@ +namespace Library.Console; + +[Flags] +public enum CommonActions +{ + Repeat = 0, + Select = 1, + Quit = 2, + SearchPatrons = 4, + RenewPatronMembership = 8, + ReturnLoanedBook = 16, + ExtendLoanedBook = 32, + SearchBooks = 64 +} diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs new file mode 100644 index 0000000..64d0bb4 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs @@ -0,0 +1,326 @@ +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; +using Library.Console; +using System.Globalization; +using Library.Infrastructure.Data; + +public class ConsoleApp +{ + ConsoleState _currentState = ConsoleState.PatronSearch; + + List matchingPatrons = new List(); + + Patron? selectedPatronDetails = null; + Loan selectedLoanDetails = null!; + + IPatronRepository _patronRepository; + ILoanRepository _loanRepository; + ILoanService _loanService; + IPatronService _patronService; + JsonData _jsonData; + + public ConsoleApp(ILoanService loanService, IPatronService patronService, IPatronRepository patronRepository, ILoanRepository loanRepository, JsonData jsonData) + { + _patronRepository = patronRepository; + _loanRepository = loanRepository; + _loanService = loanService; + _patronService = patronService; + _jsonData = jsonData; + } + + public async Task Run() + { + while (true) + { + switch (_currentState) + { + case ConsoleState.PatronSearch: + _currentState = await PatronSearch(); + break; + case ConsoleState.PatronSearchResults: + _currentState = await PatronSearchResults(); + break; + case ConsoleState.PatronDetails: + _currentState = await PatronDetails(); + break; + case ConsoleState.LoanDetails: + _currentState = await LoanDetails(); + break; + } + } + } + + async Task PatronSearch() + { + string searchInput = ReadPatronName(); + + matchingPatrons = await _patronRepository.SearchPatrons(searchInput); + + // Guard-style clauses for edge cases + if (matchingPatrons.Count > 20) + { + Console.WriteLine("More than 20 patrons satisfy the search, please provide more specific input..."); + return ConsoleState.PatronSearch; + } + else if (matchingPatrons.Count == 0) + { + Console.WriteLine("No matching patrons found."); + return ConsoleState.PatronSearch; + } + + Console.WriteLine("Matching Patrons:"); + PrintPatronsList(matchingPatrons); + return ConsoleState.PatronSearchResults; + } + + static string ReadPatronName() + { + string? searchInput = null; + while (String.IsNullOrWhiteSpace(searchInput)) + { + Console.Write("Enter a string to search for patrons by name: "); + + searchInput = Console.ReadLine(); + } + return searchInput; + } + + static void PrintPatronsList(List matchingPatrons) + { + int patronNumber = 1; + foreach (Patron patron in matchingPatrons) + { + Console.WriteLine($"{patronNumber}) {patron.Name}"); + patronNumber++; + } + } + + async Task PatronSearchResults() + { + CommonActions options = CommonActions.Select | CommonActions.SearchPatrons | CommonActions.Quit; + CommonActions action = ReadInputOptions(options, out int selectedPatronNumber); + if (action == CommonActions.Select) + { + if (selectedPatronNumber >= 1 && selectedPatronNumber <= matchingPatrons.Count) + { + var selectedPatron = matchingPatrons.ElementAt(selectedPatronNumber - 1); + selectedPatronDetails = await _patronRepository.GetPatron(selectedPatron.Id)!; + return ConsoleState.PatronDetails; + } + else + { + Console.WriteLine("Invalid patron number. Please try again."); + return ConsoleState.PatronSearchResults; + } + } + else if (action == CommonActions.Quit) + { + return ConsoleState.Quit; + } + else if (action == CommonActions.SearchPatrons) + { + return ConsoleState.PatronSearch; + } + + throw new InvalidOperationException("An input option is not handled."); + } + + static CommonActions ReadInputOptions(CommonActions options, out int optionNumber) + { + CommonActions action; + optionNumber = 0; + do + { + Console.WriteLine(); + WriteInputOptions(options); + string? userInput = Console.ReadLine(); + + action = userInput switch + { + "q" when options.HasFlag(CommonActions.Quit) => CommonActions.Quit, + "s" when options.HasFlag(CommonActions.SearchPatrons) => CommonActions.SearchPatrons, + "m" when options.HasFlag(CommonActions.RenewPatronMembership) => CommonActions.RenewPatronMembership, + "e" when options.HasFlag(CommonActions.ExtendLoanedBook) => CommonActions.ExtendLoanedBook, + "r" when options.HasFlag(CommonActions.ReturnLoanedBook) => CommonActions.ReturnLoanedBook, + "b" when options.HasFlag(CommonActions.SearchBooks) => CommonActions.SearchBooks, + _ when int.TryParse(userInput, out optionNumber) => CommonActions.Select, + _ => CommonActions.Repeat + }; + + if (action == CommonActions.Repeat) + { + Console.WriteLine("Invalid input. Please try again."); + } + } while (action == CommonActions.Repeat); + return action; + } + + static void WriteInputOptions(CommonActions options) + { + Console.WriteLine("Input Options:"); + if (options.HasFlag(CommonActions.ReturnLoanedBook)) + { + Console.WriteLine(" - \"r\" to mark as returned"); + } + if (options.HasFlag(CommonActions.ExtendLoanedBook)) + { + Console.WriteLine(" - \"e\" to extend the book loan"); + } + if (options.HasFlag(CommonActions.RenewPatronMembership)) + { + Console.WriteLine(" - \"m\" to extend patron's membership"); + } + if (options.HasFlag(CommonActions.SearchPatrons)) + { + Console.WriteLine(" - \"s\" for new search"); + } + if (options.HasFlag(CommonActions.SearchBooks)) + { + Console.WriteLine(" - \"b\" to check for book availability"); + } + if (options.HasFlag(CommonActions.Quit)) + { + Console.WriteLine(" - \"q\" to quit"); + } + if (options.HasFlag(CommonActions.Select)) + { + Console.WriteLine("Or type a number to select a list item."); + } + } + + async Task PatronDetails() + { + Console.WriteLine($"Name: {selectedPatronDetails.Name}"); + Console.WriteLine($"Membership Expiration: {selectedPatronDetails.MembershipEnd}"); + Console.WriteLine(); + Console.WriteLine("Book Loans:"); + int loanNumber = 1; + foreach (Loan loan in selectedPatronDetails.Loans) + { + Console.WriteLine($"{loanNumber}) {loan.BookItem!.Book!.Title} - Due: {loan.DueDate} - Returned: {(loan.ReturnDate != null).ToString()}"); + loanNumber++; + } + + CommonActions options = CommonActions.SearchPatrons | CommonActions.Quit | CommonActions.Select | CommonActions.RenewPatronMembership | CommonActions.SearchBooks; + CommonActions action = ReadInputOptions(options, out int selectedLoanNumber); + if (action == CommonActions.Select) + { + if (selectedLoanNumber >= 1 && selectedLoanNumber <= selectedPatronDetails.Loans.Count()) + { + var selectedLoan = selectedPatronDetails.Loans.ElementAt(selectedLoanNumber - 1); + selectedLoanDetails = selectedPatronDetails.Loans.Where(l => l.Id == selectedLoan.Id).Single(); + return ConsoleState.LoanDetails; + } + else + { + Console.WriteLine("Invalid book loan number. Please try again."); + return ConsoleState.PatronDetails; + } + } + else if (action == CommonActions.Quit) + { + return ConsoleState.Quit; + } + else if (action == CommonActions.SearchPatrons) + { + return ConsoleState.PatronSearch; + } + else if (action == CommonActions.RenewPatronMembership) + { + var status = await _patronService.RenewMembership(selectedPatronDetails.Id); + Console.WriteLine(EnumHelper.GetDescription(status)); + // reloading after renewing membership + selectedPatronDetails = (await _patronRepository.GetPatron(selectedPatronDetails.Id))!; + return ConsoleState.PatronDetails; + } + else if (action == CommonActions.SearchBooks) + { + await SearchBooks(); + return ConsoleState.PatronDetails; + } + + throw new InvalidOperationException("An input option is not handled."); + } + + async Task SearchBooks() + { + string? bookTitle = null; + while (string.IsNullOrWhiteSpace(bookTitle)) + { + Console.Write("Enter a book title to search for: "); + bookTitle = Console.ReadLine(); + } + + await _jsonData.EnsureDataLoaded(); + + var book = _jsonData.Books!.FirstOrDefault(b => string.Equals(b.Title, bookTitle, StringComparison.OrdinalIgnoreCase)); + if (book == null) + { + Console.WriteLine($"No book found with the title \"{bookTitle}\"."); + return ConsoleState.PatronDetails; + } + + var bookItem = _jsonData.BookItems!.FirstOrDefault(bi => bi.BookId == book.Id); + if (bookItem == null) + { + Console.WriteLine($"No book item found for the title \"{book.Title}\"."); + return ConsoleState.PatronDetails; + } + + var loan = _jsonData.Loans!.FirstOrDefault(l => l.BookItemId == bookItem.Id && l.ReturnDate == null); + if (loan == null) + { + Console.WriteLine($"\"{book.Title}\" is available for loan."); + } + else + { + Console.WriteLine($"\"{book.Title}\" is on loan to another patron. The return due date is {loan.DueDate.ToString("d", CultureInfo.InvariantCulture)}."); + } + + return ConsoleState.PatronDetails; + } + + async Task LoanDetails() + { + Console.WriteLine($"Book title: {selectedLoanDetails.BookItem!.Book!.Title}"); + Console.WriteLine($"Book Author: {selectedLoanDetails.BookItem!.Book!.Author!.Name}"); + Console.WriteLine($"Due date: {selectedLoanDetails.DueDate}"); + Console.WriteLine($"Returned: {(selectedLoanDetails.ReturnDate != null).ToString()}"); + Console.WriteLine(); + + CommonActions options = CommonActions.SearchPatrons | CommonActions.Quit | CommonActions.ReturnLoanedBook | CommonActions.ExtendLoanedBook; + CommonActions action = ReadInputOptions(options, out int selectedLoanNumber); + + if (action == CommonActions.ExtendLoanedBook) + { + var status = await _loanService.ExtendLoan(selectedLoanDetails.Id); + Console.WriteLine(EnumHelper.GetDescription(status)); + + // reload loan after extending + selectedPatronDetails = (await _patronRepository.GetPatron(selectedPatronDetails.Id))!; + selectedLoanDetails = (await _loanRepository.GetLoan(selectedLoanDetails.Id))!; + return ConsoleState.LoanDetails; + } + else if (action == CommonActions.ReturnLoanedBook) + { + var status = await _loanService.ReturnLoan(selectedLoanDetails.Id); + + Console.WriteLine(EnumHelper.GetDescription(status)); + _currentState = ConsoleState.LoanDetails; + // reload loan after returning + selectedLoanDetails = await _loanRepository.GetLoan(selectedLoanDetails.Id); + return ConsoleState.LoanDetails; + } + else if (action == CommonActions.Quit) + { + return ConsoleState.Quit; + } + else if (action == CommonActions.SearchPatrons) + { + return ConsoleState.PatronSearch; + } + + throw new InvalidOperationException("An input option is not handled."); + } +} diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/ConsoleState.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/ConsoleState.cs new file mode 100644 index 0000000..e9117b6 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/ConsoleState.cs @@ -0,0 +1,10 @@ +namespace Library.Console; + +public enum ConsoleState +{ + PatronSearch, + PatronSearchResults, + PatronDetails, + LoanDetails, + Quit +} diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/Json/Authors.json b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/Json/Authors.json new file mode 100644 index 0000000..1357eb1 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/Json/Authors.json @@ -0,0 +1,82 @@ +[ + { + "Id": 1, + "Name": "Author One" + }, + { + "Id": 2, + "Name": "Author Two" + }, + { + "Id": 3, + "Name": "Author Three" + }, + { + "Id": 4, + "Name": "Author Four" + }, + { + "Id": 5, + "Name": "Author Five" + }, + { + "Id": 6, + "Name": "Author Six" + }, + { + "Id": 7, + "Name": "Author Seven" + }, + { + "Id": 8, + "Name": "Author Eight" + }, + { + "Id": 9, + "Name": "Author Nine" + }, + { + "Id": 10, + "Name": "Author Ten" + }, + { + "Id": 11, + "Name": "Author Eleven" + }, + { + "Id": 12, + "Name": "Author Twelve" + }, + { + "Id": 13, + "Name": "Author Thirteen" + }, + { + "Id": 14, + "Name": "Author Fourteen" + }, + { + "Id": 15, + "Name": "Author Fifteen" + }, + { + "Id": 16, + "Name": "Author Sixteen" + }, + { + "Id": 17, + "Name": "Author Seventeen" + }, + { + "Id": 18, + "Name": "Author Eighteen" + }, + { + "Id": 19, + "Name": "Author Nineteen" + }, + { + "Id": 20, + "Name": "Author Twenty" + } +] \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/Json/BookItems.json b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/Json/BookItems.json new file mode 100644 index 0000000..ed659c5 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/Json/BookItems.json @@ -0,0 +1,122 @@ +[ + { + "Id": 1, + "BookId": 1, + "AcquisitionDate": "2023-09-20T00:40:43.1716563", + "Condition": "Good" + }, + { + "Id": 2, + "BookId": 2, + "AcquisitionDate": "2023-09-20T00:40:43.1717503", + "Condition": "Fair" + }, + { + "Id": 3, + "BookId": 3, + "AcquisitionDate": "2023-09-20T00:40:43.1717511", + "Condition": "Excellent" + }, + { + "Id": 4, + "BookId": 4, + "AcquisitionDate": "2023-09-20T00:40:43.1717513", + "Condition": "Poor" + }, + { + "Id": 5, + "BookId": 5, + "AcquisitionDate": "2023-09-20T00:40:43.1717516", + "Condition": "Good" + }, + { + "Id": 6, + "BookId": 6, + "AcquisitionDate": "2023-09-20T00:40:43.1717521", + "Condition": "Fair" + }, + { + "Id": 7, + "BookId": 7, + "AcquisitionDate": "2023-09-20T00:40:43.1717523", + "Condition": "Excellent" + }, + { + "Id": 8, + "BookId": 8, + "AcquisitionDate": "2023-09-20T00:40:43.1717526", + "Condition": "Poor" + }, + { + "Id": 9, + "BookId": 9, + "AcquisitionDate": "2023-09-20T00:40:43.171757", + "Condition": "Good" + }, + { + "Id": 10, + "BookId": 10, + "AcquisitionDate": "2023-09-20T00:40:43.1717574", + "Condition": "Fair" + }, + { + "Id": 11, + "BookId": 11, + "AcquisitionDate": "2023-09-20T00:40:43.1717576", + "Condition": "Excellent" + }, + { + "Id": 12, + "BookId": 12, + "AcquisitionDate": "2023-09-20T00:40:43.1717578", + "Condition": "Poor" + }, + { + "Id": 13, + "BookId": 13, + "AcquisitionDate": "2023-09-20T00:40:43.171758", + "Condition": "Good" + }, + { + "Id": 14, + "BookId": 14, + "AcquisitionDate": "2023-09-20T00:40:43.1717609", + "Condition": "Fair" + }, + { + "Id": 15, + "BookId": 15, + "AcquisitionDate": "2023-09-20T00:40:43.1717611", + "Condition": "Excellent" + }, + { + "Id": 16, + "BookId": 16, + "AcquisitionDate": "2023-09-20T00:40:43.1717613", + "Condition": "Poor" + }, + { + "Id": 17, + "BookId": 17, + "AcquisitionDate": "2023-09-20T00:40:43.1717616", + "Condition": "Good" + }, + { + "Id": 18, + "BookId": 18, + "AcquisitionDate": "2023-09-20T00:40:43.1717619", + "Condition": "Fair" + }, + { + "Id": 19, + "BookId": 19, + "AcquisitionDate": "2023-09-20T00:40:43.1717621", + "Condition": "Excellent" + }, + { + "Id": 20, + "BookId": 20, + "AcquisitionDate": "2023-09-20T00:40:43.1717626", + "Condition": "Poor" + } +] \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/Json/Books.json b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/Json/Books.json new file mode 100644 index 0000000..51f3339 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/Json/Books.json @@ -0,0 +1,162 @@ +[ + { + "Id": 1, + "Title": "Book One", + "AuthorId": 1, + "Genre": "Dystopian", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524935" + }, + { + "Id": 2, + "Title": "Book Two", + "AuthorId": 2, + "Genre": "Classic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524936" + }, + { + "Id": 3, + "Title": "Book Three", + "AuthorId": 3, + "Genre": "Romance", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524937" + }, + { + "Id": 4, + "Title": "Book Four", + "AuthorId": 4, + "Genre": "Classic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524938" + }, + { + "Id": 5, + "Title": "Book Five", + "AuthorId": 5, + "Genre": "Coming-of-age", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524939" + }, + { + "Id": 6, + "Title": "Book Six", + "AuthorId": 6, + "Genre": "Modernist", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524940" + }, + { + "Id": 7, + "Title": "Book Seven", + "AuthorId": 7, + "Genre": "Adventure", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524941" + }, + { + "Id": 8, + "Title": "Book Eight", + "AuthorId": 8, + "Genre": "Fantasy", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524942" + }, + { + "Id": 9, + "Title": "Book Nine", + "AuthorId": 9, + "Genre": "Fantasy", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524943" + }, + { + "Id": 10, + "Title": "Book Ten", + "AuthorId": 10, + "Genre": "Epic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524944" + }, + { + "Id": 11, + "Title": "Book Eleven", + "AuthorId": 11, + "Genre": "Psychological", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524945" + }, + { + "Id": 12, + "Title": "Book Twelve", + "AuthorId": 12, + "Genre": "Psychological", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524946" + }, + { + "Id": 13, + "Title": "Book Thirteen", + "AuthorId": 13, + "Genre": "Magical realism", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524947" + }, + { + "Id": 14, + "Title": "Book Fourteen", + "AuthorId": 14, + "Genre": "Classic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524948" + }, + { + "Id": 15, + "Title": "Book Fifteen", + "AuthorId": 15, + "Genre": "Adventure", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524949" + }, + { + "Id": 16, + "Title": "Book Sixteen", + "AuthorId": 16, + "Genre": "Historical", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524950" + }, + { + "Id": 17, + "Title": "Book Seventeen", + "AuthorId": 17, + "Genre": "Gothic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524951" + }, + { + "Id": 18, + "Title": "Book Eighteen", + "AuthorId": 18, + "Genre": "Dystopian", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524952" + }, + { + "Id": 19, + "Title": "Book Nineteen", + "AuthorId": 19, + "Genre": "Classic", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524953" + }, + { + "Id": 20, + "Title": "Book Twenty", + "AuthorId": 20, + "Genre": "Adventure", + "ImageName": "BookCover01.jpg", + "ISBN": "978-0451524954" + } +] \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/Json/Loans.json b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/Json/Loans.json new file mode 100644 index 0000000..a84491d --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/Json/Loans.json @@ -0,0 +1,402 @@ +[ + { + "Id": 1, + "BookItemId": 17, + "PatronId": 22, + "LoanDate": "2023-12-08T00:40:43.1808862", + "DueDate": "2023-12-22T00:40:43.1808862", + "ReturnDate": null + }, + { + "Id": 2, + "BookItemId": 6, + "PatronId": 28, + "LoanDate": "2023-12-17T00:40:43.1809243", + "DueDate": "2023-12-31T00:40:43.1809243", + "ReturnDate": null + }, + { + "Id": 3, + "BookItemId": 16, + "PatronId": 4, + "LoanDate": "2023-12-23T00:40:43.1809289", + "DueDate": "2024-01-06T00:40:43.1809289", + "ReturnDate": null + }, + { + "Id": 4, + "BookItemId": 17, + "PatronId": 14, + "LoanDate": "2023-12-22T00:40:43.1809292", + "DueDate": "2024-01-05T00:40:43.1809292", + "ReturnDate": null + }, + { + "Id": 5, + "BookItemId": 6, + "PatronId": 9, + "LoanDate": "2023-12-09T00:40:43.1809295", + "DueDate": "2023-12-23T00:40:43.1809295", + "ReturnDate": null + }, + { + "Id": 6, + "BookItemId": 14, + "PatronId": 25, + "LoanDate": "2023-12-27T00:40:43.18093", + "DueDate": "2024-01-10T00:40:43.18093", + "ReturnDate": null + }, + { + "Id": 7, + "BookItemId": 12, + "PatronId": 50, + "LoanDate": "2023-12-27T00:40:43.1809304", + "DueDate": "2024-01-10T00:40:43.1809304", + "ReturnDate": null + }, + { + "Id": 8, + "BookItemId": 18, + "PatronId": 28, + "LoanDate": "2023-12-26T00:40:43.1809306", + "DueDate": "2024-01-09T00:40:43.1809306", + "ReturnDate": null + }, + { + "Id": 9, + "BookItemId": 8, + "PatronId": 9, + "LoanDate": "2023-12-10T00:40:43.1809309", + "DueDate": "2023-12-24T00:40:43.1809309", + "ReturnDate": null + }, + { + "Id": 10, + "BookItemId": 16, + "PatronId": 3, + "LoanDate": "2023-12-26T00:40:43.1809312", + "DueDate": "2024-01-09T00:40:43.1809312", + "ReturnDate": null + }, + { + "Id": 11, + "BookItemId": 4, + "PatronId": 42, + "LoanDate": "2023-12-15T00:40:43.1809315", + "DueDate": "2023-12-29T00:40:43.1809315", + "ReturnDate": null + }, + { + "Id": 12, + "BookItemId": 17, + "PatronId": 7, + "LoanDate": "2023-12-23T00:40:43.1809331", + "DueDate": "2024-01-06T00:40:43.1809331", + "ReturnDate": null + }, + { + "Id": 13, + "BookItemId": 12, + "PatronId": 5, + "LoanDate": "2023-12-27T00:40:43.1809333", + "DueDate": "2024-01-10T00:40:43.1809333", + "ReturnDate": null + }, + { + "Id": 14, + "BookItemId": 4, + "PatronId": 9, + "LoanDate": "2023-12-10T00:40:43.1809337", + "DueDate": "2023-12-24T00:40:43.1809337", + "ReturnDate": null + }, + { + "Id": 15, + "BookItemId": 7, + "PatronId": 28, + "LoanDate": "2023-12-23T00:40:43.1809339", + "DueDate": "2024-01-06T00:40:43.1809339", + "ReturnDate": null + }, + { + "Id": 16, + "BookItemId": 14, + "PatronId": 3, + "LoanDate": "2023-12-08T00:40:43.1809342", + "DueDate": "2023-12-22T00:40:43.1809342", + "ReturnDate": null + }, + { + "Id": 17, + "BookItemId": 5, + "PatronId": 48, + "LoanDate": "2023-12-16T00:40:43.1809344", + "DueDate": "2023-12-30T00:40:43.1809344", + "ReturnDate": null + }, + { + "Id": 18, + "BookItemId": 4, + "PatronId": 49, + "LoanDate": "2023-12-19T00:40:43.1809348", + "DueDate": "2024-01-02T00:40:43.1809348", + "ReturnDate": null + }, + { + "Id": 19, + "BookItemId": 13, + "PatronId": 33, + "LoanDate": "2023-12-28T00:40:43.180935", + "DueDate": "2024-01-11T00:40:43.180935", + "ReturnDate": null + }, + { + "Id": 20, + "BookItemId": 14, + "PatronId": 48, + "LoanDate": "2023-12-27T00:40:43.1809353", + "DueDate": "2024-01-10T00:40:43.1809353", + "ReturnDate": null + }, + { + "Id": 21, + "BookItemId": 7, + "PatronId": 5, + "LoanDate": "2023-12-12T00:40:43.1809368", + "DueDate": "2023-12-26T00:40:43.1809368", + "ReturnDate": null + }, + { + "Id": 22, + "BookItemId": 9, + "PatronId": 1, + "LoanDate": "2023-12-09T00:40:43.1809371", + "DueDate": "2023-12-23T00:40:43.1809371", + "ReturnDate": null + }, + { + "Id": 23, + "BookItemId": 11, + "PatronId": 33, + "LoanDate": "2023-12-26T00:40:43.1809374", + "DueDate": "2024-01-09T00:40:43.1809374", + "ReturnDate": null + }, + { + "Id": 24, + "BookItemId": 10, + "PatronId": 46, + "LoanDate": "2023-12-28T00:40:43.1809376", + "DueDate": "2024-01-11T00:40:43.1809376", + "ReturnDate": null + }, + { + "Id": 25, + "BookItemId": 20, + "PatronId": 41, + "LoanDate": "2023-12-12T00:40:43.1809379", + "DueDate": "2023-12-26T00:40:43.1809379", + "ReturnDate": null + }, + { + "Id": 26, + "BookItemId": 13, + "PatronId": 15, + "LoanDate": "2023-12-16T00:40:43.1809382", + "DueDate": "2023-12-30T00:40:43.1809382", + "ReturnDate": null + }, + { + "Id": 27, + "BookItemId": 15, + "PatronId": 23, + "LoanDate": "2023-12-18T00:40:43.1809384", + "DueDate": "2024-01-01T00:40:43.1809384", + "ReturnDate": null + }, + { + "Id": 28, + "BookItemId": 15, + "PatronId": 31, + "LoanDate": "2023-12-11T00:40:43.1809387", + "DueDate": "2023-12-25T00:40:43.1809387", + "ReturnDate": null + }, + { + "Id": 29, + "BookItemId": 4, + "PatronId": 10, + "LoanDate": "2023-12-18T00:40:43.1809402", + "DueDate": "2024-01-01T00:40:43.1809402", + "ReturnDate": null + }, + { + "Id": 30, + "BookItemId": 6, + "PatronId": 18, + "LoanDate": "2023-12-12T00:40:43.1809405", + "DueDate": "2023-12-26T00:40:43.1809405", + "ReturnDate": null + }, + { + "Id": 31, + "BookItemId": 11, + "PatronId": 3, + "LoanDate": "2023-12-16T00:40:43.1809408", + "DueDate": "2023-12-30T00:40:43.1809408", + "ReturnDate": null + }, + { + "Id": 32, + "BookItemId": 8, + "PatronId": 20, + "LoanDate": "2023-12-22T00:40:43.1809411", + "DueDate": "2024-01-05T00:40:43.1809411", + "ReturnDate": null + }, + { + "Id": 33, + "BookItemId": 14, + "PatronId": 12, + "LoanDate": "2023-12-28T00:40:43.1809415", + "DueDate": "2024-01-11T00:40:43.1809415", + "ReturnDate": null + }, + { + "Id": 34, + "BookItemId": 19, + "PatronId": 29, + "LoanDate": "2023-12-28T00:40:43.1809458", + "DueDate": "2024-01-11T00:40:43.1809458", + "ReturnDate": "2023-12-29T00:40:54.582495" + }, + { + "Id": 35, + "BookItemId": 7, + "PatronId": 45, + "LoanDate": "2023-12-17T00:40:43.180946", + "DueDate": "2023-12-31T00:40:43.180946", + "ReturnDate": null + }, + { + "Id": 36, + "BookItemId": 11, + "PatronId": 3, + "LoanDate": "2023-12-10T00:40:43.1809463", + "DueDate": "2023-12-24T00:40:43.1809463", + "ReturnDate": null + }, + { + "Id": 37, + "BookItemId": 1, + "PatronId": 5, + "LoanDate": "2023-12-18T00:40:43.1809466", + "DueDate": "2024-01-18T00:40:43.1809466", + "ReturnDate": "2024-01-17T00:40:43.1809466" + }, + { + "Id": 38, + "BookItemId": 15, + "PatronId": 25, + "LoanDate": "2023-12-26T00:40:43.1809481", + "DueDate": "2024-01-09T00:40:43.1809481", + "ReturnDate": null + }, + { + "Id": 39, + "BookItemId": 4, + "PatronId": 33, + "LoanDate": "2023-12-18T00:40:43.1809484", + "DueDate": "2024-01-01T00:40:43.1809484", + "ReturnDate": null + }, + { + "Id": 40, + "BookItemId": 5, + "PatronId": 33, + "LoanDate": "2023-12-25T00:40:43.1809487", + "DueDate": "2024-01-08T00:40:43.1809487", + "ReturnDate": null + }, + { + "Id": 41, + "BookItemId": 14, + "PatronId": 13, + "LoanDate": "2023-12-15T00:40:43.1809489", + "DueDate": "2023-12-29T00:40:43.1809489", + "ReturnDate": null + }, + { + "Id": 42, + "BookItemId": 11, + "PatronId": 10, + "LoanDate": "2023-12-12T00:40:43.1809493", + "DueDate": "2023-12-26T00:40:43.1809493", + "ReturnDate": null + }, + { + "Id": 43, + "BookItemId": 9, + "PatronId": 45, + "LoanDate": "2023-12-14T00:40:43.1809496", + "DueDate": "2023-12-28T00:40:43.1809496", + "ReturnDate": "2023-12-29T00:49:42.3406277" + }, + { + "Id": 44, + "BookItemId": 3, + "PatronId": 46, + "LoanDate": "2023-12-08T00:40:43.1809498", + "DueDate": "2023-12-22T00:40:43.1809498", + "ReturnDate": null + }, + { + "Id": 45, + "BookItemId": 5, + "PatronId": 10, + "LoanDate": "2023-12-24T00:40:43.1809501", + "DueDate": "2024-01-07T00:40:43.1809501", + "ReturnDate": null + }, + { + "Id": 46, + "BookItemId": 1, + "PatronId": 49, + "LoanDate": "2024-07-09T00:40:43.1809503", + "DueDate": "2024-09-09T00:40:43.1809503", + "ReturnDate": null + }, + { + "Id": 47, + "BookItemId": 8, + "PatronId": 36, + "LoanDate": "2023-12-11T00:40:43.1809507", + "DueDate": "2023-12-25T00:40:43.1809507", + "ReturnDate": null + }, + { + "Id": 48, + "BookItemId": 5, + "PatronId": 10, + "LoanDate": "2023-12-18T00:40:43.1809509", + "DueDate": "2024-01-01T00:40:43.1809509", + "ReturnDate": null + }, + { + "Id": 49, + "BookItemId": 20, + "PatronId": 24, + "LoanDate": "2023-12-16T00:40:43.1809512", + "DueDate": "2023-12-30T00:40:43.1809512", + "ReturnDate": null + }, + { + "Id": 50, + "BookItemId": 3, + "PatronId": 45, + "LoanDate": "2023-12-13T00:40:43.1809514", + "DueDate": "2023-12-27T00:40:43.1809514", + "ReturnDate": "2023-12-29T00:49:48.9561798" + } + ] diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/Json/Patrons.json b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/Json/Patrons.json new file mode 100644 index 0000000..5d44d83 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/Json/Patrons.json @@ -0,0 +1,352 @@ +[ + { + "Id": 1, + "Name": "Patron One", + "MembershipEnd": "2024-12-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron One.jpg" + }, + { + "Id": 2, + "Name": "Patron Two", + "MembershipEnd": "2025-02-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Two.jpg" + }, + { + "Id": 3, + "Name": "Patron Three", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Three.jpg" + }, + { + "Id": 4, + "Name": "Patron Four", + "MembershipEnd": "2025-04-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Four.jpg" + }, + { + "Id": 5, + "Name": "Patron Five", + "MembershipEnd": "2025-05-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Five.jpg" + }, + { + "Id": 6, + "Name": "Patron Six", + "MembershipEnd": "2025-06-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Six.jpg" + }, + { + "Id": 7, + "Name": "Patron Seven", + "MembershipEnd": "2025-07-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Seven.jpg" + }, + { + "Id": 8, + "Name": "Patron Eight", + "MembershipEnd": "2024-01-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Eight.jpg" + }, + { + "Id": 9, + "Name": "Patron Nine", + "MembershipEnd": "2024-02-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Nine.jpg" + }, + { + "Id": 10, + "Name": "Patron Ten", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Ten.jpg" + }, + { + "Id": 11, + "Name": "Patron Eleven", + "MembershipEnd": "2024-04-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Eleven.jpg" + }, + { + "Id": 12, + "Name": "Patron Twelve", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twelve.jpg" + }, + { + "Id": 13, + "Name": "Patron Thirteen", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirteen.jpg" + }, + { + "Id": 14, + "Name": "Patron Fourteen", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Fourteen.jpg" + }, + { + "Id": 15, + "Name": "Patron Fifteen", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Fifteen.jpg" + }, + { + "Id": 16, + "Name": "Patron Sixteen", + "MembershipEnd": "2024-04-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Sixteen.jpg" + }, + { + "Id": 17, + "Name": "Patron Seventeen", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Seventeen.jpg" + }, + { + "Id": 18, + "Name": "Patron Eighteen", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Eighteen.jpg" + }, + { + "Id": 19, + "Name": "Patron Nineteen", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Nineteen.jpg" + }, + { + "Id": 20, + "Name": "Patron Twenty", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty.jpg" + }, + { + "Id": 21, + "Name": "Patron Twenty-One", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-One.jpg" + }, + { + "Id": 22, + "Name": "Patron Twenty-Two", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Two.jpg" + }, + { + "Id": 23, + "Name": "Patron Twenty-Three", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Three.jpg" + }, + { + "Id": 24, + "Name": "Patron Twenty-Four", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Four.jpg" + }, + { + "Id": 25, + "Name": "Patron Twenty-Five", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Five.jpg" + }, + { + "Id": 26, + "Name": "Patron Twenty-Six", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Six.jpg" + }, + { + "Id": 27, + "Name": "Patron Twenty-Seven", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Seven.jpg" + }, + { + "Id": 28, + "Name": "Patron Twenty-Eight", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Eight.jpg" + }, + { + "Id": 29, + "Name": "Patron Twenty-Nine", + "MembershipEnd": "2024-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Twenty-Nine.jpg" + }, + { + "Id": 30, + "Name": "Patron Thirty", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty.jpg" + }, + { + "Id": 31, + "Name": "Patron Thirty-One", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-One.jpg" + }, + { + "Id": 32, + "Name": "Patron Thirty-Two", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Two.jpg" + }, + { + "Id": 33, + "Name": "Patron Thirty-Three", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Three.jpg" + }, + { + "Id": 34, + "Name": "Patron Thirty-Four", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Four.jpg" + }, + { + "Id": 35, + "Name": "Patron Thirty-Five", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Five.jpg" + }, + { + "Id": 36, + "Name": "Patron Thirty-Six", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Six.jpg" + }, + { + "Id": 37, + "Name": "Patron Thirty-Seven", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Seven.jpg" + }, + { + "Id": 38, + "Name": "Patron Thirty-Eight", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Eight.jpg" + }, + { + "Id": 39, + "Name": "Patron Thirty-Nine", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Thirty-Nine.jpg" + }, + { + "Id": 40, + "Name": "Patron Forty", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty.jpg" + }, + { + "Id": 41, + "Name": "Patron Forty-One", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-One.jpg" + }, + { + "Id": 42, + "Name": "Patron Forty-Two", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Two.jpg" + }, + { + "Id": 43, + "Name": "Patron Forty-Three", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Three.jpg" + }, + { + "Id": 44, + "Name": "Patron Forty-Four", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Four.jpg" + }, + { + "Id": 45, + "Name": "Patron Forty-Five", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Five.jpg" + }, + { + "Id": 46, + "Name": "Patron Forty-Six", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Six.jpg" + }, + { + "Id": 47, + "Name": "Patron Forty-Seven", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Seven.jpg" + }, + { + "Id": 48, + "Name": "Patron Forty-Eight", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Eight.jpg" + }, + { + "Id": 49, + "Name": "Patron Forty-Nine", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Forty-Nine.jpg" + }, + { + "Id": 50, + "Name": "Patron Fifty", + "MembershipEnd": "2025-03-01T00:40:43.1589724", + "MembershipStart": "2001-01-01T00:40:43.1589724", + "ImageName": "Patron Fifty.jpg" + } +] \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/Library.Console.csproj b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/Library.Console.csproj new file mode 100644 index 0000000..359cee9 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/Library.Console.csproj @@ -0,0 +1,34 @@ + + + + + + + + + Exe + net9.0 + enable + enable + + + + + PreserveNewest + + + + + + PreserveNewest + + + + + + + + + + + diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/Program.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/Program.cs new file mode 100644 index 0000000..1a21671 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/Program.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.DependencyInjection; +using Library.Infrastructure.Data; +using Library.ApplicationCore; +using Microsoft.Extensions.Configuration; + +var services = new ServiceCollection(); + +var configuration = new ConfigurationBuilder() +.SetBasePath(Directory.GetCurrentDirectory()) +.AddJsonFile("appSettings.json") +.Build(); + +services.AddSingleton(configuration); + +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); + +services.AddSingleton(); +services.AddSingleton(); + +var servicesProvider = services.BuildServiceProvider(); + +var consoleApp = servicesProvider.GetRequiredService(); +consoleApp.Run().Wait(); diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/appSettings.json b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/appSettings.json new file mode 100644 index 0000000..3aed751 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Console/appSettings.json @@ -0,0 +1,9 @@ +{ + "JsonPaths": { + "Authors": "Json/Authors.json", + "Books": "Json/Books.json", + "BookItems": "Json/BookItems.json", + "Patrons": "Json/Patrons.json", + "Loans": "Json/Loans.json" + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonData.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonData.cs new file mode 100644 index 0000000..7af26a7 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonData.cs @@ -0,0 +1,212 @@ +using System.Text.Json; +using Library.ApplicationCore.Entities; +using Microsoft.Extensions.Configuration; + +namespace Library.Infrastructure.Data; + +public class JsonData +{ + public List? Authors { get; set; } + public List? Books { get; set; } + public List? BookItems { get; set; } + public List? Patrons { get; set; } + public List? Loans { get; set; } + + private readonly string _authorsPath; + private readonly string _booksPath; + private readonly string _bookItemsPath; + private readonly string _patronsPath; + private readonly string _loansPath; + + public JsonData(IConfiguration configuration) + { + var section = configuration.GetSection("JsonPaths"); + _authorsPath = section["Authors"] ?? Path.Combine("Json", "Authors.json"); + _booksPath = section["Books"] ?? Path.Combine("Json", "Books.json"); + _bookItemsPath = section["BookItems"] ?? Path.Combine("Json", "BookItems.json"); + _patronsPath = section["Patrons"] ?? Path.Combine("Json", "Patrons.json"); + _loansPath = section["Loans"] ?? Path.Combine("Json", "Loans.json"); + } + + public async Task EnsureDataLoaded() + { + if (Patrons == null) + { + await LoadData(); + } + } + + public async Task LoadData() + { + Authors = await LoadJson>(_authorsPath); + Books = await LoadJson>(_booksPath); + BookItems = await LoadJson>(_bookItemsPath); + Patrons = await LoadJson>(_patronsPath); + Loans = await LoadJson>(_loansPath); + } + + public async Task SaveLoans(IEnumerable loans) + { + List loanList = new List(); + foreach (var l in loans) + { + Loan loan = new Loan + { + // making sure only a subset of properties is set and saved + Id = l.Id, + BookItemId = l.BookItemId, + PatronId = l.PatronId, + LoanDate = l.LoanDate, + DueDate = l.DueDate, + ReturnDate = l.ReturnDate + }; + loanList.Add(loan); + } + await SaveJson(_loansPath, loanList); + } + + public async Task SavePatrons(IEnumerable patrons) + { + await SaveJson(_patronsPath, patrons.Select(p => new Patron + { + Id = p.Id, + Name = p.Name, + MembershipStart = p.MembershipStart, + MembershipEnd = p.MembershipEnd, + ImageName = p.ImageName, + }).ToList()); + } + + private async Task SaveJson(string filePath, T data) + { + using (FileStream jsonStream = File.Create(filePath)) + { + await JsonSerializer.SerializeAsync(jsonStream, data); + } + } + + public List GetPopulatedPatrons(IEnumerable patrons) + { + List populated = new List(); + foreach (Patron patron in patrons) + { + populated.Add(GetPopulatedPatron(patron)); + } + return populated; + } + + public Patron GetPopulatedPatron(Patron p) + { + Patron populated = new Patron + { + Id = p.Id, + Name = p.Name, + ImageName = p.ImageName, + MembershipStart = p.MembershipStart, + MembershipEnd = p.MembershipEnd, + Loans = new List() + }; + + foreach (Loan loan in Loans!) + { + if (loan.PatronId == p.Id) + { + populated.Loans.Add(GetPopulatedLoan(loan)); + } + } + + return populated; + } + + public Loan GetPopulatedLoan(Loan l) + { + Loan populated = new Loan + { + Id = l.Id, + BookItemId = l.BookItemId, + PatronId = l.PatronId, + LoanDate = l.LoanDate, + DueDate = l.DueDate, + ReturnDate = l.ReturnDate + }; + + foreach (BookItem bi in BookItems!) + { + if (bi.Id == l.BookItemId) + { + populated.BookItem = GetPopulatedBookItem(bi); + break; + } + } + + foreach (Patron p in Patrons!) + { + if (p.Id == l.PatronId) + { + populated.Patron = p; + break; + } + } + + return populated; + } + + public BookItem GetPopulatedBookItem(BookItem bi) + { + BookItem populated = new BookItem + { + Id = bi.Id, + BookId = bi.BookId, + AcquisitionDate = bi.AcquisitionDate, + Condition = bi.Condition + }; + + foreach (Book b in Books!) + { + if (b.Id == bi.BookId) + { + populated.Book = GetPopulatedBook(b); + break; + } + } + + return populated; + } + + public Book GetPopulatedBook(Book b) + { + Book populated = new Book + { + Id = b.Id, + Title = b.Title, + AuthorId = b.AuthorId, + Genre = b.Genre, + ISBN = b.ISBN, + ImageName = b.ImageName + }; + + foreach (Author a in Authors!) + { + if (a.Id == b.AuthorId) + { + populated.Author = new Author + { + Id = a.Id, + Name = a.Name + }; + break; + } + } + + return populated; + } + + private async Task LoadJson(string filePath) + { + using (FileStream jsonStream = File.OpenRead(filePath)) + { + return await JsonSerializer.DeserializeAsync(jsonStream); + } + } + +} diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonLoanRepository.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonLoanRepository.cs new file mode 100644 index 0000000..2683283 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonLoanRepository.cs @@ -0,0 +1,55 @@ +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; + +namespace Library.Infrastructure.Data; + +public class JsonLoanRepository : ILoanRepository +{ + private readonly JsonData _jsonData; + + public JsonLoanRepository(JsonData jsonData) + { + _jsonData = jsonData; + } + + public async Task GetLoan(int id) + { + await _jsonData.EnsureDataLoaded(); + + foreach (Loan loan in _jsonData.Loans!) + { + if (loan.Id == id) + { + Loan populated = _jsonData.GetPopulatedLoan(loan); + return populated; + } + } + return null; + } + + public async Task UpdateLoan(Loan loan) + { + Loan? existingLoan = null; + foreach (Loan l in _jsonData.Loans!) + { + if (l.Id == loan.Id) + { + existingLoan = l; + break; + } + } + + if (existingLoan != null) + { + existingLoan.BookItemId = loan.BookItemId; + existingLoan.PatronId = loan.PatronId; + existingLoan.LoanDate = loan.LoanDate; + existingLoan.DueDate = loan.DueDate; + existingLoan.ReturnDate = loan.ReturnDate; + + await _jsonData.SaveLoans(_jsonData.Loans!); + + await _jsonData.LoadData(); + } + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonPatronRepository.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonPatronRepository.cs new file mode 100644 index 0000000..efb05f8 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonPatronRepository.cs @@ -0,0 +1,73 @@ +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; + +namespace Library.Infrastructure.Data; + +public class JsonPatronRepository : IPatronRepository +{ + private readonly JsonData _jsonData; + + public JsonPatronRepository(JsonData jsonData) + { + _jsonData = jsonData; + } + + public async Task> SearchPatrons(string searchInput) + { + await _jsonData.EnsureDataLoaded(); + + List searchResults = new List(); + foreach (Patron patron in _jsonData.Patrons) + { + if (patron.Name.Contains(searchInput)) + { + searchResults.Add(patron); + } + } + searchResults.Sort((p1, p2) => String.Compare(p1.Name, p2.Name)); + + searchResults = _jsonData.GetPopulatedPatrons(searchResults); + + return searchResults; + } + + public async Task GetPatron(int id) + { + await _jsonData.EnsureDataLoaded(); + + foreach (Patron patron in _jsonData.Patrons!) + { + if (patron.Id == id) + { + Patron populated = _jsonData.GetPopulatedPatron(patron); + return populated; + } + } + return null; + } + + public async Task UpdatePatron(Patron patron) + { + await _jsonData.EnsureDataLoaded(); + var patrons = _jsonData.Patrons!; + Patron existingPatron = null; + foreach (var p in patrons) + { + if (p.Id == patron.Id) + { + existingPatron = p; + break; + } + } + if (existingPatron != null) + { + existingPatron.Name = patron.Name; + existingPatron.ImageName = patron.ImageName; + existingPatron.MembershipStart = patron.MembershipStart; + existingPatron.MembershipEnd = patron.MembershipEnd; + existingPatron.Loans = patron.Loans; + await _jsonData.SavePatrons(patrons); + await _jsonData.LoadData(); + } + } +} diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Library.Infrastructure.csproj b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Library.Infrastructure.csproj new file mode 100644 index 0000000..1a7e6eb --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Library.Infrastructure.csproj @@ -0,0 +1,17 @@ + + + + + + + + + + + + net9.0 + enable + enable + + + diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ExtendLoan.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ExtendLoan.cs new file mode 100644 index 0000000..d3e695b --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ExtendLoan.cs @@ -0,0 +1,104 @@ +using NSubstitute; +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; + +namespace Library.UnitTests.ApplicationCore.LoanServiceTests; + +public class ExtendLoanTest +{ + private readonly ILoanRepository _mockLoanRepository; + private readonly LoanService _loanService; + + public ExtendLoanTest() + { + _mockLoanRepository = Substitute.For(); + _loanService = new LoanService(_mockLoanRepository); + } + + [Fact(DisplayName = "LoanService.ExtendLoan: Extends the loan successfully")] + public async Task ExtendLoan_ExtendsLoanSuccessfully() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateCurrentLoanForPatron(patron); + var loanDueDate = loan.DueDate; + var loanId = loan.Id; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanExtensionStatus extensionStatus = await _loanService.ExtendLoan(loanId); + + // Assert + Assert.Equal(LoanExtensionStatus.Success, extensionStatus); + Assert.Equal(loanDueDate.AddDays(LoanService.ExtendByDays), loan.DueDate); + } + + [Fact(DisplayName = "LoanService.ExtendLoan: Returns LoanNotFound if loan is not found")] + public async Task ExtendLoan_ReturnsLoanNotFound() + { + // Arrange + var loanId = 1; + _mockLoanRepository.GetLoan(loanId).Returns((Loan?)null); + + // Act + LoanExtensionStatus extensionStatus = await _loanService.ExtendLoan(loanId); + + // Assert + Assert.Equal(LoanExtensionStatus.LoanNotFound, extensionStatus); + } + + [Fact(DisplayName = "LoanService.ExtendLoan: Returns MembershipExpired if patron's membership is expired")] + public async Task ExtendLoan_ReturnsMembershipExpired() + { + // Arrange + var patron = PatronFactory.CreateExpiredPatron(); + var loan = LoanFactory.CreateCurrentLoanForPatron(patron); + var loanId = loan.Id; + var loanDueDate = loan.DueDate; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanExtensionStatus extensionStatus = await _loanService.ExtendLoan(loanId); + + // Assert + Assert.Equal(LoanExtensionStatus.MembershipExpired, extensionStatus); + Assert.Equal(loanDueDate, loan.DueDate); + } + + [Fact(DisplayName = "LoanService.ExtendLoan: Returns LoanReturned if loan is already returned")] + public async Task ExtendLoan_ReturnsLoanReturned() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateReturnedLoanForPatron(patron); + var loanId = loan.Id; + var loanDueDate = loan.DueDate; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanExtensionStatus extensionStatus = await _loanService.ExtendLoan(loanId); + + // Assert + Assert.Equal(LoanExtensionStatus.LoanReturned, extensionStatus); + Assert.Equal(loanDueDate, loan.DueDate); + } + + [Fact(DisplayName = "LoanService.ExtendLoan: Returns LoanExpired if loan is already expired")] + public async Task ExtendLoan_ReturnsLoanExpired() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateExpiredLoanForPatron(patron); + var loanId = loan.Id; + var loanDueDate = loan.DueDate; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanExtensionStatus extensionStatus = await _loanService.ExtendLoan(loanId); + + // Assert + Assert.Equal(LoanExtensionStatus.LoanExpired, extensionStatus); + Assert.Equal(loanDueDate, loan.DueDate); + } +} diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ReturnLoan.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ReturnLoan.cs new file mode 100644 index 0000000..68c3a0d --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/LoanService/ReturnLoan.cs @@ -0,0 +1,99 @@ +using NSubstitute; +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; + +namespace Library.UnitTests.ApplicationCore.LoanServiceTests; + +public class ReturnLoanTest +{ + private readonly ILoanRepository _mockLoanRepository; + private readonly LoanService _loanService; + + public ReturnLoanTest() + { + _mockLoanRepository = Substitute.For(); + _loanService = new LoanService(_mockLoanRepository); + } + + [Fact(DisplayName = "LoanService.ReturnLoan: Returns LoanNotFound if loan is not found")] + public async Task ReturnLoan_ReturnsLoanNotFound() + { + // Arrange + var loanId = 1; + _mockLoanRepository.GetLoan(loanId).Returns((Loan?)null); + + // Act + LoanReturnStatus returnStatus = await _loanService.ReturnLoan(loanId); + + // Assert + Assert.Equal(LoanReturnStatus.LoanNotFound, returnStatus); + } + + [Fact(DisplayName = "LoanService.ReturnLoan: Returns AlreadyReturned if loan is already returned")] + public async Task ReturnLoan_ReturnsAlreadyReturned() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateReturnedLoanForPatron(patron); + var loanId = loan.Id; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanReturnStatus returnStatus = await _loanService.ReturnLoan(loanId); + + // Assert + Assert.Equal(LoanReturnStatus.AlreadyReturned, returnStatus); + } + + [Fact(DisplayName = "LoanService.ReturnLoan: Returns Success and updates return date for a patron with current membership")] + public async Task ReturnLoan_ReturnsSuccessAndUpdateReturnDate() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateCurrentLoanForPatron(patron); + var loanId = loan.Id; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanReturnStatus returnStatus = await _loanService.ReturnLoan(loanId); + + // Assert + Assert.Equal(LoanReturnStatus.Success, returnStatus); + Assert.NotNull(loan.ReturnDate); + } + + [Fact(DisplayName = "LoanService.ReturnLoan: Returns Success and updates return date for an expired loan")] + public async Task ReturnLoan_ReturnsSuccessAndUpdateReturnDateForExpiredLoan() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var loan = LoanFactory.CreateExpiredLoanForPatron(patron); + var loanId = loan.Id; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanReturnStatus returnStatus = await _loanService.ReturnLoan(loanId); + + // Assert + Assert.Equal(LoanReturnStatus.Success, returnStatus); + Assert.NotNull(loan.ReturnDate); + } + + [Fact(DisplayName = "LoanService.ReturnLoan: Returns Success and updates return date for a patron with expired membership")] + public async Task ReturnLoan_ReturnsSuccessAndUpdateReturnDateForExpiredPatron() + { + // Arrange + var patron = PatronFactory.CreateExpiredPatron(); + var loan = LoanFactory.CreateCurrentLoanForPatron(patron); + var loanId = loan.Id; + _mockLoanRepository.GetLoan(loanId).Returns(loan); + + // Act + LoanReturnStatus returnStatus = await _loanService.ReturnLoan(loanId); + + // Assert + Assert.Equal(LoanReturnStatus.Success, returnStatus); + Assert.NotNull(loan.ReturnDate); + } +} diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/PatronService/RenewMembership.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/PatronService/RenewMembership.cs new file mode 100644 index 0000000..ff4d24c --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/PatronService/RenewMembership.cs @@ -0,0 +1,142 @@ +using NSubstitute; +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.ApplicationCore.Enums; + +namespace Library.UnitTests.ApplicationCore.PatronServiceTests; + +public class RenewMembershipTest +{ + private readonly IPatronRepository _mockPatronRepository; + private readonly PatronService _patronService; + + public RenewMembershipTest() + { + _mockPatronRepository = Substitute.For(); + _patronService = new PatronService(_mockPatronRepository); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Renews the membership successfully without loans")] + public async Task RenewMembership_RenewsMembershipSuccessfully() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var membershipEnd = patron.MembershipEnd; + var patronId = patron.Id; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.Success, renewalStatus); + Assert.Equal(membershipEnd.AddYears(1), patron.MembershipEnd); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Renews the membership successfully with expired membership")] + public async Task RenewMembership_RenewsMembershipSuccessfullyWithExpiredMembership() + { + // Arrange + //var membershipEnd = DateTime.Now.AddMonths(-2); + var patron = PatronFactory.CreateExpiredPatron(); + var membershipEnd = patron.MembershipEnd; + var patronId = patron.Id; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.Success, renewalStatus); + Assert.Equal(membershipEnd.AddYears(1), patron.MembershipEnd); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Renews the membership successfully with returned loans")] + public async Task RenewMembership_RenewsMembershipSuccessfullyWithReturnedLoans() + { + // Arrange + var membershipEnd = DateTime.Now.AddDays(1); + var patron = PatronFactory.CreateCurrentPatron(); + patron.MembershipEnd = membershipEnd; + var patronId = patron.Id; + patron.Loans = new List { + LoanFactory.CreateReturnedLoanForPatron(patron) + }; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.Success, renewalStatus); + Assert.Equal(membershipEnd.AddYears(1), patron.MembershipEnd); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Renews the membership successfully with current loans")] + public async Task RenewMembership_RenewsMembershipSuccessfullyWithCurrentLoans() + { + // Arrange + var membershipEnd = DateTime.Now.AddDays(1); + var patron = PatronFactory.CreateCurrentPatron(); + patron.MembershipEnd = membershipEnd; + var patronId = patron.Id; + patron.Loans = new List { + LoanFactory.CreateCurrentLoanForPatron(patron) + }; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.Success, renewalStatus); + Assert.Equal(membershipEnd.AddYears(1), patron.MembershipEnd); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Returns PatronNotFound if patron is not found")] + public async Task RenewMembership_ReturnsPatronNotFound() + { + // Arrange + var patronId = 42; + _mockPatronRepository.GetPatron(patronId).Returns((Patron?)null); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.PatronNotFound, renewalStatus); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Returns TooEarlyToRenew if renewal is not allowed yet")] + public async Task RenewMembership_ReturnsTooEarlyToRenew() + { + // Arrange + var patron = PatronFactory.CreateTooEarlyToRenewPatron(); + var patronId = patron.Id; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.TooEarlyToRenew, renewalStatus); + } + + [Fact(DisplayName = "PatronService.RenewMembership: Returns LoanNotReturned if patron has overdue loans")] + public async Task RenewMembership_ReturnsLoanNotReturned() + { + // Arrange + var patron = PatronFactory.CreateCurrentPatron(); + var patronId = patron.Id; + patron.Loans = new List { + LoanFactory.CreateExpiredLoanForPatron(patron) + }; + _mockPatronRepository.GetPatron(patronId).Returns(patron); + + // Act + MembershipRenewalStatus renewalStatus = await _patronService.RenewMembership(patronId); + + // Assert + Assert.Equal(MembershipRenewalStatus.LoanNotReturned, renewalStatus); + } +} diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/tests/UnitTests/Infrastructure/JsonLoanRepository/GetLoan.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/tests/UnitTests/Infrastructure/JsonLoanRepository/GetLoan.cs new file mode 100644 index 0000000..2211e96 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/tests/UnitTests/Infrastructure/JsonLoanRepository/GetLoan.cs @@ -0,0 +1,72 @@ +using Library.ApplicationCore; +using Library.Infrastructure.Data; +using Library.ApplicationCore.Entities; +using Microsoft.Extensions.Configuration; +using NSubstitute; +using Xunit; + +namespace UnitTests.Infrastructure.JsonLoanRepositoryTests +{ + public class GetLoan + { + private readonly ILoanRepository _mockLoanRepository; + private readonly JsonLoanRepository _jsonLoanRepository; + private readonly IConfiguration _configuration; + private readonly JsonData _jsonData; + + public GetLoan() + { + _mockLoanRepository = Substitute.For(); + _configuration = new ConfigurationBuilder().Build(); + _jsonData = new JsonData(_configuration); + _jsonLoanRepository = new JsonLoanRepository(_jsonData); + } + + [Fact(DisplayName = "JsonLoanRepository.GetLoan: Returns loan when ID is found")] + public async Task GetLoan_ReturnsLoanWhenIdIsFound() + { + // Arrange + var loanId = 1; // Loan ID that exists in Loans.json + var expectedLoan = new Loan + { + Id = loanId, + BookItemId = 17, + PatronId = 22, + LoanDate = DateTime.Parse("2023-12-08T00:40:43.1808862"), + DueDate = DateTime.Parse("2023-12-22T00:40:43.1808862"), + ReturnDate = null + }; + + _mockLoanRepository.GetLoan(loanId).Returns(expectedLoan); + + // Act + var actualLoan = await _jsonLoanRepository.GetLoan(loanId); + + // Assert + Assert.NotNull(actualLoan); + Assert.Equal(expectedLoan.Id, actualLoan!.Id); + Assert.Equal(expectedLoan.BookItemId, actualLoan.BookItemId); + Assert.Equal(expectedLoan.PatronId, actualLoan.PatronId); + Assert.Equal(expectedLoan.LoanDate, actualLoan.LoanDate); + Assert.Equal(expectedLoan.DueDate, actualLoan.DueDate); + Assert.Equal(expectedLoan.ReturnDate, actualLoan.ReturnDate); + } + + [Fact(DisplayName = "JsonLoanRepository.GetLoan: Returns null when ID is not found")] + public async Task GetLoan_ReturnsNullWhenIdIsNotFound() + { + // Arrange + var loanId = 999; // Loan ID that does not exist in Loans.json + + _mockLoanRepository.GetLoan(loanId).Returns((Loan?)null); + + // Act + var actualLoan = await _jsonLoanRepository.GetLoan(loanId); + + // Assert + Assert.Null(actualLoan); + } + + // Add more test methods here + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/tests/UnitTests/LoanFactory.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/tests/UnitTests/LoanFactory.cs new file mode 100644 index 0000000..251000d --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/tests/UnitTests/LoanFactory.cs @@ -0,0 +1,42 @@ +using Library.ApplicationCore.Entities; + +public static class LoanFactory +{ + public static int loanId = 777; + + public static Loan CreateReturnedLoanForPatron(Patron patron) + { + return new Loan + { + Id = loanId++, + DueDate = DateTime.Now.AddDays(1), + ReturnDate = DateTime.Now.AddDays(-1), + PatronId = patron.Id, + Patron = patron + }; + } + + public static Loan CreateCurrentLoanForPatron(Patron patron) + { + return new Loan + { + Id = loanId++, + DueDate = DateTime.Now.AddDays(1), + ReturnDate = null, + PatronId = patron.Id, + Patron = patron + }; + } + + public static Loan CreateExpiredLoanForPatron(Patron patron) + { + return new Loan + { + Id = loanId++, + DueDate = DateTime.Now.AddDays(-1), + ReturnDate = null, + PatronId = patron.Id, + Patron = patron + }; + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/tests/UnitTests/PatronFactory.cs b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/tests/UnitTests/PatronFactory.cs new file mode 100644 index 0000000..a9c36b7 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/tests/UnitTests/PatronFactory.cs @@ -0,0 +1,39 @@ +using Library.ApplicationCore.Entities; + +public static class PatronFactory +{ + public static int patronId = 42; + + public static Patron CreateCurrentPatron() + { + return new Patron + { + Id = patronId++, + Name = "John Doe", + MembershipEnd = DateTime.Now.AddDays(1), + Loans = new List() + }; + } + + public static Patron CreateTooEarlyToRenewPatron() + { + return new Patron + { + Id = patronId++, + Name = "John Doe", + MembershipEnd = DateTime.Now.AddMonths(2), + Loans = new List() + }; + } + + public static Patron CreateExpiredPatron() + { + return new Patron + { + Id = patronId++, + Name = "John Doe", + MembershipEnd = DateTime.Now.AddMonths(-2), + Loans = new List() + }; + } +} \ No newline at end of file diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/tests/UnitTests/UnitTests.csproj b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/tests/UnitTests/UnitTests.csproj new file mode 100644 index 0000000..eb46c31 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/AccelerateDevGHCopilot/tests/UnitTests/UnitTests.csproj @@ -0,0 +1,39 @@ + + + + net9.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + + + + + Json\%(RecursiveDir)%(FileName)%(Extension) + PreserveNewest + + + + diff --git a/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/readme.txt b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/readme.txt new file mode 100644 index 0000000..b3a4252 --- /dev/null +++ b/DownloadableCodeProjects/az-2007-m5-refactor-improve-code/readme.txt @@ -0,0 +1 @@ +placeholder \ No newline at end of file diff --git a/DownloadableCodeProjects/readme.txt b/DownloadableCodeProjects/readme.txt new file mode 100644 index 0000000..b3a4252 --- /dev/null +++ b/DownloadableCodeProjects/readme.txt @@ -0,0 +1 @@ +placeholder \ No newline at end of file diff --git a/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Configuration/AppConfig.cs b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Configuration/AppConfig.cs new file mode 100644 index 0000000..ffa6ce7 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Configuration/AppConfig.cs @@ -0,0 +1,19 @@ +// Configuration.cs - Application configuration settings +namespace EcommerceApp.Configuration; + +public static class AppConfig +{ + // Shipping configuration + public static decimal BaseShippingRate { get; } = 5.99m; + public static decimal WeightBasedRatePerPound { get; } = 1.25m; + public static decimal FreeShippingThreshold { get; } = 50.00m; + public static decimal ReturnProcessingFee { get; } = 2.99m; + + // Security configuration + public static int MaxIdLength { get; } = 20; + public static int MinIdLength { get; } = 6; + + // Business rules + public static int MaxReturnDays { get; } = 30; + public static decimal MaxRefundAmount { get; } = 1000.00m; +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/ECommerceOrderAndReturn.csproj b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/ECommerceOrderAndReturn.csproj new file mode 100644 index 0000000..fd4bd08 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/ECommerceOrderAndReturn.csproj @@ -0,0 +1,10 @@ + + + + Exe + net9.0 + enable + enable + + + diff --git a/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/EXPECTED_OUTPUT.md b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/EXPECTED_OUTPUT.md new file mode 100644 index 0000000..a2f7bbe --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/EXPECTED_OUTPUT.md @@ -0,0 +1,177 @@ +// This file documents the expected behavior that should be maintained after consolidating duplicate code + +/* +EXPECTED OUTPUT WHEN RUNNING THE APPLICATION: + +=== E-Commerce Order and Return Processing System === +Starting application tests... + +INITIAL INVENTORY STATUS: +[INVENTORY] Current Stock Levels: + PROD001: 50 units + PROD002: 100 units + PROD003: 25 units + PROD004: 75 units + +TEST 1: Processing a valid order +=== Starting Order Processing === +Processing order: ORD12345 +[VALIDATION] Validating order ID... +Security check: Validating Order ID: ORD***** +Security check passed: Order ID is valid +[AUDIT] Logging order activity: PROCESSING_STARTED +[AUDIT] Audit entry validation passed +[AUDIT] Storing: 2025-08-15 22:03:17 | ORDER | ORD12345 | PROCESSING_STARTED +[AUDIT] Details: Order validation passed +[COMPLIANCE] Checking compliance requirements... +[COMPLIANCE] Audit entry is compliant. +[SHIPPING] Calculating shipping cost... +Order ORD12345 shipping cost: $4.00 +Order ORD12345 status updated to: Processing +Processing payment for order ORD12345, amount: $75.50 +[AUDIT] Logging order activity: PAYMENT_STARTED +[AUDIT] Audit entry validation passed +[AUDIT] Storing: 2025-08-15 22:03:17 | ORDER | ORD12345 | PAYMENT_STARTED +[AUDIT] Details: Amount: $75.50 +[COMPLIANCE] Checking compliance requirements... +[COMPLIANCE] Audit entry is compliant. +Payment processed successfully +[AUDIT] Logging order activity: PAYMENT_COMPLETED +[AUDIT] Audit entry validation passed +[AUDIT] Storing: 2025-08-15 22:03:17 | ORDER | ORD12345 | PAYMENT_COMPLETED +[AUDIT] Details: Payment successful +[COMPLIANCE] Checking compliance requirements... +[COMPLIANCE] Audit entry is compliant. +[INVENTORY] Processing inventory reservation for order ORD12345 +[INVENTORY] Validating inventory availability... +[INVENTORY] Updated PROD001: 50 → 49 (Change: -1, Reason: RESERVED) +[INVENTORY] Logging inventory transaction... +[INVENTORY] Action: RESERVE, Transaction ID: ORD12345, Product ID: PROD001, Quantity: 1, Details: Order processing, Transaction Type: Restoration +[INVENTORY] Validating inventory availability... +[INVENTORY] Updated PROD002: 100 → 98 (Change: -2, Reason: RESERVED) +[INVENTORY] Logging inventory transaction... +[INVENTORY] Action: RESERVE, Transaction ID: ORD12345, Product ID: PROD002, Quantity: 2, Details: Order processing, Transaction Type: Restoration +[EMAIL] Preparing order confirmation email for CUST001 +[EMAIL] Sending to customer CUST001: [E-Commerce] Order Confirmation - Transaction ID: ORD12345 +[EMAIL] Content preview: Dear Customer CUST001, + +Thank you for your purchas... +[EMAIL] Successfully sent! +[AUDIT] [2025-08-15 15:03:17] EMAIL_SENT | Type: OrderConfirmation | Customer: CUST001 | Transaction: ORD12345 +[AUDIT] Logging order activity: PROCESSING_COMPLETED +[AUDIT] Audit entry validation passed +[AUDIT] Storing: 2025-08-15 22:03:17 | ORDER | ORD12345 | PROCESSING_COMPLETED +[AUDIT] Details: Total amount: $75.50, Shipping: $4.00 +[COMPLIANCE] Checking compliance requirements... +[COMPLIANCE] Audit entry is compliant. +Order ORD12345 processed successfully! +=== Order Processing Complete === + +TEST 2: Processing a valid return +=== Starting Return Processing === +Processing return: RET98765 +[VALIDATION] Validating return ID... +Security check: Validating Return ID: RET***** +Security check passed: Return ID is valid +[AUDIT] Logging return activity: PROCESSING_STARTED +[AUDIT] Audit entry validation passed +[AUDIT] Storing: 2025-08-15 22:03:17 | RETURN | RET98765 | PROCESSING_STARTED +[AUDIT] Details: Return validation passed +[COMPLIANCE] Checking compliance requirements... +[COMPLIANCE] Audit entry is compliant. +Validating return eligibility for RET98765 +Return eligibility validated successfully +[SHIPPING] Calculating return shipping cost... +Return RET98765 shipping cost: $3.00 +Return RET98765 status updated to: Approved +Processing refund for return RET98765, amount: $699.99 +[AUDIT] Logging return activity: REFUND_STARTED +[AUDIT] Audit entry validation passed +[AUDIT] Storing: 2025-08-15 22:03:17 | RETURN | RET98765 | REFUND_STARTED +[AUDIT] Details: Amount: $699.99 +[COMPLIANCE] Checking compliance requirements... +[COMPLIANCE] Audit entry is compliant. +Refund processed successfully +[AUDIT] Logging return activity: REFUND_COMPLETED +[AUDIT] Audit entry validation passed +[AUDIT] Storing: 2025-08-15 22:03:18 | RETURN | RET98765 | REFUND_COMPLETED +[AUDIT] Details: Refund successful +[COMPLIANCE] Checking compliance requirements... +[COMPLIANCE] Audit entry is compliant. +[INVENTORY] Processing inventory restoration for return RET98765 +[INVENTORY] Validating inventory availability... +[INVENTORY] Updated PROD001: 49 → 50 (Change: +1, Reason: RESTORED) +[INVENTORY] Logging inventory transaction... +[INVENTORY] Action: RESTORE, Transaction ID: RET98765, Product ID: PROD001, Quantity: 1, Details: Return processing, Transaction Type: Restoration +[EMAIL] Preparing return confirmation email for CUST001 +[EMAIL] Sending to customer CUST001: [E-Commerce] Return Confirmation - Transaction ID: RET98765 +[EMAIL] Content preview: Dear Customer CUST001, + +We appreciate your return!... +[EMAIL] Successfully sent! +[AUDIT] [2025-08-15 15:03:18] EMAIL_SENT | Type: ReturnConfirmation | Customer: CUST001 | Transaction: RET98765 +[AUDIT] Logging return activity: PROCESSING_COMPLETED +[AUDIT] Audit entry validation passed +[AUDIT] Storing: 2025-08-15 22:03:18 | RETURN | RET98765 | PROCESSING_COMPLETED +[AUDIT] Details: Refund amount: $699.99, Shipping: $3.00 +[COMPLIANCE] Checking compliance requirements... +[COMPLIANCE] Audit entry is compliant. +Return RET98765 processed successfully! +=== Return Processing Complete === + +INVENTORY STATUS AFTER PROCESSING: +[INVENTORY] Current Stock Levels: + PROD001: 50 units + PROD002: 98 units + PROD003: 25 units + PROD004: 75 units + +TEST 3: Processing an invalid order (security test) +=== Starting Order Processing === +Processing order: INVALID123 +[VALIDATION] Validating order ID... +Security check: Validating Order ID: INV***** +Security check failed: Order ID format is invalid +[VALIDATION] Order ID failed security validation. +Order INVALID123 is invalid and cannot be processed. +Exception thrown: 'System.ArgumentException' in ECommerceOrderAndReturn.dll +Error processing order INVALID123: Invalid order ID: INVALID123 +=== Order Processing Complete === + +TEST 4: Processing an invalid return (security test) +=== Starting Return Processing === +Processing return: +[VALIDATION] Validating return ID... +Security check: Validating Return ID: alert('xss') is invalid and cannot be processed. +Exception thrown: 'System.ArgumentException' in ECommerceOrderAndReturn.dll +Error processing return : Invalid return ID: +=== Return Processing Complete === + +TEST 5: Processing with empty ID +=== Starting Order Processing === +Processing order: +[VALIDATION] Validating order ID... +[VALIDATION] Order ID cannot be empty. +Order is invalid and cannot be processed. +Exception thrown: 'System.ArgumentException' in ECommerceOrderAndReturn.dll +Error processing order : Invalid order ID: +=== Order Processing Complete === + + +=== All tests completed === +Application is ready for refactoring exercise! + +Duplicate code locations found: +1. Validate() method in both OrderProcessor and ReturnProcessor +2. CalculateShipping() method in both OrderProcessor and ReturnProcessor +3. Email template and sending logic in EmailService (SendOrderConfirmation & SendReturnConfirmation) +4. Audit entry creation and logging in AuditService (LogOrderActivity & LogReturnActivity) +5. Inventory validation and update logic in InventoryService (ReserveOrderInventory & RestoreReturnInventory) + +These methods demonstrate common real-world duplicate code patterns and should be consolidated during the lab exercise. +Students can use GitHub Copilot to help identify and extract common functionality into shared services or base classes. + +*/ diff --git a/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Models/Order.cs b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Models/Order.cs new file mode 100644 index 0000000..1e8801e --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Models/Order.cs @@ -0,0 +1,36 @@ +// Order.cs - Represents an e-commerce order +using System; +using System.Collections.Generic; + +namespace EcommerceApp.Models; + +public class Order +{ + public string OrderId { get; set; } = string.Empty; + public string CustomerId { get; set; } = string.Empty; + public DateTime OrderDate { get; set; } + public List Items { get; set; } = new List(); + public decimal TotalAmount { get; set; } + public string ShippingAddress { get; set; } = string.Empty; + public OrderStatus Status { get; set; } + public double TotalWeight { get; set; } + public bool ContainsFragileItems { get; set; } +} + +public class OrderItem +{ + public string ProductId { get; set; } = string.Empty; + public string ProductName { get; set; } = string.Empty; + public int Quantity { get; set; } + public decimal UnitPrice { get; set; } + public decimal Weight { get; set; } // in pounds, for shipping calculation +} + +public enum OrderStatus +{ + Pending, + Processing, + Shipped, + Delivered, + Cancelled +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Models/Return.cs b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Models/Return.cs new file mode 100644 index 0000000..babc55a --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Models/Return.cs @@ -0,0 +1,31 @@ +// Return.cs - Represents a product return +using System; + +namespace EcommerceApp.Models; + +public class Return +{ + public string ReturnId { get; set; } = string.Empty; + public string OriginalOrderId { get; set; } = string.Empty; + public string CustomerId { get; set; } = string.Empty; + public DateTime ReturnDate { get; set; } + public string ProductId { get; set; } = string.Empty; + public string ProductName { get; set; } = string.Empty; + public int Quantity { get; set; } + public decimal RefundAmount { get; set; } + public string Reason { get; set; } = string.Empty; + public ReturnStatus Status { get; set; } + public decimal Weight { get; set; } // in pounds, for return shipping calculation + public double TotalWeight { get; set; } + public decimal TotalAmount { get; set; } + public bool IsOversized { get; set; } +} + +public enum ReturnStatus +{ + Pending, + Approved, + Rejected, + Processing, + Completed +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/OrderProcessor.cs b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/OrderProcessor.cs new file mode 100644 index 0000000..b0da7c2 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/OrderProcessor.cs @@ -0,0 +1,164 @@ +// OrderProcessor.cs – Processes customer orders. +using System; +using EcommerceApp.Models; +using EcommerceApp.Security; +using EcommerceApp.Configuration; +using EcommerceApp.Services; + +namespace EcommerceApp; + +public class OrderProcessor +{ + public void ProcessOrder(string orderId) + { + try + { + Console.WriteLine($"=== Starting Order Processing ==="); + Console.WriteLine($"Processing order: {orderId}"); + + // Before proceeding, ensure the order ID is valid. + if (Validate(orderId)) + { + // Get order details (simulated) + var order = GetOrderDetails(orderId); + + // Audit the start of order processing + AuditService.LogOrderActivity(order, "PROCESSING_STARTED", "Order validation passed"); + + // Calculate shipping costs based on order details + decimal shippingCost = CalculateShipping(orderId, order); + Console.WriteLine($"Order {orderId} shipping cost: ${shippingCost:F2}"); + + // Update order status + order.Status = OrderStatus.Processing; + Console.WriteLine($"Order {orderId} status updated to: {order.Status}"); + + // Additional order processing logic + ProcessPayment(order); + + // Handle inventory reservation + InventoryService.ReserveOrderInventory(order); + + // Send confirmation email + EmailService.SendOrderConfirmation(order); + + // Final audit log + AuditService.LogOrderActivity(order, "PROCESSING_COMPLETED", $"Total amount: ${order.TotalAmount:F2}, Shipping: ${shippingCost:F2}"); + + Console.WriteLine($"Order {orderId} processed successfully!"); + } + else + { + Console.WriteLine($"Order {orderId} is invalid and cannot be processed."); + throw new ArgumentException($"Invalid order ID: {orderId}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error processing order {orderId}: {ex.Message}"); + // In a real application, this would be logged to a proper logging system + } + finally + { + Console.WriteLine($"=== Order Processing Complete ===\n"); + } + } + + // Validation logic + private bool Validate(string orderId) + { + Console.WriteLine("[VALIDATION] Validating order ID..."); + + // Check for null or whitespace + if (string.IsNullOrWhiteSpace(orderId)) + { + Console.WriteLine("[VALIDATION] Order ID cannot be empty."); + return false; + } + + // Security validation + if (!SecurityValidator.IsValidId(orderId, "Order")) + { + Console.WriteLine("[VALIDATION] Order ID failed security validation."); + return false; + } + + // Length validation (evolved logic) + if (orderId.Length < AppConfig.MinIdLength || orderId.Length > AppConfig.MaxIdLength) + { + Console.WriteLine("[VALIDATION] Order ID length is invalid."); + return false; + } + + // Additional prefix validation (evolutionary change) + if (!orderId.StartsWith("ORD")) + { + Console.WriteLine("[VALIDATION] Order ID must start with 'ORD'."); + return false; + } + + return true; + } + + private decimal CalculateShipping(string orderId, Order order) + { + Console.WriteLine("[SHIPPING] Calculating shipping cost..."); + + // Base shipping cost + decimal shippingCost = 5.00m; + + // Weight-based adjustment (evolved logic) + if (order.TotalWeight > 10) + { + shippingCost += 2.00m; + } + + // Value-based adjustment (evolved logic) + if (order.TotalAmount > 50) + { + shippingCost -= 1.00m; // Discount for high-value orders + } + + // Additional handling fee for fragile items (evolutionary change) + if (order.ContainsFragileItems) + { + shippingCost += 3.00m; + } + + return shippingCost; + } + + private Order GetOrderDetails(string orderId) + { + // Simulate retrieving order from database + return new Order + { + OrderId = orderId, + CustomerId = "CUST001", + OrderDate = DateTime.Now.AddDays(-1), + TotalAmount = 75.50m, + ShippingAddress = "123 Main St, Anytown, ST 12345", + Status = OrderStatus.Pending, + Items = new List + { + new OrderItem { ProductId = "PROD001", ProductName = "Laptop", Quantity = 1, UnitPrice = 699.99m, Weight = 4.5m }, + new OrderItem { ProductId = "PROD002", ProductName = "Mouse", Quantity = 2, UnitPrice = 25.99m, Weight = 0.2m } + } + }; + } + + private void ProcessPayment(Order order) + { + Console.WriteLine($"Processing payment for order {order.OrderId}, amount: ${order.TotalAmount:F2}"); + + // Audit payment processing start + AuditService.LogOrderActivity(order, "PAYMENT_STARTED", $"Amount: ${order.TotalAmount:F2}"); + + // Simulate payment processing + System.Threading.Thread.Sleep(100); // Simulate processing time + Console.WriteLine("Payment processed successfully"); + + // Audit payment completion + AuditService.LogOrderActivity(order, "PAYMENT_COMPLETED", "Payment successful"); + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Program.cs b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Program.cs new file mode 100644 index 0000000..577cd2a --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Program.cs @@ -0,0 +1,77 @@ +using System; +using EcommerceApp.Services; + +namespace EcommerceApp; + +class Program +{ + static void Main(string[] args) + { + Console.WriteLine("=== E-Commerce Order and Return Processing System ==="); + Console.WriteLine("Starting application tests...\n"); + + // Show initial inventory + Console.WriteLine("INITIAL INVENTORY STATUS:"); + InventoryService.DisplayCurrentInventory(); + Console.WriteLine(); + + // Test successful order processing + Console.WriteLine("TEST 1: Processing a valid order"); + var orderProcessor = new OrderProcessor(); + orderProcessor.ProcessOrder("ORD12345"); // simulate processing a valid order + + // Test successful return processing + Console.WriteLine("TEST 2: Processing a valid return"); + var returnProcessor = new ReturnProcessor(); + returnProcessor.ProcessReturn("RET98765"); // simulate processing a valid return + + // Show inventory after processing + Console.WriteLine("INVENTORY STATUS AFTER PROCESSING:"); + InventoryService.DisplayCurrentInventory(); + Console.WriteLine(); + + // Test invalid order processing (for security validation) + Console.WriteLine("TEST 3: Processing an invalid order (security test)"); + try + { + orderProcessor.ProcessOrder("INVALID123"); // This should fail validation + } + catch (Exception ex) + { + Console.WriteLine($"Expected error caught: {ex.Message}"); + } + + // Test invalid return processing (for security validation) + Console.WriteLine("TEST 4: Processing an invalid return (security test)"); + try + { + returnProcessor.ProcessReturn(""); // This should fail security validation + } + catch (Exception ex) + { + Console.WriteLine($"Expected error caught: {ex.Message}"); + } + + // Test empty ID validation + Console.WriteLine("TEST 5: Processing with empty ID"); + try + { + orderProcessor.ProcessOrder(""); // This should fail validation + } + catch (Exception ex) + { + Console.WriteLine($"Expected error caught: {ex.Message}"); + } + + Console.WriteLine("\n=== All tests completed ==="); + Console.WriteLine("Application is ready for refactoring exercise!"); + Console.WriteLine("\nDuplicate code locations found:"); + Console.WriteLine("1. Validate() method in both OrderProcessor and ReturnProcessor"); + Console.WriteLine("2. CalculateShipping() method in both OrderProcessor and ReturnProcessor"); + Console.WriteLine("3. Email template and sending logic in EmailService (SendOrderConfirmation & SendReturnConfirmation)"); + Console.WriteLine("4. Audit entry creation and logging in AuditService (LogOrderActivity & LogReturnActivity)"); + Console.WriteLine("5. Inventory validation and update logic in InventoryService (ReserveOrderInventory & RestoreReturnInventory)"); + Console.WriteLine("\nThese methods demonstrate common real-world duplicate code patterns and should be consolidated during the lab exercise."); + Console.WriteLine("Students can use GitHub Copilot to help identify and extract common functionality into shared services or base classes."); + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/README.md b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/README.md new file mode 100644 index 0000000..bb6ed59 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/README.md @@ -0,0 +1,84 @@ +# E-Commerce Order and Return Processing System + +## Overview + +This is a sample e-commerce application designed for a GitHub Copilot training course focused on consolidating duplicate code. The application demonstrates real-world scenarios while containing intentional code duplication that students will learn to refactor. + +## Purpose + +- **Training Focus**: Learn to identify and consolidate duplicate code using GitHub Copilot +- **Real-world Context**: Represents actual e-commerce order and return processing logic +- **Security Awareness**: Includes basic security validation patterns +- **Testing**: Provides verifiable output before and after refactoring + +## Project Structure + +```text +├── Models/ +│ ├── Order.cs # Order data model with items and status +│ └── Return.cs # Return data model with refund details +├── Security/ +│ └── SecurityValidator.cs # Security validation for IDs and input +├── Configuration/ +│ └── AppConfig.cs # Application configuration settings +├── Services/ +│ ├── EmailService.cs # Email notifications (contains duplicate logic) +│ ├── InventoryService.cs # Inventory management (contains duplicate logic) +│ └── AuditService.cs # Audit logging (contains duplicate logic) +├── OrderProcessor.cs # Processes customer orders (contains duplicate code) +├── ReturnProcessor.cs # Processes product returns (contains duplicate code) +└── Program.cs # Main application with test scenarios +``` + +## Features + +- **Order Processing**: Complete order workflow with payment and inventory +- **Return Processing**: Return eligibility validation and refund processing +- **Security Validation**: Input validation with XSS/injection prevention +- **Error Handling**: Proper exception handling and logging +- **Configuration**: Centralized configuration for business rules +- **Testing**: Multiple test scenarios with expected outputs + +## Running the Application + +```bash +dotnet run +``` + +## Expected Output + +The application runs 5 test scenarios: + +1. Valid order processing +2. Valid return processing +3. Invalid order ID (should fail) +4. Malicious input (should fail security check) +5. Empty ID (should fail validation) + +## Lab Exercise Goals + +Students will use GitHub Copilot to: + +1. Identify the duplicate validation and shipping calculation logic +2. Extract common functionality into shared services +3. Refactor both processors to use the shared services +4. Verify the application still produces the same output after refactoring + +## Security Features + +- Input validation with pattern matching +- XSS/SQL injection prevention +- Data masking for logging +- Length validation to prevent buffer overflows +- Suspicious pattern detection + +## Business Rules + +- Order IDs must follow pattern: `^[A-Z]{3}\d{5}$` (e.g., ORD12345) +- Return IDs must follow pattern: `^[A-Z]{3}\d{5}$` (e.g., RET98765) +- Free shipping on orders over $50 +- Returns allowed within 30 days +- Maximum refund amount: $1,000 +- Weight-based shipping calculation available + +This application provides a realistic foundation for learning code consolidation techniques while maintaining security best practices and business logic integrity. diff --git a/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/ReturnProcessor.cs b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/ReturnProcessor.cs new file mode 100644 index 0000000..5a69089 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/ReturnProcessor.cs @@ -0,0 +1,197 @@ +// ReturnProcessor.cs – Processes product returns. +using System; +using EcommerceApp.Models; +using EcommerceApp.Security; +using EcommerceApp.Configuration; +using EcommerceApp.Services; + +namespace EcommerceApp; + +public class ReturnProcessor +{ + public void ProcessReturn(string returnId) + { + try + { + Console.WriteLine($"=== Starting Return Processing ==="); + Console.WriteLine($"Processing return: {returnId}"); + + // Ensure the return ID is valid before continuing. + if (Validate(returnId)) + { + // Get return details (simulated) + var returnRequest = GetReturnDetails(returnId); + + // Audit the start of return processing + AuditService.LogReturnActivity(returnRequest, "PROCESSING_STARTED", "Return validation passed"); + + // Validate return eligibility + if (ValidateReturnEligibility(returnRequest)) + { + // Calculate return shipping costs + decimal shippingCost = CalculateShipping(returnId, returnRequest); + Console.WriteLine($"Return {returnId} shipping cost: ${shippingCost:F2}"); + + // Update return status + returnRequest.Status = ReturnStatus.Approved; + Console.WriteLine($"Return {returnId} status updated to: {returnRequest.Status}"); + + // Additional return processing logic + ProcessRefund(returnRequest); + + // Handle inventory restoration + InventoryService.RestoreReturnInventory(returnRequest); + + // Send confirmation email + EmailService.SendReturnConfirmation(returnRequest); + + // Final audit log + AuditService.LogReturnActivity(returnRequest, "PROCESSING_COMPLETED", $"Refund amount: ${returnRequest.RefundAmount:F2}, Shipping: ${shippingCost:F2}"); + + Console.WriteLine($"Return {returnId} processed successfully!"); + } + else + { + Console.WriteLine($"Return {returnId} is not eligible for processing."); + AuditService.LogReturnActivity(returnRequest, "REJECTED", "Failed eligibility validation"); + } + } + else + { + Console.WriteLine($"Return {returnId} is invalid and cannot be processed."); + throw new ArgumentException($"Invalid return ID: {returnId}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error processing return {returnId}: {ex.Message}"); + // In a real application, this would be logged to a proper logging system + } + finally + { + Console.WriteLine($"=== Return Processing Complete ===\n"); + } + } + + // Validation logic + private bool Validate(string returnId) + { + Console.WriteLine("[VALIDATION] Validating return ID..."); + + // Check for null or whitespace + if (string.IsNullOrWhiteSpace(returnId)) + { + Console.WriteLine("[VALIDATION] Return ID cannot be empty."); + return false; + } + + // Security validation + if (!SecurityValidator.IsValidId(returnId, "Return")) + { + Console.WriteLine("[VALIDATION] Return ID failed security validation."); + return false; + } + + // Length validation (evolved logic) + if (returnId.Length < AppConfig.MinIdLength || returnId.Length > AppConfig.MaxIdLength) + { + Console.WriteLine("[VALIDATION] Return ID length is invalid."); + return false; + } + + // Additional prefix validation (evolutionary change) + if (!returnId.StartsWith("RET")) + { + Console.WriteLine("[VALIDATION] Return ID must start with 'RET'."); + return false; + } + + return true; + } + + private decimal CalculateShipping(string returnId, Return returnRequest) + { + Console.WriteLine("[SHIPPING] Calculating return shipping cost..."); + + // Base shipping cost + decimal shippingCost = 3.00m; + + // Weight-based adjustment (evolved logic) + if (returnRequest.TotalWeight > 5) + { + shippingCost += 1.50m; + } + + // Value-based adjustment (evolved logic) + if (returnRequest.TotalAmount > 30) + { + shippingCost -= 0.50m; // Discount for high-value returns + } + + // Additional handling fee for oversized items (evolutionary change) + if (returnRequest.IsOversized) + { + shippingCost += 4.00m; + } + + return shippingCost; + } + + private Return GetReturnDetails(string returnId) + { + // Simulate retrieving return request from database + return new Return + { + ReturnId = returnId, + OriginalOrderId = "ORD12345", + CustomerId = "CUST001", + ReturnDate = DateTime.Now, + ProductId = "PROD001", + ProductName = "Laptop", + Quantity = 1, + RefundAmount = 699.99m, + Reason = "Product defective", + Status = ReturnStatus.Pending, + Weight = 4.5m + }; + } + + private bool ValidateReturnEligibility(Return returnRequest) + { + Console.WriteLine($"Validating return eligibility for {returnRequest.ReturnId}"); + + // Check if return is within allowed timeframe + var daysSinceOrder = (returnRequest.ReturnDate - returnRequest.ReturnDate.AddDays(-AppConfig.MaxReturnDays)).TotalDays; + if (daysSinceOrder > AppConfig.MaxReturnDays) + { + Console.WriteLine($"Return rejected: Exceeds {AppConfig.MaxReturnDays} day return policy"); + return false; + } + + // Check refund amount limits + if (returnRequest.RefundAmount > AppConfig.MaxRefundAmount) + { + Console.WriteLine($"Return rejected: Refund amount exceeds maximum of ${AppConfig.MaxRefundAmount:F2}"); + return false; + } + + // Additional business rules could be added here + Console.WriteLine("Return eligibility validated successfully"); + return true; + } + + private void ProcessRefund(Return returnRequest) + { + Console.WriteLine($"Processing refund for return {returnRequest.ReturnId}, amount: ${returnRequest.RefundAmount:F2}"); + + // Audit refund processing start + AuditService.LogReturnActivity(returnRequest, "REFUND_STARTED", $"Amount: ${returnRequest.RefundAmount:F2}"); + + // Simulate refund processing + System.Threading.Thread.Sleep(100); // Simulate processing time + Console.WriteLine("Refund processed successfully"); + + // Audit refund completion + AuditService.LogReturnActivity(returnRequest, "REFUND_COMPLETED", "Refund successful"); + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Security/SecurityValidator.cs b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Security/SecurityValidator.cs new file mode 100644 index 0000000..10315a9 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Security/SecurityValidator.cs @@ -0,0 +1,58 @@ +// SecurityValidator.cs - Handles security validation for orders and returns +using System; +using System.Text.RegularExpressions; + +namespace EcommerceApp.Security; + +public class SecurityValidator +{ + private static readonly Regex IdPattern = new Regex(@"^[A-Z]{3}\d{5}$", RegexOptions.Compiled); + private static readonly string[] SuspiciousPatterns = { " 20) + { + Console.WriteLine($"Security check failed: {idType} ID exceeds maximum length"); + return false; + } + + // Pattern validation (e.g., ORD12345 or RET98765) + if (!IdPattern.IsMatch(id)) + { + Console.WriteLine($"Security check failed: {idType} ID format is invalid"); + return false; + } + + // Check for suspicious patterns (basic XSS/SQL injection prevention) + foreach (string pattern in SuspiciousPatterns) + { + if (id.ToLowerInvariant().Contains(pattern)) + { + Console.WriteLine($"Security check failed: {idType} ID contains suspicious pattern"); + return false; + } + } + + Console.WriteLine($"Security check passed: {idType} ID is valid"); + return true; + } + + private static string MaskSensitiveData(string data) + { + if (string.IsNullOrEmpty(data) || data.Length <= 4) + return "****"; + + return data.Substring(0, 3) + "*****"; + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Services/AuditService.cs b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Services/AuditService.cs new file mode 100644 index 0000000..51144cc --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Services/AuditService.cs @@ -0,0 +1,132 @@ +// Services/AuditService.cs - Audit and logging service (contains duplicate logic) +using System; +using EcommerceApp.Models; + +namespace EcommerceApp.Services; + +public class AuditService +{ + // Very common duplication pattern - audit logging appears everywhere in e-commerce systems + + public static void LogOrderActivity(Order order, string action, string details = "") + { + Console.WriteLine($"[AUDIT] Logging order activity: {action}"); + + // Duplicate audit entry creation logic - will also appear in return logging + var auditEntry = CreateAuditEntry("ORDER", order.OrderId, order.CustomerId, action, details); + + // Duplicate validation logic + if (ValidateAuditEntry(auditEntry)) + { + // Duplicate storage logic + StoreAuditEntry(auditEntry); + + // Duplicate compliance checking + CheckComplianceRequirements(auditEntry); + } + } + + public static void LogReturnActivity(Return returnRequest, string action, string details = "") + { + Console.WriteLine($"[AUDIT] Logging return activity: {action}"); + + // Duplicate audit entry creation logic - same pattern as order logging + var auditEntry = CreateAuditEntry("RETURN", returnRequest.ReturnId, returnRequest.CustomerId, action, details); + + // Duplicate validation logic + if (ValidateAuditEntry(auditEntry)) + { + // Duplicate storage logic + StoreAuditEntry(auditEntry); + + // Duplicate compliance checking + CheckComplianceRequirements(auditEntry); + } + } + + // Duplicate Helper Methods Start - These are duplicated across order and return processing + private static AuditEntry CreateAuditEntry(string transactionType, string transactionId, string customerId, string action, string details) + { + // Evolutionary change: Add a new field for user role + return new AuditEntry + { + Id = Guid.NewGuid().ToString(), + Timestamp = DateTime.UtcNow, + TransactionType = transactionType, + TransactionId = transactionId, + CustomerId = customerId, + Action = action, + Details = details, + UserAgent = "ECommerceApp/1.0", + IpAddress = "127.0.0.1", // Would be actual IP in real app + UserRole = "Customer" // New field added + }; + } + + private static bool ValidateAuditEntry(AuditEntry entry) + { + if (string.IsNullOrWhiteSpace(entry.TransactionId)) + { + Console.WriteLine("[AUDIT] Validation failed: Transaction ID is required"); + return false; + } + + if (string.IsNullOrWhiteSpace(entry.CustomerId)) + { + Console.WriteLine("[AUDIT] Validation failed: Customer ID is required"); + return false; + } + + if (string.IsNullOrWhiteSpace(entry.Action)) + { + Console.WriteLine("[AUDIT] Validation failed: Action is required"); + return false; + } + + Console.WriteLine("[AUDIT] Audit entry validation passed"); + return true; + } + + private static void StoreAuditEntry(AuditEntry entry) + { + // Simulate storing to audit database/file + Console.WriteLine($"[AUDIT] Storing: {entry.Timestamp:yyyy-MM-dd HH:mm:ss} | {entry.TransactionType} | {entry.TransactionId} | {entry.Action}"); + + if (!string.IsNullOrWhiteSpace(entry.Details)) + { + Console.WriteLine($"[AUDIT] Details: {entry.Details}"); + } + + // In real application, this would write to secure audit storage + } + + private static void CheckComplianceRequirements(AuditEntry auditEntry) + { + Console.WriteLine("[COMPLIANCE] Checking compliance requirements..."); + + // Evolutionary change: Add a new compliance check for user role + if (auditEntry.UserRole != "Customer") + { + Console.WriteLine("[COMPLIANCE] Non-customer roles require additional checks."); + } + + // Existing compliance logic + Console.WriteLine("[COMPLIANCE] Audit entry is compliant."); + } + // Duplicate Helper Methods End +} + +// Supporting class for audit entries +public class AuditEntry +{ + public string Id { get; set; } = string.Empty; + public DateTime Timestamp { get; set; } + public string TransactionType { get; set; } = string.Empty; + public string TransactionId { get; set; } = string.Empty; + public string CustomerId { get; set; } = string.Empty; + public string Action { get; set; } = string.Empty; + public string Details { get; set; } = string.Empty; + public string UserAgent { get; set; } = string.Empty; + public string IpAddress { get; set; } = string.Empty; + public string UserRole { get; set; } = string.Empty; // New field for user role +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Services/EmailService.cs b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Services/EmailService.cs new file mode 100644 index 0000000..90cf0ea --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Services/EmailService.cs @@ -0,0 +1,88 @@ +// Services/EmailService.cs - Email notification service (contains duplicate logic) +using System; +using EcommerceApp.Models; +using EcommerceApp.Configuration; + +namespace EcommerceApp.Services; + +public class EmailService +{ + // This class would normally handle email sending, but for the lab we'll simulate it + // The duplicate patterns here are very common in real e-commerce applications + + public static void SendOrderConfirmation(Order order) + { + Console.WriteLine($"[EMAIL] Preparing order confirmation email for {order.CustomerId}"); + + // Duplicate email template logic - will also appear in return notifications + var emailContent = BuildEmailTemplate("order", order.OrderId, order.CustomerId); + var subject = FormatEmailSubject("Order Confirmation", order.OrderId); + + // Duplicate sending logic + SendEmail(order.CustomerId, subject, emailContent); + LogEmailActivity("OrderConfirmation", order.CustomerId, order.OrderId); + } + + public static void SendReturnConfirmation(Return returnRequest) + { + Console.WriteLine($"[EMAIL] Preparing return confirmation email for {returnRequest.CustomerId}"); + + // Duplicate email template logic - same pattern as order emails + var emailContent = BuildEmailTemplate("return", returnRequest.ReturnId, returnRequest.CustomerId); + var subject = FormatEmailSubject("Return Confirmation", returnRequest.ReturnId); + + // Duplicate sending logic + SendEmail(returnRequest.CustomerId, subject, emailContent); + LogEmailActivity("ReturnConfirmation", returnRequest.CustomerId, returnRequest.ReturnId); + } + + // Duplicate Helper Methods Start - These appear in both order and return processing + private static string BuildEmailTemplate(string type, string transactionId, string customerId) + { + var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + + // Evolutionary change: Add a personalized greeting + var greeting = type == "order" ? "Thank you for your purchase!" : "We appreciate your return!"; + + return $""" + Dear Customer {customerId}, + + {greeting} + + Your {type} with ID {transactionId} has been received and is being processed. + + Transaction Details: + - ID: {transactionId} + - Date: {timestamp} + - Status: Confirmed + + Thank you for your business! + + Best regards, + E-Commerce Team + """; + } + + private static string FormatEmailSubject(string actionType, string transactionId) + { + // Evolutionary change: Add a prefix to the subject line + return $"[E-Commerce] {actionType} - Transaction ID: {transactionId}"; + } + + private static void SendEmail(string customerId, string subject, string content) + { + // Simulate email sending + Console.WriteLine($"[EMAIL] Sending to customer {customerId}: {subject}"); + Console.WriteLine($"[EMAIL] Content preview: {content.Substring(0, Math.Min(50, content.Length))}..."); + System.Threading.Thread.Sleep(50); // Simulate email sending delay + Console.WriteLine($"[EMAIL] Successfully sent!"); + } + + private static void LogEmailActivity(string emailType, string customerId, string transactionId) + { + var logEntry = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] EMAIL_SENT | Type: {emailType} | Customer: {customerId} | Transaction: {transactionId}"; + Console.WriteLine($"[AUDIT] {logEntry}"); + // In real application, this would write to a log file or database + } + // Duplicate Helper Methods End +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Services/InventoryService.cs b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Services/InventoryService.cs new file mode 100644 index 0000000..c6e132a --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/consolidate-duplicate-code/ECommerceOrderAndReturn/Services/InventoryService.cs @@ -0,0 +1,118 @@ +// Services/InventoryService.cs - Inventory management service (contains duplicate logic) +using System; +using System.Collections.Generic; +using EcommerceApp.Models; + +namespace EcommerceApp.Services; + +public class InventoryService +{ + // Simulated inventory database + private static Dictionary _inventory = new Dictionary + { + { "PROD001", 50 }, // Laptop + { "PROD002", 100 }, // Mouse + { "PROD003", 25 }, // Keyboard + { "PROD004", 75 } // Monitor + }; + + public static void ReserveOrderInventory(Order order) + { + Console.WriteLine($"[INVENTORY] Processing inventory reservation for order {order.OrderId}"); + + foreach (var item in order.Items) + { + // Duplicate inventory validation logic - also used in returns + if (!ValidateInventoryAvailability(item.ProductId, item.Quantity)) + { + Console.WriteLine($"[INVENTORY] Warning: Insufficient stock for {item.ProductId}"); + continue; + } + + // Duplicate inventory update logic + UpdateInventoryLevel(item.ProductId, -item.Quantity, "RESERVED"); + + // Duplicate logging logic + LogInventoryTransaction("RESERVE", order.OrderId, item.ProductId, item.Quantity, "Order processing"); + } + } + + public static void RestoreReturnInventory(Return returnRequest) + { + Console.WriteLine($"[INVENTORY] Processing inventory restoration for return {returnRequest.ReturnId}"); + + // Duplicate inventory validation logic - same pattern as order processing + if (!ValidateInventoryAvailability(returnRequest.ProductId, 0)) // Check if product exists + { + Console.WriteLine($"[INVENTORY] Warning: Product {returnRequest.ProductId} not found in inventory"); + return; + } + + // Duplicate inventory update logic - but with positive quantity + UpdateInventoryLevel(returnRequest.ProductId, returnRequest.Quantity, "RESTORED"); + + // Duplicate logging logic - same pattern as order processing + LogInventoryTransaction("RESTORE", returnRequest.ReturnId, returnRequest.ProductId, returnRequest.Quantity, "Return processing"); + } + + // Duplicate Helper Methods Start - These methods are used in both order and return flows + private static bool ValidateInventoryAvailability(string productId, int requiredQuantity) + { + Console.WriteLine("[INVENTORY] Validating inventory availability..."); + + // Evolutionary change: Add a check for discontinued products + if (_inventory.ContainsKey(productId) && _inventory[productId] == 0) + { + Console.WriteLine($"[INVENTORY] Product {productId} is discontinued."); + return false; + } + + // Existing validation logic + if (!_inventory.ContainsKey(productId) || _inventory[productId] < requiredQuantity) + { + Console.WriteLine($"[INVENTORY] Insufficient stock for {productId}."); + return false; + } + + return true; + } + + private static void UpdateInventoryLevel(string productId, int quantityChange, string reason) + { + if (!_inventory.ContainsKey(productId)) + { + Console.WriteLine($"[INVENTORY] Cannot update inventory: Product {productId} not found"); + return; + } + + var oldLevel = _inventory[productId]; + _inventory[productId] += quantityChange; + var newLevel = _inventory[productId]; + + Console.WriteLine($"[INVENTORY] Updated {productId}: {oldLevel} → {newLevel} (Change: {quantityChange:+#;-#;0}, Reason: {reason})"); + + // Alert if inventory is low + if (newLevel < 10) + { + Console.WriteLine($"[INVENTORY] LOW STOCK ALERT: {productId} has only {newLevel} units remaining!"); + } + } + + private static void LogInventoryTransaction(string action, string transactionId, string productId, int quantity, string details) + { + Console.WriteLine("[INVENTORY] Logging inventory transaction..."); + + // Evolutionary change: Add a new log field for transaction type + Console.WriteLine($"[INVENTORY] Action: {action}, Transaction ID: {transactionId}, Product ID: {productId}, Quantity: {quantity}, Details: {details}, Transaction Type: {(quantity > 0 ? "Restoration" : "Reservation")}"); + } + // Duplicate Helper Methods End + + public static void DisplayCurrentInventory() + { + Console.WriteLine("[INVENTORY] Current Stock Levels:"); + foreach (var item in _inventory) + { + Console.WriteLine($" {item.Key}: {item.Value} units"); + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/github-copilot-sdk-starter-app/StakeholderDocuments/AppFeatures.md b/DownloadableCodeProjects/standalone-lab-projects/github-copilot-sdk-starter-app/StakeholderDocuments/AppFeatures.md new file mode 100644 index 0000000..a0ba2a0 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/github-copilot-sdk-starter-app/StakeholderDocuments/AppFeatures.md @@ -0,0 +1,111 @@ +# ContosoShop E-commerce Support Portal – Feature Description + +This document describes the functional features of the ContosoShop E-commerce Support Portal. The application is a simplified customer-facing support website for an online retailer, focused on allowing a user to manage orders and get post-purchase support. Below is a breakdown of the key features, user workflows, and how they operate in the base application (before adding the AI agent). + +## 1. User and Authentication (Simplified for Lab) + +**User Profile:** In this sample, the application assumes a single demo user (e.g., "John Doe") representing the signed-in customer. In a full production app, we would have an authentication system (allowing each real customer to log in and view their own data). For the purposes of this lab and to keep things simple, authentication is not fully implemented – the frontend automatically uses a preset user identity. This means: + +- When the app is launched, it behaves as if John Doe is logged in. His user ID is used for data retrieval (orders, etc.). +- No login page is presented in the base app. (In a real scenario, adding Azure AD B2C or Identity for auth is straightforward, but beyond our current scope.) + +**Authorization:** Since there's effectively one user context, all features are available to that user. The backend endpoints in this lab do not enforce authorization rules (again, assuming a trusted environment or that auth would be added later). However, the design assumes that in a real system, every API call would verify the caller's identity and restrict data (e.g., you can only retrieve orders that belong to your user ID). + +**Cloud-readiness note:** The app is structured to easily plug in an auth mechanism later. For instance, controllers are coded in a way that obtaining the current user's ID is abstracted (currently, it's a constant in our demo; later it could come from an Auth token or HttpContext). This prepares the ground for using Azure AD or another identity provider in the future without major refactoring. + +## 2. Order Management Features + +These features allow the user to see information about their purchases. All order-related data is stored in a database and accessed via the backend API. + +**Order History Page:** The "Orders" page on the Blazor client displays a list of the user's past orders. For each order, it shows an overview: Order Number/ID, date of purchase, total amount, and current status. + +- Example: Order #1001 – Placed on Jan 5, 2026 – Total: $59.99 – Status: Delivered. +- The data is fetched from the backend by calling GET /api/orders (which returns all orders for the demo user). In the base app, this API uses the user's ID to query the database (in our simplified scenario, it returns a static list of sample orders seeded for John Doe). +- If no orders exist (e.g., in a fresh database), the page will indicate that the user has no order history. + +**Order Details View:** By clicking on an order in the history list, the user navigates to an Order Details page. This shows more granular information: + +- Items in the order (product names, quantities, prices). +- Order timeline information: purchase date, shipment date (if shipped), delivery date (if delivered), etc. +- The current status is highlighted (e.g., "Delivered on Feb 10, 2026"). +- This page calls GET /api/orders/{orderId} to fetch details for the selected order. The API returns a detailed order object including associated items. In the UI, a list of order items is displayed with their name, SKU, price, and quantity. +- If the order is still in process (not delivered yet), the page might show an estimated delivery or current shipping step (for example, "Your package is in transit – expected by Jul 20" if such info were available; our base sample keeps it simple with basic statuses). +- If the order ID requested doesn't belong to the user or doesn't exist, the base API would return an error. However, since our demo user only sees their own seed data, this situation doesn't occur in normal use. + +**Order Status Indicators:** The possible order statuses in the base system include: Processing, Shipped, Delivered, Returned. Each status is assigned automatically by the system logic or via data seeding. + +- **Processing:** Order placed but not yet shipped. +- **Shipped:** Order handed over to carrier, on the way. +- **Delivered:** Order delivered to the customer (eligible for return). +- **Returned:** Order was returned by the customer and refund processed. + +These statuses are shown on both the Order History and Details pages for clarity. If an order is returned, it's clearly labeled as such (and items might be shown as returned). + +**Data Persistence:** In the base app, order information is stored in a local SQLite database via Entity Framework Core. There are two main tables (entities): + +- **Orders:** Contains fields like OrderId, UserId, OrderDate, Status, TotalAmount, etc. Possibly also a field for DeliveryDate. +- **OrderItems:** Contains individual line items for each order (OrderItemId, OrderId (foreign key), ProductName, Quantity, Price). + +Each time the user requests their orders, the API queries this DB. The SQLite DB is pre-populated with a few orders for demonstration. (For example, Order #1001 might be a delivered order with two items, Order #1002 a shipped order with one item, etc.) + +## 3. Return and Refund Capability + +One of the major support functions of an e-commerce site is handling returns. The base application includes a simplified return/refund workflow: + +**Return Eligibility & UI:** If an order's status is Delivered, the Order Details page will display a "Return Order" or "Initiate Return" button. This is the entry point for the user to request a return/refund for that order. (For orders not delivered or already returned, no such button is shown, preventing invalid actions.) + +- The base app determines eligibility by checking the status field. Optionally, it could also check a timeframe (e.g., only allow returns within 30 days of delivery). In our demo, we assume all delivered orders are returnable (the training focus is AI integration, so we keep business rules simple). + +**Return Process (Base Implementation):** When the user clicks "Return Order," in the base app the following happens: + +- The frontend calls POST /api/orders/{orderId}/return (this endpoint is implemented in the ASP.NET Core API). The request includes the order ID (and could include a reason for return, though our UI doesn't ask for one in the base version). +- The backend API ReturnOrder handler will verify that the order is indeed deliverable/returnable. (It checks the status isn't already "Returned" and belongs to the user, etc. If any check fails, it returns an appropriate error or status code.) +- If valid, the API updates the Order's status to Returned in the database. It also creates a Refund record or, in this simple case, just notes that a refund is due. (We have a conceptual Refunds table or simply treat the status change as implying the refund is processed.) +- The API then (in the base app) simulates sending a confirmation email to the customer. Rather than actually sending an email, it uses a service that logs the email content. For example, it might log: "Refund initiated for Order #1001 – amount $59.99 will be returned to your original payment method." This log simulates what an email would contain. (This design uses an EmailService interface, with a development implementation that just writes to console. Later, this can be swapped with a real email sender backed by SendGrid or SMTP without changing the controller logic – demonstrating a production-oriented design even in a local app.) +- Finally, the API responds to the client indicating success. The Blazor UI, upon success, might show a confirmation message like "Your return has been processed. You will receive a confirmation email shortly." and update the order status on the page to "Returned". + +**Post-Return Behavior:** After a return, if the user checks the order list, Order #1001's status will now show as Returned. If they go into details, they'll see it marked returned (and no return button, since it's already done). Essentially, the system now treats it as a completed return. (The base app does not track refunds money movement beyond the status, but in a real system this is where integration with a payment gateway would happen.) + +**Partial Returns:** For simplicity, the base app's return is all-or-nothing per order (we assume one order = one shipment). Partial item returns are not separately handled in our scenario. In a real world, you might have per-item returns; for this lab's scope, we consider the whole order returned. + +## 4. Customer Support Interface + +The application has a section dedicated to customer support, which is where our AI integration will come into play. In the base application: + +**Contact Support Page:** There is a page (likely accessible via a "Support" or "Contact Us" link) that is meant to assist the user in getting help. Currently, this page contains static content, such as: + +- Support contact information (e.g., "For any issues, email support@contososhop.com or call 1-800-CONTOSO"). +- Perhaps an input form or button that says "Chat with an Agent" or "Ask a question" – but it might be non-functional or placeholder in the base version. For example, a disabled text box that says "Support chat coming soon" or instructions like "Type your question below and click send." However, since we haven't wired up the backend for chat yet, clicking send might either do nothing or show a dummy response ("Thanks, we will get back to you."). +- The reason to include a stub here is to set the stage for the lab: this page is exactly where we'll embed the AI agent. + +**Current Limitations:** Without the AI agent, the support page cannot dynamically answer user queries. If a user wanted to know "Where is my order?" currently they would have to look at the Orders page themselves. The support page might just say "Contact us via email." In essence, the base app doesn't yet have interactive Q&A or support automation. + +**Vision for Enhancement:** The design anticipates adding an interactive element. The page already has a layout conducive to a chat interface (e.g., an area where conversation could be displayed and an input box at the bottom). This was done intentionally to make the integration of the Copilot SDK agent smoother. After the lab, this page will allow the user to ask questions in natural language and receive answers or actions (like initiating returns) from the AI agent, instead of the static info. + +## 5. Error Handling and User Feedback + +Even in a simple app, providing feedback for errors or important events is crucial: + +**API Error Handling:** The backend APIs return proper HTTP status codes for error scenarios. For example, if a return is requested for an order that's not delivered (say status is Shipped), the API might return a 400 Bad Request with a message "Order not deliverable, cannot return yet." In the base UI, such error messages would be caught and displayed to the user, possibly as a notification or modal. (The current UI has a basic mechanism for showing an error alert if an API call fails – this uses Blazor's error boundary or a simple try/catch around the API call followed by showing a message in the page.) + +**Confirmation Messages:** Conversely, when actions succeed (like a return processed), the UI immediately reflects the change (status updated) and may show a one-time confirmation message, e.g., "Return processed successfully." The base app's Order Details page, for instance, might have a banner that appears after a successful return action. + +**Input Validation:** There is minimal user input in the base app (mostly just the action of clicking return). The forms that do exist – e.g., if we had a support question form – validate that required fields are filled. In the "Contact Support" stub form, if present, we ensure the user can't send an empty query (the send button might be disabled until they type something, for example). These checks are done on the client side (Blazor can data-bind and validate inputs easily), and critical checks are repeated on the server (never trust client entirely; for instance, the ReturnOrder API double-checks that the order is valid for return regardless of what the UI did or didn't show). + +**Navigation & State:** The single-page nature of Blazor means users can navigate between pages (Orders list -> Order details -> Support, etc.) without full reloads. The app preserves necessary state (like selected order details are fetched each time or cached briefly). If the user performs an action and then goes back, the Orders list will refresh to show updated status (our base implementation simply re-calls the API on navigation, but we could optimize with caching). This approach ensures that the user always sees up-to-date info, even though it might re-fetch data (acceptable in a small app). + +## 6. Roadmap for Cloud-Scale Features + +While the base application is feature-complete for a demo, it leaves out some advanced features that a production system would have, which can be added later without restructuring: + +**User Authentication & Profiles:** As noted, adding a robust authentication system (with identity management, password reset, multi-user support) is a logical next step. The front-end nav bar already has a placeholder for "Hello, [Username]" which in our case is fixed, but could tie into an auth system easily. Azure AD B2C or Identity Server could be integrated so each user sees only their orders. The database already associates orders with a UserId, which is how multi-tenancy would be enforced. + +**Payment and Refund Integration:** The return process is simulated. In real life, upon marking an order as Returned, we'd call a Payment Gateway API to actually issue the refund to the customer's credit card or account. The code is structured so that the step of "issue refund" can be abstracted to a service class. Currently it's a stub that just logs, but one could plug in, say, Stripe or PayPal API calls in that spot. + +**Inventory and Product Catalog:** Our focus was customer support, so we don't have product browsing or inventory management in this project. However, if one were to extend this into a full e-commerce app, one could add a Products API and pages for browsing items, adding to cart, placing orders, etc. The addition of those features would not conflict with what's built – the Orders and Support parts would continue to function and would benefit from more data. + +**Admin Portal:** Another extension might be an admin interface for support reps to intervene. For instance, an admin could use a similar web UI to look up any customer's orders and manually process returns or answer queries. That would require authentication roles and exposing data by admin queries. The base app doesn't include this, but our API and DB design (with clear user IDs and order relationships) would allow an admin to retrieve any order by ID if authorized. + +**Internationalization and Localization:** Currently all text is in English and amounts are in dollars. The app could be localized (Blazor has support for localization) to different languages and currencies. We didn't do it here to avoid complexity in the lab, but it's a consideration for production. Similarly, date and number formats are fixed in code but could be culture-sensitive. + +The above features are outside the immediate scope of the lab, but it's important to note that the base app's design does not paint us into a corner; it can evolve. For now, the primary mission is to integrate an AI agent into the Contact Support experience. This will transform the static support page into a smart assistant that can leverage the app's existing Order and Return features. The next document (TechStack.md) explains how this app is built under the hood, which will clarify where and how the AI agent will hook into the system. diff --git a/DownloadableCodeProjects/standalone-lab-projects/github-copilot-sdk-starter-app/StakeholderDocuments/ProjectGoals.md b/DownloadableCodeProjects/standalone-lab-projects/github-copilot-sdk-starter-app/StakeholderDocuments/ProjectGoals.md new file mode 100644 index 0000000..d9a69f6 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/github-copilot-sdk-starter-app/StakeholderDocuments/ProjectGoals.md @@ -0,0 +1,57 @@ +# ContosoShop E-commerce Support Portal (Local Edition) + +**Project Name:** ContosoShop E-commerce Support Portal (Local Edition) + +## Overview + +ContosoShop E-commerce Support Portal is a sample web application that simulates an online store's customer support interface. It allows a user to view their orders, check order status, and initiate returns/refunds through a self-service portal. The project is designed as a production-ready application that runs locally (using a lightweight SQLite database and local email logging) while being architecturally ready to migrate to cloud services (such as Azure SQL Database, Azure App Service, and Azure email services). In a companion lab exercise, we will enhance this application by integrating an AI-powered support agent using the GitHub Copilot SDK, enabling intelligent automated assistance for customer service scenarios. + +## Key Features + +- **Order History & Details:** Users can view a list of their past orders and see detailed information for each order (order items, status, dates). + +- **Order Status Tracking:** The application shows the current status of each order (e.g., *Processing*, *Shipped*, *Delivered*, *Returned*). This data is stored in a local SQLite database for easy setup and can be migrated to Azure SQL for production. + +- **Initiate Returns/Refunds:** For delivered orders, users can initiate a return. In the base application, this updates the order status to *Returned* and (simulated) triggers a refund process. The logic is contained in the backend API and designed to be expanded or connected to real payment systems later. + +- **Contact Support (to be enhanced):** The application includes a "Contact Support" page. Initially, this page provides guidance on how to reach customer service (and may allow submitting a support request form). In the lab, this page will be transformed into an interactive AI chat interface where the GitHub Copilot SDK agent will handle user queries about orders. + +- **Blazor WebAssembly Frontend:** A rich client-side UI built with Blazor WebAssembly provides a responsive single-page application experience. The UI is implemented with production best practices (e.g., responsive layout, error handling, loading indicators) and communicates with the backend via HTTP API calls. + +- **ASP.NET Core Web API Backend:** A robust backend powered by ASP.NET Core Web API (.NET 8) handles all business logic and data access. It exposes RESTful endpoints for retrieving orders, updating order status, and other operations. This separation ensures the frontend and backend are decoupled and can scale independently (or even be replaced by other client apps). + +- **Local Development Friendly:** The app uses EF Core with a local SQLite database file for easy setup – no external dependencies needed to run locally. The database is seeded with sample data (e.g., a demo user account and a few orders) so the app works out-of-the-box. For emailing (e.g., sending a confirmation when a refund is processed), the base app simply logs the email content to the console, avoiding external email service requirements during development. + +- **Cloud-Ready Architecture:** Although running locally, the app's architecture aligns with cloud deployment practices. Configuration is managed via appsettings.json (with override support for environment-specific settings), making it easy to switch connection strings or service URLs for cloud environments. The application is divided into projects (Client and Server, plus shared models) similar to the Blazor WASM Hosted model, facilitating deployment to Azure App Service (for the API) and Azure Static Web Apps or Azure Storage (for the Blazor client). The EF Core data access layer can point to Azure SQL by changing a connection string, and the email service can be swapped with an actual email provider (like SendGrid) without changing the core logic. + +## Running the App Locally + +1. **Prerequisites:** .NET 8 SDK or later is required (the project targets .NET 8). You'll also need a recent version of Node.js if using any build steps for front-end (Blazor WASM doesn't require Node for standard use). Visual Studio 2022 or VS Code with the C# extension is recommended for editing and running the project. + +2. **Clone the Repository:** Retrieve the project source code (the exact steps depend on how the lab provides the code – typically by downloading or cloning a GitHub repository). + +3. **Database Setup:** The project includes a SQLite database file (App_Data/ContosoShop.db for example) with seed data. EF Core Migrations have been run and the database is up-to-date. There's no additional setup needed; the database will be copied on build if not present. + +4. **Configure (optional):** By default, the app uses the included SQLite DB and defaults to development settings. No modification is necessary for the lab scenario. If you want to test using a different database (e.g., SQL Server), update the connection string in appsettings.json and ensure the database is reachable. + +5. **Build and Run:** Open the solution in Visual Studio and press **F5** (or use dotnet run on the API project and a static file server for the Blazor client, if running manually). The backend API will start (e.g., on https://localhost:5001) and the Blazor client will be served (e.g., on https://localhost:5002 or via the same server depending on configuration). By default, the solution is set up to run both projects together. + +6. **Using the App:** In your browser, navigate to the provided URL (usually https://localhost:5002 for the Blazor app). You should see the ContosoShop portal homepage. From there, you can click "Orders" to view sample orders. Clicking an order will show its details. If an order is delivered and eligible for return, a "Return Item" button will be visible. The Contact Support page is also accessible (it currently shows contact info or a placeholder – which the lab will turn into an AI chat). + +7. **Observe Logs:** The backend API will output console logs for key events. For example, if you initiate a return, the backend might log a message that a refund email was "sent" (simulated). These logs appear in the output window of VS or the console where dotnet run was executed. + +## Next Steps – Integrating AI Agent + +This base application sets the stage for integrating an AI-powered support agent. In the following lab exercises, you will add an AI agent to the Contact Support page using the GitHub Copilot SDK. The agent will be capable of understanding user questions (e.g., "Where is my order?" or "I want to return my last order") and will use the application's backend capabilities (via new API tools you'll implement) to perform actions like checking order status, initiating a return, and confirming outcomes to the user – all through a conversational interface. The modular design of ContosoShop's backend (with clearly defined services for orders and returns) will make it straightforward to expose these operations to the AI agent in a controlled manner. + +## Cloud Deployment Path + +While this lab runs everything locally for simplicity, the app is prepared for cloud deployment. To deploy to Azure, you could: + +- Host the ASP.NET Core Web API on **Azure App Service** (or Azure Container Apps). Simply publish the Server project, and switch the EF Core provider to Azure SQL by updating the connection string to point to an Azure SQL Database. The same EF Core migrations apply – you can run them on Azure or generate SQL scripts to set up the schema in the cloud. + +- Host the Blazor WASM client on **Azure Static Web Apps** or as part of the App Service. In a production scenario, you might combine the deployment so that the API and Blazor UI are served from the same domain for simplicity. The project structure supports this (the Blazor app can be published into the API's wwwroot if desired for a single deployment unit, or deployed separately as a static site). + +- Integrate **Azure Services** as needed: e.g., swap out the email logger with an **Azure SendGrid** or **Microsoft Graph Mail** integration to send real emails, plug in **Application Insights** for monitoring and diagnostics, and consider using **Azure OpenAI Service** to host the AI model behind the Copilot SDK (if you want full control of the AI in production rather than relying on the GitHub Copilot service). + +This README provides a high-level orientation. For more details on what the app does and how it's built, see the **AppFeatures** and **TechStack** documentation below. Happy coding! diff --git a/DownloadableCodeProjects/standalone-lab-projects/github-copilot-sdk-starter-app/StakeholderDocuments/TechStack.md b/DownloadableCodeProjects/standalone-lab-projects/github-copilot-sdk-starter-app/StakeholderDocuments/TechStack.md new file mode 100644 index 0000000..ef528f9 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/github-copilot-sdk-starter-app/StakeholderDocuments/TechStack.md @@ -0,0 +1,149 @@ +# ContosoShop E-commerce Support Portal – Technical Architecture and Stack + +This document provides a technical overview of how the application's features (described in AppFeatures.md) are implemented. We outline the architecture, frameworks, and key components, and highlight how the design facilitates local development as well as future cloud migration. + +## 1. Solution Architecture Overview + +The project follows a **Blazor WebAssembly Hosted architecture**, which means it is split into a client and a server: + +- **ContosoShop.Client (Blazor WebAssembly):** This is the front-end running in the browser. It's a single-page application (SPA) written in C# and HTML (Razor components). It contains the UI logic, forms, and calls the backend API via HttpClient. It was created as a standalone Blazor WASM project and later configured to work with the API. (In our solution, it's a separate project that can be deployed independently of the server if needed.) + +- **ContosoShop.Server (ASP.NET Core Web API):** This is the back-end REST API built with ASP.NET Core 8 (running on .NET 8). It exposes endpoints under /api/ that the client calls. It also hosts the Blazor client's static files when run in a combined mode (for simplicity in local dev, we actually serve the Blazor app from the same domain via the ASP.NET project, using the ASP.NET Core Hosted template setup). The server contains all business logic – like querying the database or updating an order – and enforces rules (e.g., "don't return an order that isn't yours" in a real auth scenario). + +- **ContosoShop.Shared (class library, if used):** We have a small library for shared code, primarily to share model definitions between Client and Server. For example, the Order and OrderItem classes are defined in Shared, so both the server (when producing JSON) and the client (when decoding JSON) use the same definitions, reducing duplication and errors. We also share any validation or enums this way. (If this were a combined solution template, the Shared project is optional, but our solution does include it to illustrate good practice for code sharing). + +### Project Structure: + +- **Client/Pages** – Razor components for pages (e.g., Orders.razor, OrderDetails.razor, Support.razor). +- **Client/Services** – Service classes for API calls (e.g., OrderService that calls the API endpoints, encapsulating HttpClient use). +- **Client/Shared** – Shared UI components (e.g., a MainLayout, NavMenu, and maybe smaller components like an OrderCard). +- **Server/Controllers** – Web API controllers (e.g., OrdersController, possibly SupportController). Each controller corresponds to a set of endpoints for a resource. Our OrdersController handles /api/orders routes. We might not need a separate SupportController yet, but will add an endpoint for the agent in the lab (perhaps under a new SupportAgentController). +- **Server/Data** – EF Core DbContext and Configuration. Contains ContosoContext (our EF Core DbContext) and the entity classes (Order, OrderItem, etc., if not in Shared). Also likely contains code for seeding initial data into the SQLite DB on first run. +- **Server/Services** – Classes that encapsulate business logic, used by controllers. For example, an OrderService in the backend might contain methods like GetOrdersForUser(userId), ProcessReturn(orderId), etc. The controller can call these, which in turn call the DbContext and other services like EmailService. This layering isn't strictly needed in a small app but demonstrates how to separate concerns (especially if some logic is complex or reused). +- **Server/Utilities** – Utility classes (e.g., an EmailService interface and an implementation EmailServiceDev that logs emails). Also, configuration classes or helpers for mapping data. +- **Shared/Models** – Definitions for data models (Order, OrderItem, possibly an OrderStatus enum). + +This structured approach makes it easier to maintain and test the app. For instance, one could unit-test OrderService methods independently of the controllers. + +## 2. Frameworks and Libraries + +- **.NET 8:** The entire solution is on .NET 8. Using .NET 8 ensures we have the latest C# features and performance improvements, and it aligns with the timelines of modern Azure services and the GitHub Copilot SDK (which expects a recent .NET runtime). .NET 8 is required to run this project, so ensure your environment is updated accordingly. + +- **Blazor WebAssembly:** Our client is a Blazor WASM app. It runs the UI and client logic in the browser on WebAssembly, using Mono/WASM to execute C#. This means the user gets a rich interactive experience without constant page reloads. The Blazor app has been configured to call the backend for data. In Program.cs of the client, we register an HttpClient with the base address pointing to the server's URL so that HttpClient calls automatically target the correct domain (during dev, likely https://localhost:5001 for the API). We use dependency injection to provide services (like OrderService) to our components. The UI is built with Razor (which mixes HTML and C#). We've opted for a clean, Bootstrap-based styling (the default Blazor template's Bootstrap is used, giving us a responsive layout out of the box). No JavaScript frameworks are needed; however, we could interop with JS for things like copy-to-clipboard or other niceties if required. + +- **ASP.NET Core Web API:** The server uses ASP.NET Core's minimal API/Controller approach. We created controllers with [ApiController] and routing attributes, returning strongly-typed models. For example, OrdersController.GetOrders() returns IEnumerable which ASP.NET Core automatically serializes to JSON. We rely on the default JSON (System.Text.Json) which is efficient and symmetric with Blazor's deserialization. CORS is configured to allow the Blazor client to call (when both run on same origin in dev, it's not an issue, but if separated, we allowed the client origin or used the fact it's hosted to avoid CORS issues). + +- **Entity Framework Core (EF Core):** This is used for data access. The ContosoContext DbContext is configured with SQLite provider in development. We used code-first migrations to set up the database schema. The context has a DbSet and DbSet, and possibly DbSet if we had a user table (in our simplified case, user info might be minimal, but we can assume an in-memory user or a simple Users table with one entry). We run migrations on app startup (the app either ensures the SQLite DB exists or uses EnsureCreated in development). Entities have relationships (Order has a collection of OrderItems). EF Core tracking is used so when we update an Order's status and call SaveChanges(), it commits to the SQLite file. + + - **SQLite:** The connection string for SQLite is in appsettings.json (e.g., "ConnectionStrings": { "DefaultConnection": "Data Source=App_Data/ContosoShop.db" }). SQLite is chosen for local run because it requires no separate server installation and is lightweight. It is fully supported by EF Core. We included the .db file in the project so that it deploys if needed, and configured it to copy to output. In development mode, EF migrations are not automatically applied (we either ran them and checked in the DB, or we call context.Database.EnsureCreated() to auto-create tables for simplicity). + + - If we were to scale up or go to production, we would switch to Azure SQL. EF Core makes this easy: we'd change the UseSqlite to UseSqlServer with an Azure SQL connection string. Our code (repos, services) does not need to change. Migration to Azure SQL would involve deploying the migrations or generating a script—EF Core can handle differences in SQL dialects. Also, the app's repository pattern (if present) and service logic are database-agnostic beyond the configuration. + +- **Logging and Configuration:** We use built-in .NET Logging (Microsoft.Extensions.Logging). In development, the default console logger is enabled so we see logs in the output. Configuration is done via the standard ASP.NET Core mechanism (appsettings files and environment variables). For example, the connection string and maybe a flag like "UseDevelopmentEmailService: true" are in appsettings.Development.json. In production (Azure), we'd likely override those with environment-specific values (Azure App Service application settings can directly override configuration keys). This means the app is prepared to accept config from Azure, such as a real SMTP server endpoint or an Azure Storage connection if we had one. + +## 3. Backend: Key Components and Classes + +**OrderController / OrderService:** This pair (or just controller, depending on how we structured it) is responsible for all order-related endpoints. Key methods include: + +- **GET /api/orders** – calls something like _orderService.GetOrdersForUser(userId) which returns a list from the DbContext (e.g., context.Orders.Include(o=>o.Items).Where(o => o.UserId == userId)). If not using a separate service class, the controller might directly inject ContosoContext and query it. We decided to implement an OrderService to illustrate the pattern and to encapsulate logic like seeding or business rules (e.g., filtering by user, sorting orders by date). This service is registered in DI so the controller gets it. + +- **GET /api/orders/{id}** – returns a single order with items. Internally ensures the order belongs to the authenticating user (in our simplified case, we skip that or assume it's correct). Returns NotFound if not found. + +- **POST /api/orders/{id}/return** – the endpoint to initiate a return (used when "Return Order" is clicked). This likely calls _orderService.ProcessReturn(orderId, userId). That method will: + - Load the order from DB, check status and maybe date. + - If invalid (e.g., status != Delivered), throw or return an error. + - If valid, update the order's Status = Returned, and set a ReturnDate or similar. + - Save changes. + - Then call an Email service to send confirmation (we'll discuss Email service next). + - Possibly log an audit entry or return details about the refund. + - Return a result (maybe the updated order or a simple success response). + - Our base implementation might actually simply return 204 NoContent on success, and the client then knows to update UI. + +- The use of a service class here means we can reuse ProcessReturn logic if, for example, an AI agent or an admin console also needs to trigger a return. In fact, that's exactly what we'll do in the lab: the AI agent's "ProcessRefund" tool will essentially call the same logic that the API endpoint uses (we might directly call _orderService.ProcessReturn inside the tool implementation). This avoids duplicating refund logic in multiple places. + +**EmailService (Dev):** As mentioned, we have an abstraction IEmailService with a method like SendEmail(to, subject, body). In Startup (or builder.Services setup), we register EmailServiceDev as the implementation for IEmailService. This EmailServiceDev doesn't actually send anything; it logs the email content to console or a file for debugging. For example, calling SendEmail("john@example.com", "Your refund was processed", "We have refunded $X to your card...") will result in a log entry that looks like an email. This pattern ensures that our business logic (like the OrderService) is unaware of the specifics of email sending – it just calls emailService.SendEmail(...) and trusts it works. In a cloud deployment, we could have another implementation EmailServiceSendGrid that uses SendGrid API, and swap the registration to that (likely via config or environment detection). None of the controller/service code changes when we do that – a benefit of the DI architecture. + +**Database Context and Entities:** We use EF Core code-first: + +- **ContosoContext : DbContext** defines DbSet Orders and DbSet OrderItems. Perhaps a DbSet Users if needed (in our lab, not heavily used). + +- It's configured in Program.cs (Server) with something like: + ```csharp + builder.Services.AddDbContext(options => + options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"))); + ``` + +- In a cloud scenario, DefaultConnection can be changed to a SQL Server connection string and the code simply switched to UseSqlServer. + +- We created a migration for the initial model (and any subsequent changes). In development, we ensure the SQLite DB has this schema. The solution includes the migration files under Server/Migrations/ for reference. For example, you might see 20231201010101_InitialCreate.cs which EF generated, showing the Orders and OrderItems tables creation. + +- **Order Entity:** fields like Id (int, primary key), UserId (could be int or GUID), OrderDate, Status (we use an OrderStatus enum in code but store as string or int in DB), TotalAmount (decimal). We might also have a DeliveryDate. The relationship: public List Items to collect items. Using EF's [ForeignKey] or by naming convention, OrderItem has OrderId. + +- **OrderItem Entity:** fields: Id (pk), OrderId (foreign key), ProductName (string), Quantity (int), Price (decimal). For simplicity, we denormalize product data into OrderItem (no separate Product table in this small demo). + +- The data seeder (perhaps in Context.OnModelCreating or separate initializer) creates a few orders for our demo user. For example, Order #1001 (Id=1001) with two items (Product "Wireless Mouse", qty 1, price $25; "Keyboard", qty 1, price $34.99, making total $59.99), status Delivered, delivered date last week. Order #1002, one item ("HDMI Cable", $15), status Shipped. Etc. This gives us realistic content to test the agent on. + +- Because SQLite is file-based, when running the app, the DB file (ContosoShop.db) gets created in Server/App_Data/ or similar folder. This file persists data between runs unless deleted. The lab instructions ensure this is set up so that you don't need to manually do any seeding – it's automatic on first run. + +**HTTP API security and CORS:** In Program.cs of Server, we enable the controllers (app.MapControllers()) and probably allow any origin for simplicity in development (or specifically allow the Blazor client origin if running on a different port). Since in dev we likely use the hosted model, the Blazor files are served by the same server on a subpath, so CORS issues are minimal. For cloud, we would set up proper CORS or use the same domain. + +## 4. Frontend: Key Components and Interaction with Backend + +**State Management:** Blazor WASM allows us to use in-memory state for the current user's data. However, since our data is small, we fetch fresh data when needed rather than store it in a complex client-side state. For example: + +- The Orders page, on initialization (OnInitializedAsync), calls OrderService.GetOrdersAsync() which GETs from the server and populates a local list orders. Blazor then renders the list. This retrieval happens each time the user navigates to Orders page (which ensures updated data if something changed). We could optimize by caching the result in a state container if needed, but not necessary for a few orders. + +- The Order Details page likely receives an order ID via query parameter or route parameter (@page "/orders/{id:int}"). It then calls OrderService.GetOrderDetailsAsync(id) to retrieve the full order (or the Orders page might have passed the order in memory to avoid second call – but to keep it simple we do an API call here too, which would hit the DB again). + +- After calling a return, we might refresh the data or at least update the bound objects to reflect new status. For instance, our OrderService.ReturnOrderAsync(id) calls the POST API. If it succeeds, we can either manually set the current order's status to Returned in the UI model (so the UI updates immediately) and perhaps even update the Orders list cached in memory (if we have it) so the list page is consistent. In our base flow, we simply navigate the user back to the orders list after a return and call the API again to load updated data – a simple and consistent approach. + +**OrderService (Client side):** An Angular or React app would use a service or hook for API calls; similarly, in Blazor we created an OrderService class. This is registered via builder.Services.AddScoped() in Program.cs (Client). It wraps HttpClient calls: + +- **GetOrdersAsync()** does `return await http.GetFromJsonAsync>("api/orders");`. Blazor's HttpClient is configured with base URI, so "api/orders" goes to the backend. The Shared models ensure the JSON maps to the Order class properly. + +- **GetOrderDetailsAsync(id)** might call `http.GetFromJsonAsync($"api/orders/{id}")`. + +- **ReturnOrderAsync(id)** would likely do `var response = await http.PostAsync($"api/orders/{id}/return", null); response.EnsureSuccessStatusCode();`. We didn't need to send a body, as the act of hitting the endpoint is enough. Alternatively, we could use PostAsJsonAsync if we needed to send data with the request (like a return reason). In base, not required. + +- The service abstracts away those calls so our Razor components don't have to write boilerplate. In the component, we just do `await OrderService.ReturnOrderAsync(order.Id)` and handle exceptions if any. + +**Razor Components:** + +- **Orders.razor:** loops through orders and displays each in a table or list. Each item has a link (perhaps using `View Details`). It might also show a status badge (we can color-code statuses, e.g., Delivered = green badge, Shipped = blue, etc., using a bit of conditional logic with Bootstrap classes). + +- **OrderDetails.razor:** displays details of a single order. Possibly uses a child component for item list (or just loops inside). If order.Status == OrderStatus.Delivered, show the Return button. Also, maybe show a delivery address or any other info if we had it (we assume minimal details here). If the Return button is clicked, it calls a method that invokes the service to do the return. + +- **Support.razor:** currently might have something like a simple form where the user can type questions. The lab will significantly change this by wiring it up to the Copilot SDK agent. The base UI is ready to display a response from the agent. We intentionally keep the design minimal here so it's easy to integrate the dynamic behavior in the lab. + +**User Experience considerations:** We used Bootstrap for quick styling. The nav menu on the left (in NavMenu.razor) has links to "Orders" and "Contact Support". The main layout provides a header maybe saying "ContosoShop Support Portal" for context. The app is responsive (Bootstrap ensures the layout works on mobile; e.g., the nav collapses to a hamburger). This is not a major focus, but it means the support agent UI we add will also be mobile-friendly out of the box. + +## 5. Design for Local vs Cloud Environments + +We've emphasized that the app is cloud-ready. Here are specific ways it's designed for easy migration: + +**Separation of Concerns & Loose Coupling:** The clear split between front-end and back-end means we could scale them independently. In Azure, you could host the API on an App Service and the Blazor WASM on a CDN or Static Web App; they'd communicate over HTTPS. This separation follows the backend-for-frontend pattern and allows using Azure's best services for each (e.g., Azure CDN for static content, Azure App Service for the API logic). During local dev we combine them for convenience (the hosted Blazor model serving the static files), which is configurable via a flag. For instance, in Program.cs we might use app.UseBlazorFrameworkFiles() and app.MapFallbackToFile("index.html") on the server to serve the client app. This is active in dev; in a separate deployment, we could turn that off and deploy separately. + +**Configuration and Secrets:** No secrets are needed for local run (we're not calling external APIs in base). But we have the infrastructure to introduce secrets via user secrets or environment variables if needed. For example, if using SendGrid, we'd store the API key in Azure's config and load it via Configuration["SendGridApiKey"]. The code might be ready to consume such config even if in base it's not set. In appsettings.json we keep sensitive things out (or in dev json only if not sensitive like a local filepath). This means pushing to Azure is just a matter of setting configurations appropriately. + +**Database Migration Path:** Using SQLite in development is convenient, but for an Azure production, one would typically use Azure SQL. The EF Core migrations and model are fully compatible with SQL Server. The team could do one of: + +- Use `dotnet ef database update` pointing to the Azure SQL connection to create schema. +- Or use EF Core's ability to generate a differential script and run that on Azure SQL. +- Also consider using Azure DevOps or GitHub Actions pipeline to apply migrations during deployment (ensuring zero downtime strategies, etc.). The code doesn't need changes - it's devops process. +- The codebase includes some conditional logic if needed (like maybe a compiler directive or config flag to choose UseSqlServer vs UseSqlite). More simply, we might rely on the connection string format to determine provider; but in practice, we can let the lab environment always use SQLite. Documenting the path: "Switching to Azure SQL involves adding the Microsoft.EntityFrameworkCore.SqlServer NuGet package and changing one line in Program.cs (UseSqlServer). Then update the DefaultConnection string to the Azure SQL connection string in production settings. That's it." This highlights ease of migration. + +**Scalability and Performance:** For a local lab, performance is a non-issue. But the use of EF Core (with appropriate indexing if needed) and streaming of data in Web API (we're returning all orders at once, which is fine for small numbers; pagination could be added for very large histories), and the efficient static content loading from Blazor's published output all mean the app can handle typical load. On Azure, enabling response compression, and using Azure Front Door or CDN for static files could vastly improve global performance. None of these require code changes, just configuration and Azure toggles. For instance, ASP.NET Core by default has gzip compression (if enabled in config) for API responses; we can ensure it's on in production. + +**Azure Integration Points:** We considered possible Azure services: + +- **Azure App Service:** Ideal for hosting the ASP.NET Core API (and even the Blazor client). We ensure the app writes logs to console (which App Service can capture) and doesn't write to disk (except the SQLite DB which is in the content folder; on App Service that's fine but in production we'd use Azure SQL to avoid file write). + +- **Azure Static Web Apps:** If splitting client, this could host the Blazor WASM and provide an auto CI/CD from a GitHub repo. Meanwhile, an Azure Function or App Service could host the API. We'd then configure CORS accordingly. The code would not change except possibly the base addresses. + +- **Azure Monitor/Application Insights:** We can add Application Insights SDK to monitor server performance and track requests, which is straightforward with one line addition in Program (builder.Services.AddApplicationInsightsTelemetry()). We have not included it in base (to avoid extra setup for lab) but it's an easy add that doesn't alter our logic. + +- **Azure OpenAI:** When scaling the AI agent portion, instead of relying on GitHub's Copilot service (which is convenient for development), a production system might use Azure OpenAI Service with models like GPT-4. The Copilot SDK integration we'll do is abstracted enough that we could replace the behind-the-scenes calls with Azure OpenAI's API plus our own orchestration logic (though that would require building something similar to Copilot's planner). Since this is cutting-edge, we mention it as a path if an enterprise has concerns about data or customization – they can swap to their controlled AI endpoint. + +In summary, the tech stack is contemporary and robust: C# full-stack (Blazor + ASP.NET Core) with EF Core for ORM, targeting .NET 8 for best performance and features. All these choices align directly with Microsoft's cloud offerings, making the journey from a local SQLite/VS Code experience to an Azure-deployed, scalable solution very smooth. We've enforced clean separation and used interfaces/DI for things like email and data access to ensure that improving or changing implementations (like switching to Azure services) is just a matter of configuration or adding new classes, not rewriting core features. + +With the base application's architecture understood, we can proceed to the Lab Exercise where you'll integrate the GitHub Copilot SDK into this app. The lab will guide you through adding an AI agent on the backend (within the ASP.NET Core server) and creating a user interface in the Blazor client to converse with it, leveraging the structures described here (like OrderService and EmailService) so the agent can do useful work (check orders, process returns) securely and effectively. diff --git a/DownloadableCodeProjects/standalone-lab-projects/github-copilot-sdk-starter-app/placeholder.txt b/DownloadableCodeProjects/standalone-lab-projects/github-copilot-sdk-starter-app/placeholder.txt new file mode 100644 index 0000000..b3a4252 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/github-copilot-sdk-starter-app/placeholder.txt @@ -0,0 +1 @@ +placeholder \ No newline at end of file diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore.Tests/ContosoOnlineStore.Tests.csproj b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore.Tests/ContosoOnlineStore.Tests.csproj new file mode 100644 index 0000000..3cc6716 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore.Tests/ContosoOnlineStore.Tests.csproj @@ -0,0 +1,18 @@ + + + net9.0 + false + enable + enable + + + + + + + + + + + + diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore.Tests/ContosoOnlineStoreTests.cs b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore.Tests/ContosoOnlineStoreTests.cs new file mode 100644 index 0000000..8c580c4 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore.Tests/ContosoOnlineStoreTests.cs @@ -0,0 +1,194 @@ +using ContosoOnlineStore; +using ContosoOnlineStore.Configuration; +using ContosoOnlineStore.Services; +using ContosoOnlineStore.Exceptions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; + +namespace ContosoOnlineStore.Tests; + +public class ProductCatalogTests +{ + private readonly IProductCatalog _catalog; + private readonly Mock _mockSecurity; + private readonly Mock> _mockLogger; + + public ProductCatalogTests() + { + _mockSecurity = new Mock(); + _mockLogger = new Mock>(); + var appSettings = Options.Create(new AppSettings()); + + _catalog = new ProductCatalog(_mockSecurity.Object, _mockLogger.Object, appSettings); + } + + [Fact] + public void GetProductById_ValidId_ReturnsProduct() + { + var productId = 1; + var product = _catalog.GetProductById(productId); + Assert.NotNull(product); + Assert.Equal(productId, product.Id); + } + + [Fact] + public void GetProductById_InvalidId_ReturnsNull() + { + var invalidId = 999; + var product = _catalog.GetProductById(invalidId); + Assert.Null(product); + } + + [Fact] + public void GetAllProducts_ReturnsAllProducts() + { + var products = _catalog.GetAllProducts(); + Assert.NotEmpty(products); + Assert.True(products.Count >= 20); + } + + [Fact] + public void SearchProducts_ValidTerm_ReturnsMatchingProducts() + { + _mockSecurity.Setup(s => s.SanitizeInput(It.IsAny())).Returns("phone"); + var results = _catalog.SearchProducts("phone"); + Assert.NotEmpty(results); + Assert.All(results, p => + Assert.True(p.Name.ToLower().Contains("phone") || + p.Category.ToLower().Contains("phone") || + p.Description.ToLower().Contains("phone"))); + } +} + +public class OrderTests +{ + [Fact] + public void Constructor_ValidParameters_CreatesOrder() + { + var email = "test@example.com"; + var address = "123 Test St"; + var order = new Order(email, address); + Assert.Equal(email, order.CustomerEmail); + Assert.Equal(address, order.ShippingAddress); + Assert.Equal(OrderStatus.Pending, order.Status); + Assert.True(order.OrderId > 0); + } + + [Fact] + public void AddItem_ValidItem_AddsToOrder() + { + var order = new Order(); + var item = new OrderItem(1, 2); + order.AddItem(item); + Assert.Contains(item, order.Items); + var single = Assert.Single(order.Items); + Assert.Equal(item.ProductId, single.ProductId); + Assert.Equal(item.Quantity, single.Quantity); + } + + [Fact] + public void AddItem_SameProduct_CombinesQuantity() + { + var order = new Order(); + var item1 = new OrderItem(1, 2); + var item2 = new OrderItem(1, 3); + order.AddItem(item1); + order.AddItem(item2); + var single = Assert.Single(order.Items); + Assert.Equal(5, single.Quantity); + Assert.Equal(1, single.ProductId); + } +} + +public class SecurityValidationServiceTests +{ + private readonly ISecurityValidationService _service; + private readonly Mock> _mockLogger; + + public SecurityValidationServiceTests() + { + _mockLogger = new Mock>(); + var appSettings = Options.Create(new AppSettings()); + _service = new SecurityValidationService(appSettings, _mockLogger.Object); + } + + [Fact] + public void ValidateProduct_ValidProduct_DoesNotThrow() + { + var product = new Product(1, "Test Product", 10.99m, 100); + var exception = Record.Exception(() => _service.ValidateProduct(product)); + Assert.Null(exception); + } + + [Fact] + public void ValidateProduct_NullProduct_ThrowsArgumentNullException() + { + Assert.Throws(() => _service.ValidateProduct((Product)null!)); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void ValidateProduct_InvalidName_ThrowsSecurityValidationException(string invalidName) + { + if (string.IsNullOrWhiteSpace(invalidName)) + { + Assert.Throws(() => new Product(1, invalidName, 10.99m, 100)); + } + } + + [Fact] + public void SanitizeInput_ValidInput_ReturnsSanitized() + { + var input = "Test"; + var result = _service.SanitizeInput(input); + Assert.DoesNotContain("<", result); + Assert.DoesNotContain(">", result); + } +} + +public class InventoryManagerTests +{ + private readonly Mock _mockCatalog; + private readonly Mock> _mockLogger; + private readonly IInventoryManager _inventoryManager; + + public InventoryManagerTests() + { + _mockCatalog = new Mock(); + _mockLogger = new Mock>(); + var appSettings = Options.Create(new AppSettings()); + + var products = new List + { + new Product(1, "Product 1", 10.0m, 100), + new Product(2, "Product 2", 20.0m, 50) + }; + + _mockCatalog.Setup(c => c.GetAllProducts()).Returns(products); + _inventoryManager = new InventoryManager(_mockCatalog.Object, _mockLogger.Object, appSettings); + } + + [Fact] + public void GetStockLevel_ValidProductId_ReturnsStock() + { + var stock = _inventoryManager.GetStockLevel(1); + Assert.Equal(100, stock); + } + + [Fact] + public void IsInStock_SufficientStock_ReturnsTrue() + { + var inStock = _inventoryManager.IsInStock(1, 50); + Assert.True(inStock); + } + + [Fact] + public void IsInStock_InsufficientStock_ReturnsFalse() + { + var inStock = _inventoryManager.IsInStock(1, 150); + Assert.False(inStock); + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore.Tests/Usings.cs b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore.Tests/Usings.cs new file mode 100644 index 0000000..c802f44 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Benchmarks/OrderProcessingBenchmarks.cs b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Benchmarks/OrderProcessingBenchmarks.cs new file mode 100644 index 0000000..1ba6a61 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Benchmarks/OrderProcessingBenchmarks.cs @@ -0,0 +1,106 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; +using ContosoOnlineStore; +using ContosoOnlineStore.Configuration; +using ContosoOnlineStore.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace ContosoOnlineStore.Benchmarks +{ + [MemoryDiagnoser] + [SimpleJob] + public class OrderProcessingBenchmarks + { + private IOrderProcessor? _orderProcessor; + private IProductCatalog? _catalog; + private IInventoryManager? _inventory; + private Order? _testOrder; + private ServiceProvider? _serviceProvider; + + [GlobalSetup] + public void Setup() + { + var services = new ServiceCollection(); + + // Configure logging + services.AddLogging(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Warning)); + + // Configure settings + var appSettings = new AppSettings(); + services.AddSingleton(Options.Create(appSettings)); + + // Register services + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + _serviceProvider = services.BuildServiceProvider(); + + _catalog = _serviceProvider.GetRequiredService(); + _inventory = _serviceProvider.GetRequiredService(); + _orderProcessor = _serviceProvider.GetRequiredService(); + + // Create test order + _testOrder = new Order("customer@example.com", "123 Test Street, Test City, WA 98101"); + _testOrder.AddItem(new OrderItem(1, 2)); + _testOrder.AddItem(new OrderItem(5, 1)); + _testOrder.AddItem(new OrderItem(10, 3)); + } + + [Benchmark] + public decimal CalculateOrderTotal() + { + return _orderProcessor!.CalculateOrderTotal(_testOrder!); + } + + [Benchmark] + public async Task ValidateOrderAsync() + { + return await _orderProcessor!.ValidateOrderAsync(_testOrder!); + } + + [Benchmark] + public List GetAllProducts() + { + return _catalog!.GetAllProducts(); + } + + [Benchmark] + public Product? GetProductById() + { + return _catalog!.GetProductById(5); + } + + [Benchmark] + public List SearchProducts() + { + return _catalog!.SearchProducts("phone"); + } + + [Benchmark] + public Dictionary GetLowStockProducts() + { + return _inventory!.GetLowStockProducts(50); + } + + [GlobalCleanup] + public void Cleanup() + { + (_serviceProvider as IDisposable)?.Dispose(); + } + } + + public class BenchmarkRunner + { + public static void RunBenchmarks() + { + Console.WriteLine("Starting performance benchmarks..."); + var summary = BenchmarkDotNet.Running.BenchmarkRunner.Run(); + Console.WriteLine("Benchmarks completed!"); + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Configuration/AppSettings.cs b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Configuration/AppSettings.cs new file mode 100644 index 0000000..dd44f57 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Configuration/AppSettings.cs @@ -0,0 +1,42 @@ +using System.ComponentModel.DataAnnotations; + +namespace ContosoOnlineStore.Configuration +{ + public class AppSettings + { + [Range(1, 1000)] + public int MaxOrderItems { get; set; } = 50; + + [Range(100, 30000)] + public int EmailTimeoutMs { get; set; } = 2000; + + public bool EnableDetailedLogging { get; set; } = true; + + public SecuritySettings SecuritySettings { get; set; } = new(); + + public PerformanceSettings PerformanceSettings { get; set; } = new(); + } + + public class SecuritySettings + { + [Range(0.01, 100000.00)] + public decimal MaxProductPrice { get; set; } = 10000.00m; + + [Range(0.01, 1000.00)] + public decimal MinProductPrice { get; set; } = 0.01m; + + public bool AllowNegativeInventory { get; set; } = false; + } + + public class PerformanceSettings + { + [Range(1, 1440)] + public int CacheExpirationMinutes { get; set; } = 30; + + [Range(1000, 60000)] + public int DatabaseTimeoutMs { get; set; } = 5000; + + [Range(1, 1000)] + public int MaxConcurrentOrders { get; set; } = 100; + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/ContosoOnlineStore.csproj b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/ContosoOnlineStore.csproj new file mode 100644 index 0000000..744196c --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/ContosoOnlineStore.csproj @@ -0,0 +1,31 @@ + + + + Exe + net9.0 + enable + enable + + + true + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Exceptions/CustomExceptions.cs b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Exceptions/CustomExceptions.cs new file mode 100644 index 0000000..b12ad46 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Exceptions/CustomExceptions.cs @@ -0,0 +1,50 @@ +using System; + +namespace ContosoOnlineStore.Exceptions +{ + public class ProductNotFoundException : Exception + { + public int ProductId { get; } + + public ProductNotFoundException(int productId) + : base($"Product with ID {productId} was not found.") + { + ProductId = productId; + } + + public ProductNotFoundException(int productId, Exception innerException) + : base($"Product with ID {productId} was not found.", innerException) + { + ProductId = productId; + } + } + + public class InsufficientInventoryException : Exception + { + public int ProductId { get; } + public int RequestedQuantity { get; } + public int AvailableQuantity { get; } + + public InsufficientInventoryException(int productId, int requestedQuantity, int availableQuantity) + : base($"Insufficient inventory for product ID {productId}. Requested: {requestedQuantity}, Available: {availableQuantity}") + { + ProductId = productId; + RequestedQuantity = requestedQuantity; + AvailableQuantity = availableQuantity; + } + } + + public class InvalidOrderException : Exception + { + public InvalidOrderException(string message) : base(message) { } + + public InvalidOrderException(string message, Exception innerException) : base(message, innerException) { } + } + + public class SecurityValidationException : Exception + { + public SecurityValidationException(string message) : base(message) { } + + public SecurityValidationException(string message, Exception innerException) : base(message, innerException) { } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/InventoryManager.cs b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/InventoryManager.cs new file mode 100644 index 0000000..63f85b2 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/InventoryManager.cs @@ -0,0 +1,224 @@ +using ContosoOnlineStore.Configuration; +using ContosoOnlineStore.Exceptions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Collections.Concurrent; + +namespace ContosoOnlineStore +{ + public interface IInventoryManager + { + int GetStockLevel(int productId); + bool IsInStock(int productId, int requestedQuantity); + void UpdateStockLevels(Order order); + void ReserveStock(Order order); + void ReleaseReservedStock(Order order); + Dictionary GetLowStockProducts(int threshold = 10); + Task RestockProductAsync(int productId, int quantity); + } + + public class InventoryManager : IInventoryManager + { + private readonly ConcurrentDictionary _stockByProductId; + private readonly ConcurrentDictionary _reservedStock; + private readonly Dictionary _lastStockUpdate; + private readonly IProductCatalog _catalog; + private readonly ILogger _logger; + private readonly AppSettings _appSettings; + private readonly object _stockLock = new object(); + + public InventoryManager(IProductCatalog catalog, ILogger logger, IOptions appSettings) + { + _catalog = catalog; + _logger = logger; + _appSettings = appSettings.Value; + _stockByProductId = new ConcurrentDictionary(); + _reservedStock = new ConcurrentDictionary(); + _lastStockUpdate = new Dictionary(); + + InitializeInventory(); + } + + private void InitializeInventory() + { + var products = _catalog.GetAllProducts(); + foreach (var product in products) + { + _stockByProductId[product.Id] = product.InitialStock; + _reservedStock[product.Id] = 0; + _lastStockUpdate[product.Id] = DateTime.UtcNow; + _logger.LogDebug("Initialized stock for product {ProductId}: {Stock} units", product.Id, product.InitialStock); + } + + _logger.LogInformation("Initialized inventory for {ProductCount} products", products.Count); + } + + public int GetStockLevel(int productId) + { + if (productId <= 0) + { + _logger.LogWarning("Invalid product ID requested for stock level: {ProductId}", productId); + return 0; + } + + // Performance bottleneck: Simulate database query delay + if (productId % 5 == 0) // Intentional performance issue + { + Thread.Sleep(50); // Simulate slow database query + _logger.LogDebug("Used slow stock lookup for product {ProductId}", productId); + } + + var stock = _stockByProductId.GetValueOrDefault(productId, 0); + var reserved = _reservedStock.GetValueOrDefault(productId, 0); + var availableStock = stock - reserved; + + _logger.LogDebug("Stock level for product {ProductId}: {Stock} total, {Reserved} reserved, {Available} available", + productId, stock, reserved, availableStock); + + return Math.Max(0, availableStock); + } + + public bool IsInStock(int productId, int requestedQuantity) + { + if (requestedQuantity <= 0) + return false; + + var availableStock = GetStockLevel(productId); + return availableStock >= requestedQuantity; + } + + public void UpdateStockLevels(Order order) + { + if (order == null) + throw new ArgumentNullException(nameof(order)); + + lock (_stockLock) + { + var stockChanges = new Dictionary(); + + foreach (OrderItem item in order.Items) + { + var currentStock = _stockByProductId.GetValueOrDefault(item.ProductId, 0); + var newStock = currentStock - item.Quantity; + + // Security check: Prevent negative inventory if not allowed + if (!_appSettings.SecuritySettings.AllowNegativeInventory && newStock < 0) + { + var availableStock = GetStockLevel(item.ProductId); + throw new InsufficientInventoryException(item.ProductId, item.Quantity, availableStock); + } + + _stockByProductId[item.ProductId] = newStock; + _lastStockUpdate[item.ProductId] = DateTime.UtcNow; + stockChanges[item.ProductId] = newStock; + + _logger.LogInformation("Updated stock for product {ProductId}: {OldStock} -> {NewStock} (Change: -{Quantity})", + item.ProductId, currentStock, newStock, item.Quantity); + } + + // Performance bottleneck: Inefficient logging of all stock changes + LogAllStockChanges(stockChanges); // Could be optimized + } + } + + public void ReserveStock(Order order) + { + if (order == null) + throw new ArgumentNullException(nameof(order)); + + lock (_stockLock) + { + // Check availability first + foreach (OrderItem item in order.Items) + { + if (!IsInStock(item.ProductId, item.Quantity)) + { + var availableStock = GetStockLevel(item.ProductId); + throw new InsufficientInventoryException(item.ProductId, item.Quantity, availableStock); + } + } + + // Reserve stock + foreach (OrderItem item in order.Items) + { + _reservedStock.AddOrUpdate(item.ProductId, item.Quantity, (key, existing) => existing + item.Quantity); + _logger.LogDebug("Reserved {Quantity} units of product {ProductId} for order {OrderId}", + item.Quantity, item.ProductId, order.OrderId); + } + } + } + + public void ReleaseReservedStock(Order order) + { + if (order == null) + throw new ArgumentNullException(nameof(order)); + + lock (_stockLock) + { + foreach (OrderItem item in order.Items) + { + _reservedStock.AddOrUpdate(item.ProductId, 0, (key, existing) => Math.Max(0, existing - item.Quantity)); + _logger.LogDebug("Released {Quantity} reserved units of product {ProductId} for order {OrderId}", + item.Quantity, item.ProductId, order.OrderId); + } + } + } + + public Dictionary GetLowStockProducts(int threshold = 10) + { + var lowStockProducts = new Dictionary(); + + // Performance bottleneck: Check all products individually instead of batch operation + foreach (var productId in _stockByProductId.Keys) + { + Thread.Sleep(1); // Simulate individual database queries + var stock = GetStockLevel(productId); + if (stock <= threshold) + { + lowStockProducts[productId] = stock; + } + } + + _logger.LogInformation("Found {LowStockCount} products with stock below {Threshold}", + lowStockProducts.Count, threshold); + + return lowStockProducts; + } + + public async Task RestockProductAsync(int productId, int quantity) + { + if (productId <= 0) + throw new ArgumentException("Product ID must be positive", nameof(productId)); + + if (quantity <= 0) + throw new ArgumentException("Restock quantity must be positive", nameof(quantity)); + + // Simulate async operation with delay + await Task.Delay(200); // Performance bottleneck: Unnecessary delay + + lock (_stockLock) + { + var currentStock = _stockByProductId.GetValueOrDefault(productId, 0); + _stockByProductId[productId] = currentStock + quantity; + _lastStockUpdate[productId] = DateTime.UtcNow; + + _logger.LogInformation("Restocked product {ProductId}: {OldStock} -> {NewStock} (+{Quantity})", + productId, currentStock, currentStock + quantity, quantity); + } + + return true; + } + + private void LogAllStockChanges(Dictionary stockChanges) + { + // Performance bottleneck: Inefficient logging implementation + var logMessage = "Stock changes: "; + foreach (var change in stockChanges) + { + logMessage += $"Product {change.Key}: {change.Value} units; "; + Thread.Sleep(1); // Simulate slow logging + } + _logger.LogDebug(logMessage.TrimEnd(';', ' ')); + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Order.cs b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Order.cs new file mode 100644 index 0000000..a0aac59 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Order.cs @@ -0,0 +1,86 @@ +using System.ComponentModel.DataAnnotations; + +namespace ContosoOnlineStore +{ + public class Order + { + [Range(1, int.MaxValue)] + public int OrderId { get; } + + [Required] + [StringLength(100, MinimumLength = 1)] + public string CustomerEmail { get; set; } = string.Empty; + + [Required] + [StringLength(200, MinimumLength = 1)] + public string ShippingAddress { get; set; } = string.Empty; + + public List Items { get; } + + public DateTime OrderDate { get; } + + public OrderStatus Status { get; set; } + + public decimal TotalAmount { get; set; } + + private static int _nextOrderId = 1; + private static readonly object _orderIdLock = new object(); + + public Order() + { + lock (_orderIdLock) + { + OrderId = _nextOrderId++; + } + Items = new List(); + OrderDate = DateTime.UtcNow; + Status = OrderStatus.Pending; + } + + public Order(string customerEmail, string shippingAddress) : this() + { + CustomerEmail = customerEmail?.Trim() ?? throw new ArgumentNullException(nameof(customerEmail)); + ShippingAddress = shippingAddress?.Trim() ?? throw new ArgumentNullException(nameof(shippingAddress)); + } + + public void AddItem(OrderItem item) + { + if (item == null) + throw new ArgumentNullException(nameof(item)); + + if (Items.Count >= 50) + throw new InvalidOperationException("Cannot add more than 50 items to an order"); + + // Check if item already exists and combine quantities + var existingItem = Items.FirstOrDefault(i => i.ProductId == item.ProductId); + if (existingItem != null) + { + existingItem.Quantity += item.Quantity; + } + else + { + Items.Add(item); + } + } + + public bool RemoveItem(int productId) + { + return Items.RemoveAll(i => i.ProductId == productId) > 0; + } + + public int GetTotalItemCount() + { + return Items.Sum(i => i.Quantity); + } + } + + public enum OrderStatus + { + Pending = 0, + Processing = 1, + Shipped = 2, + Delivered = 3, + Cancelled = 4, + Returned = 5 + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/OrderItem.cs b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/OrderItem.cs new file mode 100644 index 0000000..6cc260f --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/OrderItem.cs @@ -0,0 +1,41 @@ +using System.ComponentModel.DataAnnotations; + +namespace ContosoOnlineStore +{ + public class OrderItem + { + [Range(1, int.MaxValue, ErrorMessage = "Product ID must be positive")] + public int ProductId { get; set; } + + [Range(1, 1000, ErrorMessage = "Quantity must be between 1 and 1000")] + public int Quantity { get; set; } + + public decimal UnitPrice { get; set; } + + public decimal TotalPrice => UnitPrice * Quantity; + + public OrderItem(int productId, int quantity, decimal unitPrice = 0) + { + if (productId <= 0) + throw new ArgumentException("Product ID must be positive", nameof(productId)); + + if (quantity <= 0) + throw new ArgumentException("Quantity must be positive", nameof(quantity)); + + if (quantity > 1000) + throw new ArgumentException("Quantity cannot exceed 1000", nameof(quantity)); + + if (unitPrice < 0) + throw new ArgumentException("Unit price cannot be negative", nameof(unitPrice)); + + ProductId = productId; + Quantity = quantity; + UnitPrice = unitPrice; + } + + public override string ToString() + { + return $"OrderItem[ProductId={ProductId}, Quantity={Quantity}, UnitPrice={UnitPrice:C}, Total={TotalPrice:C}]"; + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/OrderProcessor.cs b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/OrderProcessor.cs new file mode 100644 index 0000000..283dee8 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/OrderProcessor.cs @@ -0,0 +1,377 @@ +using ContosoOnlineStore.Configuration; +using ContosoOnlineStore.Exceptions; +using ContosoOnlineStore.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Collections.Concurrent; + +namespace ContosoOnlineStore +{ + public interface IOrderProcessor + { + decimal CalculateOrderTotal(Order order); + Task FinalizeOrderAsync(Order order); + Task ValidateOrderAsync(Order order); + Task GenerateOrderReceiptAsync(Order order); + Task ProcessOrderWithValidationAsync(Order order); + decimal CalculateTax(decimal subtotal, string state = "WA"); + decimal CalculateShipping(Order order); + } + + public class OrderProcessingResult + { + public bool Success { get; set; } + public decimal TotalAmount { get; set; } + public string? ErrorMessage { get; set; } + public List Warnings { get; set; } = new(); + public TimeSpan ProcessingTime { get; set; } + public int ProcessedItems { get; set; } + } + + public class OrderProcessor : IOrderProcessor + { + private readonly IProductCatalog _catalog; + private readonly IInventoryManager _inventory; + private readonly IEmailService _emailService; + private readonly ISecurityValidationService _securityValidation; + private readonly ILogger _logger; + private readonly AppSettings _appSettings; + private readonly ConcurrentDictionary _priceCache; + private static int _orderCounter = 0; + + public OrderProcessor( + IProductCatalog catalog, + IInventoryManager inventory, + IEmailService emailService, + ISecurityValidationService securityValidation, + ILogger logger, + IOptions appSettings) + { + _catalog = catalog; + _inventory = inventory; + _emailService = emailService; + _securityValidation = securityValidation; + _logger = logger; + _appSettings = appSettings.Value; + _priceCache = new ConcurrentDictionary(); + } + + public decimal CalculateOrderTotal(Order order) + { + if (order == null) + throw new ArgumentNullException(nameof(order)); + + decimal subtotal = 0; + var productCache = new Dictionary(); + + foreach (OrderItem item in order.Items) + { + // Performance bottleneck: Individual product lookups instead of batch + if (!productCache.ContainsKey(item.ProductId)) + { + Thread.Sleep(5); // Simulate database query delay + productCache[item.ProductId] = _catalog.GetProductById(item.ProductId); + } + + var product = productCache[item.ProductId]; + if (product != null) + { + // Security validation for each item + _securityValidation.ValidateOrderItem(item, product); + + var itemTotal = product.Price * item.Quantity; + subtotal += itemTotal; + + // Update order item with unit price for receipt + item.UnitPrice = product.Price; + + _logger.LogDebug("Calculated item total for product {ProductId}: {ItemTotal:C}", + product.Id, itemTotal); + } + else + { + _logger.LogWarning("Product {ProductId} not found in catalog", item.ProductId); + throw new ProductNotFoundException(item.ProductId); + } + } + + // Performance bottleneck: Recalculate tax and shipping every time + var tax = CalculateTax(subtotal); + var shipping = CalculateShipping(order); + var total = subtotal + tax + shipping; + + _logger.LogInformation("Order total calculated: Subtotal={Subtotal:C}, Tax={Tax:C}, Shipping={Shipping:C}, Total={Total:C}", + subtotal, tax, shipping, total); + + return total; + } + + public async Task FinalizeOrderAsync(Order order) + { + if (order == null) + throw new ArgumentNullException(nameof(order)); + + var startTime = DateTime.UtcNow; + Interlocked.Increment(ref _orderCounter); + + try + { + _logger.LogInformation("Finalizing order {OrderId} (Processing #{ProcessingNumber})", + order.OrderId, _orderCounter); + + // Validate order first + if (!await ValidateOrderAsync(order)) + { + throw new InvalidOrderException("Order validation failed"); + } + + // Reserve inventory + _inventory.ReserveStock(order); + + // Calculate total with all fees + decimal total = CalculateOrderTotal(order); + order.TotalAmount = total; + order.Status = OrderStatus.Processing; + + // Update actual inventory + _inventory.UpdateStockLevels(order); + + // Send confirmation email + bool emailSent = await _emailService.SendConfirmationAsync(order); + if (!emailSent) + { + _logger.LogWarning("Failed to send confirmation email for order {OrderId}", order.OrderId); + } + + // Performance bottleneck: Generate receipt immediately (could be deferred) + await GenerateOrderReceiptAsync(order); + + order.Status = OrderStatus.Shipped; // Simulate immediate shipping for demo + + var processingTime = DateTime.UtcNow - startTime; + _logger.LogInformation("Order {OrderId} finalized successfully in {ProcessingTime}ms. Total: {Total:C}", + order.OrderId, processingTime.TotalMilliseconds, total); + + return total; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to finalize order {OrderId}", order.OrderId); + + // Release reserved stock on failure + try + { + _inventory.ReleaseReservedStock(order); + order.Status = OrderStatus.Cancelled; + } + catch (Exception releaseEx) + { + _logger.LogError(releaseEx, "Failed to release reserved stock for order {OrderId}", order.OrderId); + } + + throw; + } + } + + public async Task ValidateOrderAsync(Order order) + { + if (order == null) + return false; + + try + { + // Basic order validation + _securityValidation.ValidateOrder(order); + + // Check inventory availability + foreach (var item in order.Items) + { + // Performance bottleneck: Individual inventory checks + await Task.Delay(10); // Simulate database query + + if (!_inventory.IsInStock(item.ProductId, item.Quantity)) + { + var availableStock = _inventory.GetStockLevel(item.ProductId); + _logger.LogWarning("Insufficient inventory for product {ProductId}. Requested: {Requested}, Available: {Available}", + item.ProductId, item.Quantity, availableStock); + return false; + } + + // Validate product exists + var product = _catalog.GetProductById(item.ProductId); + if (product == null) + { + _logger.LogWarning("Product {ProductId} not found during validation", item.ProductId); + return false; + } + + // Validate item against product + _securityValidation.ValidateOrderItem(item, product); + } + + // Validate email format + if (!await _emailService.ValidateEmailAsync(order.CustomerEmail)) + { + _logger.LogWarning("Invalid email format for order {OrderId}: {Email}", order.OrderId, order.CustomerEmail); + return false; + } + + _logger.LogDebug("Order {OrderId} validation completed successfully", order.OrderId); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Order validation failed for order {OrderId}", order.OrderId); + return false; + } + } + + public async Task GenerateOrderReceiptAsync(Order order) + { + if (order == null) + throw new ArgumentNullException(nameof(order)); + + // Performance bottleneck: Generate receipt synchronously with delays + await Task.Delay(100); // Simulate receipt generation delay + + var receiptBuilder = new System.Text.StringBuilder(); + receiptBuilder.AppendLine("=".PadRight(50, '=')); + receiptBuilder.AppendLine("CONTOSO ONLINE STORE"); + receiptBuilder.AppendLine("Order Receipt"); + receiptBuilder.AppendLine("=".PadRight(50, '=')); + receiptBuilder.AppendLine($"Order ID: {order.OrderId}"); + receiptBuilder.AppendLine($"Date: {order.OrderDate:yyyy-MM-dd HH:mm:ss}"); + receiptBuilder.AppendLine($"Customer: {order.CustomerEmail}"); + receiptBuilder.AppendLine($"Shipping: {order.ShippingAddress}"); + receiptBuilder.AppendLine("-".PadRight(50, '-')); + + decimal subtotal = 0; + foreach (var item in order.Items) + { + // Performance bottleneck: Individual product lookups for receipt + await Task.Delay(5); + var product = _catalog.GetProductById(item.ProductId); + if (product != null) + { + var itemTotal = product.Price * item.Quantity; + subtotal += itemTotal; + receiptBuilder.AppendLine($"{product.Name.PadRight(20)} {item.Quantity}x {product.Price:C} = {itemTotal:C}"); + } + } + + var tax = CalculateTax(subtotal); + var shipping = CalculateShipping(order); + var total = subtotal + tax + shipping; + + receiptBuilder.AppendLine("-".PadRight(50, '-')); + receiptBuilder.AppendLine($"Subtotal: {subtotal:C}".PadLeft(50)); + receiptBuilder.AppendLine($"Tax: {tax:C}".PadLeft(50)); + receiptBuilder.AppendLine($"Shipping: {shipping:C}".PadLeft(50)); + receiptBuilder.AppendLine($"TOTAL: {total:C}".PadLeft(50)); + receiptBuilder.AppendLine("=".PadRight(50, '=')); + + var receipt = receiptBuilder.ToString(); + _logger.LogDebug("Generated receipt for order {OrderId}", order.OrderId); + + return receipt; + } + + public async Task ProcessOrderWithValidationAsync(Order order) + { + var startTime = DateTime.UtcNow; + var result = new OrderProcessingResult(); + + try + { + if (order == null) + { + result.ErrorMessage = "Order cannot be null"; + return result; + } + + // Comprehensive validation + if (!await ValidateOrderAsync(order)) + { + result.ErrorMessage = "Order validation failed"; + return result; + } + + // Process the order + result.TotalAmount = await FinalizeOrderAsync(order); + result.ProcessedItems = order.Items.Count; + result.Success = true; + + // Check for warnings (low stock items) + await CheckForLowStockWarningsAsync(order, result); + + } + catch (Exception ex) + { + result.Success = false; + result.ErrorMessage = ex.Message; + _logger.LogError(ex, "Order processing failed for order {OrderId}", order?.OrderId); + } + finally + { + result.ProcessingTime = DateTime.UtcNow - startTime; + } + + return result; + } + + public decimal CalculateTax(decimal subtotal, string state = "WA") + { + // Performance bottleneck: Inefficient tax calculation with lookup + Thread.Sleep(20); // Simulate tax service lookup delay + + var taxRates = new Dictionary + { + ["WA"] = 0.095m, + ["CA"] = 0.0875m, + ["NY"] = 0.08m, + ["TX"] = 0.0825m, + ["FL"] = 0.06m + }; + + var rate = taxRates.GetValueOrDefault(state.ToUpper(), 0.05m); + return Math.Round(subtotal * rate, 2); + } + + public decimal CalculateShipping(Order order) + { + if (order == null) + return 0; + + // Performance bottleneck: Complex shipping calculation + Thread.Sleep(15); // Simulate shipping calculator delay + + var itemCount = order.GetTotalItemCount(); + var baseShipping = 5.99m; + var perItemFee = 0.99m; + + var shippingCost = baseShipping + (perItemFee * Math.Max(0, itemCount - 1)); + + // Free shipping for orders over $100 + if (order.TotalAmount > 100) + { + shippingCost = 0; + } + + return Math.Round(shippingCost, 2); + } + + private async Task CheckForLowStockWarningsAsync(Order order, OrderProcessingResult result) + { + foreach (var item in order.Items) + { + await Task.Delay(5); // Performance bottleneck + var remainingStock = _inventory.GetStockLevel(item.ProductId); + if (remainingStock <= 10) // Low stock threshold + { + var product = _catalog.GetProductById(item.ProductId); + result.Warnings.Add($"Low stock warning: {product?.Name} has only {remainingStock} units remaining"); + } + } + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/PERFORMANCE_GUIDE.md b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/PERFORMANCE_GUIDE.md new file mode 100644 index 0000000..fc2749e --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/PERFORMANCE_GUIDE.md @@ -0,0 +1,381 @@ +# Performance Improvement Guide + +## Quick Start Performance Issues + +This document outlines the intentional performance bottlenecks in the Contoso Online Store application and provides guidance for optimization exercises. + +## Performance Issues Overview + +### 🔴 Critical Performance Issues + +#### 1. Product Catalog Linear Search + +**File**: `ProductCatalog.cs` +**Method**: `GetProductById()` +**Issue**: Uses linear search (FirstOrDefault) instead of dictionary lookup +**Impact**: O(n) complexity instead of O(1) + +```csharp +// Current (slow) implementation +return _products.FirstOrDefault(p => p.Id == productId); + +// Optimized approach +return _productIndex.TryGetValue(productId, out var product) ? product : null; +``` + +#### 2. Inefficient Search Implementation + +**File**: `ProductCatalog.cs` +**Method**: `SearchProducts()` +**Issues**: + +- Multiple string operations per product +- Inefficient cache key generation +- Sequential processing with artificial delays + +**Impact**: High latency for search operations + +#### 3. Sequential Order Processing + +**File**: `OrderProcessor.cs` +**Method**: `FinalizeOrderAsync()` +**Issues**: + +- Individual product lookups in loops +- Sequential inventory checks +- Synchronous receipt generation + +### 🟡 Moderate Performance Issues + +#### 4. Inventory Management Bottlenecks + +**File**: `InventoryManager.cs` +**Method**: `GetLowStockProducts()` +**Issues**: + +- Individual database queries simulation +- Inefficient logging implementation +- No batch operations + +#### 5. Email Service Delays + +**File**: `EmailService.cs` +**Method**: `SendConfirmationAsync()` +**Issues**: + +- Sequential email content generation +- Individual product lookups in email templates +- Synchronous validation operations + +### 🟢 Minor Performance Issues + +#### 6. Excessive Logging Overhead + +**Throughout the application** +**Issues**: + +- Detailed logging in hot paths +- String concatenation in logging +- Synchronous logging operations + +#### 7. Memory Allocation Patterns + +**Various files** +**Issues**: + +- Frequent list creation and sorting +- String concatenation without StringBuilder +- Cache dictionary overhead + +## Performance Optimization Exercise Guide + +### Exercise 1: Optimize Product Lookup (Beginner) + +**Goal**: Improve product lookup performance from O(n) to O(1) +**Files**: `ProductCatalog.cs` +**Expected Improvement**: 90%+ reduction in lookup time + +**Steps**: + +1. Identify the linear search in `GetProductById()` +2. Implement dictionary-based product index +3. Update index when products are added/modified +4. Measure performance improvement + +**Success Criteria**: Product lookups complete in <1ms + +### Exercise 2: Batch Inventory Operations (Intermediate) + +**Goal**: Reduce individual database calls +**Files**: `InventoryManager.cs`, `OrderProcessor.cs` +**Expected Improvement**: 70%+ reduction in inventory check time + +**Steps**: + +1. Identify individual inventory checks in loops +2. Implement batch inventory validation +3. Create bulk stock update operations +4. Optimize low stock checking + +**Success Criteria**: Batch operations 5x faster than individual calls + +### Exercise 3: Async Processing Pipeline (Advanced) + +**Goal**: Implement parallel processing for order operations +**Files**: `OrderProcessor.cs`, `EmailService.cs` +**Expected Improvement**: 60%+ reduction in total processing time + +**Steps**: + +1. Identify sequential operations that can be parallelized +2. Implement async/await patterns properly +3. Create parallel processing for order validation +4. Optimize email sending pipeline + +**Success Criteria**: Order processing completes in <500ms + +### Exercise 4: Intelligent Caching (Advanced) + +**Goal**: Implement comprehensive caching strategy +**Files**: `ProductCatalog.cs`, `OrderProcessor.cs` +**Expected Improvement**: 80%+ improvement in repeated operations + +**Steps**: + +1. Implement product search result caching +2. Add price calculation caching +3. Create smart cache invalidation +4. Implement cache warming strategies + +**Success Criteria**: Cache hit ratio >80% for common operations + +## Measurement and Benchmarking + +### Using Built-in Performance Tracking + +The application includes performance counters that display: + +- Order processing times +- Individual operation durations +- Memory allocation patterns +- Cache hit/miss ratios + +### Running Benchmarks + +To use BenchmarkDotNet for detailed analysis, run the following command: + +```bash +dotnet run -c Release -- benchmark +``` + +This command will run the application in Release mode and execute the benchmarks defined in your project. + +If you omit the `-c Release` option, the compiler defaults to Debug mode. Since the default value for `Optimize` in a Debug build is `false`, BenchmarkDotNet will detect a non‑optimized assembly. The result is a warning or error “Assembly ... is non-optimized... build it in RELEASE.” + +You can update the .csproj file to enable optimizations even for Debug mode so that running the app with the 'benchmark' argument via 'dotnet run' should produce valid BenchmarkDotNet results. + +Add the following line inside the main `` in the .csproj file. + +```xml +true +``` + +Without this, a Debug build shows the warning/error that the assembly is non-optimized. + +It's best to explicitly use Release (recommended for keeping Debug truly debuggable): + +```bash +dotnet run -c Release -- benchmark +``` + +Warning: Always optimizing Debug can make stepping through code less intuitive. If you +prefer traditional debugging, revert the global true> and instead: + +Run benchmarks with -c Release: + +```bash +dotnet run -c Release -- benchmark +``` + +Or add a dedicated configuration: + +```xml + + true + +``` + +And then run: + +```bash +dotnet run -c Benchmarks -- benchmark +``` + +Using BenchmarkDotNet provides: + +- Precise timing measurements +- Memory allocation tracking +- Statistical analysis +- Performance regression detection + +### Performance Targets + +#### Before Optimization (Baseline) + +- Order processing: 2000-3000ms +- Product lookup: 10-50ms per operation +- Search operations: 100-500ms +- Inventory checks: 50-200ms + +#### After Optimization (Target) + +- Order processing: <500ms +- Product lookup: <1ms per operation +- Search operations: <50ms +- Inventory checks: <20ms + +## Common Optimization Patterns + +### 1. Dictionary Lookups + +Replace linear searches with dictionary/hash table lookups: + +```csharp +// Instead of: list.FirstOrDefault(x => x.Id == id) +// Use: dictionary.TryGetValue(id, out var item) +``` + +### 2. Batch Operations + +Group multiple database operations: + +```csharp +// Instead of: multiple individual queries +// Use: single batch query with multiple IDs +``` + +### 3. Async/Await Best Practices + +Properly implement asynchronous operations: + +```csharp +// Instead of: Task.Wait() or .Result +// Use: await Task.WhenAll(tasks) +``` + +### 4. Caching Strategies + +Implement multi-level caching: + +```csharp +// Memory cache for frequently accessed data +// Distributed cache for shared data +// Smart cache invalidation +``` + +### 5. Object Pooling + +Reuse expensive objects: + +```csharp +// Pool StringBuilder, HttpClient, etc. +// Reduce garbage collection pressure +``` + +## Profiling Tools Integration + +### Visual Studio Diagnostic Tools + +- CPU Usage analysis +- Memory Usage tracking +- Events timeline +- Performance tips + +### dotMemory/dotTrace + +- Memory profiling +- Performance profiling +- Timeline analysis +- Comparison reports + +### Application Insights (Simulated) + +The application logs performance metrics that simulate: + +- Request/response times +- Dependency call durations +- Exception tracking +- Performance counter data + +## Validation and Testing + +### Performance Tests + +Run the included performance test suite: + +```bash +dotnet test --logger trx --collect:"XPlat Code Coverage" +``` + +### Load Testing Simulation + +The application includes concurrent operation testing: + +- Multiple simultaneous orders +- Concurrent product lookups +- Search load testing +- Inventory contention handling + +### Regression Testing + +Ensure optimizations don't break functionality: + +- Unit test coverage >80% +- Integration test scenarios +- Performance benchmark baselines +- Memory leak detection + +## Real-World Considerations + +### Production Deployment + +Consider these factors when applying optimizations: + +- Database connection pooling +- CDN for static content +- Load balancing strategies +- Auto-scaling configurations + +### Monitoring and Alerting + +Implement production monitoring: + +- Performance threshold alerts +- Error rate monitoring +- Resource utilization tracking +- User experience metrics + +### Security Impact + +Ensure optimizations don't compromise security: + +- Input validation performance +- Rate limiting implementation +- Authentication/authorization caching +- Audit log performance + +## Next Steps + +1. **Baseline Measurement**: Run the application and record current performance metrics +2. **Identify Bottlenecks**: Use profiling tools to find the highest-impact issues +3. **Prioritize Improvements**: Focus on changes with the best ROI +4. **Implement Changes**: Apply optimizations systematically +5. **Measure Impact**: Validate improvements with benchmarks +6. **Iterate**: Continue optimization cycle + +## Resources + +- [.NET Performance Best Practices](https://docs.microsoft.com/en-us/dotnet/framework/performance/) +- [Async Programming Patterns](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/) +- [Memory Management in .NET](https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/) +- [BenchmarkDotNet Documentation](https://benchmarkdotnet.org/articles/overview.html) diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Product.cs b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Product.cs new file mode 100644 index 0000000..304169d --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Product.cs @@ -0,0 +1,65 @@ +using System.ComponentModel.DataAnnotations; + +namespace ContosoOnlineStore +{ + public class Product + { + [Range(1, int.MaxValue, ErrorMessage = "Product ID must be positive")] + public int Id { get; } + + [Required(ErrorMessage = "Product name is required")] + [StringLength(100, MinimumLength = 1, ErrorMessage = "Product name must be between 1 and 100 characters")] + [RegularExpression(@"^[a-zA-Z0-9\s\-_\.]+$", ErrorMessage = "Product name contains invalid characters")] + public string Name { get; } + + [Range(0.01, 100000.00, ErrorMessage = "Product price must be between $0.01 and $100,000.00")] + public decimal Price { get; } + + [Range(0, int.MaxValue, ErrorMessage = "Initial stock cannot be negative")] + public int InitialStock { get; } + + public string Category { get; } + + public string Description { get; } + + public DateTime CreatedAt { get; } + + public Product(int id, string name, decimal price, int initialStock, string category = "General", string description = "") + { + if (id <= 0) + throw new ArgumentException("Product ID must be positive", nameof(id)); + + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("Product name cannot be empty", nameof(name)); + + if (price < 0.01m) + throw new ArgumentException("Product price must be at least $0.01", nameof(price)); + + if (initialStock < 0) + throw new ArgumentException("Initial stock cannot be negative", nameof(initialStock)); + + Id = id; + Name = name.Trim(); + Price = price; + InitialStock = initialStock; + Category = category.Trim(); + Description = description.Trim(); + CreatedAt = DateTime.UtcNow; + } + + public override string ToString() + { + return $"Product[{Id}]: {Name} - {Price:C}"; + } + + public override bool Equals(object? obj) + { + return obj is Product other && Id == other.Id; + } + + public override int GetHashCode() + { + return Id.GetHashCode(); + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/ProductCatalog.cs b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/ProductCatalog.cs new file mode 100644 index 0000000..19c0f68 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/ProductCatalog.cs @@ -0,0 +1,222 @@ +using ContosoOnlineStore.Configuration; +using ContosoOnlineStore.Exceptions; +using ContosoOnlineStore.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Collections.Concurrent; + +namespace ContosoOnlineStore +{ + public interface IProductCatalog + { + Product? GetProductById(int productId); + List GetAllProducts(); + List SearchProducts(string searchTerm); + List GetProductsByCategory(string category); + bool IsProductAvailable(int productId); + Task> GetProductsAsync(); + void InvalidateCache(); + } + + public class ProductCatalog : IProductCatalog + { + private readonly List _products; + private readonly Dictionary _productIndex; + private readonly ConcurrentDictionary> _searchCache; + private readonly ISecurityValidationService _securityValidation; + private readonly ILogger _logger; + private readonly AppSettings _appSettings; + private DateTime _lastCacheUpdate = DateTime.UtcNow; + + public ProductCatalog(ISecurityValidationService securityValidation, ILogger logger, IOptions appSettings) + { + _securityValidation = securityValidation; + _logger = logger; + _appSettings = appSettings.Value; + _products = new List(); + _productIndex = new Dictionary(); + _searchCache = new ConcurrentDictionary>(); + + InitializeProducts(); + BuildProductIndex(); + } + + private void InitializeProducts() + { + var products = new List + { + new Product(1, "iPhone 15 Pro", 999.99m, 50, "Electronics", "Latest iPhone with advanced camera system"), + new Product(2, "Sony WH-1000XM5", 399.99m, 200, "Electronics", "Premium noise-canceling headphones"), + new Product(3, "MacBook Pro 16-inch", 2499.00m, 20, "Computers", "High-performance laptop for professionals"), + new Product(4, "Dell UltraSharp 27", 389.50m, 75, "Monitors", "4K professional monitor with USB-C"), + new Product(5, "Anker PowerCore 20K", 49.99m, 500, "Accessories", "High-capacity portable charger"), + new Product(6, "Bose SoundLink Revolve", 199.95m, 120, "Audio", "360-degree Bluetooth speaker"), + new Product(7, "Samsung 980 PRO 2TB", 159.49m, 60, "Storage", "High-speed NVMe SSD drive"), + new Product(8, "Logitech MX Master 3", 99.99m, 150, "Accessories", "Advanced wireless mouse for productivity"), + new Product(9, "Logitech C920 HD Pro", 79.99m, 300, "Electronics", "Full HD webcam with autofocus"), + new Product(10, "Apple AirPods Pro 2", 249.99m, 80, "Audio", "Wireless earbuds with active noise cancellation"), + new Product(11, "Nintendo Switch OLED", 349.99m, 45, "Gaming", "Handheld gaming console with vibrant OLED screen"), + new Product(12, "Xbox Wireless Controller", 59.99m, 200, "Gaming", "Official wireless controller for Xbox"), + new Product(13, "Kindle Paperwhite", 139.99m, 150, "Electronics", "Waterproof e-reader with adjustable backlight"), + new Product(14, "Ring Video Doorbell", 99.99m, 100, "Security", "Smart doorbell with HD video and motion detection"), + new Product(15, "Echo Dot 5th Gen", 49.99m, 300, "Smart Home", "Compact smart speaker with Alexa"), + new Product(16, "iPad Air 5th Gen", 599.99m, 75, "Tablets", "Powerful tablet with M1 chip"), + new Product(17, "Samsung Galaxy Watch 6", 299.99m, 120, "Wearables", "Advanced smartwatch with health tracking"), + new Product(18, "GoPro HERO12 Black", 399.99m, 60, "Cameras", "Waterproof action camera with 5.3K video"), + new Product(19, "Fitbit Charge 6", 159.95m, 180, "Fitness", "Advanced fitness tracker with built-in GPS"), + new Product(20, "Tesla Model Y Wall Charger", 475.00m, 25, "Automotive", "High-powered home charging solution") + }; + + foreach (var product in products) + { + try + { + _securityValidation.ValidateProduct(product); + _products.Add(product); + _logger.LogDebug("Added product: {ProductName} (ID: {ProductId})", product.Name, product.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to add product {ProductId}: {ProductName}", product.Id, product.Name); + } + } + + _logger.LogInformation("Initialized product catalog with {ProductCount} products", _products.Count); + } + + private void BuildProductIndex() + { + _productIndex.Clear(); + foreach (var product in _products) + { + _productIndex[product.Id] = product; + } + _logger.LogDebug("Built product index for {ProductCount} products", _productIndex.Count); + } + + public Product? GetProductById(int productId) + { + if (productId <= 0) + { + _logger.LogWarning("Invalid product ID requested: {ProductId}", productId); + return null; + } + + // Performance bottleneck: Sometimes use inefficient linear search instead of index + if (productId % 3 == 0) // Intentional performance issue for training + { + _logger.LogDebug("Using linear search for product ID: {ProductId}", productId); + Thread.Sleep(10); // Simulate slow database query + return _products.FirstOrDefault(p => p.Id == productId); + } + + // Use efficient lookup most of the time + _productIndex.TryGetValue(productId, out var product); + return product; + } + + public List GetAllProducts() + { + _logger.LogDebug("Retrieved all {ProductCount} products", _products.Count); + + // Performance bottleneck: Create new list and sort every time + var result = new List(_products); + result.Sort((a, b) => a.Name.CompareTo(b.Name)); // Expensive sorting operation + + return result; + } + + public List SearchProducts(string searchTerm) + { + if (string.IsNullOrWhiteSpace(searchTerm)) + return new List(); + + var sanitizedTerm = _securityValidation.SanitizeInput(searchTerm.ToLowerInvariant()); + + // Check cache first (but with inefficient cache key generation) + var cacheKey = GenerateSlowCacheKey(sanitizedTerm); // Performance bottleneck + + if (_searchCache.TryGetValue(cacheKey, out var cachedResults)) + { + // Check if cache is still valid + if (DateTime.UtcNow.Subtract(_lastCacheUpdate).TotalMinutes < _appSettings.PerformanceSettings.CacheExpirationMinutes) + { + _logger.LogDebug("Retrieved search results from cache for term: {SearchTerm}", sanitizedTerm); + return cachedResults; + } + } + + // Performance bottleneck: Multiple iterations and string operations + var results = new List(); + foreach (var product in _products) // Could be optimized with LINQ + { + if (product.Name.ToLowerInvariant().Contains(sanitizedTerm) || + product.Category.ToLowerInvariant().Contains(sanitizedTerm) || + product.Description.ToLowerInvariant().Contains(sanitizedTerm)) + { + results.Add(product); + Thread.Sleep(1); // Simulate database query delay + } + } + + // Cache the results + _searchCache[cacheKey] = results; + _logger.LogInformation("Found {ResultCount} products for search term: {SearchTerm}", results.Count, sanitizedTerm); + + return results; + } + + public List GetProductsByCategory(string category) + { + if (string.IsNullOrWhiteSpace(category)) + return new List(); + + var sanitizedCategory = _securityValidation.SanitizeInput(category); + + // Performance bottleneck: Linear search instead of category index + var results = new List(); + foreach (var product in _products) + { + if (string.Equals(product.Category, sanitizedCategory, StringComparison.OrdinalIgnoreCase)) + { + results.Add(product); + Thread.Sleep(2); // Simulate slow query + } + } + + _logger.LogDebug("Found {ProductCount} products in category: {Category}", results.Count, sanitizedCategory); + return results; + } + + public bool IsProductAvailable(int productId) + { + var product = GetProductById(productId); + return product != null; + } + + public async Task> GetProductsAsync() + { + // Simulate async database operation with unnecessary delay + await Task.Delay(100); // Performance bottleneck + return GetAllProducts(); + } + + public void InvalidateCache() + { + _searchCache.Clear(); + _lastCacheUpdate = DateTime.UtcNow; + _logger.LogInformation("Product catalog cache invalidated"); + } + + private string GenerateSlowCacheKey(string searchTerm) + { + // Performance bottleneck: Inefficient cache key generation + var key = searchTerm; + for (int i = 0; i < 100; i++) // Unnecessary loop + { + key = key + i.ToString(); + } + return key.GetHashCode().ToString(); + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Program.cs b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Program.cs new file mode 100644 index 0000000..00e2794 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Program.cs @@ -0,0 +1,327 @@ +using ContosoOnlineStore; +using ContosoOnlineStore.Benchmarks; +using ContosoOnlineStore.Configuration; +using ContosoOnlineStore.Exceptions; +using ContosoOnlineStore.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Diagnostics; + +namespace ContosoOnlineStore +{ + class Program + { + private static ServiceProvider? _serviceProvider; + private static ILogger? _logger; + + static async Task Main(string[] args) + { + try + { + Console.WriteLine("=== Contoso Online Store - Performance Profiling Demo ==="); + Console.WriteLine(); + + // Setup dependency injection and configuration + SetupServices(); + _logger = _serviceProvider!.GetRequiredService>(); + + _logger.LogInformation("Starting Contoso Online Store application"); + + // Check for benchmark argument + if (args.Length > 0 && args[0].Equals("benchmark", StringComparison.OrdinalIgnoreCase)) + { + BenchmarkRunner.RunBenchmarks(); + return; + } + + // Run the main application demo + await RunApplicationDemoAsync(); + + // Run performance tests + await RunPerformanceTestsAsync(); + + _logger.LogInformation("Application completed successfully"); + } + catch (Exception ex) + { + Console.WriteLine($"Application error: {ex.Message}"); + _logger?.LogError(ex, "Application failed with error"); + Environment.Exit(1); + } + finally + { + (_serviceProvider as IDisposable)?.Dispose(); + Console.WriteLine("\nPress any key to exit..."); + Console.ReadKey(); + } + } + + private static void SetupServices() + { + // Build configuration + var configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .Build(); + + // Setup dependency injection + var services = new ServiceCollection(); + + // Configure logging + services.AddLogging(builder => + { + builder.AddConsole(); + builder.SetMinimumLevel(LogLevel.Information); + }); + + // Configure settings + services.Configure(configuration.GetSection("AppSettings")); + + // Register application services + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + _serviceProvider = services.BuildServiceProvider(); + } + + private static async Task RunApplicationDemoAsync() + { + try + { + Console.WriteLine("🏪 Initializing store components..."); + + var catalog = _serviceProvider!.GetRequiredService(); + var inventory = _serviceProvider.GetRequiredService(); + var processor = _serviceProvider.GetRequiredService(); + + Console.WriteLine($"✅ Loaded {catalog.GetAllProducts().Count} products"); + + // Create a realistic test order + Console.WriteLine("\n📋 Creating test order..."); + var order = new Order("customer@example.com", "123 Main Street, Seattle, WA 98101"); + order.AddItem(new OrderItem(1, 2)); // iPhone 15 Pro x2 + order.AddItem(new OrderItem(5, 1)); // Anker PowerCore 20K x1 + order.AddItem(new OrderItem(10, 3)); // Apple AirPods Pro 2 x3 + order.AddItem(new OrderItem(15, 1)); // Echo Dot 5th Gen x1 + + Console.WriteLine($"📦 Order {order.OrderId} created with {order.Items.Count} different products"); + + // Display initial inventory + Console.WriteLine("\n📊 Initial inventory levels:"); + foreach (var item in order.Items) + { + var product = catalog.GetProductById(item.ProductId); + var stock = inventory.GetStockLevel(item.ProductId); + Console.WriteLine($" - {product?.Name}: {stock} units available"); + } + + // Process order with timing + Console.WriteLine("\n⏱️ Processing order..."); + var stopwatch = Stopwatch.StartNew(); + + var result = await processor.ProcessOrderWithValidationAsync(order); + + stopwatch.Stop(); + + // Display results + if (result.Success) + { + Console.WriteLine($"✅ Order processed successfully!"); + Console.WriteLine($"💰 Total Cost: {result.TotalAmount:C}"); + Console.WriteLine($"⏱️ Processing Time: {stopwatch.ElapsedMilliseconds} ms"); + Console.WriteLine($"📋 Items Processed: {result.ProcessedItems}"); + + if (result.Warnings.Any()) + { + Console.WriteLine("\n⚠️ Warnings:"); + foreach (var warning in result.Warnings) + { + Console.WriteLine($" - {warning}"); + } + } + } + else + { + Console.WriteLine($"❌ Order processing failed: {result.ErrorMessage}"); + } + + // Display final inventory + Console.WriteLine("\n📊 Final inventory levels:"); + foreach (var item in order.Items) + { + var product = catalog.GetProductById(item.ProductId); + var stock = inventory.GetStockLevel(item.ProductId); + Console.WriteLine($" - {product?.Name}: {stock} units remaining"); + } + + // Generate receipt + if (result.Success) + { + Console.WriteLine("\n🧾 Generating receipt..."); + var receipt = await processor.GenerateOrderReceiptAsync(order); + Console.WriteLine(receipt); + } + + } + catch (Exception ex) + { + _logger!.LogError(ex, "Demo execution failed"); + Console.WriteLine($"❌ Demo failed: {ex.Message}"); + throw; + } + } + + private static async Task RunPerformanceTestsAsync() + { + Console.WriteLine("\n🔍 Running Performance Analysis..."); + Console.WriteLine("=" + new string('=', 49)); + + var catalog = _serviceProvider!.GetRequiredService(); + var inventory = _serviceProvider.GetRequiredService(); + var processor = _serviceProvider.GetRequiredService(); + + // Test 1: Product lookup performance + await TestProductLookupPerformance(catalog); + + // Test 2: Search performance + await TestSearchPerformance(catalog); + + // Test 3: Order processing performance + await TestOrderProcessingPerformance(processor, catalog); + + // Test 4: Inventory operations performance + await TestInventoryPerformance(inventory); + + // Test 5: Concurrent operations + await TestConcurrentOperations(processor, catalog); + + Console.WriteLine("\n📈 Performance analysis completed!"); + Console.WriteLine("💡 Tip: Run with 'benchmark' argument for detailed BenchmarkDotNet analysis"); + } + + private static async Task TestProductLookupPerformance(IProductCatalog catalog) + { + Console.WriteLine("\n🔎 Testing product lookup performance..."); + + var lookups = new List { 1, 3, 5, 7, 9, 12, 15, 18 }; + var sw = Stopwatch.StartNew(); + + for (int i = 0; i < 100; i++) + { + foreach (var productId in lookups) + { + var product = catalog.GetProductById(productId); + } + } + + sw.Stop(); + Console.WriteLine($" 800 product lookups completed in {sw.ElapsedMilliseconds} ms"); + Console.WriteLine($" Average: {(double)sw.ElapsedMilliseconds / 800:F2} ms per lookup"); + } + + private static async Task TestSearchPerformance(IProductCatalog catalog) + { + Console.WriteLine("\n🔍 Testing search performance..."); + + var searchTerms = new[] { "phone", "laptop", "audio", "gaming", "apple" }; + var sw = Stopwatch.StartNew(); + + foreach (var term in searchTerms) + { + var results = catalog.SearchProducts(term); + Console.WriteLine($" Search '{term}': {results.Count} results"); + } + + sw.Stop(); + Console.WriteLine($" {searchTerms.Length} searches completed in {sw.ElapsedMilliseconds} ms"); + } + + private static async Task TestOrderProcessingPerformance(IOrderProcessor processor, IProductCatalog catalog) + { + Console.WriteLine("\n📋 Testing order processing performance..."); + + var orders = CreateTestOrders(5); + var sw = Stopwatch.StartNew(); + var successCount = 0; + + foreach (var order in orders) + { + try + { + var result = await processor.ProcessOrderWithValidationAsync(order); + if (result.Success) successCount++; + } + catch (Exception ex) + { + Console.WriteLine($" Order {order.OrderId} failed: {ex.Message}"); + } + } + + sw.Stop(); + Console.WriteLine($" {successCount}/{orders.Count} orders processed in {sw.ElapsedMilliseconds} ms"); + Console.WriteLine($" Average: {(double)sw.ElapsedMilliseconds / orders.Count:F2} ms per order"); + } + + private static async Task TestInventoryPerformance(IInventoryManager inventory) + { + Console.WriteLine("\n📦 Testing inventory operations performance..."); + + var sw = Stopwatch.StartNew(); + var lowStockProducts = inventory.GetLowStockProducts(50); + sw.Stop(); + + Console.WriteLine($" Low stock check: {lowStockProducts.Count} products found in {sw.ElapsedMilliseconds} ms"); + } + + private static async Task TestConcurrentOperations(IOrderProcessor processor, IProductCatalog catalog) + { + Console.WriteLine("\n🔄 Testing concurrent operations..."); + + var tasks = new List(); + var sw = Stopwatch.StartNew(); + + // Simulate concurrent product lookups + for (int i = 0; i < 10; i++) + { + tasks.Add(Task.Run(() => + { + for (int j = 1; j <= 20; j++) + { + catalog.GetProductById(j % 20 + 1); + } + })); + } + + await Task.WhenAll(tasks); + sw.Stop(); + + Console.WriteLine($" 10 concurrent tasks (200 total operations) completed in {sw.ElapsedMilliseconds} ms"); + } + + private static List CreateTestOrders(int count) + { + var orders = new List(); + var random = new Random(12345); // Fixed seed for consistent results + + for (int i = 0; i < count; i++) + { + var order = new Order($"customer{i}@example.com", $"{i + 100} Test Street, Test City, WA 98101"); + + // Add random items to each order + for (int j = 0; j < random.Next(1, 5); j++) + { + var productId = random.Next(1, 21); + var quantity = random.Next(1, 4); + order.AddItem(new OrderItem(productId, quantity)); + } + + orders.Add(order); + } + + return orders; + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/README.md b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/README.md new file mode 100644 index 0000000..628b2ad --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/README.md @@ -0,0 +1,244 @@ +# Contoso Online Store - Performance Profiling Training + +## Overview + +This project is a realistic e-commerce application designed for training developers on performance profiling and optimization using GitHub Copilot. The application simulates a real-world online store with intentional performance bottlenecks and security considerations. + +## Project Structure + +``` +ContosoOnlineStore/ +├── Program.cs # Main application entry point +├── appsettings.json # Configuration settings +├── ContosoOnlineStore.csproj # Project file with dependencies +├── Configuration/ +│ └── AppSettings.cs # Configuration models +├── Services/ +│ ├── SecurityValidationService.cs # Input validation and security +│ └── EmailService.cs # Email notification service +├── Exceptions/ +│ └── CustomExceptions.cs # Domain-specific exceptions +├── Benchmarks/ +│ └── OrderProcessingBenchmarks.cs # BenchmarkDotNet performance tests +├── Product.cs # Product entity with validation +├── Order.cs # Order entity with business logic +├── OrderItem.cs # Order line item +├── ProductCatalog.cs # Product management with search/caching +├── InventoryManager.cs # Inventory tracking and management +└── OrderProcessor.cs # Order processing workflow +``` + +## Features + +### Core Business Logic +- **Product Catalog**: 20 realistic products across different categories +- **Inventory Management**: Stock tracking, reservations, and low-stock alerts +- **Order Processing**: Complete order workflow with validation and receipts +- **Email Notifications**: Order confirmations and shipping notifications +- **Security Validation**: Input sanitization and business rule enforcement + +### Security Features (Training Appropriate) +- Input validation and sanitization +- SQL injection prevention patterns +- Business rule validation +- Error handling and logging +- Configuration-based security settings + +### Performance Bottlenecks (Intentional for Training) +1. **Inefficient Database Queries** + - Linear searches instead of indexed lookups + - N+1 query patterns + - Unnecessary delays simulating slow queries + +2. **Caching Issues** + - Inefficient cache key generation + - Missing cache implementations + - Cache invalidation problems + +3. **Synchronous Operations** + - Blocking async operations + - Sequential processing where parallel would be better + - Unnecessary Task.Delay calls + +4. **Memory and Resource Issues** + - Inefficient string building + - Excessive object allocation + - Resource leaks in loops + +5. **Algorithmic Inefficiencies** + - O(n) operations that could be O(1) + - Redundant calculations + - Inefficient data structures + +## Getting Started + +### Prerequisites +- .NET 9.0 SDK +- Visual Studio Code or Visual Studio +- GitHub Copilot extension + +### Running the Application + +1. **Basic Demo**: + ```bash + dotnet run + ``` + +2. **Performance Benchmarks**: + ```bash + dotnet run benchmark + ``` + +3. **Build and Test**: + ```bash + dotnet build + dotnet test + ``` + +### Expected Output + +The application will demonstrate: +- Order processing with performance timing +- Inventory management +- Email notifications (simulated) +- Performance metrics and bottleneck identification + +## Performance Training Scenarios + +### Scenario 1: Product Lookup Optimization +**Problem**: Linear search through product catalog +**Location**: `ProductCatalog.GetProductById()` +**Improvement**: Implement dictionary-based lookups + +### Scenario 2: Search Performance +**Problem**: Inefficient string searching and caching +**Location**: `ProductCatalog.SearchProducts()` +**Improvement**: Optimize search algorithms and caching strategy + +### Scenario 3: Order Processing Bottlenecks +**Problem**: Sequential processing and redundant operations +**Location**: `OrderProcessor.FinalizeOrderAsync()` +**Improvement**: Parallel processing and operation batching + +### Scenario 4: Inventory Management +**Problem**: Individual stock checks and slow logging +**Location**: `InventoryManager.GetLowStockProducts()` +**Improvement**: Batch operations and async logging + +### Scenario 5: Email Service Delays +**Problem**: Sequential email sending and content generation +**Location**: `EmailService.SendConfirmationAsync()` +**Improvement**: Parallel processing and content caching + +## Security Considerations (Training Context) + +This project includes basic security practices appropriate for training: + +### Input Validation +- Email format validation +- Product name sanitization +- Quantity bounds checking +- Price validation ranges + +### Business Logic Security +- Inventory overflow protection +- Order size limitations +- Negative inventory prevention +- Price calculation validation + +### Error Handling +- Custom exception types +- Proper error logging +- Resource cleanup +- Transaction rollback simulation + +## Configuration + +The application uses `appsettings.json` for configuration: + +```json +{ + "AppSettings": { + "MaxOrderItems": 50, + "EmailTimeoutMs": 2000, + "SecuritySettings": { + "MaxProductPrice": 10000.00, + "AllowNegativeInventory": false + }, + "PerformanceSettings": { + "CacheExpirationMinutes": 30, + "DatabaseTimeoutMs": 5000 + } + } +} +``` + +## Performance Monitoring + +### Built-in Metrics +The application tracks: +- Order processing times +- Product lookup performance +- Search operation timing +- Inventory update duration +- Email sending delays + +### BenchmarkDotNet Integration +For detailed performance analysis: +```bash +dotnet run benchmark +``` + +This will generate detailed performance reports including: +- Method execution times +- Memory allocation patterns +- Garbage collection impact +- Performance comparisons + +## Learning Objectives + +By working with this project, developers will learn to: + +1. **Identify Performance Bottlenecks** + - Using profiling tools + - Analyzing execution patterns + - Understanding performance metrics + +2. **Apply Optimization Techniques** + - Algorithmic improvements + - Caching strategies + - Asynchronous programming patterns + +3. **Use GitHub Copilot for Performance** + - Generating optimized code + - Suggesting performance improvements + - Creating benchmark tests + +4. **Implement Security Best Practices** + - Input validation patterns + - Error handling strategies + - Configuration management + +## Performance Improvement Checklist + +When optimizing this application, consider: + +- [ ] Replace linear searches with indexed lookups +- [ ] Implement efficient caching mechanisms +- [ ] Convert synchronous operations to asynchronous +- [ ] Batch database operations +- [ ] Optimize string operations and memory usage +- [ ] Add connection pooling (simulated) +- [ ] Implement parallel processing where appropriate +- [ ] Reduce unnecessary delays and waits +- [ ] Optimize logging and monitoring overhead + +## Support and Resources + +- [BenchmarkDotNet Documentation](https://benchmarkdotnet.org/) +- [.NET Performance Best Practices](https://docs.microsoft.com/en-us/dotnet/core/performance/) +- [GitHub Copilot for Performance](https://docs.github.com/en/copilot) + +## License + +This project is designed for educational purposes as part of Microsoft Learn training materials. diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Services/EmailService.cs b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Services/EmailService.cs new file mode 100644 index 0000000..9b11ae1 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Services/EmailService.cs @@ -0,0 +1,259 @@ +using ContosoOnlineStore.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Net.Mail; +using System.Text; +using System.Text.RegularExpressions; + +namespace ContosoOnlineStore.Services +{ + public interface IEmailService + { + Task SendConfirmationAsync(Order order); + Task SendShippingNotificationAsync(Order order, string trackingNumber); + Task SendLowStockAlertAsync(Dictionary lowStockProducts); + Task ValidateEmailAsync(string email); + } + + public class EmailService : IEmailService + { + private readonly ILogger _logger; + private readonly AppSettings _appSettings; + private readonly IProductCatalog _catalog; + private static readonly Regex EmailRegex = new(@"^[^@\s]+@[^@\s]+\.[^@\s]+$", RegexOptions.Compiled); + + public EmailService(ILogger logger, IOptions appSettings, IProductCatalog catalog) + { + _logger = logger; + _appSettings = appSettings.Value; + _catalog = catalog; + } + + public async Task SendConfirmationAsync(Order order) + { + if (order == null) + throw new ArgumentNullException(nameof(order)); + + try + { + _logger.LogInformation("Sending order confirmation email for order {OrderId}", order.OrderId); + + // Validate email address + if (!await ValidateEmailAsync(order.CustomerEmail)) + { + _logger.LogWarning("Invalid email address for order {OrderId}: {Email}", order.OrderId, order.CustomerEmail); + return false; + } + + // Performance bottleneck: Generate email content inefficiently + var emailContent = await GenerateOrderConfirmationEmailAsync(order); + + // Simulate sending email with configurable delay + await Task.Delay(_appSettings.EmailTimeoutMs); + + // Performance bottleneck: Log detailed email information + await LogEmailDetailsAsync("Order Confirmation", order.CustomerEmail, emailContent); + + _logger.LogInformation("Order confirmation email sent successfully for order {OrderId}", order.OrderId); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send order confirmation email for order {OrderId}", order.OrderId); + return false; + } + } + + public async Task SendShippingNotificationAsync(Order order, string trackingNumber) + { + if (order == null) + throw new ArgumentNullException(nameof(order)); + + if (string.IsNullOrWhiteSpace(trackingNumber)) + throw new ArgumentException("Tracking number cannot be empty", nameof(trackingNumber)); + + try + { + _logger.LogInformation("Sending shipping notification email for order {OrderId}", order.OrderId); + + if (!await ValidateEmailAsync(order.CustomerEmail)) + { + _logger.LogWarning("Invalid email address for shipping notification {OrderId}: {Email}", order.OrderId, order.CustomerEmail); + return false; + } + + var emailContent = await GenerateShippingNotificationEmailAsync(order, trackingNumber); + + // Simulate email sending delay + await Task.Delay(_appSettings.EmailTimeoutMs / 2); + + await LogEmailDetailsAsync("Shipping Notification", order.CustomerEmail, emailContent); + + _logger.LogInformation("Shipping notification email sent successfully for order {OrderId}", order.OrderId); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send shipping notification email for order {OrderId}", order.OrderId); + return false; + } + } + + public async Task SendLowStockAlertAsync(Dictionary lowStockProducts) + { + if (lowStockProducts == null || !lowStockProducts.Any()) + return true; + + try + { + _logger.LogInformation("Sending low stock alert for {ProductCount} products", lowStockProducts.Count); + + var emailContent = await GenerateLowStockAlertEmailAsync(lowStockProducts); + + // Simulate sending to multiple administrators + var adminEmails = new[] { "admin@contoso.com", "inventory@contoso.com", "manager@contoso.com" }; + + foreach (var adminEmail in adminEmails) + { + await Task.Delay(100); // Performance bottleneck: Sequential email sending + await LogEmailDetailsAsync("Low Stock Alert", adminEmail, emailContent); + } + + _logger.LogInformation("Low stock alert emails sent successfully"); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send low stock alert emails"); + return false; + } + } + + public async Task ValidateEmailAsync(string email) + { + if (string.IsNullOrWhiteSpace(email)) + return false; + + // Performance bottleneck: Unnecessary async operation for simple validation + await Task.Delay(10); // Simulate validation delay + + // Basic email validation + if (email.Length > 254) // RFC 5321 limit + return false; + + if (!EmailRegex.IsMatch(email)) + return false; + + // Additional security checks + var suspiciousPatterns = new[] { "script", "javascript", "<", ">", "eval", "exec" }; + var lowerEmail = email.ToLowerInvariant(); + + foreach (var pattern in suspiciousPatterns) + { + if (lowerEmail.Contains(pattern)) + { + _logger.LogWarning("Suspicious email pattern detected: {Email}", email); + return false; + } + } + + return true; + } + + private async Task GenerateOrderConfirmationEmailAsync(Order order) + { + // Performance bottleneck: Inefficient string building + var emailBuilder = new StringBuilder(); + emailBuilder.AppendLine("Dear Customer,"); + emailBuilder.AppendLine(); + emailBuilder.AppendLine($"Thank you for your order #{order.OrderId}!"); + emailBuilder.AppendLine($"Order Date: {order.OrderDate:yyyy-MM-dd HH:mm:ss}"); + emailBuilder.AppendLine(); + emailBuilder.AppendLine("Order Details:"); + + decimal totalAmount = 0; + foreach (var item in order.Items) + { + // Performance bottleneck: Individual product lookups in loop + await Task.Delay(5); // Simulate database query + var product = _catalog.GetProductById(item.ProductId); + if (product != null) + { + var itemTotal = product.Price * item.Quantity; + totalAmount += itemTotal; + emailBuilder.AppendLine($"- {product.Name} x {item.Quantity} = {itemTotal:C}"); + } + } + + emailBuilder.AppendLine(); + emailBuilder.AppendLine($"Total Amount: {totalAmount:C}"); + emailBuilder.AppendLine(); + emailBuilder.AppendLine($"Shipping Address: {order.ShippingAddress}"); + emailBuilder.AppendLine(); + emailBuilder.AppendLine("Your order will be processed within 24 hours."); + emailBuilder.AppendLine(); + emailBuilder.AppendLine("Thank you for shopping with Contoso Online Store!"); + + return emailBuilder.ToString(); + } + + private async Task GenerateShippingNotificationEmailAsync(Order order, string trackingNumber) + { + await Task.Delay(50); // Performance bottleneck: Unnecessary delay + + return $@"Dear Customer, + +Your order #{order.OrderId} has been shipped! + +Tracking Number: {trackingNumber} +Estimated Delivery: {DateTime.Now.AddDays(3):yyyy-MM-dd} + +You can track your package at: https://tracking.contoso.com/{trackingNumber} + +Thank you for shopping with Contoso Online Store!"; + + } + + private async Task GenerateLowStockAlertEmailAsync(Dictionary lowStockProducts) + { + var emailBuilder = new StringBuilder(); + emailBuilder.AppendLine("Low Stock Alert - Contoso Online Store"); + emailBuilder.AppendLine(); + emailBuilder.AppendLine("The following products are running low on stock:"); + emailBuilder.AppendLine(); + + foreach (var kvp in lowStockProducts) + { + // Performance bottleneck: Individual product lookups + await Task.Delay(10); + var product = _catalog.GetProductById(kvp.Key); + if (product != null) + { + emailBuilder.AppendLine($"- {product.Name} (ID: {kvp.Key}): {kvp.Value} units remaining"); + } + } + + emailBuilder.AppendLine(); + emailBuilder.AppendLine("Please consider restocking these items."); + emailBuilder.AppendLine(); + emailBuilder.AppendLine($"Generated at: {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); + + return emailBuilder.ToString(); + } + + private async Task LogEmailDetailsAsync(string emailType, string recipient, string content) + { + // Performance bottleneck: Detailed logging that could be optimized + await Task.Delay(20); + + _logger.LogDebug("Email sent - Type: {EmailType}, Recipient: {Recipient}, Content Length: {ContentLength}", + emailType, recipient, content.Length); + + if (_appSettings.EnableDetailedLogging) + { + _logger.LogTrace("Email content preview: {ContentPreview}...", + content.Length > 100 ? content[..100] : content); + } + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Services/SecurityValidationService.cs b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Services/SecurityValidationService.cs new file mode 100644 index 0000000..2c12b41 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/Services/SecurityValidationService.cs @@ -0,0 +1,108 @@ +using ContosoOnlineStore.Configuration; +using ContosoOnlineStore.Exceptions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.ComponentModel.DataAnnotations; +using System.Text.RegularExpressions; + +namespace ContosoOnlineStore.Services +{ + public interface ISecurityValidationService + { + void ValidateProduct(Product product); + void ValidateOrder(Order order); + void ValidateOrderItem(OrderItem item, Product product); + string SanitizeInput(string input); + } + + public class SecurityValidationService : ISecurityValidationService + { + private readonly SecuritySettings _securitySettings; + private readonly ILogger _logger; + private static readonly Regex AllowedCharactersRegex = new(@"^[a-zA-Z0-9\s\-_\.@]+$", RegexOptions.Compiled); + + public SecurityValidationService(IOptions appSettings, ILogger logger) + { + _securitySettings = appSettings.Value.SecuritySettings; + _logger = logger; + } + + public void ValidateProduct(Product product) + { + if (product == null) + throw new ArgumentNullException(nameof(product)); + + if (string.IsNullOrWhiteSpace(product.Name)) + throw new SecurityValidationException("Product name cannot be empty."); + + if (product.Name.Length > 100) + throw new SecurityValidationException("Product name exceeds maximum length of 100 characters."); + + if (product.Price < _securitySettings.MinProductPrice || product.Price > _securitySettings.MaxProductPrice) + throw new SecurityValidationException($"Product price must be between {_securitySettings.MinProductPrice:C} and {_securitySettings.MaxProductPrice:C}."); + + if (product.InitialStock < 0) + throw new SecurityValidationException("Initial stock cannot be negative."); + + // Validate product name contains only allowed characters + if (!AllowedCharactersRegex.IsMatch(product.Name)) + throw new SecurityValidationException("Product name contains invalid characters."); + + _logger.LogDebug("Product {ProductId} validated successfully", product.Id); + } + + public void ValidateOrder(Order order) + { + if (order == null) + throw new ArgumentNullException(nameof(order)); + + if (order.Items == null || !order.Items.Any()) + throw new InvalidOrderException("Order must contain at least one item."); + + if (order.Items.Count > 50) // Hard limit for security + throw new InvalidOrderException("Order cannot contain more than 50 items."); + + _logger.LogDebug("Order with {ItemCount} items validated successfully", order.Items.Count); + } + + public void ValidateOrderItem(OrderItem item, Product product) + { + if (item == null) + throw new ArgumentNullException(nameof(item)); + + if (product == null) + throw new ArgumentNullException(nameof(product)); + + if (item.Quantity <= 0) + throw new InvalidOrderException("Order item quantity must be positive."); + + if (item.Quantity > 1000) // Reasonable limit to prevent abuse + throw new InvalidOrderException("Order item quantity cannot exceed 1000 units."); + + // Calculate potential overflow + try + { + var totalPrice = product.Price * item.Quantity; + if (totalPrice > decimal.MaxValue / 2) // Conservative check + throw new InvalidOrderException("Order item total price would cause overflow."); + } + catch (OverflowException) + { + throw new InvalidOrderException("Order item calculation would cause numeric overflow."); + } + + _logger.LogDebug("Order item for product {ProductId} with quantity {Quantity} validated successfully", + item.ProductId, item.Quantity); + } + + public string SanitizeInput(string input) + { + if (string.IsNullOrEmpty(input)) + return string.Empty; + + // Remove potentially dangerous characters and limit length + var sanitized = Regex.Replace(input.Trim(), @"[<>""'%;()&+]", ""); + return sanitized.Length > 255 ? sanitized[..255] : sanitized; + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/appsettings.json b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/appsettings.json new file mode 100644 index 0000000..0797f54 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/ContosoOnlineStore/appsettings.json @@ -0,0 +1,24 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AppSettings": { + "MaxOrderItems": 50, + "EmailTimeoutMs": 2000, + "EnableDetailedLogging": true, + "SecuritySettings": { + "MaxProductPrice": 10000.00, + "MinProductPrice": 0.01, + "AllowNegativeInventory": false + }, + "PerformanceSettings": { + "CacheExpirationMinutes": 30, + "DatabaseTimeoutMs": 5000, + "MaxConcurrentOrders": 100 + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/DataAnalyzer.cs b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/DataAnalyzer.cs new file mode 100644 index 0000000..c5575f4 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/DataAnalyzer.cs @@ -0,0 +1,35 @@ +using System; +using System.IO; + +namespace DataAnalyzerReporter +{ + public static class DataAnalyzer + { + public static void ProcessAllRecords(string[] records, string outputPath) + { + int count = 0; + + foreach (string record in records) + { + if (string.IsNullOrWhiteSpace(record)) continue; + + string[] parts = record.Split(','); + double sum = 0; + + foreach (string part in parts) + { + if (double.TryParse(part, out double value)) + { + sum += value; + } + } + + string result = $"Sum={sum} for record: {record}"; + ReportGenerator.AppendLineToReport(outputPath, result); + count++; + } + + Console.WriteLine($"Processed {count} records"); + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/DataAnalyzerReporter.csproj b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/DataAnalyzerReporter.csproj new file mode 100644 index 0000000..fd4bd08 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/DataAnalyzerReporter.csproj @@ -0,0 +1,10 @@ + + + + Exe + net9.0 + enable + enable + + + diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/FileLoader.cs b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/FileLoader.cs new file mode 100644 index 0000000..3ed5f9d --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/FileLoader.cs @@ -0,0 +1,15 @@ +using System; +using System.IO; + +namespace DataAnalyzerReporter +{ + public static class FileLoader + { + public static string[] LoadAllData(string filePath) + { + Console.WriteLine($"Reading file: {filePath}"); + // Performance Issue: High memory usage + return File.ReadAllLines(filePath); + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/Program.cs b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/Program.cs new file mode 100644 index 0000000..6ea6cc7 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/Program.cs @@ -0,0 +1,41 @@ +using System; +using System.Diagnostics; +using System.IO; + +namespace DataAnalyzerReporter +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("=== DataAnalyzerReporter ==="); + + if (args.Length < 1 || !File.Exists(args[0])) + { + Console.WriteLine("Usage: DataAnalyzerReporter "); + return; + } + + string inputFile = args[0]; + string outputFile = "output.txt"; + + if (File.Exists(outputFile)) + { + File.Delete(outputFile); + } + + string[] records = FileLoader.LoadAllData(inputFile); + Console.WriteLine($"Loaded {records.Length} records"); + + long memUsage = GC.GetTotalMemory(false); + Console.WriteLine($"Memory usage: {memUsage / 1024} KB"); + + Stopwatch sw = Stopwatch.StartNew(); + DataAnalyzer.ProcessAllRecords(records, outputFile); + sw.Stop(); + + Console.WriteLine($"Processing time: {sw.ElapsedMilliseconds} ms"); + Console.WriteLine($"Results written to {outputFile}"); + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/README.md b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/README.md new file mode 100644 index 0000000..b1a7075 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/README.md @@ -0,0 +1,32 @@ +# DataAnalyzerReporter + +A C# console application that reads CSV data from a file, processes it by calculating sums for each record, and generates a report. + +## Usage + +``` +DataAnalyzerReporter +``` + +## Example + +``` +DataAnalyzerReporter data.txt +``` + +This will read the data.txt file, process each line as comma-separated values, calculate the sum of numeric values in each line, and output the results to output.txt. + +## Performance Notes + +The application includes performance monitoring and reports: +- Memory usage +- Processing time +- Number of records processed + +## Files + +- `Program.cs` - Main entry point +- `FileLoader.cs` - File reading utilities +- `DataAnalyzer.cs` - Data processing logic +- `ReportGenerator.cs` - Report generation +- `data.txt` - Sample input data diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/ReportGenerator.cs b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/ReportGenerator.cs new file mode 100644 index 0000000..8dde7c7 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/ReportGenerator.cs @@ -0,0 +1,13 @@ +using System.IO; + +namespace DataAnalyzerReporter +{ + public static class ReportGenerator + { + public static void AppendLineToReport(string filePath, string line) + { + // Performance Issue: Inefficient I/O + File.AppendAllText(filePath, line + "\n"); + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/data.txt b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/data.txt new file mode 100644 index 0000000..9e1a878 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/data.txt @@ -0,0 +1,10 @@ +1.5,2.3,4.7,8.1 +3.2,5.8,7.4,2.9 +9.1,1.6,3.5,6.2 +4.8,7.3,2.1,9.4 +8.7,3.4,6.9,1.2 +5.5,9.8,4.3,7.6 +2.9,6.1,8.4,3.7 +7.2,4.5,9.3,1.8 +6.4,2.7,5.1,8.9 +1.3,7.9,4.6,2.5 diff --git a/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/output.txt b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/output.txt new file mode 100644 index 0000000..aaf65dd --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/implement-performance-profiling/DataAnalyzerReporter/output.txt @@ -0,0 +1,10 @@ +Sum=16.6 for record: 1.5,2.3,4.7,8.1 +Sum=19.299999999999997 for record: 3.2,5.8,7.4,2.9 +Sum=20.4 for record: 9.1,1.6,3.5,6.2 +Sum=23.6 for record: 4.8,7.3,2.1,9.4 +Sum=20.2 for record: 8.7,3.4,6.9,1.2 +Sum=27.200000000000003 for record: 5.5,9.8,4.3,7.6 +Sum=21.099999999999998 for record: 2.9,6.1,8.4,3.7 +Sum=22.8 for record: 7.2,4.5,9.3,1.8 +Sum=23.1 for record: 6.4,2.7,5.1,8.9 +Sum=16.3 for record: 1.3,7.9,4.6,2.5 diff --git a/DownloadableCodeProjects/standalone-lab-projects/readme.txt b/DownloadableCodeProjects/standalone-lab-projects/readme.txt new file mode 100644 index 0000000..a047f5b --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/readme.txt @@ -0,0 +1,3 @@ +The "Resolve GitHub issues using GitHub Copilot" lab exercise imports a code project repository rather than downloading a Zip file. + +The code repository can be found here: https://github.com/MicrosoftLearning/resolve-github-issues-lab-project diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/ARCHITECTURE_COMPARISON.md b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/ARCHITECTURE_COMPARISON.md new file mode 100644 index 0000000..5165aa3 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/ARCHITECTURE_COMPARISON.md @@ -0,0 +1,113 @@ +# Architecture Comparison: Before vs After + +## Before: Single-File Monolithic Design + +### Problems with Original Approach: +- **876 lines** in a single `Program.cs` file +- **20+ classes** mixed together without organization +- **No separation of concerns** - domain models mixed with infrastructure services +- **Tight coupling** - services created with `new` keyword directly in OrderProcessor +- **Hard to test** - no interfaces, everything concrete +- **Not realistic** - real applications don't structure code this way +- **Poor maintainability** - difficult to find and modify specific functionality + +### Original File Structure: +``` +ECommerceOrderProcessing/ +├── Program.cs (876 lines - everything in one file!) +├── ECommerceOrderProcessing.csproj +└── README.md +``` + +## After: Layered Architecture Design + +### Improvements with New Approach: +- **Proper layered architecture** following Clean Architecture principles +- **Separation of concerns** - domain, infrastructure, and presentation layers +- **Dependency injection** - services injected via constructor +- **Interface-based design** - all services implement interfaces +- **Testable design** - services can be easily mocked for unit testing +- **Realistic structure** - mirrors real-world enterprise applications +- **Better maintainability** - related code grouped in logical folders +- **Scalable design** - easy to add new features and services + +### New File Structure: +``` +ECommerceOrderProcessing/ +├── src/ +│ ├── ECommerce.ApplicationCore/ # Domain Layer (46 lines avg per file) +│ │ ├── Entities/ # 6 focused entity classes +│ │ ├── Interfaces/ # 6 service interfaces +│ │ └── Services/ +│ │ └── OrderProcessor.cs # LARGE METHOD PRESERVED HERE +│ ├── ECommerce.Infrastructure/ # Infrastructure Layer +│ │ └── Services/ # Service implementations +│ └── ECommerce.Console/ # Presentation Layer +│ └── Program.cs # Clean startup code +├── ECommerceOrderProcessing.sln # Solution file +└── README.md # Updated documentation +``` + +## Key Architectural Benefits + +### 1. **Domain-Driven Design** +- **Entities folder**: Clear domain models (`Order`, `OrderItem`, `PaymentInfo`) +- **Interfaces folder**: Service contracts defining business operations +- **Services folder**: Business logic (`OrderProcessor` with the large method to refactor) + +### 2. **Dependency Inversion Principle** +- OrderProcessor depends on interfaces, not concrete implementations +- Services can be easily swapped or mocked for testing +- Better testability and flexibility + +### 3. **Single Responsibility Principle** +- Each class has a focused responsibility +- Easier to understand and maintain +- Clear boundaries between different concerns + +### 4. **Real-World Patterns** +- **Repository pattern**: `IInventoryService` for data access abstraction +- **Service pattern**: `IPaymentGateway`, `IShippingService` for external services +- **Factory pattern**: Services created via dependency injection +- **Strategy pattern**: Different implementations can be plugged in + +## The Large Method Preservation + +The large `ProcessOrder()` method is **intentionally preserved** in `OrderProcessor.cs` for educational purposes: + +### Why It's Still There: +- **Training focus**: This is the method students will refactor +- **Realistic complexity**: Contains multiple responsibilities that mirror real-world scenarios +- **Clear boundaries**: Now properly separated from infrastructure concerns +- **Better context**: Surrounded by proper architecture makes the problem clearer + +### What Changed: +- **Dependency injection**: Services are injected, not created with `new` +- **Interface-based**: Uses abstractions instead of concrete types +- **Focused location**: Lives in the ApplicationCore.Services layer where it belongs +- **Clear documentation**: Well-documented with refactoring goals + +## Benefits for the Training Course + +### For Students: +1. **Real-world exposure**: Learn proper .NET architecture patterns +2. **Best practices**: See how enterprise applications are structured +3. **Clear focus**: The large method is now easier to identify and understand +4. **Better refactoring context**: Understand where different responsibilities should go + +### For Instructors: +1. **Teaching opportunities**: Can explain layered architecture concepts +2. **Before/after comparison**: Show evolution from monolith to clean architecture +3. **Multiple learning objectives**: Architecture + refactoring in one exercise +4. **Professional relevance**: Students learn patterns used in real jobs + +## Testing Verification + +Both architectures produce **identical output**: +- All 4 test cases pass +- Same console output +- Same audit logging behavior +- Same processing times +- Same functionality + +This proves that refactoring to better architecture **maintains functionality** while **improving code quality**. diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/Program.cs b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/Program.cs new file mode 100644 index 0000000..e69de29 diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/README.md b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/README.md new file mode 100644 index 0000000..c9c0686 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/README.md @@ -0,0 +1,174 @@ +# E-Commerce Order Processing System + +## Overview + +This is a sample e-commerce order processing application designed for educational purposes in a training course that teaches developers how to refactor large functions into smaller, single-purpose functions using GitHub Copilot. + +## Project Structure + +The application follows a layered architecture pattern commonly used in real-world .NET applications: + +``` +src/ +├── ECommerce.ApplicationCore/ # Domain layer (entities, interfaces, business logic) +│ ├── Entities/ # Domain models +│ │ ├── Order.cs +│ │ ├── OrderItem.cs +│ │ ├── PaymentInfo.cs +│ │ ├── OrderStatus.cs +│ │ ├── ShippingDetails.cs +│ │ └── OrderResult.cs +│ ├── Interfaces/ # Service abstractions +│ │ ├── IInventoryService.cs +│ │ ├── IPaymentGateway.cs +│ │ ├── IShippingService.cs +│ │ ├── INotificationService.cs +│ │ ├── ISecurityValidator.cs +│ │ └── IAuditLogger.cs +│ └── Services/ +│ └── OrderProcessor.cs # MAIN LARGE METHOD TO REFACTOR +├── ECommerce.Infrastructure/ # Infrastructure layer (service implementations) +│ └── Services/ +│ ├── SecurityValidator.cs +│ ├── AuditLogger.cs +│ └── ExternalServices.cs +└── ECommerce.Console/ # Presentation layer (console app) + └── Program.cs +``` + +## Key Features + +This application demonstrates real-world e-commerce scenarios with: + +### Business Logic + +- **Order Processing**: Complete order lifecycle from validation to completion +- **Inventory Management**: Stock checking and reservation +- **Payment Processing**: Secure payment validation and processing +- **Shipping Management**: Shipment scheduling and tracking +- **Customer Notifications**: Order confirmations and alerts + +### Security Features + +- **Input Validation**: Comprehensive validation of all user inputs +- **Email Security**: Email format validation and suspicious domain detection +- **Payment Security**: Credit card validation and fraud detection +- **Risk Assessment**: Automated risk scoring for suspicious orders +- **Data Masking**: Sensitive information masking in logs + +### Audit & Compliance + +- **Audit Logging**: Complete audit trail of all order processing activities +- **Security Events**: Logging of security-related events and failures +- **Error Handling**: Comprehensive error handling with cleanup procedures + +### Real-World Architecture + +- **Dependency Injection**: Proper service abstractions and implementations +- **Separation of Concerns**: Clear separation between domain, infrastructure, and presentation layers +- **Interface-Based Design**: All services implement interfaces for testability +- **Layered Architecture**: Follows Clean Architecture principles + +## The Refactoring Challenge + +### Current State: Large Function + +The `OrderProcessor.ProcessOrder()` method is intentionally large (200+ lines) and contains multiple responsibilities: + +1. **Input Validation** - Validating order data, email formats, payment info +2. **Security Checks** - Risk assessment, fraud detection, suspicious pattern detection +3. **Inventory Management** - Stock checking, reservation, and release +4. **Payment Processing** - Payment validation, processing, and error handling +5. **Shipping Management** - Shipping validation, scheduling, and tracking +6. **Notification Management** - Sending confirmations and alerts +7. **Order Finalization** - Status updates, completion tracking, and cleanup +8. **Audit Logging** - Security and business event logging +9. **Error Handling** - Exception management and recovery procedures + +### Refactoring Goals + +Students should refactor this large method into smaller, focused methods such as: + +- `ValidateOrderInput()` +- `PerformSecurityChecks()` +- `ReserveInventory()` +- `ProcessPayment()` +- `ScheduleShipping()` +- `SendNotifications()` +- `FinalizeOrder()` + +## Testing + +### Running the Application + +```bash +# Build the solution +dotnet build ECommerceOrderProcessing.sln + +# Run the console application +cd src/ECommerce.Console +dotnet run + +# Or use the provided batch file (Windows) +run.bat +``` + +### Test Cases Included + +The application includes four comprehensive test cases: + +1. **Valid Order** - Complete successful order processing +2. **Invalid Email** - Tests email validation +3. **Declined Payment** - Tests payment failure handling +4. **Suspicious Order** - Tests security risk assessment + +### Expected Output + +- All tests should pass (4/4) +- Successful orders generate tracking numbers +- Invalid orders are rejected with appropriate error messages +- Audit log file is created with detailed event tracking + +## Security Considerations + +This sample application includes basic security practices suitable for educational purposes: + +- Input validation and sanitization +- Sensitive data masking in logs +- Basic fraud detection patterns +- Secure error handling without information leakage +- Audit trail for compliance + +## File Outputs + +- **Console Output**: Real-time processing information and test results +- **order_audit_log.txt**: Complete audit trail of all processing activities + +## Educational Value + +This project provides an excellent foundation for learning: + +- How to identify code that needs refactoring +- Breaking down large functions into single-responsibility methods +- Maintaining functionality while improving code structure +- Using GitHub Copilot to assist with refactoring tasks +- Testing to ensure behavior remains consistent after refactoring +- Understanding real-world software architecture patterns + +## Course Integration + +Before refactoring: + +1. Run the application and document the output +2. Note the processing time and functionality +3. Review the audit log contents +4. Study the `OrderProcessor.ProcessOrder()` method structure + +After refactoring: + +1. Verify the output matches the original +2. Confirm all test cases still pass +3. Ensure audit logging continues to work correctly +4. Validate that the processing behavior is identical + +This ensures students can confidently refactor the code while maintaining all existing functionality and learning proper software architecture principles. diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/run.bat b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/run.bat new file mode 100644 index 0000000..a16048e --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/run.bat @@ -0,0 +1,21 @@ +@echo off +echo Building and running E-Commerce Order Processing System... +echo. + +dotnet build ECommerceOrderProcessing.sln +if %ERRORLEVEL% neq 0 ( + echo Build failed! Please check for errors. + pause + exit /b 1 +) + +echo. +echo Build successful! Running application... +echo. + +cd src\ECommerce.Console +dotnet run + +echo. +echo Application completed. Check order_audit_log.txt for detailed logs. +pause diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/ECommerce.ApplicationCore.csproj b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/ECommerce.ApplicationCore.csproj new file mode 100644 index 0000000..2584143 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/ECommerce.ApplicationCore.csproj @@ -0,0 +1,10 @@ + + + + net9.0 + ECommerce.ApplicationCore + enable + enable + + + diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Entities/Order.cs b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Entities/Order.cs new file mode 100644 index 0000000..fe83f50 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Entities/Order.cs @@ -0,0 +1,21 @@ +namespace ECommerce.ApplicationCore.Entities; + +/// +/// Represents an e-commerce order with all associated data +/// +public class Order +{ + public required string Id { get; set; } + public required List Items { get; set; } + public required string CustomerEmail { get; set; } + public required string ShippingAddress { get; set; } + public required PaymentInfo PaymentInfo { get; set; } + public decimal TotalAmount { get; set; } + public OrderStatus Status { get; set; } = OrderStatus.Pending; + public DateTime OrderDate { get; set; } = DateTime.UtcNow; + public DateTime? CompletionDate { get; set; } + public TimeSpan ProcessingDuration { get; set; } + public string? PaymentReference { get; set; } + public string? TrackingNumber { get; set; } + public DateTime? EstimatedDeliveryDate { get; set; } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Entities/OrderItem.cs b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Entities/OrderItem.cs new file mode 100644 index 0000000..8250f32 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Entities/OrderItem.cs @@ -0,0 +1,13 @@ +namespace ECommerce.ApplicationCore.Entities; + +/// +/// Represents an individual item within an order +/// +public class OrderItem +{ + public required string ProductId { get; set; } + public int Quantity { get; set; } + public decimal Price { get; set; } + public string? ProductName { get; set; } + public string? Category { get; set; } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Entities/OrderResult.cs b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Entities/OrderResult.cs new file mode 100644 index 0000000..2840acb --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Entities/OrderResult.cs @@ -0,0 +1,26 @@ +namespace ECommerce.ApplicationCore.Entities; + +/// +/// Represents the result of order processing operations +/// +public class OrderResult +{ + public bool IsSuccess { get; private set; } + public string OrderId { get; private set; } = string.Empty; + public string TrackingNumber { get; private set; } = string.Empty; + public string ErrorMessage { get; private set; } = string.Empty; + + private OrderResult(bool success, string orderId = "", string trackingNumber = "", string error = "") + { + IsSuccess = success; + OrderId = orderId; + TrackingNumber = trackingNumber; + ErrorMessage = error; + } + + public static OrderResult Success(string id, string trackingNumber = "") => + new(true, orderId: id, trackingNumber: trackingNumber); + + public static OrderResult Failure(string error) => + new(false, error: error); +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Entities/OrderStatus.cs b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Entities/OrderStatus.cs new file mode 100644 index 0000000..359b036 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Entities/OrderStatus.cs @@ -0,0 +1,16 @@ +namespace ECommerce.ApplicationCore.Entities; + +/// +/// Represents the various states an order can be in +/// +public enum OrderStatus +{ + Pending, + Processing, + PaymentConfirmed, + Shipped, + Delivered, + Completed, + Cancelled, + Refunded +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Entities/PaymentInfo.cs b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Entities/PaymentInfo.cs new file mode 100644 index 0000000..d6a3a57 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Entities/PaymentInfo.cs @@ -0,0 +1,25 @@ +namespace ECommerce.ApplicationCore.Entities; + +/// +/// Contains payment information for order processing +/// +public class PaymentInfo +{ + public required string CardNumber { get; set; } + public required string CardCVV { get; set; } + public required string CardHolderName { get; set; } + public required string ExpiryMonth { get; set; } + public required string ExpiryYear { get; set; } + public required string BillingAddress { get; set; } + + /// + /// Security helper to get masked card number for logging + /// + /// Masked card number with only last 4 digits visible + public string GetMaskedCardNumber() + { + if (string.IsNullOrEmpty(CardNumber) || CardNumber.Length < 4) + return "****"; + return "****-****-****-" + CardNumber.Substring(CardNumber.Length - 4); + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Entities/ShippingDetails.cs b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Entities/ShippingDetails.cs new file mode 100644 index 0000000..27d6ecb --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Entities/ShippingDetails.cs @@ -0,0 +1,12 @@ +namespace ECommerce.ApplicationCore.Entities; + +/// +/// Contains shipping details for an order +/// +public class ShippingDetails +{ + public required string TrackingNumber { get; set; } + public DateTime EstimatedDelivery { get; set; } + public string ShippingMethod { get; set; } = string.Empty; + public decimal ShippingCost { get; set; } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Exceptions/PaymentException.cs b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Exceptions/PaymentException.cs new file mode 100644 index 0000000..abdf89a --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Exceptions/PaymentException.cs @@ -0,0 +1,16 @@ +using System; + +namespace ECommerce.ApplicationCore.Exceptions; + +/// +/// Custom exception for payment processing errors +/// +public class PaymentException : Exception +{ + public string PaymentErrorCode { get; } + + public PaymentException(string message, string errorCode = "UNKNOWN") : base(message) + { + PaymentErrorCode = errorCode; + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Interfaces/IAuditLogger.cs b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Interfaces/IAuditLogger.cs new file mode 100644 index 0000000..000833f --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Interfaces/IAuditLogger.cs @@ -0,0 +1,21 @@ +namespace ECommerce.ApplicationCore.Interfaces; + +/// +/// Interface for audit logging operations +/// +public interface IAuditLogger +{ + void LogOrderProcessingStarted(string orderId, string email); + void LogOrderCompleted(string orderId, decimal amount); + void LogSecurityEvent(string eventType, string details); + void LogValidationFailure(string orderId, string reason); + void LogInventoryIssue(string orderId, string productId, string issue); + void LogInventoryReserved(string orderId, int itemCount); + void LogPaymentProcessed(string orderId, decimal amount, string reference); + void LogPaymentFailure(string orderId, string reason); + void LogShippingScheduled(string orderId, string trackingNumber); + void LogShippingFailure(string orderId, string reason); + void LogNotificationSent(string orderId, string type); + void LogNotificationFailure(string orderId, string reason); + void LogUnexpectedError(string orderId, string error); +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Interfaces/IInventoryService.cs b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Interfaces/IInventoryService.cs new file mode 100644 index 0000000..3e03a12 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Interfaces/IInventoryService.cs @@ -0,0 +1,24 @@ +using ECommerce.ApplicationCore.Entities; + +namespace ECommerce.ApplicationCore.Interfaces; + +/// +/// Interface for inventory management operations +/// +public interface IInventoryService +{ + /// + /// Check if sufficient stock is available for a product + /// + bool CheckStock(string productId, int quantity); + + /// + /// Reserve stock for the items in an order + /// + bool ReserveStock(List items); + + /// + /// Release previously reserved stock + /// + void ReleaseStock(List items); +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Interfaces/INotificationService.cs b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Interfaces/INotificationService.cs new file mode 100644 index 0000000..6fb28fc --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Interfaces/INotificationService.cs @@ -0,0 +1,19 @@ +using ECommerce.ApplicationCore.Entities; + +namespace ECommerce.ApplicationCore.Interfaces; + +/// +/// Interface for customer notification operations +/// +public interface INotificationService +{ + /// + /// Send order confirmation to customer + /// + void SendOrderConfirmation(string email, string orderId, string? trackingNumber = null); + + /// + /// Send high-value order alert to internal systems + /// + void SendHighValueOrderAlert(Order order); +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Interfaces/IPaymentGateway.cs b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Interfaces/IPaymentGateway.cs new file mode 100644 index 0000000..dab7b3c --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Interfaces/IPaymentGateway.cs @@ -0,0 +1,17 @@ +using ECommerce.ApplicationCore.Entities; + +namespace ECommerce.ApplicationCore.Interfaces; + +/// +/// Interface for payment processing operations +/// +public interface IPaymentGateway +{ + /// + /// Process a payment charge + /// + /// Payment information + /// Amount to charge + /// Payment reference ID + string Charge(PaymentInfo payment, decimal amount); +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Interfaces/ISecurityValidator.cs b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Interfaces/ISecurityValidator.cs new file mode 100644 index 0000000..96ee752 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Interfaces/ISecurityValidator.cs @@ -0,0 +1,54 @@ +using ECommerce.ApplicationCore.Entities; + +namespace ECommerce.ApplicationCore.Interfaces; + +/// +/// Interface for security validation and risk assessment +/// +public interface ISecurityValidator +{ + /// + /// Validate email address format and domain + /// + bool IsValidEmail(string email); + + /// + /// Validate shipping address format + /// + bool IsValidShippingAddress(string address); + + /// + /// Validate payment information + /// + bool IsValidPaymentInfo(PaymentInfo paymentInfo); + + /// + /// Calculate risk score for an order + /// + int CalculateRiskScore(Order order); + + /// + /// Validate order amounts and pricing + /// + bool ValidateOrderAmounts(Order order); + + /// + /// Validate individual order item + /// + bool ValidateOrderItem(OrderItem item); + + /// + /// Validate payment security + /// + bool ValidatePaymentSecurity(PaymentInfo payment, decimal amount); + + /// + /// Validate shipping requirements + /// + bool ValidateShippingRequirements(Order order); + + /// + /// Mask email address for logging + /// + string MaskEmail(string email); +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Interfaces/IShippingService.cs b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Interfaces/IShippingService.cs new file mode 100644 index 0000000..c27d712 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Interfaces/IShippingService.cs @@ -0,0 +1,16 @@ +using ECommerce.ApplicationCore.Entities; + +namespace ECommerce.ApplicationCore.Interfaces; + +/// +/// Interface for shipping and logistics operations +/// +public interface IShippingService +{ + /// + /// Schedule a shipment for an order + /// + /// Order to ship + /// Shipping details including tracking number + ShippingDetails ScheduleShipment(Order order); +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Services/OrderProcessor.cs b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Services/OrderProcessor.cs new file mode 100644 index 0000000..0d6e9c8 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.ApplicationCore/Services/OrderProcessor.cs @@ -0,0 +1,246 @@ +using ECommerce.ApplicationCore.Entities; +using ECommerce.ApplicationCore.Exceptions; +using ECommerce.ApplicationCore.Interfaces; + +namespace ECommerce.ApplicationCore.Services; + +/// +/// Main order processing service +/// +public class OrderProcessor +{ + private readonly IInventoryService _inventoryService; + private readonly IPaymentGateway _paymentGateway; + private readonly IShippingService _shippingService; + private readonly INotificationService _notificationService; + private readonly ISecurityValidator _securityValidator; + private readonly IAuditLogger _auditLogger; + + public OrderProcessor( + IInventoryService inventoryService, + IPaymentGateway paymentGateway, + IShippingService shippingService, + INotificationService notificationService, + ISecurityValidator securityValidator, + IAuditLogger auditLogger) + { + _inventoryService = inventoryService; + _paymentGateway = paymentGateway; + _shippingService = shippingService; + _notificationService = notificationService; + _securityValidator = securityValidator; + _auditLogger = auditLogger; + } + + /// + /// Responsibilities include: + /// 1. Input Validation & Security Checks + /// 2. Inventory Management + /// 3. Payment Processing + /// 4. Shipping Management + /// 5. Customer Notifications + /// 6. Order Finalization + /// 7. Audit Logging + /// 8. Error Handling & Cleanup + /// + public OrderResult ProcessOrder(Order order) + { + // Log the start of order processing for audit trail + _auditLogger.LogOrderProcessingStarted(order.Id, order.CustomerEmail); + + try + { + // Comprehensive Input Validation and Security Checks + if (order == null) + { + _auditLogger.LogSecurityEvent("NULL_ORDER_ATTEMPT", "Attempted to process null order"); + return OrderResult.Failure("No order provided"); + } + + if (order.Items == null || order.Items.Count == 0) + { + _auditLogger.LogValidationFailure(order.Id, "Empty order items"); + return OrderResult.Failure("Order has no items"); + } + + // Validate email format and domain + if (!_securityValidator.IsValidEmail(order.CustomerEmail)) + { + _auditLogger.LogValidationFailure(order.Id, "Invalid email format"); + return OrderResult.Failure("Invalid email address format"); + } + + // Validate shipping address format + if (!_securityValidator.IsValidShippingAddress(order.ShippingAddress)) + { + _auditLogger.LogValidationFailure(order.Id, "Invalid shipping address"); + return OrderResult.Failure("Invalid shipping address format"); + } + + // Validate payment information + if (order.PaymentInfo == null || !_securityValidator.IsValidPaymentInfo(order.PaymentInfo)) + { + _auditLogger.LogValidationFailure(order.Id, "Invalid payment information"); + return OrderResult.Failure("Payment information is invalid or incomplete"); + } + + // Security risk assessment - check for suspicious patterns + var riskScore = _securityValidator.CalculateRiskScore(order); + if (riskScore > 75) // High risk threshold + { + _auditLogger.LogSecurityEvent("HIGH_RISK_ORDER", $"Order {order.Id} flagged with risk score: {riskScore}"); + return OrderResult.Failure("Order flagged for manual review due to security concerns"); + } + + // Validate order amounts and item prices + if (!_securityValidator.ValidateOrderAmounts(order)) + { + _auditLogger.LogValidationFailure(order.Id, "Invalid order amounts or pricing"); + return OrderResult.Failure("Order amounts validation failed"); + } + + Console.WriteLine($"Processing Order {order.Id} for {_securityValidator.MaskEmail(order.CustomerEmail)}..."); + Console.WriteLine($"Order contains {order.Items.Count} items, Total: ${order.TotalAmount:F2}"); + + // Inventory Management - Check Stock Availability and Reserve Items + Console.WriteLine("Checking inventory availability..."); + foreach (OrderItem item in order.Items) + { + bool inStock = _inventoryService.CheckStock(item.ProductId, item.Quantity); + if (!inStock) + { + _auditLogger.LogInventoryIssue(order.Id, item.ProductId, "Out of stock"); + Console.WriteLine($"Item {item.ProductId} is out of stock. Aborting order."); + return OrderResult.Failure($"Item {item.ProductId} is out of stock"); + } + + // Validate item details for security + if (!_securityValidator.ValidateOrderItem(item)) + { + _auditLogger.LogSecurityEvent("INVALID_ITEM_DATA", $"Invalid item data for {item.ProductId}"); + return OrderResult.Failure($"Invalid item data for {item.ProductId}"); + } + } + + // Reserve inventory for all items + bool inventoryReserved = _inventoryService.ReserveStock(order.Items); + if (!inventoryReserved) + { + _auditLogger.LogInventoryIssue(order.Id, "ALL_ITEMS", "Failed to reserve inventory"); + Console.WriteLine("Failed to reserve inventory for the order."); + return OrderResult.Failure("Inventory reservation failed"); + } + Console.WriteLine("Inventory reserved successfully."); + _auditLogger.LogInventoryReserved(order.Id, order.Items.Count); + + // Payment Processing with Enhanced Security + Console.WriteLine("Processing payment..."); + try + { + // Additional fraud checks before processing payment + if (!_securityValidator.ValidatePaymentSecurity(order.PaymentInfo, order.TotalAmount)) + { + _inventoryService.ReleaseStock(order.Items); + _auditLogger.LogSecurityEvent("PAYMENT_FRAUD_DETECTED", $"Suspicious payment attempt for order {order.Id}"); + return OrderResult.Failure("Payment failed security validation"); + } + + // Attempt to charge the customer's card + var paymentReference = _paymentGateway.Charge(order.PaymentInfo, order.TotalAmount); + order.PaymentReference = paymentReference; + Console.WriteLine($"Payment processed successfully. Reference: {paymentReference}"); + _auditLogger.LogPaymentProcessed(order.Id, order.TotalAmount, paymentReference); + } + catch (PaymentException ex) + { + // If payment fails, release reserved stock and return failure + _inventoryService.ReleaseStock(order.Items); + _auditLogger.LogPaymentFailure(order.Id, ex.Message); + Console.WriteLine($"Payment failed for Order {order.Id}: {ex.Message}"); + return OrderResult.Failure("Payment processing failed: " + ex.Message); + } + + // Shipping and Logistics Management + Console.WriteLine("Scheduling shipping..."); + try + { + // Validate shipping requirements and restrictions + if (!_securityValidator.ValidateShippingRequirements(order)) + { + _auditLogger.LogSecurityEvent("SHIPPING_VALIDATION_FAILED", $"Shipping validation failed for order {order.Id}"); + return OrderResult.Failure("Shipping requirements validation failed"); + } + + var shippingDetails = _shippingService.ScheduleShipment(order); + order.TrackingNumber = shippingDetails.TrackingNumber; + order.EstimatedDeliveryDate = shippingDetails.EstimatedDelivery; + Console.WriteLine($"Shipping scheduled successfully. Tracking: {shippingDetails.TrackingNumber}"); + _auditLogger.LogShippingScheduled(order.Id, shippingDetails.TrackingNumber); + } + catch (Exception ex) + { + _auditLogger.LogShippingFailure(order.Id, ex.Message); + Console.WriteLine($"Error scheduling shipment: {ex.Message}"); + return OrderResult.Failure("Shipping scheduling failed: " + ex.Message); + } + + // Customer Communication and Notifications + Console.WriteLine("Sending notifications..."); + try + { + // Send order confirmation with all details + _notificationService.SendOrderConfirmation(order.CustomerEmail, order.Id, order.TrackingNumber); + Console.WriteLine($"Confirmation sent to {_securityValidator.MaskEmail(order.CustomerEmail)}."); + _auditLogger.LogNotificationSent(order.Id, "ORDER_CONFIRMATION"); + + // Send internal notifications for high-value orders + if (order.TotalAmount > 1000) + { + _notificationService.SendHighValueOrderAlert(order); + _auditLogger.LogNotificationSent(order.Id, "HIGH_VALUE_ALERT"); + } + } + catch (Exception ex) + { + // If confirmation fails, log a warning but do not abort the order + _auditLogger.LogNotificationFailure(order.Id, ex.Message); + Console.WriteLine($"Warning: failed to send notification: {ex.Message}"); + } + + // Order Finalization and Data Recording + Console.WriteLine("Finalizing order..."); + order.Status = OrderStatus.Completed; + order.CompletionDate = DateTime.UtcNow; + order.ProcessingDuration = DateTime.UtcNow - order.OrderDate; + + // In a real app, this would update the order record in a database + // _orderRepository.UpdateOrder(order); + + Console.WriteLine($"Order {order.Id} completed successfully in {order.ProcessingDuration.TotalSeconds:F1} seconds."); + _auditLogger.LogOrderCompleted(order.Id, order.TotalAmount); + + return OrderResult.Success(order.Id, order.TrackingNumber ?? ""); + } + catch (Exception ex) + { + // Catch-all exception handler for unexpected errors + _auditLogger.LogUnexpectedError(order?.Id ?? "UNKNOWN", ex.Message); + Console.WriteLine($"Unexpected error processing order: {ex.Message}"); + + // Attempt cleanup if order ID exists + if (order?.Id != null) + { + try + { + _inventoryService.ReleaseStock(order.Items); + } + catch (Exception cleanupEx) + { + _auditLogger.LogUnexpectedError(order.Id, $"Cleanup failed: {cleanupEx.Message}"); + } + } + + return OrderResult.Failure("An unexpected error occurred during order processing"); + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.Console/ECommerce.Console.csproj b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.Console/ECommerce.Console.csproj new file mode 100644 index 0000000..88e17a3 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.Console/ECommerce.Console.csproj @@ -0,0 +1,16 @@ + + + + Exe + net9.0 + ECommerce.Console + enable + enable + + + + + + + + diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.Console/Program.cs b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.Console/Program.cs new file mode 100644 index 0000000..9d263ad --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.Console/Program.cs @@ -0,0 +1,109 @@ +using ECommerce.ApplicationCore.Entities; +using ECommerce.ApplicationCore.Services; +using ECommerce.Infrastructure.Services; + +namespace ECommerce.Console; + +/// +/// Main entry point for the e-commerce order processing application +/// Demonstrates dependency injection and proper layered architecture +/// +public class Program +{ + public static void Main(string[] args) + { + System.Console.WriteLine("=== E-Commerce Order Processing System ==="); + System.Console.WriteLine("Starting order processing tests...\n"); + + // Setup dependencies (In a real app, this would use DI container) + var inventoryService = new InventoryService(); + var paymentGateway = new PaymentGateway(); + var shippingService = new ShippingService(); + var notificationService = new NotificationService(); + var securityValidator = new SecurityValidator(); + var auditLogger = new AuditLogger(); + + // Create the main order processor with injected dependencies + var processor = new OrderProcessor( + inventoryService, + paymentGateway, + shippingService, + notificationService, + securityValidator, + auditLogger); + + var testResults = new List(); + + // Test Case 1: Valid order with multiple items + System.Console.WriteLine("--- Test Case 1: Valid Order ---"); + var validOrder = CreateSampleOrder("ORD-001", "john.doe@email.com", "123 Main St, City, State 12345", + new List + { + new() { ProductId = "LAPTOP-001", Quantity = 1, Price = 999.99m }, + new() { ProductId = "MOUSE-001", Quantity = 2, Price = 25.00m } + }, + new PaymentInfo + { + CardNumber = "4111111111111111", + CardCVV = "123", + CardHolderName = "Ane Pedersen", + ExpiryMonth = "12", + ExpiryYear = "2025", + BillingAddress = "123 Main St, City, State 12345" + }); + + var result1 = processor.ProcessOrder(validOrder); + testResults.Add($"Test 1: {(result1.IsSuccess ? "PASSED" : "FAILED")} - {(result1.IsSuccess ? result1.OrderId : result1.ErrorMessage)}"); + + System.Console.WriteLine("\n--- Test Case 2: Invalid Email Address ---"); + var invalidEmailOrder = CreateSampleOrder("ORD-002", "invalid-email", "123 Main St", + new List { new() { ProductId = "BOOK-001", Quantity = 1, Price = 15.99m } }, + new PaymentInfo { CardNumber = "4111111111111111", CardCVV = "123", CardHolderName = "Jennet Nazarowa", ExpiryMonth = "06", ExpiryYear = "2026", BillingAddress = "123 Main St" }); + + var result2 = processor.ProcessOrder(invalidEmailOrder); + testResults.Add($"Test 2: {(result2.IsSuccess ? "FAILED" : "PASSED")} - Should reject invalid email"); + + System.Console.WriteLine("\n--- Test Case 3: Declined Payment ---"); + var declinedPaymentOrder = CreateSampleOrder("ORD-003", "customer@test.com", "456 Oak Ave", + new List { new() { ProductId = "PHONE-001", Quantity = 1, Price = 699.99m } }, + new PaymentInfo { CardNumber = "0000000000000000", CardCVV = "999", CardHolderName = "Nikki Vestergaard", ExpiryMonth = "01", ExpiryYear = "2024", BillingAddress = "456 Oak Ave" }); + + var result3 = processor.ProcessOrder(declinedPaymentOrder); + testResults.Add($"Test 3: {(result3.IsSuccess ? "FAILED" : "PASSED")} - Should reject declined payment"); + + System.Console.WriteLine("\n--- Test Case 4: Suspicious Order (Security Check) ---"); + var suspiciousOrder = CreateSampleOrder("ORD-004", "suspicious.user@valid.com", "123 Suspicious Ave, City, State 99999", + new List { new() { ProductId = "EXPENSIVE-001", Quantity = 2, Price = 25000.00m } }, + new PaymentInfo { CardNumber = "4111111111111111", CardCVV = "123", CardHolderName = "AB", ExpiryMonth = "12", ExpiryYear = "2026", BillingAddress = "123 Suspicious Ave, City, State 99999" }); + + var result4 = processor.ProcessOrder(suspiciousOrder); + testResults.Add($"Test 4: {(result4.IsSuccess ? "FAILED" : "PASSED")} - Should flag suspicious order"); + + // Display test summary + System.Console.WriteLine("\n=== TEST SUMMARY ==="); + foreach (var testResult in testResults) + { + System.Console.WriteLine(testResult); + } + + var passedTests = testResults.Count(r => r.Contains("PASSED")); + System.Console.WriteLine($"\nTests Passed: {passedTests}/{testResults.Count}"); + System.Console.WriteLine("Order processing system test completed."); + } + + private static Order CreateSampleOrder(string orderId, string email, string address, List items, PaymentInfo payment) + { + var order = new Order + { + Id = orderId, + CustomerEmail = email, + ShippingAddress = address, + Items = items, + PaymentInfo = payment, + Status = OrderStatus.Pending, + OrderDate = DateTime.UtcNow + }; + order.TotalAmount = items.Sum(i => i.Price * i.Quantity) + 9.99m; // Add shipping + return order; + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.Console/order_audit_log.txt b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.Console/order_audit_log.txt new file mode 100644 index 0000000..e69de29 diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.Infrastructure/ECommerce.Infrastructure.csproj b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.Infrastructure/ECommerce.Infrastructure.csproj new file mode 100644 index 0000000..01e5150 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.Infrastructure/ECommerce.Infrastructure.csproj @@ -0,0 +1,14 @@ + + + + net9.0 + ECommerce.Infrastructure + enable + enable + + + + + + + diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.Infrastructure/Services/AuditLogger.cs b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.Infrastructure/Services/AuditLogger.cs new file mode 100644 index 0000000..52b0a77 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.Infrastructure/Services/AuditLogger.cs @@ -0,0 +1,104 @@ +using ECommerce.ApplicationCore.Interfaces; + +namespace ECommerce.Infrastructure.Services; + +/// +/// Audit logging service for security compliance and debugging +/// +public class AuditLogger : IAuditLogger +{ + private static readonly string LogFileName = "order_audit_log.txt"; + + public void LogOrderProcessingStarted(string orderId, string email) + { + LogEvent("ORDER_PROCESSING_STARTED", orderId, $"Started processing order for {MaskEmail(email)}"); + } + + public void LogOrderCompleted(string orderId, decimal amount) + { + LogEvent("ORDER_COMPLETED", orderId, $"Order completed successfully, amount: ${amount:F2}"); + } + + public void LogSecurityEvent(string eventType, string details) + { + LogEvent($"SECURITY_{eventType}", "SYSTEM", details); + } + + public void LogValidationFailure(string orderId, string reason) + { + LogEvent("VALIDATION_FAILURE", orderId, reason); + } + + public void LogInventoryIssue(string orderId, string productId, string issue) + { + LogEvent("INVENTORY_ISSUE", orderId, $"Product: {productId}, Issue: {issue}"); + } + + public void LogInventoryReserved(string orderId, int itemCount) + { + LogEvent("INVENTORY_RESERVED", orderId, $"Reserved {itemCount} item(s)"); + } + + public void LogPaymentProcessed(string orderId, decimal amount, string reference) + { + LogEvent("PAYMENT_PROCESSED", orderId, $"Amount: ${amount:F2}, Reference: {reference}"); + } + + public void LogPaymentFailure(string orderId, string reason) + { + LogEvent("PAYMENT_FAILURE", orderId, reason); + } + + public void LogShippingScheduled(string orderId, string trackingNumber) + { + LogEvent("SHIPPING_SCHEDULED", orderId, $"Tracking: {trackingNumber}"); + } + + public void LogShippingFailure(string orderId, string reason) + { + LogEvent("SHIPPING_FAILURE", orderId, reason); + } + + public void LogNotificationSent(string orderId, string type) + { + LogEvent("NOTIFICATION_SENT", orderId, $"Type: {type}"); + } + + public void LogNotificationFailure(string orderId, string reason) + { + LogEvent("NOTIFICATION_FAILURE", orderId, reason); + } + + public void LogUnexpectedError(string orderId, string error) + { + LogEvent("UNEXPECTED_ERROR", orderId, error); + } + + private void LogEvent(string eventType, string orderId, string details) + { + var logEntry = $"{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss.fff} UTC | {eventType} | Order: {orderId} | {details}"; + + try + { + // In a real application, this would use a proper logging framework like Serilog + File.AppendAllText(LogFileName, logEntry + Environment.NewLine); + Console.WriteLine($"[AUDIT] {logEntry}"); + } + catch + { + // Fail silently for logging issues to not interrupt order processing + Console.WriteLine($"[AUDIT-ERROR] Failed to log: {logEntry}"); + } + } + + private string MaskEmail(string email) + { + if (string.IsNullOrWhiteSpace(email) || !email.Contains('@')) + return "***@***.***"; + + var parts = email.Split('@'); + return parts.Length == 2 + ? $"{parts[0][0]}***@{parts[1]}" + : "***@***.***"; + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.Infrastructure/Services/ExternalServices.cs b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.Infrastructure/Services/ExternalServices.cs new file mode 100644 index 0000000..a9220cd --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.Infrastructure/Services/ExternalServices.cs @@ -0,0 +1,209 @@ +using ECommerce.ApplicationCore.Entities; +using ECommerce.ApplicationCore.Exceptions; +using ECommerce.ApplicationCore.Interfaces; +using ECommerce.ApplicationCore.Services; + +namespace ECommerce.Infrastructure.Services; + +/// +/// Inventory service implementation with realistic stock management +/// +public class InventoryService : IInventoryService +{ + private static readonly Dictionary StockLevels = new() + { + { "LAPTOP-001", 5 }, + { "MOUSE-001", 25 }, + { "BOOK-001", 100 }, + { "PHONE-001", 3 }, + { "EXPENSIVE-001", 2 } + }; + + public bool CheckStock(string productId, int quantity) + { + // Simulate database/API call delay + Thread.Sleep(50); + + if (!StockLevels.TryGetValue(productId, out int availableStock)) + { + // Unknown product - assume out of stock + return false; + } + + return availableStock >= quantity; + } + + public bool ReserveStock(List items) + { + Thread.Sleep(100); + + // In a real system, this would be an atomic transaction + foreach (var item in items) + { + if (!StockLevels.TryGetValue(item.ProductId, out int currentStock)) + return false; + + if (currentStock < item.Quantity) + return false; + + StockLevels[item.ProductId] = currentStock - item.Quantity; + } + + return true; + } + + public void ReleaseStock(List items) + { + Thread.Sleep(50); + + // Return items to stock + foreach (var item in items) + { + if (StockLevels.ContainsKey(item.ProductId)) + { + StockLevels[item.ProductId] += item.Quantity; + } + } + } +} + +/// +/// Payment gateway implementation with enhanced validation and fraud detection +/// +public class PaymentGateway : IPaymentGateway +{ + public string Charge(PaymentInfo payment, decimal amount) + { + // Simulate processing time + Thread.Sleep(200); + + // Enhanced validation and fraud detection + if (string.IsNullOrEmpty(payment.CardNumber) || payment.CardNumber.Length < 13) + { + throw new PaymentException("Invalid card number format", "INVALID_CARD"); + } + + if (payment.CardNumber.StartsWith("0000")) + { + throw new PaymentException("Card declined by issuer", "CARD_DECLINED"); + } + + if (amount > 10000 && payment.CardHolderName.Length < 3) + { + throw new PaymentException("High-value transaction requires valid cardholder name", "FRAUD_DETECTED"); + } + + // Simulate different card types and responses + if (payment.CardNumber.StartsWith("4111")) + { + // Valid Visa test card + return $"PAY_{DateTime.UtcNow:yyyyMMddHHmmss}_{Random.Shared.Next(1000, 9999)}"; + } + + if (payment.CardNumber.StartsWith("5555")) + { + // Valid MasterCard test card + return $"PAY_MC_{DateTime.UtcNow:yyyyMMddHHmmss}_{Random.Shared.Next(1000, 9999)}"; + } + + // Generic successful payment + return $"PAY_GENERIC_{DateTime.UtcNow:yyyyMMddHHmmss}_{Random.Shared.Next(1000, 9999)}"; + } +} + +/// +/// Shipping service implementation with realistic logistics management +/// +public class ShippingService : IShippingService +{ + public ShippingDetails ScheduleShipment(Order order) + { + Thread.Sleep(150); + + if (string.IsNullOrWhiteSpace(order.ShippingAddress)) + { + throw new Exception("Invalid shipping address provided"); + } + + if (order.ShippingAddress.ToLower().Contains("invalid")) + { + throw new Exception("Cannot ship to specified address"); + } + + // Generate tracking number and estimated delivery + var trackingNumber = $"TRK{DateTime.UtcNow:yyyyMMdd}{Random.Shared.Next(100000, 999999)}"; + var estimatedDelivery = DateTime.UtcNow.AddDays(Random.Shared.Next(3, 8)); + + return new ShippingDetails + { + TrackingNumber = trackingNumber, + EstimatedDelivery = estimatedDelivery, + ShippingMethod = DetermineShippingMethod(order.TotalAmount), + ShippingCost = CalculateShippingCost(order) + }; + } + + private string DetermineShippingMethod(decimal orderValue) + { + return orderValue > 1000 ? "Express Shipping" : "Standard Shipping"; + } + + private decimal CalculateShippingCost(Order order) + { + // Simple shipping cost calculation + var baseCost = 9.99m; + var itemCount = order.Items.Sum(i => i.Quantity); + + if (order.TotalAmount > 100) + baseCost = 0; // Free shipping over $100 + else if (itemCount > 3) + baseCost += 5.00m; // Extra charge for multiple items + + return baseCost; + } +} + +/// +/// Notification service implementation for customer communications +/// +public class NotificationService : INotificationService +{ + public void SendOrderConfirmation(string email, string orderId, string? trackingNumber = null) + { + Thread.Sleep(100); + + // Basic email validation + if (string.IsNullOrWhiteSpace(email) || !email.Contains("@")) + { + throw new Exception("Invalid email address for notification"); + } + + // Simulate email sending + var message = $"Order {orderId} confirmed."; + if (!string.IsNullOrEmpty(trackingNumber)) + { + message += $" Tracking: {trackingNumber}"; + } + + Console.WriteLine($"[EMAIL] Sent to {MaskEmail(email)}: {message}"); + } + + public void SendHighValueOrderAlert(Order order) + { + Thread.Sleep(50); + + // Internal notification for high-value orders + Console.WriteLine($"[ALERT] High-value order {order.Id} processed: ${order.TotalAmount:F2}"); + } + + private string MaskEmail(string email) + { + if (string.IsNullOrWhiteSpace(email) || !email.Contains('@')) + return "***@***.***"; + + var parts = email.Split('@'); + return parts.Length == 2 + ? $"{parts[0][0]}***@{parts[1]}" + : "***@***.***"; + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.Infrastructure/Services/SecurityValidator.cs b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.Infrastructure/Services/SecurityValidator.cs new file mode 100644 index 0000000..464b8a2 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ECommerceOrderProcessing/src/ECommerce.Infrastructure/Services/SecurityValidator.cs @@ -0,0 +1,185 @@ +using System.Text.RegularExpressions; +using ECommerce.ApplicationCore.Entities; +using ECommerce.ApplicationCore.Interfaces; + +namespace ECommerce.Infrastructure.Services; + +/// +/// Security validation service implementation with comprehensive input validation and risk assessment +/// +public class SecurityValidator : ISecurityValidator +{ + private static readonly Regex EmailRegex = new( + @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly HashSet SuspiciousDomains = new() + { + "temp.com", "tempmail.com", "10minutemail.com", "guerrillamail.com" + }; + + public bool IsValidEmail(string email) + { + if (string.IsNullOrWhiteSpace(email)) + return false; + + return EmailRegex.IsMatch(email) && !IsSuspiciousDomain(email); + } + + private bool IsSuspiciousDomain(string email) + { + var domain = email.Split('@').LastOrDefault()?.ToLower(); + return domain != null && SuspiciousDomains.Contains(domain); + } + + public bool IsValidShippingAddress(string address) + { + if (string.IsNullOrWhiteSpace(address) || address.Length < 10) + return false; + + // Basic address validation - should contain numbers and letters + return Regex.IsMatch(address, @"^\d+.*[a-zA-Z].*") && + !address.ToLower().Contains("unknown"); + } + + public bool IsValidPaymentInfo(PaymentInfo paymentInfo) + { + if (paymentInfo == null) + return false; + + // Validate card number (basic length check) + if (string.IsNullOrWhiteSpace(paymentInfo.CardNumber) || + paymentInfo.CardNumber.Length < 13 || paymentInfo.CardNumber.Length > 19) + return false; + + // Validate CVV + if (string.IsNullOrWhiteSpace(paymentInfo.CardCVV) || + !Regex.IsMatch(paymentInfo.CardCVV, @"^\d{3,4}$")) + return false; + + // Validate cardholder name + if (string.IsNullOrWhiteSpace(paymentInfo.CardHolderName) || + paymentInfo.CardHolderName.Trim().Length < 2) + return false; + + // Validate expiry date + if (!IsValidExpiryDate(paymentInfo.ExpiryMonth, paymentInfo.ExpiryYear)) + return false; + + return true; + } + + private bool IsValidExpiryDate(string month, string year) + { + if (!int.TryParse(month, out int mm) || !int.TryParse(year, out int yy)) + return false; + + if (mm < 1 || mm > 12) + return false; + + var expiry = new DateTime(yy, mm, DateTime.DaysInMonth(yy, mm)); + return expiry > DateTime.Now; + } + + public int CalculateRiskScore(Order order) + { + int riskScore = 0; + + // High-value order risk + if (order.TotalAmount > 5000) + riskScore += 30; + else if (order.TotalAmount > 2000) + riskScore += 15; + + // Large quantity risk + var totalQuantity = order.Items.Sum(i => i.Quantity); + if (totalQuantity > 10) + riskScore += 25; + else if (totalQuantity > 5) + riskScore += 10; + + // Suspicious email patterns + if (IsSuspiciousDomain(order.CustomerEmail)) + riskScore += 40; + + // Short cardholder name (potential fake) + if (order.PaymentInfo.CardHolderName.Trim().Length <= 2) + riskScore += 30; + + // CVV all zeros (suspicious) + if (order.PaymentInfo.CardCVV == "000") + riskScore += 25; + + return riskScore; + } + + public bool ValidateOrderAmounts(Order order) + { + if (order.TotalAmount <= 0) + return false; + + decimal calculatedTotal = order.Items.Sum(i => i.Price * i.Quantity); + + // Allow for shipping, tax, etc. - total should be reasonable compared to item total + return order.TotalAmount >= calculatedTotal && + order.TotalAmount <= calculatedTotal * 1.5m; // Max 50% markup for shipping/tax + } + + public bool ValidateOrderItem(OrderItem item) + { + if (string.IsNullOrWhiteSpace(item.ProductId) || item.Quantity <= 0 || item.Price < 0) + return false; + + // Reasonable quantity limits + if (item.Quantity > 100) + return false; + + // Reasonable price limits + if (item.Price > 50000) // $50k max per item + return false; + + return true; + } + + public bool ValidatePaymentSecurity(PaymentInfo payment, decimal amount) + { + // Additional fraud detection logic + if (payment.CardNumber.StartsWith("0000")) + return false; // Test cards that should be declined + + if (amount > 10000 && payment.CardHolderName.Length < 5) + return false; // High-value orders need realistic names + + return true; + } + + public bool ValidateShippingRequirements(Order order) + { + // Validate shipping to known problematic addresses + var address = order.ShippingAddress.ToLower(); + + if (address.Contains("unknown") || address.Contains("invalid")) + return false; + + return true; + } + + public string MaskEmail(string email) + { + if (string.IsNullOrWhiteSpace(email) || !email.Contains('@')) + return "***@***.***"; + + var parts = email.Split('@'); + if (parts.Length != 2) + return "***@***.***"; + + var localPart = parts[0]; + var domainPart = parts[1]; + + var maskedLocal = localPart.Length > 2 + ? localPart[0] + "***" + localPart[^1] + : "***"; + + return $"{maskedLocal}@{domainPart}"; + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ServerLogAnalysisUtility/Program.cs b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ServerLogAnalysisUtility/Program.cs new file mode 100644 index 0000000..4601fd1 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ServerLogAnalysisUtility/Program.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace LogProcessing; + +public class LogAnalyzer +{ + public void AnalyzeLogs(string[] logFiles) + { + // Step 1: File I/O Setup (open files, prepare to read) + if (logFiles == null || logFiles.Length == 0) + { + Console.WriteLine("No log files specified. Exiting."); + return; + } + int totalLines = 0; + double totalResponseTime = 0.0; + var errorTypeCounts = new Dictionary(); + + foreach (string filePath in logFiles) + { + if (string.IsNullOrWhiteSpace(filePath)) + { + Console.WriteLine("Skipped an empty file path."); + continue; + } + try + { + using var reader = new StreamReader(filePath); + string line; + while ((line = reader.ReadLine()) != null) + { + totalLines++; + // Step 2: Parsing and Filtering + // Assume log format: Date|Level|Message|...|Duration (fields separated by '|') + string[] parts = line.Split('|'); + if (parts.Length < 3) + { + // Skip lines that don't have the expected format + continue; + } + string level = parts[1].Trim(); // e.g., "INFO" or "ERROR" + string message = parts[2].Trim(); + // Filter: focus on ERROR level entries for error stats + if (level.Equals("ERROR", StringComparison.OrdinalIgnoreCase)) + { + // Determine a simple error category from the message content + string errCategory; + if (message.Contains("NullReference")) errCategory = "NullReferenceException"; + else if (message.Contains("OutOfMemory")) errCategory = "OutOfMemoryError"; + else errCategory = "GeneralError"; + if (errorTypeCounts.ContainsKey(errCategory)) + errorTypeCounts[errCategory]++; + else + errorTypeCounts[errCategory] = 1; + } + // Step 3: Aggregation (accumulate response time if present) + if (parts.Length >= 5) + { + string durationStr = parts[4].Trim(); + if (double.TryParse(durationStr, out double duration)) + { + totalResponseTime += duration; + } + // If parsing fails, ignore that line's duration + } + } + } + catch (FileNotFoundException) + { + Console.WriteLine($"Error: Log file not found -> {filePath}"); + continue; + } + catch (UnauthorizedAccessException) + { + Console.WriteLine($"Error: Access denied to log file -> {filePath}"); + continue; + } + catch (IOException ex) + { + Console.WriteLine($"Error reading file {filePath}: {ex.Message}"); + continue; + } + } + + // Step 4: Output/Reporting + Console.WriteLine("=== Log Analysis Summary ==="); + Console.WriteLine($"Total Lines Processed: {totalLines}"); + if (totalLines > 0) + { + double avgResponseTime = totalResponseTime / totalLines; + Console.WriteLine($"Average Response Time: {avgResponseTime:F3}"); + } + else + { + Console.WriteLine("Average Response Time: N/A (no lines processed)"); + } + Console.WriteLine("Error Type Counts:"); + if (errorTypeCounts.Count > 0) + { + foreach (var kv in errorTypeCounts) + { + Console.WriteLine($" {kv.Key}: {kv.Value}"); + } + } + else + { + Console.WriteLine(" (No errors encountered)"); + } + } +} + +// A simple Program class to demonstrate usage of LogAnalyzer +public class Program +{ + public static void Main(string[] args) + { + var analyzer = new LogAnalyzer(); + analyzer.AnalyzeLogs(args); + // Usage: pass log file paths as command-line arguments. + // E.g., `dotnet run logs\\app.log logs\\db.log` + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ServerLogAnalysisUtility/ServerLogAnalysisUtility.csproj b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ServerLogAnalysisUtility/ServerLogAnalysisUtility.csproj new file mode 100644 index 0000000..fd4bd08 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/refactor-large-functions/ServerLogAnalysisUtility/ServerLogAnalysisUtility.csproj @@ -0,0 +1,10 @@ + + + + Exe + net9.0 + enable + enable + + + diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-get-started-rss-feed/README.md b/DownloadableCodeProjects/standalone-lab-projects/sdd-get-started-rss-feed/README.md new file mode 100644 index 0000000..522094f --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-get-started-rss-feed/README.md @@ -0,0 +1 @@ +GitHub Spec Kit commands will use stakeholder documentation to help generate the constitution.md, spec.md, and plan.md files. diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-get-started-rss-feed/StakeholderDocuments/AppFeatures.md b/DownloadableCodeProjects/standalone-lab-projects/sdd-get-started-rss-feed/StakeholderDocuments/AppFeatures.md new file mode 100644 index 0000000..ceab2c7 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-get-started-rss-feed/StakeholderDocuments/AppFeatures.md @@ -0,0 +1,85 @@ +# App features + +This RSS feed reader demonstrates subscription management as the foundation for a feed reader application. + +## MVP scope (proof-of-concept version) + +The MVP demonstrates the minimal viable functionality: managing a subscription list. + +For the MVP, the app MUST: + +- Let a user add a feed subscription by pasting a feed URL +- Display the list of subscriptions in the UI + +For the MVP, the app MAY: + +- Store data only in memory (data is lost when the app closes) +- Accept any URL without validation (assume valid RSS/Atom feed URLs) +- Display subscriptions in a simple list format + +## MVP behavior + +The MVP follows simple rules: + +- Users can add subscriptions by entering a URL +- The subscription list updates immediately when a subscription is added +- No feed fetching, parsing, or validation +- No error handling needed (no network operations) + +## Extended-MVP features + +After the basic MVP (subscription management) is working, the Extended-MVP adds feed fetching and display: + +- **Manual refresh**: Users can click "refresh" to fetch feed content +- **Item display**: Show items with title and link +- **Basic error handling**: Show "Failed to load feed" if something goes wrong +- **No automatic polling**: Manual refresh only, no background updates + +## Post-MVP features + +After developing a successful Extended-MVP app, the following features could be considered for future versions: + +### Essential improvements + +- **Persistence**: Store subscriptions and items in a database so they remain available after restarting the app +- **Remove subscriptions**: Allow users to delete feeds +- **Better item display**: Show item summaries/content, not just titles +- **Newest-first sorting**: Display items in chronological order + +### Additional capabilities + +- **Background polling**: Automatically refresh feeds on a schedule +- **Read/unread tracking**: Mark items as read and filter by read status +- **Website-to-feed discovery**: Let users paste a website URL and automatically find its RSS feed +- **Folders/organization**: Group feeds into categories +- **Better error handling**: Show specific error messages (feed moved, access denied, malformed XML, etc.) +- **De-duplication**: Ensure the same item isn't stored multiple times +- **HTML rendering**: Safely display rich content from feeds + +### Practical notes for developers + +**For MVP (subscription management only):** + +- Use simple in-memory storage (List in C#) +- No need for feed parsing libraries yet +- No HTTP client needed for MVP +- Focus on basic UI and state management + +**For Extended-MVP (add feed fetching):** + +- Use `System.ServiceModel.Syndication` for parsing +- Test with known-good feeds (e.g., ) +- Avoid complex parsing edge cases - handle basic RSS/Atom formats only + +## Additional features (longer-term) + +If the app grows beyond a basic demonstration, these features could be considered: + +- **Search and filtering**: Find items by keyword, filter by date or category +- **OPML import/export**: Transfer subscriptions between feed readers +- **Advanced organization**: Tags, saved items, priorities +- **Multi-device sync**: Share subscriptions and read state across devices +- **Notifications**: Alert on new items from important feeds +- **Integrations**: Share to email, chat tools, or read-later services +- **Offline reading**: Cache full article content for offline access +- **Mobile apps**: Native apps for phones and tablets diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-get-started-rss-feed/StakeholderDocuments/ProjectGoals.md b/DownloadableCodeProjects/standalone-lab-projects/sdd-get-started-rss-feed/StakeholderDocuments/ProjectGoals.md new file mode 100644 index 0000000..d56ae06 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-get-started-rss-feed/StakeholderDocuments/ProjectGoals.md @@ -0,0 +1,81 @@ +# Project goals + +Build a simple RSS/Atom feed reader. The goal is to demonstrate the most basic capability (managing a subscription list) without the complexity of fetching and displaying feed content. + +## Purpose + +The app exists to demonstrate how a user can build a subscription list for RSS feeds. This is a proof-of-concept focused on the subscription management UI. + +## Target scope (MVP only) + +This is a minimal POC application for a single user, running locally. It is designed to be developed and tested on Windows, macOS, or Linux. + +The MVP includes only: + +- Adding a feed subscription by URL +- Displaying the list of subscriptions in the UI + +All other features (fetching feeds, displaying items, persistence, removing subscriptions, etc.) are deferred to Extended-MVP or post-MVP. + +## Delivery approach + +The focus is on rapid development of the MVP feature. Build the minimal functionality first: + +- Add a subscription by URL +- Display the list of subscriptions + +To keep development fast: + +- No feed fetching or parsing needed for MVP +- No validation of feed URLs (assume user provides valid URLs) +- Store subscriptions in memory only (simplest approach) +- Keep the UI simple and functional rather than polished + +## What "MVP working" means + +The MVP is complete when: + +1. A user can add a feed subscription by pasting a URL +2. The UI displays the updated list of subscriptions + +No actual feed fetching, parsing, or item display is required for MVP. + +## Extended-MVP (next phase) + +After the basic MVP is working, the Extended-MVP adds feed fetching and display capabilities: + +1. A user can click a button to manually refresh the feed +2. Items from the feed are displayed (title and link minimum) + +Test with a known-good RSS feed like . + +### Local development checklist + +Before testing the MVP, verify: + +- [ ] Backend runs without errors and listens on the configured port +- [ ] Frontend runs without errors and loads in the browser +- [ ] Frontend configuration (`wwwroot/appsettings.json`) points to the correct backend URL +- [ ] Backend CORS allows the frontend origin +- [ ] Browser DevTools console shows no connection errors when loading the page + +## Future enhancements (post-MVP) + +Once the Extended-MVP is working (subscription management + feed fetching + item display), these features could be added: + +- **Persistence**: Save subscriptions and items between sessions (requires database implementation) +- **Remove subscriptions**: Allow users to delete feeds they no longer want +- **Background polling**: Automatically refresh feeds on a schedule +- **Better error handling**: Show detailed error messages for different failure scenarios +- **Content rendering**: Display full item content, not just title and link +- **Read/unread tracking**: Mark items as read and filter accordingly +- **Organization**: Group feeds into folders or categories + +## Technology selection note + +While this MVP is intentionally simple, the technology choices (ASP.NET Core + Blazor) should support future production-ready features without requiring a complete rewrite. The architecture allows for adding persistence, background operations, and enhanced UI capabilities as needed. + +## How this document fits with the others + +- [AppFeatures.md](AppFeatures.md) describes the specific user-facing features for the MVP +- [TechStack.md](TechStack.md) explains the technology choices and how they support the MVP goals diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-get-started-rss-feed/StakeholderDocuments/TechStack.md b/DownloadableCodeProjects/standalone-lab-projects/sdd-get-started-rss-feed/StakeholderDocuments/TechStack.md new file mode 100644 index 0000000..8f39316 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-get-started-rss-feed/StakeholderDocuments/TechStack.md @@ -0,0 +1,203 @@ +# Tech stack for RSS Feed Reader + +Our RSS feed reader will use an ASP.NET Core Web API backend and a Blazor WebAssembly frontend. This combination allows for rapid development of the MVP while supporting future production-ready enhancements. + +## Why ASP.NET Core Web API + Blazor WebAssembly? + +Building an RSS feed reader with an **ASP.NET Core Web API** backend and a **Blazor WebAssembly** frontend offers several advantages: + +1. **Quick Development**: Both technologies work well together with minimal setup, allowing for rapid development of the demonstration. + +2. **Separation of Concerns**: The backend handles data management and (in Extended-MVP) feed operations, while the frontend focuses on user interaction. + +3. **Cross-Platform**: Both ASP.NET Core and Blazor are cross-platform, allowing the application to run on Windows, macOS, and Linux. + +4. **Incremental Complexity**: Start with simple subscription management (MVP), then add feed fetching (Extended-MVP), then add persistence and advanced features. + +5. **Future-Ready Architecture**: While the MVP is minimal (just subscription list management), this architecture supports adding: + + - Feed fetching and parsing (`System.ServiceModel.Syndication`) + - Database persistence (EF Core + SQLite) + - Background processing (`BackgroundService` for polling) + - Advanced features (read/unread, folders, etc.) + +6. **Shared Code**: Blazor WebAssembly uses C#, allowing code sharing between frontend and backend if needed. + +## Responsibilities + +For the MVP (subscription management only): + +**Backend** is responsible for: + +- Exposing an API to add subscriptions +- Storing subscriptions in memory +- Returning the list of subscriptions + +**Frontend** is responsible for: + +- Subscription management UI (input field + add button) +- Displaying the list of subscriptions + +For the Extended-MVP (add feed fetching): + +**Backend** adds: + +- Fetching and parsing RSS/Atom feeds when requested +- Returning feed items to the UI + +**Frontend** adds: + +- Manual refresh button +- Displaying items (title and link minimum) +- Basic error messages + +## MVP-first implementation approach + +To deliver the MVP quickly: + +**MVP (subscription management only):** + +- **Storage**: Use in-memory storage (List or simple model). Subscriptions are lost when the app stops. +- **No feed operations**: No HTTP client, no parsing library, no feed fetching +- **Focus**: Basic UI and API communication (add subscription, get subscriptions list) + +**Extended-MVP (add feed fetching):** + +- **Parsing**: Add `System.ServiceModel.Syndication` for basic RSS/Atom parsing +- **HTTP client**: Add HttpClient for fetching feeds +- **Refresh**: Manual only - no background polling or scheduling +- **Error handling**: Simple "failed to load" messages, no detailed diagnostics +- **Content display**: Plain text only (title + link), no HTML rendering needed + +This incremental approach makes development extremely fast while keeping the architecture clean for future enhancements. + +## Local development + +### Blazor project initialization + +When creating a new Blazor WebAssembly project from the template, the project includes demonstration pages that must be removed to avoid conflicts with MVP features. + +**⚠️ CRITICAL: This cleanup must be completed in Phase 2 (Foundational) and VERIFIED before any UI feature implementation begins. Runtime errors from incomplete cleanup will waste development time.** + +**Required cleanup steps:** + +1. **Remove template demo pages** from `frontend/[ProjectName].UI/Pages/`: + - Delete `Home.razor` (conflicts with root route) + - Delete `Counter.razor` (demo page) + - Delete `Weather.razor` (demo page) + +2. **Update navigation menu** in `frontend/[ProjectName].UI/Layout/NavMenu.razor`: + - Remove navigation links to deleted demo pages + - Update menu items to reflect MVP features only + - Change root navigation link text to match your landing page (e.g., "Subscriptions") + +3. **Verify routing**: + - Ensure only ONE page uses `@page "/"` directive (your main MVP page) + - All other pages should use unique routes (e.g., `@page "/settings"`) + +4. **Verify cleanup completion** before proceeding with implementation: + + ```powershell + # List all Razor pages - should show ONLY your MVP pages (e.g., NotFound.razor, Subscriptions.razor) + Get-ChildItem frontend/[ProjectName].UI/Pages/ -Filter *.razor | Select-Object Name + ``` + + **STOP: Do not proceed with feature implementation until:** + - ✗ Home.razor is GONE + - ✗ Counter.razor is GONE + - ✗ Weather.razor is GONE + - ✓ Only your MVP pages remain + +5. **Test for routing conflicts immediately** after cleanup: + + ```powershell + # Clean build to remove cached assemblies + dotnet clean frontend/[ProjectName].UI/[ProjectName].UI.csproj + dotnet build frontend/[ProjectName].UI/[ProjectName].UI.csproj + + # Start frontend to verify no routing errors + dotnet run --project frontend/[ProjectName].UI + ``` + + Navigate to the frontend URL in your browser. If you see an "ambiguous route" error in the browser console (F12 Developer Tools), cleanup is incomplete. **Fix the issue before implementing any features.** + +**Why this matters:** + +Blazor templates include demonstration pages with pre-configured routes. If you create new pages with the same routes (especially the root route `/`), you'll encounter **ambiguous route exceptions** at runtime. The error message will look like: + +``` +System.InvalidOperationException: The following routes are ambiguous: +'' in '[ProjectName].UI.Pages.Home' +'' in '[ProjectName].UI.Pages.YourFeature' +``` + +These errors only appear at runtime after you've already implemented features, making them costly to debug. The verification steps above catch this issue immediately during Phase 2 cleanup, before any feature work begins. + +Cleaning up template pages before implementing MVP features prevents these conflicts and ensures a clean project structure focused on business requirements. + +### Port configuration + +The backend API and frontend UI run on separate localhost ports. **Port consistency is critical** - the ports must be coordinated between three locations: + +1. **Backend port** (defined in `backend/RSSFeedReader.Api/Properties/launchSettings.json`): + + - Default: `http://localhost:5151` + - This is where the API listens for requests + +2. **Frontend port** (defined in `frontend/RSSFeedReader.UI/Properties/launchSettings.json`): + + - Default: `http://localhost:5213` + - This is where the Blazor app runs + +3. **API base URL** (configured in `frontend/RSSFeedReader.UI/wwwroot/appsettings.json`): + + - Must match the backend port from step 1 + - Example: `{"ApiBaseUrl": "http://localhost:5151/api/"}` + +4. **CORS policy** (configured in `backend/RSSFeedReader.Api/Program.cs`): + + - Must allow the frontend port from step 2 + - Example: `.WithOrigins("http://localhost:5213", "https://localhost:7025")` + +### Configuration best practices + +- **Frontend Program.cs**: Read API URL from configuration, don't hardcode: + + ```csharp + var apiBaseUrl = builder.Configuration["ApiBaseUrl"] ?? "http://localhost:5151/api/"; + builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(apiBaseUrl) }); + ``` + +- **Backend CORS**: Allow the actual frontend ports from launchSettings.json + +- **Testing setup**: Before testing, verify: + + 1. Backend is running and accessible at the configured port + 2. Frontend appsettings.json points to the correct backend port + 3. CORS allows the frontend origin + +**For MVP:** Test by adding subscription URLs and verifying they appear in the list. + +**For Extended-MVP:** Test with a known-good feed like + +## Future enhancements (post-MVP) + +When ready to extend beyond the basic demonstration, this architecture supports: + +- **Database persistence**: Add EF Core + SQLite for storing subscriptions and items between sessions +- **Background polling**: Implement `BackgroundService` to automatically refresh feeds on a schedule +- **HTML sanitization**: Add `HtmlSanitizer` library to safely display rich content from feeds +- **Website-to-feed discovery**: Use `HtmlAgilityPack` to find feed URLs from website links +- **Better error handling**: Implement retry logic, timeouts, and detailed error messages +- **Testing**: Add unit and integration tests using xUnit +- **Optimization**: Implement HTTP caching (ETag/Last-Modified), de-duplication, and performance improvements + +## Summary + +ASP.NET Core Web API with Blazor WebAssembly provides a straightforward path to building the RSS feed reader incrementally: + +- **MVP**: Subscription management only (add + list) - extremely simple, no feed operations +- **Extended-MVP**: Add feed fetching and item display - still simple with in-memory storage and manual refresh +- **Future**: Add persistence, background processing, and advanced features + +The architecture is intentionally minimal to enable fast development, while the technology choices support adding production-ready features later without requiring a complete rewrite. diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/.gitignore b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/.gitignore new file mode 100644 index 0000000..dfcfd56 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/.gitignore @@ -0,0 +1,350 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/App.razor b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/App.razor new file mode 100644 index 0000000..5f8190b --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/App.razor @@ -0,0 +1,43 @@ +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Authorization +@using ContosoDashboard.Shared + + + + + + + @if (context.User.Identity?.IsAuthenticated != true) + { + + } + else + { +
+
+

Access Denied

+

You do not have permission to access this page.

+ Go to Home +
+
+ } +
+
+ +
+ + Not found + +
+
+

Page not found

+

Sorry, there's nothing at this address.

+ Go to Home +
+
+
+
+
+
+ diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/ContosoDashboard.csproj b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/ContosoDashboard.csproj new file mode 100644 index 0000000..27c91c5 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/ContosoDashboard.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Data/ApplicationDbContext.cs b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Data/ApplicationDbContext.cs new file mode 100644 index 0000000..3931257 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Data/ApplicationDbContext.cs @@ -0,0 +1,225 @@ +using Microsoft.EntityFrameworkCore; +using ContosoDashboard.Models; + +namespace ContosoDashboard.Data; + +public class ApplicationDbContext : DbContext +{ + public ApplicationDbContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Users { get; set; } = null!; + public DbSet Tasks { get; set; } = null!; + public DbSet Projects { get; set; } = null!; + public DbSet TaskComments { get; set; } = null!; + public DbSet Notifications { get; set; } = null!; + public DbSet ProjectMembers { get; set; } = null!; + public DbSet Announcements { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // Configure User relationships + modelBuilder.Entity() + .HasMany(u => u.AssignedTasks) + .WithOne(t => t.AssignedUser) + .HasForeignKey(t => t.AssignedUserId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasMany(u => u.CreatedTasks) + .WithOne(t => t.CreatedByUser) + .HasForeignKey(t => t.CreatedByUserId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasMany(u => u.ManagedProjects) + .WithOne(p => p.ProjectManager) + .HasForeignKey(p => p.ProjectManagerId) + .OnDelete(DeleteBehavior.Restrict); + + // Configure indexes for performance + modelBuilder.Entity() + .HasIndex(t => t.AssignedUserId); + + modelBuilder.Entity() + .HasIndex(t => t.Status); + + modelBuilder.Entity() + .HasIndex(t => t.DueDate); + + modelBuilder.Entity() + .HasIndex(p => p.ProjectManagerId); + + modelBuilder.Entity() + .HasIndex(p => p.Status); + + modelBuilder.Entity() + .HasIndex(n => new { n.UserId, n.IsRead }); + + modelBuilder.Entity() + .HasIndex(u => u.Email) + .IsUnique(); + + // Seed initial data + SeedData(modelBuilder); + } + + private void SeedData(ModelBuilder modelBuilder) + { + // Seed an admin user + modelBuilder.Entity().HasData( + new User + { + UserId = 1, + Email = "admin@contoso.com", + DisplayName = "System Administrator", + Department = "IT", + JobTitle = "Administrator", + Role = UserRole.Administrator, + AvailabilityStatus = AvailabilityStatus.Available, + CreatedDate = DateTime.UtcNow, + EmailNotificationsEnabled = true, + InAppNotificationsEnabled = true + }, + new User + { + UserId = 2, + Email = "camille.nicole@contoso.com", + DisplayName = "Camille Nicole", + Department = "Engineering", + JobTitle = "Project Manager", + Role = UserRole.ProjectManager, + AvailabilityStatus = AvailabilityStatus.Available, + CreatedDate = DateTime.UtcNow, + EmailNotificationsEnabled = true, + InAppNotificationsEnabled = true + }, + new User + { + UserId = 3, + Email = "floris.kregel@contoso.com", + DisplayName = "Floris Kregel", + Department = "Engineering", + JobTitle = "Team Lead", + Role = UserRole.TeamLead, + AvailabilityStatus = AvailabilityStatus.Available, + CreatedDate = DateTime.UtcNow, + EmailNotificationsEnabled = true, + InAppNotificationsEnabled = true + }, + new User + { + UserId = 4, + Email = "ni.kang@contoso.com", + DisplayName = "Ni Kang", + Department = "Engineering", + JobTitle = "Software Engineer", + Role = UserRole.Employee, + AvailabilityStatus = AvailabilityStatus.Available, + CreatedDate = DateTime.UtcNow, + EmailNotificationsEnabled = true, + InAppNotificationsEnabled = true + } + ); + + // Seed a sample project + modelBuilder.Entity().HasData( + new Project + { + ProjectId = 1, + Name = "ContosoDashboard Development", + Description = "Internal employee productivity dashboard", + ProjectManagerId = 2, + StartDate = DateTime.UtcNow.AddDays(-30), + TargetCompletionDate = DateTime.UtcNow.AddDays(60), + Status = ProjectStatus.Active, + CreatedDate = DateTime.UtcNow.AddDays(-30), + UpdatedDate = DateTime.UtcNow + } + ); + + // Seed sample tasks + modelBuilder.Entity().HasData( + new TaskItem + { + TaskId = 1, + Title = "Design database schema", + Description = "Create entity relationship diagram and database design", + Priority = TaskPriority.High, + Status = Models.TaskStatus.Completed, + DueDate = DateTime.UtcNow.AddDays(-20), + AssignedUserId = 4, + CreatedByUserId = 2, + ProjectId = 1, + CreatedDate = DateTime.UtcNow.AddDays(-30), + UpdatedDate = DateTime.UtcNow.AddDays(-20) + }, + new TaskItem + { + TaskId = 2, + Title = "Implement authentication", + Description = "Set up Microsoft Entra ID authentication", + Priority = TaskPriority.Critical, + Status = Models.TaskStatus.InProgress, + DueDate = DateTime.UtcNow.AddDays(5), + AssignedUserId = 4, + CreatedByUserId = 2, + ProjectId = 1, + CreatedDate = DateTime.UtcNow.AddDays(-25), + UpdatedDate = DateTime.UtcNow + }, + new TaskItem + { + TaskId = 3, + Title = "Create UI mockups", + Description = "Design user interface mockups for all main pages", + Priority = TaskPriority.Medium, + Status = Models.TaskStatus.NotStarted, + DueDate = DateTime.UtcNow.AddDays(10), + AssignedUserId = 4, + CreatedByUserId = 2, + ProjectId = 1, + CreatedDate = DateTime.UtcNow.AddDays(-20), + UpdatedDate = DateTime.UtcNow.AddDays(-20) + } + ); + + // Seed project members + modelBuilder.Entity().HasData( + new ProjectMember + { + ProjectMemberId = 1, + ProjectId = 1, + UserId = 3, + Role = "TeamLead", + AssignedDate = DateTime.UtcNow.AddDays(-30) + }, + new ProjectMember + { + ProjectMemberId = 2, + ProjectId = 1, + UserId = 4, + Role = "Developer", + AssignedDate = DateTime.UtcNow.AddDays(-30) + } + ); + + // Seed announcement + modelBuilder.Entity().HasData( + new Announcement + { + AnnouncementId = 1, + Title = "Welcome to ContosoDashboard", + Content = "Welcome to the new ContosoDashboard application. This platform will help you manage your tasks and projects more efficiently.", + CreatedByUserId = 1, + PublishDate = DateTime.UtcNow, + ExpiryDate = DateTime.UtcNow.AddDays(30), + IsActive = true + } + ); + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Models/Announcement.cs b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Models/Announcement.cs new file mode 100644 index 0000000..7384729 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Models/Announcement.cs @@ -0,0 +1,31 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace ContosoDashboard.Models; + +public class Announcement +{ + [Key] + public int AnnouncementId { get; set; } + + [Required] + [MaxLength(255)] + public string Title { get; set; } = string.Empty; + + [Required] + [MaxLength(5000)] + public string Content { get; set; } = string.Empty; + + [Required] + public int CreatedByUserId { get; set; } + + public DateTime PublishDate { get; set; } = DateTime.UtcNow; + + public DateTime? ExpiryDate { get; set; } + + public bool IsActive { get; set; } = true; + + // Navigation properties + [ForeignKey("CreatedByUserId")] + public virtual User CreatedByUser { get; set; } = null!; +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Models/Notification.cs b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Models/Notification.cs new file mode 100644 index 0000000..ecdf386 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Models/Notification.cs @@ -0,0 +1,53 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace ContosoDashboard.Models; + +public class Notification +{ + [Key] + public int NotificationId { get; set; } + + [Required] + public int UserId { get; set; } + + [Required] + [MaxLength(255)] + public string Title { get; set; } = string.Empty; + + [Required] + [MaxLength(1000)] + public string Message { get; set; } = string.Empty; + + [Required] + public NotificationType Type { get; set; } + + [Required] + public NotificationPriority Priority { get; set; } = NotificationPriority.Informational; + + public bool IsRead { get; set; } = false; + + public DateTime CreatedDate { get; set; } = DateTime.UtcNow; + + // Navigation properties + [ForeignKey("UserId")] + public virtual User User { get; set; } = null!; +} + +public enum NotificationType +{ + TaskAssignment, + TaskUpdate, + TaskDueSoon, + TaskCompleted, + TaskComment, + ProjectUpdate, + SystemAnnouncement +} + +public enum NotificationPriority +{ + Urgent, + Important, + Informational +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Models/Project.cs b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Models/Project.cs new file mode 100644 index 0000000..3de5b84 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Models/Project.cs @@ -0,0 +1,58 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace ContosoDashboard.Models; + +public class Project +{ + [Key] + public int ProjectId { get; set; } + + [Required] + [MaxLength(255)] + public string Name { get; set; } = string.Empty; + + [MaxLength(2000)] + public string? Description { get; set; } + + [Required] + public int ProjectManagerId { get; set; } + + public DateTime StartDate { get; set; } = DateTime.UtcNow; + + public DateTime? TargetCompletionDate { get; set; } + + [Required] + public ProjectStatus Status { get; set; } = ProjectStatus.Planning; + + public DateTime CreatedDate { get; set; } = DateTime.UtcNow; + + public DateTime UpdatedDate { get; set; } = DateTime.UtcNow; + + // Navigation properties + [ForeignKey("ProjectManagerId")] + public virtual User ProjectManager { get; set; } = null!; + + public virtual ICollection Tasks { get; set; } = new List(); + public virtual ICollection ProjectMembers { get; set; } = new List(); + + // Computed property + [NotMapped] + public int CompletionPercentage + { + get + { + if (Tasks == null || !Tasks.Any()) return 0; + var completedCount = Tasks.Count(t => t.Status == TaskStatus.Completed); + return (int)((double)completedCount / Tasks.Count * 100); + } + } +} + +public enum ProjectStatus +{ + Planning, + Active, + OnHold, + Completed +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Models/ProjectMember.cs b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Models/ProjectMember.cs new file mode 100644 index 0000000..ce42a57 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Models/ProjectMember.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace ContosoDashboard.Models; + +public class ProjectMember +{ + [Key] + public int ProjectMemberId { get; set; } + + [Required] + public int ProjectId { get; set; } + + [Required] + public int UserId { get; set; } + + [MaxLength(50)] + public string Role { get; set; } = "TeamMember"; + + public DateTime AssignedDate { get; set; } = DateTime.UtcNow; + + // Navigation properties + [ForeignKey("ProjectId")] + public virtual Project Project { get; set; } = null!; + + [ForeignKey("UserId")] + public virtual User User { get; set; } = null!; +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Models/TaskComment.cs b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Models/TaskComment.cs new file mode 100644 index 0000000..57fc8c1 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Models/TaskComment.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace ContosoDashboard.Models; + +public class TaskComment +{ + [Key] + public int CommentId { get; set; } + + [Required] + public int TaskId { get; set; } + + [Required] + public int UserId { get; set; } + + [Required] + [MaxLength(2000)] + public string CommentText { get; set; } = string.Empty; + + public DateTime CreatedDate { get; set; } = DateTime.UtcNow; + + // Navigation properties + [ForeignKey("TaskId")] + public virtual TaskItem Task { get; set; } = null!; + + [ForeignKey("UserId")] + public virtual User User { get; set; } = null!; +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Models/TaskItem.cs b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Models/TaskItem.cs new file mode 100644 index 0000000..6f4de3f --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Models/TaskItem.cs @@ -0,0 +1,64 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace ContosoDashboard.Models; + +public class TaskItem +{ + [Key] + public int TaskId { get; set; } + + [Required] + [MaxLength(255)] + public string Title { get; set; } = string.Empty; + + [MaxLength(2000)] + public string? Description { get; set; } + + [Required] + public TaskPriority Priority { get; set; } = TaskPriority.Medium; + + [Required] + public TaskStatus Status { get; set; } = TaskStatus.NotStarted; + + public DateTime? DueDate { get; set; } + + [Required] + public int AssignedUserId { get; set; } + + [Required] + public int CreatedByUserId { get; set; } + + public int? ProjectId { get; set; } + + public DateTime CreatedDate { get; set; } = DateTime.UtcNow; + + public DateTime UpdatedDate { get; set; } = DateTime.UtcNow; + + // Navigation properties + [ForeignKey("AssignedUserId")] + public virtual User AssignedUser { get; set; } = null!; + + [ForeignKey("CreatedByUserId")] + public virtual User CreatedByUser { get; set; } = null!; + + [ForeignKey("ProjectId")] + public virtual Project? Project { get; set; } + + public virtual ICollection Comments { get; set; } = new List(); +} + +public enum TaskPriority +{ + Low, + Medium, + High, + Critical +} + +public enum TaskStatus +{ + NotStarted, + InProgress, + Completed +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Models/User.cs b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Models/User.cs new file mode 100644 index 0000000..cfe32c6 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Models/User.cs @@ -0,0 +1,67 @@ +using System.ComponentModel.DataAnnotations; + +namespace ContosoDashboard.Models; + +public class User +{ + [Key] + public int UserId { get; set; } + + [Required] + [EmailAddress] + [MaxLength(255)] + public string Email { get; set; } = string.Empty; + + [Required] + [MaxLength(255)] + public string DisplayName { get; set; } = string.Empty; + + [MaxLength(100)] + public string? Department { get; set; } + + [MaxLength(100)] + public string? JobTitle { get; set; } + + [Required] + public UserRole Role { get; set; } = UserRole.Employee; + + [MaxLength(500)] + public string? ProfilePhotoUrl { get; set; } + + public AvailabilityStatus AvailabilityStatus { get; set; } = AvailabilityStatus.Available; + + public DateTime CreatedDate { get; set; } = DateTime.UtcNow; + + public DateTime? LastLoginDate { get; set; } + + [MaxLength(20)] + public string? PhoneNumber { get; set; } + + public bool EmailNotificationsEnabled { get; set; } = true; + + public bool InAppNotificationsEnabled { get; set; } = true; + + // Navigation properties + public virtual ICollection AssignedTasks { get; set; } = new List(); + public virtual ICollection CreatedTasks { get; set; } = new List(); + public virtual ICollection ManagedProjects { get; set; } = new List(); + public virtual ICollection ProjectMemberships { get; set; } = new List(); + public virtual ICollection Notifications { get; set; } = new List(); + public virtual ICollection Comments { get; set; } = new List(); +} + +public enum UserRole +{ + Employee, + TeamLead, + ProjectManager, + Administrator +} + +public enum AvailabilityStatus +{ + Available, + Busy, + InMeeting, + OutOfOffice +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Index.razor b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Index.razor new file mode 100644 index 0000000..cd1697f --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Index.razor @@ -0,0 +1,167 @@ +@page "/" +@using ContosoDashboard.Services +@using ContosoDashboard.Models +@using Microsoft.AspNetCore.Authorization +@attribute [Authorize] +@inject IDashboardService DashboardService +@inject INotificationService NotificationService +@inject AuthenticationStateProvider AuthenticationStateProvider + +Dashboard - ContosoDashboard + +
+
+
+

Welcome, @userName!

+

@DateTime.Now.ToString("dddd, MMMM dd, yyyy")

+
+
+ + @if (summary != null) + { +
+
+
+
+
Active Tasks
+

@summary.TotalActiveTasks

+
+
+
+
+
+
+
Due Today
+

@summary.TasksDueToday

+
+
+
+
+
+
+
Active Projects
+

@summary.ActiveProjects

+
+
+
+
+
+
+
Notifications
+

@summary.UnreadNotifications

+
+
+
+
+ } + + @if (announcements != null && announcements.Any()) + { +
+
+

Announcements

+ @foreach (var announcement in announcements) + { +
+
+
@announcement.Title
+

@announcement.Content

+ + Posted by @announcement.CreatedByUser.DisplayName on @announcement.PublishDate.ToString("MMM dd, yyyy") + +
+
+ } +
+
+ } + +
+ + +
+

Recent Notifications

+ @if (recentNotifications != null && recentNotifications.Any()) + { +
+ @foreach (var notification in recentNotifications.Take(5)) + { +
+
+
@notification.Title
+ @GetRelativeTime(notification.CreatedDate) +
+

@notification.Message

+
+ } +
+ } + else + { +

No recent notifications

+ } +
+
+
+ +@code { + private DashboardSummary? summary; + private List? announcements; + private List? recentNotifications; + private string userName = "User"; + private int currentUserId = 0; + + protected override async Task OnInitializedAsync() + { + // Get the current user from authentication + var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); + var user = authState.User; + userName = user.Identity?.Name ?? "User"; + + // Get user ID from claims + var userIdClaim = user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier); + if (userIdClaim != null && int.TryParse(userIdClaim.Value, out int userId)) + { + currentUserId = userId; + } + + if (currentUserId > 0) + { + summary = await DashboardService.GetDashboardSummaryAsync(currentUserId); + announcements = await DashboardService.GetActiveAnnouncementsAsync(); + recentNotifications = await NotificationService.GetUserNotificationsAsync(currentUserId); + } + } + + private string GetRelativeTime(DateTime dateTime) + { + var timeSpan = DateTime.UtcNow - dateTime; + + if (timeSpan.TotalMinutes < 1) + return "Just now"; + if (timeSpan.TotalMinutes < 60) + return $"{(int)timeSpan.TotalMinutes} min ago"; + if (timeSpan.TotalHours < 24) + return $"{(int)timeSpan.TotalHours} hours ago"; + if (timeSpan.TotalDays < 7) + return $"{(int)timeSpan.TotalDays} days ago"; + + return dateTime.ToString("MMM dd, yyyy"); + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Login.cshtml b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Login.cshtml new file mode 100644 index 0000000..286272c --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Login.cshtml @@ -0,0 +1,69 @@ +@page +@model ContosoDashboard.Pages.LoginModel +@{ + Layout = null; +} + + + + + + + Login - ContosoDashboard + + + + +
+
+
+
+
+
+

ContosoDashboard

+

Training Environment - Mock Login

+
+ + @if (!string.IsNullOrEmpty(Model.ErrorMessage)) + { + + } + +
+ @Html.AntiForgeryToken() +
+ + +
+ +
+ +
+
+ + +
+
+
+
+
+ + diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Login.cshtml.cs b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Login.cshtml.cs new file mode 100644 index 0000000..0092b8c --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Login.cshtml.cs @@ -0,0 +1,96 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using System.Security.Claims; +using ContosoDashboard.Services; +using ContosoDashboard.Models; + +namespace ContosoDashboard.Pages +{ + public class LoginModel : PageModel + { + private readonly IUserService _userService; + + public LoginModel(IUserService userService) + { + _userService = userService; + } + + public List? Users { get; set; } + public string? ErrorMessage { get; set; } + + public async Task OnGetAsync() + { + // Load all users for the dropdown + Users = await _userService.GetAllUsersAsync(); + } + + public async Task OnPostAsync(int selectedUserId) + { + Console.WriteLine($"Login POST: selectedUserId = {selectedUserId}"); + + // Reload users for the form in case of error + Users = await _userService.GetAllUsersAsync(); + + if (selectedUserId == 0) + { + Console.WriteLine("Login POST: No user selected"); + ErrorMessage = "Please select a user"; + return Page(); + } + + var user = await _userService.GetUserByIdAsync(selectedUserId); + + if (user == null) + { + Console.WriteLine($"Login POST: User {selectedUserId} not found"); + ErrorMessage = "User not found"; + return Page(); + } + + try + { + Console.WriteLine($"Login POST: Attempting to sign in user {user.DisplayName}"); + + // Create claims for the authenticated user + var claims = new List + { + new Claim(ClaimTypes.NameIdentifier, user.UserId.ToString()), + new Claim(ClaimTypes.Name, user.DisplayName), + new Claim(ClaimTypes.Email, user.Email), + new Claim(ClaimTypes.Role, user.Role.ToString()) + }; + + var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); + var authProperties = new AuthenticationProperties + { + IsPersistent = true, + ExpiresUtc = DateTimeOffset.UtcNow.AddHours(8) + }; + + await HttpContext.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(claimsIdentity), + authProperties); + + Console.WriteLine($"Login POST: Sign in successful, redirecting to /"); + + // Update last login date + user.LastLoginDate = DateTime.UtcNow; + await _userService.UpdateUserProfileAsync(user, user.UserId); + + // Redirect to home page + return Redirect("/"); + } + catch (Exception ex) + { + // Log the actual error but show generic message to user + Console.WriteLine($"Login error: {ex.Message}"); + Console.WriteLine($"Login error stack: {ex.StackTrace}"); + ErrorMessage = "Login failed. Please try again."; + return Page(); + } + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Logout.cshtml b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Logout.cshtml new file mode 100644 index 0000000..cc815e1 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Logout.cshtml @@ -0,0 +1,5 @@ +@page +@model ContosoDashboard.Pages.LogoutModel +@{ + // This page will automatically redirect after signing out +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Logout.cshtml.cs b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Logout.cshtml.cs new file mode 100644 index 0000000..9c1e758 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Logout.cshtml.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; + +namespace ContosoDashboard.Pages +{ + public class LogoutModel : PageModel + { + public async Task OnGetAsync() + { + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + return Redirect("/login"); + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Notifications.razor b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Notifications.razor new file mode 100644 index 0000000..ac2a20e --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Notifications.razor @@ -0,0 +1,155 @@ +@page "/notifications" +@using ContosoDashboard.Services +@using ContosoDashboard.Models +@using Microsoft.AspNetCore.Authorization +@using System.Security.Claims +@attribute [Authorize] +@inject INotificationService NotificationService +@inject AuthenticationStateProvider AuthenticationStateProvider + +Notifications - ContosoDashboard + +
+
+
+

Notifications

+
+
+ + @if (notifications == null) + { +

Loading...

+ } + else if (!notifications.Any()) + { +
No notifications to display.
+ } + else + { +
+
+
+ @foreach (var notification in notifications) + { +
+
+
+
+
@notification.Title
+ + @notification.Priority + + @notification.Type +
+

@notification.Message

+ @GetRelativeTime(notification.CreatedDate) +
+ @if (!notification.IsRead) + { + + } +
+
+ } +
+
+ +
+
+
+
Summary
+

+ Total Notifications: @notifications.Count
+ Unread: @notifications.Count(n => !n.IsRead)
+ Read: @notifications.Count(n => n.IsRead) +

+ @if (notifications.Any(n => !n.IsRead)) + { + + } +
+
+
+
+ } +
+ +@code { + private List? notifications; + private int currentUserId = 0; + + protected override async Task OnInitializedAsync() + { + // Get current user ID from authentication claims + var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); + var user = authState.User; + var userIdClaim = user.FindFirst(ClaimTypes.NameIdentifier); + if (userIdClaim != null && int.TryParse(userIdClaim.Value, out int userId)) + { + currentUserId = userId; + } + + if (currentUserId > 0) + { + await LoadNotifications(); + } + } + + private async Task LoadNotifications() + { + notifications = await NotificationService.GetUserNotificationsAsync(currentUserId, unreadOnly: false); + } + + private async Task MarkAsRead(int notificationId) + { + if (currentUserId > 0) + { + await NotificationService.MarkAsReadAsync(notificationId, currentUserId); + await LoadNotifications(); + } + } + + private async Task MarkAllAsRead() + { + if (notifications != null && currentUserId > 0) + { + foreach (var notification in notifications.Where(n => !n.IsRead)) + { + await NotificationService.MarkAsReadAsync(notification.NotificationId, currentUserId); + } + await LoadNotifications(); + } + } + + private string GetPriorityColor(NotificationPriority priority) + { + return priority switch + { + NotificationPriority.Urgent => "danger", + NotificationPriority.Important => "warning", + NotificationPriority.Informational => "info", + _ => "secondary" + }; + } + + private string GetRelativeTime(DateTime dateTime) + { + var timeSpan = DateTime.UtcNow - dateTime; + + if (timeSpan.TotalMinutes < 1) + return "Just now"; + if (timeSpan.TotalMinutes < 60) + return $"{(int)timeSpan.TotalMinutes} min ago"; + if (timeSpan.TotalHours < 24) + return $"{(int)timeSpan.TotalHours} hours ago"; + if (timeSpan.TotalDays < 7) + return $"{(int)timeSpan.TotalDays} days ago"; + + return dateTime.ToString("MMM dd, yyyy"); + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Profile.razor b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Profile.razor new file mode 100644 index 0000000..1629697 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Profile.razor @@ -0,0 +1,187 @@ +@page "/profile" +@using ContosoDashboard.Services +@using ContosoDashboard.Models +@using Microsoft.AspNetCore.Authorization +@using System.Security.Claims +@attribute [Authorize] +@inject IUserService UserService +@inject AuthenticationStateProvider AuthenticationStateProvider + +My Profile - ContosoDashboard + +
+
+
+

My Profile

+
+
+ + @if (user != null) + { +
+
+
+
+
Profile Information
+ + + + + +
+ + +
+ +
+ + + Email cannot be changed +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + + + + + +
+ +
Notification Preferences
+ +
+ + +
+ +
+ + +
+ + + @if (showSuccessMessage) + { + + Profile updated successfully! + + } +
+
+
+
+ +
+
+
+
+ @if (!string.IsNullOrEmpty(user.ProfilePhotoUrl)) + { + Profile + } + else + { +
+ @GetInitials(user.DisplayName) +
+ } +
+ +
@user.DisplayName
+

@user.JobTitle

+

@user.Department

+ +
+ +
+

Role: @user.Role

+

Member Since: @user.CreatedDate.ToString("MMMM yyyy")

+ @if (user.LastLoginDate.HasValue) + { +

Last Login: @user.LastLoginDate.Value.ToString("MMM dd, yyyy")

+ } +
+
+
+
+
+ } + else + { +

Loading...

+ } +
+ +@code { + private User? user; + private bool showSuccessMessage = false; + private int currentUserId = 0; + + protected override async Task OnInitializedAsync() + { + // Get current user ID from authentication claims + var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); + var authUser = authState.User; + var userIdClaim = authUser.FindFirst(ClaimTypes.NameIdentifier); + if (userIdClaim != null && int.TryParse(userIdClaim.Value, out int userId)) + { + currentUserId = userId; + } + + if (currentUserId > 0) + { + user = await UserService.GetUserByIdAsync(currentUserId); + } + } + + private async Task HandleValidSubmit() + { + if (user != null && currentUserId > 0) + { + var success = await UserService.UpdateUserProfileAsync(user, currentUserId); + if (success) + { + showSuccessMessage = true; + + // Hide success message after 3 seconds + await Task.Delay(3000); + showSuccessMessage = false; + StateHasChanged(); + } + } + } + + private string GetInitials(string displayName) + { + if (string.IsNullOrEmpty(displayName)) + return "?"; + + var parts = displayName.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 1) + return parts[0].Substring(0, Math.Min(2, parts[0].Length)).ToUpper(); + + return $"{parts[0][0]}{parts[^1][0]}".ToUpper(); + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/ProjectDetails.razor b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/ProjectDetails.razor new file mode 100644 index 0000000..0019238 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/ProjectDetails.razor @@ -0,0 +1,308 @@ +@page "/projects/{projectId:int}" +@using ContosoDashboard.Services +@using ContosoDashboard.Models +@using Microsoft.AspNetCore.Authorization +@using System.Security.Claims +@attribute [Authorize] +@inject IProjectService ProjectService +@inject NavigationManager NavigationManager +@inject AuthenticationStateProvider AuthenticationStateProvider + +Project Details - ContosoDashboard + +
+ @if (project == null) + { +

Loading...

+ } + else + { +
+
+ +

@project.Name

+
+
+ +
+
+
+
+
Project Information
+
+
+
+
+ +

@project.Description

+
+
+ +

+ + @project.Status + +

+
+
+ +
+
+ @project.CompletionPercentage% +
+
+
+
+ +
+
+ +

@project.ProjectManager.DisplayName

+
+
+ +

@project.StartDate.ToString("MMM dd, yyyy")

+
+
+ +

+ @if (project.TargetCompletionDate.HasValue) + { + @project.TargetCompletionDate.Value.ToString("MMM dd, yyyy") + } + else + { + Not set + } +

+
+
+
+
+ +
+
+
Project Tasks
+
+
+ @if (!project.Tasks.Any()) + { +

No tasks assigned to this project.

+ } + else + { +
+ + + + + + + + + + + + @foreach (var task in project.Tasks.OrderBy(t => t.DueDate)) + { + "> + + + + + + + } + +
TitleAssigned ToStatusPriorityDue Date
+ @task.Title + @if (!string.IsNullOrEmpty(task.Description)) + { +
+ @task.Description + } +
@task.AssignedUser.DisplayName + + @task.Status + + + + @task.Priority + + + @(task.DueDate?.ToString("MMM dd, yyyy") ?? "No due date") + @if (task.DueDate.HasValue && task.DueDate < DateTime.Today && task.Status != Models.TaskStatus.Completed) + { + + Overdue + + } +
+
+ } +
+
+
+ +
+
+
+
Team Members
+
+
+ @if (!project.ProjectMembers.Any()) + { +

No team members assigned.

+ } + else + { +
+ @foreach (var member in project.ProjectMembers.OrderBy(m => m.User.DisplayName)) + { +
+
+ @if (!string.IsNullOrEmpty(member.User.ProfilePhotoUrl)) + { + @member.User.DisplayName + } + else + { +
+ @GetInitials(member.User.DisplayName) +
+ } +
+
+ @member.User.DisplayName +
+ @member.Role +
+
+
+ } +
+ } +
+
+ +
+
+
Statistics
+
+
+
+ +

@project.Tasks.Count

+
+
+ +

@project.Tasks.Count(t => t.Status == Models.TaskStatus.Completed)

+
+
+ +

@project.Tasks.Count(t => t.Status == Models.TaskStatus.InProgress)

+
+
+ +

+ @project.Tasks.Count(t => t.DueDate.HasValue && t.DueDate < DateTime.Today && t.Status != Models.TaskStatus.Completed) +

+
+
+ +

@project.ProjectMembers.Count

+
+
+
+
+
+ } +
+ +@code { + [Parameter] + public int ProjectId { get; set; } + + private Project? project; + private int currentUserId = 0; + + protected override async Task OnInitializedAsync() + { + // Get current user ID from authentication claims + var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); + var user = authState.User; + var userIdClaim = user.FindFirst(ClaimTypes.NameIdentifier); + if (userIdClaim != null && int.TryParse(userIdClaim.Value, out int userId)) + { + currentUserId = userId; + } + + if (currentUserId > 0) + { + project = await ProjectService.GetProjectByIdAsync(ProjectId, currentUserId); + } + + if (project == null) + { + NavigationManager.NavigateTo("/projects"); + } + } + + private string GetStatusColor(ProjectStatus status) + { + return status switch + { + ProjectStatus.Planning => "secondary", + ProjectStatus.Active => "primary", + ProjectStatus.OnHold => "warning", + ProjectStatus.Completed => "success", + _ => "secondary" + }; + } + + private string GetTaskStatusColor(Models.TaskStatus status) + { + return status switch + { + Models.TaskStatus.NotStarted => "secondary", + Models.TaskStatus.InProgress => "primary", + Models.TaskStatus.Completed => "success", + _ => "secondary" + }; + } + + private string GetPriorityColor(TaskPriority priority) + { + return priority switch + { + TaskPriority.Low => "secondary", + TaskPriority.Medium => "info", + TaskPriority.High => "warning", + TaskPriority.Critical => "danger", + _ => "secondary" + }; + } + + private string GetInitials(string displayName) + { + var names = displayName.Split(' '); + if (names.Length >= 2) + { + return $"{names[0][0]}{names[1][0]}".ToUpper(); + } + return displayName.Length > 0 ? displayName[0].ToString().ToUpper() : "?"; + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Projects.razor b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Projects.razor new file mode 100644 index 0000000..9b910e1 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Projects.razor @@ -0,0 +1,124 @@ +@page "/projects" +@using ContosoDashboard.Services +@using ContosoDashboard.Models +@using Microsoft.AspNetCore.Authorization +@using System.Security.Claims +@attribute [Authorize] +@inject IProjectService ProjectService +@inject AuthenticationStateProvider AuthenticationStateProvider + +My Projects - ContosoDashboard + +
+
+
+

My Projects

+
+
+ + @if (projects == null) + { +

Loading...

+ } + else if (!projects.Any()) + { +
No projects found.
+ } + else + { +
+ @foreach (var project in projects) + { +
+
+
+
@project.Name
+

@project.Description

+ +
+ Project Manager:
+ @project.ProjectManager.DisplayName +
+ +
+ Status:
+ + @project.Status + +
+ +
+ Progress: +
+
+ @project.CompletionPercentage% +
+
+
+ +
+ Target Completion:
+ @if (project.TargetCompletionDate.HasValue) + { + @project.TargetCompletionDate.Value.ToString("MMM dd, yyyy") + } + else + { + Not set + } +
+ +
+ Tasks: @project.Tasks.Count
+ Team Members: @project.ProjectMembers.Count +
+
+ +
+
+ } +
+ } +
+ +@code { + private List? projects; + private int currentUserId = 0; + + protected override async Task OnInitializedAsync() + { + // Get current user ID from authentication claims + var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); + var user = authState.User; + var userIdClaim = user.FindFirst(ClaimTypes.NameIdentifier); + if (userIdClaim != null && int.TryParse(userIdClaim.Value, out int userId)) + { + currentUserId = userId; + } + + if (currentUserId > 0) + { + projects = await ProjectService.GetUserProjectsAsync(currentUserId); + } + } + + private string GetStatusColor(ProjectStatus status) + { + return status switch + { + ProjectStatus.Planning => "secondary", + ProjectStatus.Active => "primary", + ProjectStatus.OnHold => "warning", + ProjectStatus.Completed => "success", + _ => "secondary" + }; + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Tasks.razor b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Tasks.razor new file mode 100644 index 0000000..4955c85 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Tasks.razor @@ -0,0 +1,204 @@ +@page "/tasks" +@using ContosoDashboard.Services +@using ContosoDashboard.Models +@using Microsoft.AspNetCore.Authorization +@using System.Security.Claims +@attribute [Authorize] +@inject ITaskService TaskService +@inject AuthenticationStateProvider AuthenticationStateProvider + +My Tasks - ContosoDashboard + +
+
+
+

My Tasks

+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + @if (tasks == null) + { +

Loading...

+ } + else if (!tasks.Any()) + { +
No tasks found.
+ } + else + { +
+ + + + + + + + + + + + + @foreach (var task in tasks) + { + + + + + + + + + } + +
TitlePriorityStatusDue DateProjectActions
+ @task.Title + @if (!string.IsNullOrEmpty(task.Description)) + { +
+ @task.Description + } +
+ + @task.Priority + + + + + @if (task.DueDate.HasValue) + { + var isOverdue = task.DueDate.Value < DateTime.UtcNow && task.Status != Models.TaskStatus.Completed; + + @task.DueDate.Value.ToString("MMM dd, yyyy") + + } + else + { + No due date + } + + @if (task.Project != null) + { + @task.Project.Name + } + else + { + - + } + + +
+
+ } +
+ +@code { + private List? tasks; + private string filterStatus = ""; + private string filterPriority = ""; + private int currentUserId = 0; + + protected override async Task OnInitializedAsync() + { + // Get current user ID from authentication claims + var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); + var user = authState.User; + var userIdClaim = user.FindFirst(ClaimTypes.NameIdentifier); + if (userIdClaim != null && int.TryParse(userIdClaim.Value, out int userId)) + { + currentUserId = userId; + } + + if (currentUserId > 0) + { + await LoadTasks(); + } + } + + private async Task LoadTasks() + { + tasks = await TaskService.GetUserTasksAsync(currentUserId); + } + + private async Task ApplyFilters() + { + Models.TaskStatus? status = string.IsNullOrEmpty(filterStatus) ? null : Enum.Parse(filterStatus); + TaskPriority? priority = string.IsNullOrEmpty(filterPriority) ? null : Enum.Parse(filterPriority); + + tasks = await TaskService.GetFilteredTasksAsync(currentUserId, status, priority, null); + } + + private async Task UpdateTaskStatus(int taskId, Models.TaskStatus newStatus) + { + if (currentUserId > 0) + { + await TaskService.UpdateTaskStatusAsync(taskId, currentUserId, newStatus); + await LoadTasks(); + } + } + + private void ShowCreateTaskModal() + { + // TODO: Implement modal dialog for creating tasks + } + + private void ViewTaskDetails(int taskId) + { + // TODO: Navigate to task details or show modal + } + + private string GetPriorityColor(TaskPriority priority) + { + return priority switch + { + TaskPriority.Critical => "danger", + TaskPriority.High => "warning", + TaskPriority.Medium => "info", + TaskPriority.Low => "secondary", + _ => "secondary" + }; + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Team.razor b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Team.razor new file mode 100644 index 0000000..4fd7fc0 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/Team.razor @@ -0,0 +1,155 @@ +@page "/team" +@using ContosoDashboard.Services +@using ContosoDashboard.Models +@using Microsoft.AspNetCore.Authorization +@using System.Security.Claims +@attribute [Authorize] +@inject IUserService UserService +@inject ITaskService TaskService +@inject AuthenticationStateProvider AuthenticationStateProvider + +Team - ContosoDashboard + +
+
+
+

My Team

+

View your team members and their current workload

+
+
+ + @if (teamMembers == null) + { +

Loading...

+ } + else if (!teamMembers.Any()) + { +
No team members found in your department.
+ } + else + { +
+ @foreach (var member in teamMembers) + { +
+
+
+
+ @if (!string.IsNullOrEmpty(member.ProfilePhotoUrl)) + { + @member.DisplayName + } + else + { +
+ @GetInitials(member.DisplayName) +
+ } +
+
@member.DisplayName
+

@member.JobTitle

+
+
+ +
+ Status:
+ + @GetStatusText(member.AvailabilityStatus) + +
+ +
+ Role:
+ @member.Role +
+ +
+ Department:
+ @member.Department +
+ + @if (!string.IsNullOrEmpty(member.Email)) + { +
+ + @member.Email + +
+ } + + @if (!string.IsNullOrEmpty(member.PhoneNumber)) + { +
+ + @member.PhoneNumber + +
+ } +
+
+
+ } +
+ } +
+ +@code { + private List? teamMembers; + private int currentUserId = 0; + + protected override async Task OnInitializedAsync() + { + // Get current user ID from authentication claims + var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); + var user = authState.User; + var userIdClaim = user.FindFirst(ClaimTypes.NameIdentifier); + if (userIdClaim != null && int.TryParse(userIdClaim.Value, out int userId)) + { + currentUserId = userId; + } + + if (currentUserId > 0) + { + teamMembers = await UserService.GetTeamMembersAsync(currentUserId); + } + } + + private string GetInitials(string displayName) + { + if (string.IsNullOrEmpty(displayName)) + return "?"; + + var parts = displayName.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 1) + return parts[0].Substring(0, Math.Min(2, parts[0].Length)).ToUpper(); + + return $"{parts[0][0]}{parts[^1][0]}".ToUpper(); + } + + private string GetStatusColor(AvailabilityStatus status) + { + return status switch + { + AvailabilityStatus.Available => "success", + AvailabilityStatus.Busy => "danger", + AvailabilityStatus.InMeeting => "warning", + AvailabilityStatus.OutOfOffice => "secondary", + _ => "secondary" + }; + } + + private string GetStatusText(AvailabilityStatus status) + { + return status switch + { + AvailabilityStatus.Available => "Available", + AvailabilityStatus.Busy => "Busy", + AvailabilityStatus.InMeeting => "In Meeting", + AvailabilityStatus.OutOfOffice => "Out of Office", + _ => "Unknown" + }; + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/_Host.cshtml b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/_Host.cshtml new file mode 100644 index 0000000..22ac896 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/_Host.cshtml @@ -0,0 +1,38 @@ +@page +@using Microsoft.AspNetCore.Components.Web +@namespace ContosoDashboard.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + + + + + + + + + + + + + + + + + + +
+ + An error has occurred. This application may no longer respond until reloaded. + + + An unhandled exception has occurred. See browser dev tools for details. + + Reload + 🗙 +
+ + + + + + diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/_Imports.razor b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/_Imports.razor new file mode 100644 index 0000000..bac5581 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Pages/_Imports.razor @@ -0,0 +1,13 @@ +@using System.Net.Http +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using ContosoDashboard +@using ContosoDashboard.Pages +@using ContosoDashboard.Shared +@using ContosoDashboard.Models +@using ContosoDashboard.Services diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Program.cs b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Program.cs new file mode 100644 index 0000000..5cd7672 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Program.cs @@ -0,0 +1,111 @@ +using Microsoft.EntityFrameworkCore; +using ContosoDashboard.Data; +using ContosoDashboard.Services; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Components.Authorization; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddRazorPages(); +builder.Services.AddServerSideBlazor(); + +// Add authentication state provider for Blazor +builder.Services.AddScoped(); + +// Configure Database +builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); + +// Configure Mock Authentication (Cookie-based for training purposes) +builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie(options => + { + options.LoginPath = "/login"; + options.LogoutPath = "/logout"; + options.AccessDeniedPath = "/login"; + options.ExpireTimeSpan = TimeSpan.FromHours(8); + options.SlidingExpiration = true; + }); + +// Add authorization +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("Employee", policy => policy.RequireRole("Employee", "TeamLead", "ProjectManager", "Administrator")); + options.AddPolicy("TeamLead", policy => policy.RequireRole("TeamLead", "ProjectManager", "Administrator")); + options.AddPolicy("ProjectManager", policy => policy.RequireRole("ProjectManager", "Administrator")); + options.AddPolicy("Administrator", policy => policy.RequireRole("Administrator")); +}); + +// Register application services +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Add HttpContextAccessor for accessing user claims +builder.Services.AddHttpContextAccessor(); + +var app = builder.Build(); + +// Initialize database +using (var scope = app.Services.CreateScope()) +{ + var services = scope.ServiceProvider; + try + { + var context = services.GetRequiredService(); + context.Database.EnsureCreated(); // For development - use migrations in production + } + catch (Exception ex) + { + var logger = services.GetRequiredService>(); + logger.LogError(ex, "An error occurred creating the database."); + } +} + +// Configure the HTTP request pipeline. +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error"); + app.UseHsts(); +} +else +{ + // Use HSTS even in development for training purposes + app.UseHsts(); +} + +// Add security headers +app.Use(async (context, next) => +{ + context.Response.Headers["X-Content-Type-Options"] = "nosniff"; + context.Response.Headers["X-Frame-Options"] = "DENY"; + context.Response.Headers["X-XSS-Protection"] = "1; mode=block"; + context.Response.Headers["Referrer-Policy"] = "strict-origin-when-cross-origin"; + + // Content Security Policy for Blazor Server + context.Response.Headers["Content-Security-Policy"] = + "default-src 'self'; " + + "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; " + + "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " + + "font-src 'self' https://cdn.jsdelivr.net; " + + "img-src 'self' data: https:; " + + "connect-src 'self' wss: ws:;"; + + await next(); +}); + +app.UseHttpsRedirection(); +app.UseStaticFiles(); +app.UseRouting(); + +// Enable authentication and authorization +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapBlazorHub(); +app.MapFallbackToPage("/_Host"); + +app.Run(); diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Properties/launchSettings.json b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Properties/launchSettings.json new file mode 100644 index 0000000..245bd46 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Properties/launchSettings.json @@ -0,0 +1,22 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Services/CustomAuthenticationStateProvider.cs b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Services/CustomAuthenticationStateProvider.cs new file mode 100644 index 0000000..3626c5d --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Services/CustomAuthenticationStateProvider.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Server; +using System.Security.Claims; + +namespace ContosoDashboard.Services +{ + /// + /// Custom authentication state provider for Blazor Server with cookie authentication + /// + public class CustomAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider + { + public CustomAuthenticationStateProvider( + ILoggerFactory loggerFactory, + IServiceScopeFactory serviceScopeFactory) + : base(loggerFactory) + { + } + + protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30); + + protected override Task ValidateAuthenticationStateAsync( + AuthenticationState authenticationState, CancellationToken cancellationToken) + { + // For the mock authentication system, we'll accept the authentication state as-is + // In a production system, you would validate the user still exists and has valid permissions + return Task.FromResult(true); + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Services/DashboardService.cs b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Services/DashboardService.cs new file mode 100644 index 0000000..ca6ebfa --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Services/DashboardService.cs @@ -0,0 +1,70 @@ +using Microsoft.EntityFrameworkCore; +using ContosoDashboard.Data; +using ContosoDashboard.Models; + +namespace ContosoDashboard.Services; + +public interface IDashboardService +{ + Task GetDashboardSummaryAsync(int userId); + Task> GetActiveAnnouncementsAsync(); +} + +public class DashboardService : IDashboardService +{ + private readonly ApplicationDbContext _context; + + public DashboardService(ApplicationDbContext context) + { + _context = context; + } + + public async Task GetDashboardSummaryAsync(int userId) + { + var now = DateTime.UtcNow; + + var summary = new DashboardSummary + { + TotalActiveTasks = await _context.Tasks + .CountAsync(t => t.AssignedUserId == userId && t.Status != Models.TaskStatus.Completed), + + TasksDueToday = await _context.Tasks + .CountAsync(t => t.AssignedUserId == userId + && t.DueDate.HasValue + && t.DueDate.Value.Date == now.Date + && t.Status != Models.TaskStatus.Completed), + + ActiveProjects = await _context.Projects + .Where(p => p.ProjectManagerId == userId || p.ProjectMembers.Any(pm => pm.UserId == userId)) + .Where(p => p.Status == ProjectStatus.Active) + .CountAsync(), + + UnreadNotifications = await _context.Notifications + .CountAsync(n => n.UserId == userId && !n.IsRead) + }; + + return summary; + } + + public async Task> GetActiveAnnouncementsAsync() + { + var now = DateTime.UtcNow; + + return await _context.Announcements + .Include(a => a.CreatedByUser) + .Where(a => a.IsActive + && a.PublishDate <= now + && (!a.ExpiryDate.HasValue || a.ExpiryDate.Value > now)) + .OrderByDescending(a => a.PublishDate) + .Take(5) + .ToListAsync(); + } +} + +public class DashboardSummary +{ + public int TotalActiveTasks { get; set; } + public int TasksDueToday { get; set; } + public int ActiveProjects { get; set; } + public int UnreadNotifications { get; set; } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Services/NotificationService.cs b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Services/NotificationService.cs new file mode 100644 index 0000000..10929ae --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Services/NotificationService.cs @@ -0,0 +1,72 @@ +using Microsoft.EntityFrameworkCore; +using ContosoDashboard.Data; +using ContosoDashboard.Models; + +namespace ContosoDashboard.Services; + +public interface INotificationService +{ + Task> GetUserNotificationsAsync(int userId, bool unreadOnly = false); + Task CreateNotificationAsync(Notification notification); + Task MarkAsReadAsync(int notificationId, int requestingUserId); + Task GetUnreadCountAsync(int userId); +} + +public class NotificationService : INotificationService +{ + private readonly ApplicationDbContext _context; + + public NotificationService(ApplicationDbContext context) + { + _context = context; + } + + public async Task> GetUserNotificationsAsync(int userId, bool unreadOnly = false) + { + var query = _context.Notifications + .Where(n => n.UserId == userId); + + if (unreadOnly) + query = query.Where(n => !n.IsRead); + + return await query + .OrderByDescending(n => n.Priority) + .ThenByDescending(n => n.CreatedDate) + .Take(50) + .ToListAsync(); + } + + public async Task CreateNotificationAsync(Notification notification) + { + notification.CreatedDate = DateTime.UtcNow; + notification.IsRead = false; + + _context.Notifications.Add(notification); + await _context.SaveChangesAsync(); + + return notification; + } + + public async Task MarkAsReadAsync(int notificationId, int requestingUserId) + { + var notification = await _context.Notifications.FindAsync(notificationId); + if (notification == null) return false; + + // Authorization: Users can only mark their own notifications as read + if (notification.UserId != requestingUserId) + { + return false; // User not authorized to mark this notification as read + } + + notification.IsRead = true; + await _context.SaveChangesAsync(); + + return true; + } + + public async Task GetUnreadCountAsync(int userId) + { + return await _context.Notifications + .CountAsync(n => n.UserId == userId && !n.IsRead); + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Services/ProjectService.cs b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Services/ProjectService.cs new file mode 100644 index 0000000..09b2a00 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Services/ProjectService.cs @@ -0,0 +1,155 @@ +using Microsoft.EntityFrameworkCore; +using ContosoDashboard.Data; +using ContosoDashboard.Models; + +namespace ContosoDashboard.Services; + +public interface IProjectService +{ + Task> GetUserProjectsAsync(int userId); + Task GetProjectByIdAsync(int projectId, int requestingUserId); + Task CreateProjectAsync(Project project); + Task UpdateProjectAsync(Project project, int requestingUserId); + Task AddProjectMemberAsync(int projectId, int userId, string role, int requestingUserId); + Task> GetProjectMembersAsync(int projectId, int requestingUserId); +} + +public class ProjectService : IProjectService +{ + private readonly ApplicationDbContext _context; + + public ProjectService(ApplicationDbContext context) + { + _context = context; + } + + public async Task> GetUserProjectsAsync(int userId) + { + // Get projects where user is manager or a member + var managedProjects = _context.Projects + .Where(p => p.ProjectManagerId == userId); + + var memberProjects = _context.Projects + .Where(p => p.ProjectMembers.Any(pm => pm.UserId == userId)); + + var projects = await managedProjects + .Union(memberProjects) + .Include(p => p.ProjectManager) + .Include(p => p.Tasks) + .Include(p => p.ProjectMembers) + .ThenInclude(pm => pm.User) + .OrderByDescending(p => p.CreatedDate) + .ToListAsync(); + + return projects; + } + + public async Task GetProjectByIdAsync(int projectId, int requestingUserId) + { + var project = await _context.Projects + .Include(p => p.ProjectManager) + .Include(p => p.Tasks) + .ThenInclude(t => t.AssignedUser) + .Include(p => p.ProjectMembers) + .ThenInclude(pm => pm.User) + .FirstOrDefaultAsync(p => p.ProjectId == projectId); + + if (project == null) return null; + + // Authorization: User must be project manager or a project member + var isProjectManager = project.ProjectManagerId == requestingUserId; + var isProjectMember = project.ProjectMembers.Any(pm => pm.UserId == requestingUserId); + + if (!isProjectManager && !isProjectMember) + { + return null; // User not authorized to view this project + } + + return project; + } + + public async Task CreateProjectAsync(Project project) + { + project.CreatedDate = DateTime.UtcNow; + project.UpdatedDate = DateTime.UtcNow; + + _context.Projects.Add(project); + await _context.SaveChangesAsync(); + + return project; + } + + public async Task UpdateProjectAsync(Project project, int requestingUserId) + { + var existingProject = await _context.Projects.FindAsync(project.ProjectId); + if (existingProject == null) return false; + + // Authorization: Only project manager can update project + if (existingProject.ProjectManagerId != requestingUserId) + { + return false; // User not authorized to update this project + } + + existingProject.Name = project.Name; + existingProject.Description = project.Description; + existingProject.Status = project.Status; + existingProject.TargetCompletionDate = project.TargetCompletionDate; + existingProject.UpdatedDate = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return true; + } + + public async Task AddProjectMemberAsync(int projectId, int userId, string role, int requestingUserId) + { + var project = await _context.Projects.FindAsync(projectId); + if (project == null) return false; + + // Authorization: Only project manager can add members + if (project.ProjectManagerId != requestingUserId) + { + return false; // User not authorized to add members to this project + } + + var existingMember = await _context.ProjectMembers + .FirstOrDefaultAsync(pm => pm.ProjectId == projectId && pm.UserId == userId); + + if (existingMember != null) return false; + + var projectMember = new ProjectMember + { + ProjectId = projectId, + UserId = userId, + Role = role, + AssignedDate = DateTime.UtcNow + }; + + _context.ProjectMembers.Add(projectMember); + await _context.SaveChangesAsync(); + + return true; + } + + public async Task> GetProjectMembersAsync(int projectId, int requestingUserId) + { + var project = await _context.Projects + .Include(p => p.ProjectMembers) + .FirstOrDefaultAsync(p => p.ProjectId == projectId); + + if (project == null) return new List(); + + // Authorization: User must be project manager or member + var isProjectManager = project.ProjectManagerId == requestingUserId; + var isProjectMember = project.ProjectMembers.Any(pm => pm.UserId == requestingUserId); + + if (!isProjectManager && !isProjectMember) + { + return new List(); // User not authorized + } + + return await _context.ProjectMembers + .Include(pm => pm.User) + .Where(pm => pm.ProjectId == projectId) + .ToListAsync(); + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Services/TaskService.cs b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Services/TaskService.cs new file mode 100644 index 0000000..958f510 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Services/TaskService.cs @@ -0,0 +1,212 @@ +using Microsoft.EntityFrameworkCore; +using ContosoDashboard.Data; +using ContosoDashboard.Models; + +namespace ContosoDashboard.Services; + +public interface ITaskService +{ + Task> GetUserTasksAsync(int userId); + Task> GetFilteredTasksAsync(int userId, Models.TaskStatus? status, TaskPriority? priority, int? projectId); + Task GetTaskByIdAsync(int taskId, int requestingUserId); + Task CreateTaskAsync(TaskItem task); + Task UpdateTaskStatusAsync(int taskId, int requestingUserId, Models.TaskStatus status); + Task AddTaskCommentAsync(int taskId, int userId, string comment); + Task> GetTaskCommentsAsync(int taskId, int requestingUserId); +} + +public class TaskService : ITaskService +{ + private readonly ApplicationDbContext _context; + private readonly INotificationService _notificationService; + + public TaskService(ApplicationDbContext context, INotificationService notificationService) + { + _context = context; + _notificationService = notificationService; + } + + public async Task> GetUserTasksAsync(int userId) + { + return await _context.Tasks + .Include(t => t.AssignedUser) + .Include(t => t.CreatedByUser) + .Include(t => t.Project) + .Where(t => t.AssignedUserId == userId) + .OrderByDescending(t => t.Priority) + .ThenBy(t => t.DueDate) + .ToListAsync(); + } + + public async Task> GetFilteredTasksAsync(int userId, Models.TaskStatus? status, TaskPriority? priority, int? projectId) + { + var query = _context.Tasks + .Include(t => t.AssignedUser) + .Include(t => t.CreatedByUser) + .Include(t => t.Project) + .Where(t => t.AssignedUserId == userId); + + if (status.HasValue) + query = query.Where(t => t.Status == status.Value); + + if (priority.HasValue) + query = query.Where(t => t.Priority == priority.Value); + + if (projectId.HasValue) + query = query.Where(t => t.ProjectId == projectId.Value); + + return await query + .OrderByDescending(t => t.Priority) + .ThenBy(t => t.DueDate) + .ToListAsync(); + } + + public async Task GetTaskByIdAsync(int taskId, int requestingUserId) + { + var task = await _context.Tasks + .Include(t => t.AssignedUser) + .Include(t => t.CreatedByUser) + .Include(t => t.Project) + .ThenInclude(p => p.ProjectMembers) + .Include(t => t.Comments) + .ThenInclude(c => c.User) + .FirstOrDefaultAsync(t => t.TaskId == taskId); + + if (task == null) return null; + + // Authorization: User can only view tasks they are assigned to, created, or are part of the project + var isAssignedUser = task.AssignedUserId == requestingUserId; + var isCreator = task.CreatedByUserId == requestingUserId; + var isProjectMember = task.Project?.ProjectMembers.Any(pm => pm.UserId == requestingUserId) ?? false; + var isProjectManager = task.Project?.ProjectManagerId == requestingUserId; + + if (!isAssignedUser && !isCreator && !isProjectMember && !isProjectManager) + { + return null; // User not authorized to view this task + } + + return task; + } + + public async Task CreateTaskAsync(TaskItem task) + { + task.CreatedDate = DateTime.UtcNow; + task.UpdatedDate = DateTime.UtcNow; + + _context.Tasks.Add(task); + await _context.SaveChangesAsync(); + + // Create notification for assigned user + await _notificationService.CreateNotificationAsync(new Notification + { + UserId = task.AssignedUserId, + Title = "New Task Assigned", + Message = $"You have been assigned a new task: {task.Title}", + Type = NotificationType.TaskAssignment, + Priority = task.Priority == TaskPriority.Critical ? NotificationPriority.Urgent : NotificationPriority.Important + }); + + return task; + } + + public async Task UpdateTaskStatusAsync(int taskId, int requestingUserId, Models.TaskStatus status) + { + var task = await _context.Tasks + .Include(t => t.Project) + .ThenInclude(p => p.ProjectMembers) + .FirstOrDefaultAsync(t => t.TaskId == taskId); + + if (task == null) return false; + + // Authorization: Only assigned user, creator, project manager, or project members can update status + var isAssignedUser = task.AssignedUserId == requestingUserId; + var isCreator = task.CreatedByUserId == requestingUserId; + var isProjectMember = task.Project?.ProjectMembers.Any(pm => pm.UserId == requestingUserId) ?? false; + var isProjectManager = task.Project?.ProjectManagerId == requestingUserId; + + if (!isAssignedUser && !isCreator && !isProjectMember && !isProjectManager) + { + return false; // User not authorized to update this task + } + + var oldStatus = task.Status; + task.Status = status; + task.UpdatedDate = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + // Send notification if task is completed + if (status == Models.TaskStatus.Completed) + { + await _notificationService.CreateNotificationAsync(new Notification + { + UserId = task.CreatedByUserId, + Title = "Task Completed", + Message = $"Task '{task.Title}' has been completed", + Type = NotificationType.TaskCompleted, + Priority = NotificationPriority.Informational + }); + } + + return true; + } + + public async Task AddTaskCommentAsync(int taskId, int userId, string comment) + { + var task = await _context.Tasks.FindAsync(taskId); + if (task == null) return false; + + var taskComment = new TaskComment + { + TaskId = taskId, + UserId = userId, + CommentText = comment, + CreatedDate = DateTime.UtcNow + }; + + _context.TaskComments.Add(taskComment); + await _context.SaveChangesAsync(); + + // Notify task assignee if commenter is different + if (userId != task.AssignedUserId) + { + await _notificationService.CreateNotificationAsync(new Notification + { + UserId = task.AssignedUserId, + Title = "New Comment on Task", + Message = $"A comment was added to task: {task.Title}", + Type = NotificationType.TaskComment, + Priority = NotificationPriority.Informational + }); + } + + return true; + } + + public async Task> GetTaskCommentsAsync(int taskId, int requestingUserId) + { + var task = await _context.Tasks + .Include(t => t.Project) + .ThenInclude(p => p.ProjectMembers) + .FirstOrDefaultAsync(t => t.TaskId == taskId); + + if (task == null) return new List(); + + // Authorization: User can only view comments if they have access to the task + var isAssignedUser = task.AssignedUserId == requestingUserId; + var isCreator = task.CreatedByUserId == requestingUserId; + var isProjectMember = task.Project?.ProjectMembers.Any(pm => pm.UserId == requestingUserId) ?? false; + var isProjectManager = task.Project?.ProjectManagerId == requestingUserId; + + if (!isAssignedUser && !isCreator && !isProjectMember && !isProjectManager) + { + return new List(); // User not authorized + } + + return await _context.TaskComments + .Include(c => c.User) + .Where(c => c.TaskId == taskId) + .OrderBy(c => c.CreatedDate) + .ToListAsync(); + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Services/UserService.cs b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Services/UserService.cs new file mode 100644 index 0000000..a2f6f9b --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Services/UserService.cs @@ -0,0 +1,149 @@ +using Microsoft.EntityFrameworkCore; +using ContosoDashboard.Data; +using ContosoDashboard.Models; + +namespace ContosoDashboard.Services; + +public interface IUserService +{ + Task GetUserByIdAsync(int userId); + Task GetUserByEmailAsync(string email); + Task CreateOrUpdateUserAsync(string email, string displayName); + Task UpdateUserProfileAsync(User user, int requestingUserId); + Task UpdateAvailabilityStatusAsync(int userId, AvailabilityStatus status); + Task> GetTeamMembersAsync(int userId); + Task> GetAllUsersAsync(); +} + +public class UserService : IUserService +{ + private readonly ApplicationDbContext _context; + + public UserService(ApplicationDbContext context) + { + _context = context; + } + + public async Task GetUserByIdAsync(int userId) + { + return await _context.Users.FindAsync(userId); + } + + public async Task GetUserByEmailAsync(string email) + { + return await _context.Users + .FirstOrDefaultAsync(u => u.Email.ToLower() == email.ToLower()); + } + + public async Task CreateOrUpdateUserAsync(string email, string displayName) + { + var user = await GetUserByEmailAsync(email); + + if (user == null) + { + user = new User + { + Email = email, + DisplayName = displayName, + Role = UserRole.Employee, + AvailabilityStatus = AvailabilityStatus.Available, + CreatedDate = DateTime.UtcNow + }; + + _context.Users.Add(user); + } + else + { + user.DisplayName = displayName; + user.LastLoginDate = DateTime.UtcNow; + } + + await _context.SaveChangesAsync(); + return user; + } + + public async Task UpdateUserProfileAsync(User user, int requestingUserId) + { + var existingUser = await _context.Users.FindAsync(user.UserId); + if (existingUser == null) return false; + + // Authorization: Users can only update their own profile + if (existingUser.UserId != requestingUserId) + { + return false; // User not authorized to update this profile + } + + // Input validation + if (!string.IsNullOrWhiteSpace(user.DisplayName)) + existingUser.DisplayName = user.DisplayName; + + // Validate phone number format if provided + if (!string.IsNullOrWhiteSpace(user.PhoneNumber)) + { + if (user.PhoneNumber.Length <= 20) + existingUser.PhoneNumber = user.PhoneNumber; + } + else + { + existingUser.PhoneNumber = null; + } + + // Validate and sanitize department and job title + if (!string.IsNullOrWhiteSpace(user.Department) && user.Department.Length <= 100) + existingUser.Department = user.Department; + + if (!string.IsNullOrWhiteSpace(user.JobTitle) && user.JobTitle.Length <= 100) + existingUser.JobTitle = user.JobTitle; + + // Validate profile photo URL + if (!string.IsNullOrWhiteSpace(user.ProfilePhotoUrl)) + { + if (Uri.TryCreate(user.ProfilePhotoUrl, UriKind.Absolute, out var uri) && + (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps) && + user.ProfilePhotoUrl.Length <= 500) + { + existingUser.ProfilePhotoUrl = user.ProfilePhotoUrl; + } + } + else + { + existingUser.ProfilePhotoUrl = null; + } + + existingUser.EmailNotificationsEnabled = user.EmailNotificationsEnabled; + existingUser.InAppNotificationsEnabled = user.InAppNotificationsEnabled; + + await _context.SaveChangesAsync(); + return true; + } + + public async Task UpdateAvailabilityStatusAsync(int userId, AvailabilityStatus status) + { + var user = await _context.Users.FindAsync(userId); + if (user == null) return false; + + user.AvailabilityStatus = status; + await _context.SaveChangesAsync(); + + return true; + } + + public async Task> GetTeamMembersAsync(int userId) + { + var user = await _context.Users.FindAsync(userId); + if (user == null) return new List(); + + // Get users in the same department + return await _context.Users + .Where(u => u.Department == user.Department && u.UserId != userId) + .OrderBy(u => u.DisplayName) + .ToListAsync(); + } + + public async Task> GetAllUsersAsync() + { + return await _context.Users + .OrderBy(u => u.DisplayName) + .ToListAsync(); + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Shared/MainLayout.razor b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Shared/MainLayout.razor new file mode 100644 index 0000000..3052b09 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Shared/MainLayout.razor @@ -0,0 +1,40 @@ +@inherits LayoutComponentBase +@using System.Security.Claims +@using Microsoft.AspNetCore.Components.Authorization +@inject AuthenticationStateProvider AuthenticationStateProvider + +
+ + +
+
+ + + + @context.User.Identity?.Name + + + Settings + + + Notifications + + + Logout + + + + + Login + + + +
+ +
+ @Body +
+
+
diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Shared/NavMenu.razor b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Shared/NavMenu.razor new file mode 100644 index 0000000..cd8fb1f --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Shared/NavMenu.razor @@ -0,0 +1,47 @@ + + + + + diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Shared/RedirectToLogin.razor b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Shared/RedirectToLogin.razor new file mode 100644 index 0000000..7e58c77 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Shared/RedirectToLogin.razor @@ -0,0 +1,8 @@ +@inject NavigationManager Navigation + +@code { + protected override void OnInitialized() + { + Navigation.NavigateTo("/login", true); + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Shared/_Imports.razor b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Shared/_Imports.razor new file mode 100644 index 0000000..bd2bb14 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/Shared/_Imports.razor @@ -0,0 +1,3 @@ +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Routing +@namespace ContosoDashboard.Shared diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/appsettings.Development.json b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/appsettings.json b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/appsettings.json new file mode 100644 index 0000000..f658b05 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/appsettings.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ContosoDashboard;Trusted_Connection=True;MultipleActiveResultSets=true" + }, + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "Domain": "contoso.onmicrosoft.com", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "CallbackPath": "/signin-oidc" + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/wwwroot/css/site.css b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/wwwroot/css/site.css new file mode 100644 index 0000000..478a398 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/ContosoDashboard/wwwroot/css/site.css @@ -0,0 +1,166 @@ +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + +#blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; +} + +.page { + position: relative; + display: flex; + flex-direction: column; +} + +main { + flex: 1; +} + +.sidebar { + background-image: linear-gradient(180deg, #0078d4 0%, #005a9e 70%); +} + +.top-row { + background-color: #f7f7f7; + border-bottom: 1px solid #d6d5d5; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + +.top-row a { + color: #0078d4; + text-decoration: none; + padding: 0.5rem 1rem; +} + +.top-row a:hover { + text-decoration: underline; +} + +.summary-card { + border-left: 4px solid #0078d4; + transition: box-shadow 0.3s ease; +} + +.summary-card:hover { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.announcement-card { + border-left: 4px solid #ffc107; +} + +.project-card { + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.project-card:hover { + transform: translateY(-5px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +@media (min-width: 641px) { + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: 100vh; + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .top-row.auth { + justify-content: space-between; + } + + main > div { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } +} + +@media (max-width: 640.98px) { + .top-row:not(.auth) { + display: none; + } + + .top-row.auth { + justify-content: space-between; + } +} + +.nav-scrollable { + display: none; +} + +.navbar-toggler:checked ~ .nav-scrollable { + display: block; +} + +@media (min-width: 641px) { + .nav-scrollable { + display: block; + overflow-y: auto; + } + + .navbar-toggler { + display: none; + } +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + +.nav-item:first-of-type { + padding-top: 1rem; +} + +.nav-item:last-of-type { + padding-bottom: 1rem; +} + +.nav-link { + color: rgba(255, 255, 255, 0.8); + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; +} + +.nav-link:hover { + background-color: rgba(255, 255, 255, 0.1); + color: white; +} + +.nav-link.active { + background-color: rgba(255, 255, 255, 0.25); + color: white; +} + +.content { + padding-top: 1.1rem; +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/LICENSE-CODE b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/LICENSE-CODE new file mode 100644 index 0000000..9e841e7 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/LICENSE-CODE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/README.md b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/README.md new file mode 100644 index 0000000..0d0135a --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/README.md @@ -0,0 +1,358 @@ +# ContosoDashboard-SSD + +The ContosoDashboard application is intended for TRAINING PURPOSES ONLY. + +The ContosoDashboard-SSD repository contains the starter code project for training that teaches Spec-Driven Development (SDD) using the GitHub Spec Kit. ContosoDashboard is a fictional application created solely for educational purposes. + +- The project codebase is NOT intended for use in production environments. +- The project architecture is NOT intended as a model for production applications. +- The project is NOT actively maintained and may contain bugs or security vulnerabilities. +- The project is provided "as-is" without warranties or support of any kind. +- **The project implements mock authentication and authorization for training purposes** + +## 🔒 Security Features (Training Implementation) + +This application includes a **mock authentication system** designed for training without external dependencies: + +- ✅ Cookie-based authentication (8-hour sliding expiration) +- ✅ Claims-based identity with user roles +- ✅ Razor Pages for login/logout (proper HTTP request handling) +- ✅ Custom authentication state provider for Blazor Server integration +- ✅ Authorization enforcement on all protected pages (`[Authorize]` attribute) +- ✅ Role-based access control (RBAC) with hierarchical permissions +- ✅ Service-level security to prevent unauthorized data access +- ✅ IDOR (Insecure Direct Object Reference) protection +- ✅ Defense in depth (middleware, page attributes, service checks) +- ✅ Security headers (CSP, X-Frame-Options, X-XSS-Protection, etc.) +- ✅ Cookie security with sliding expiration +- ✅ User isolation - each user sees only their authorized data +- ✅ No external services required (suitable for offline training) + +**Note**: The mock authentication is suitable for training only. Production deployments require proper identity providers with password hashing, MFA, OAuth 2.0/OpenID Connect, and compliance with security standards (WCAG 2.1, TLS encryption, audit logging). + +### Mock Login System + +**Available Users** (no password required - select from dropdown): + +| Display Name | Email | Role | Department | +|-------------|-------|------|------------| +| System Administrator | `admin@contoso.com` | Administrator | IT | +| Camille Nicole | `camille.nicole@contoso.com` | Project Manager | Engineering | +| Floris Kregel | `floris.kregel@contoso.com` | Team Lead | Engineering | +| Ni Kang | `ni.kang@contoso.com` | Employee | Engineering | + +**Login Process:** + +1. Navigate to `/login` (automatic redirect if not authenticated) +2. Select a user from the dropdown +3. Click "Login" - you'll be redirected to the dashboard as that user + +⚠️ **Important:** This mock authentication system is for **training only**. Production applications should use Azure AD, Identity Server, Auth0, or similar identity providers with proper password hashing, MFA, and OAuth 2.0/OpenID Connect. + +## Overview + +ContosoDashboard is built using ASP.NET Core 8.0 with Blazor Server and provides a centralized platform for: + +- Task management and tracking +- Project oversight and collaboration +- Team coordination +- Notifications and announcements +- User profile management + +## Features + +### ✅ Implemented Features + +- **Mock Authentication System**: User selection login, cookie-based auth, claims-based identity +- **Authorization Enforcement**: `[Authorize]` attributes on all protected pages, role-based policies +- **Dashboard Home Page**: Personalized dashboard with summary cards showing active tasks, due dates, projects, and notifications +- **Task Management**: View, filter, sort, and update tasks with priority levels and status tracking +- **Project Management**: Browse projects with completion percentages, team members, and status indicators +- **Project Details**: Comprehensive project view with task list, team members, and project statistics +- **Team Directory**: Browse team members by department with status, roles, and contact information +- **Notifications Center**: View and manage all notifications with read/unread status and priority badges +- **User Profile**: Update personal information, availability status, and notification preferences +- **Service-Level Security**: Authorization checks prevent IDOR vulnerabilities +- **Data Models**: Complete entity framework models for Users, Tasks, Projects, Notifications, and Announcements +- **Business Services**: Service layer for all core functionality (Tasks, Projects, Users, Notifications, Dashboard) +- **Database Context**: EF Core DbContext with relationships, indexes, and seed data + +### 🔧 Technical Stack + +- **Framework**: ASP.NET Core 8.0 +- **UI**: Blazor Server +- **Database**: SQL Server with Entity Framework Core +- **Authentication**: Cookie-based mock authentication for training (Azure AD/Microsoft Entra ID ready) +- **Authorization**: Claims-based identity with role-based access control +- **Styling**: Bootstrap 5.3 with Bootstrap Icons +- **Architecture**: Clean separation of concerns with Models, Services, Data, and Pages layers +- **Security**: IDOR protection, service-level authorization, `[Authorize]` attributes + +## Getting Started + +### Prerequisites + +- .NET 8.0 SDK or later +- SQL Server or SQL Server LocalDB +- Visual Studio 2022 or Visual Studio Code + +### Quick Start + +1. **Navigate to the project directory**: + + ```powershell + cd ContosoDashboard + ``` + +2. **Run the application** (database will be created automatically): + + ```powershell + dotnet run + ``` + +3. **Open your browser** to `http://localhost:5000` + +4. **Login** - Select any user from the dropdown (no password required) + +The application automatically creates and seeds the database on first run with sample users, projects, tasks, and announcements. + +### Testing Security Features + +#### Test 1: Authentication Required + +- Open browser in incognito mode +- Try to navigate to `https://localhost:xxxx/tasks` +- Expected: Redirect to `/login` + +#### Test 2: User Isolation + +- Login as "Ni Kang" +- Note the tasks and projects shown +- Logout and login as "Floris Kregel" +- Expected: Different tasks and projects displayed + +#### Test 3: IDOR Protection + +- Login as "Ni Kang" and view a project (note the ID in URL) +- Logout and login as "System Administrator" +- Try to access the same project by URL +- Expected: Access only if you're a member + +#### Test 4: Role-Based Features + +- Login as different users to see varying levels of access +- Employee: View assigned tasks, update status +- Project Manager: Manage projects, assign tasks +- Administrator: Full system access + +## Project Structure + +```plaintext +ContosoDashboard/ +├── Data/ +│ └── ApplicationDbContext.cs # EF Core database context +├── Models/ +│ ├── User.cs # User entity with roles +│ ├── TaskItem.cs # Task entity +│ ├── Project.cs # Project entity +│ ├── TaskComment.cs # Task comments +│ ├── Notification.cs # User notifications +│ ├── ProjectMember.cs # Project team members +│ └── Announcement.cs # System announcements +├── Services/ +│ ├── IUserService.cs / UserService.cs +│ ├── ITaskService.cs / TaskService.cs +│ ├── IProjectService.cs / ProjectService.cs +│ ├── INotificationService.cs / NotificationService.cs +│ ├── IDashboardService.cs / DashboardService.cs +│ └── CustomAuthenticationStateProvider.cs # Blazor Server auth integration +├── Pages/ +│ ├── Index.razor # Dashboard home page +│ ├── Login.cshtml / Login.cshtml.cs # Mock authentication login (Razor Page) +│ ├── Logout.cshtml / Logout.cshtml.cs # Logout handler (Razor Page) +│ ├── Tasks.razor # Task list and management +│ ├── Projects.razor # Project list view +│ ├── ProjectDetails.razor # Individual project details +│ ├── Team.razor # Team member directory +│ ├── Notifications.razor # Notification center +│ ├── Profile.razor # User profile page +│ └── _Host.cshtml # Blazor Server host page +├── Shared/ +│ ├── MainLayout.razor # Main layout template +│ └── NavMenu.razor # Navigation sidebar +├── wwwroot/ +│ └── css/site.css # Custom styles +├── Program.cs # Application entry point +├── appsettings.json # Configuration +└── ContosoDashboard.csproj # Project file +``` + +## Configuration + +### Database Connection + +The default connection string in `appsettings.json` uses SQL Server LocalDB: + +```json +"ConnectionStrings": { + "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ContosoDashboard;Trusted_Connection=True;MultipleActiveResultSets=true" +} +``` + +Update this if using a different SQL Server instance. + +### Production Authentication Guidance + +This training application uses mock authentication. For production applications, you would need to: + +- Implement proper identity providers (Azure AD, Identity Server, Auth0) +- Add password hashing and salting (e.g., bcrypt, PBKDF2) +- Enable multi-factor authentication (MFA) +- Implement OAuth 2.0/OpenID Connect protocols +- Add rate limiting and account lockout policies +- Implement comprehensive audit logging +- Use secure session management with idle timeouts +- Implement password complexity requirements and rotation policies + +See [Microsoft's ASP.NET Core Security documentation](https://docs.microsoft.com/aspnet/core/security/) for production implementation guidance. + +### User Roles + +The application supports four role levels with hierarchical permissions: + +- **Employee**: View and update assigned tasks, view projects where member, manage own profile +- **TeamLead**: All Employee permissions plus view team member activities +- **ProjectManager**: All TeamLead permissions plus create/manage projects, assign tasks +- **Administrator**: Full system access including all administrative functions + +## Sample Data + +The application includes pre-seeded data for testing: + +**Users** (all available for mock login): + +- `admin@contoso.com` - System Administrator (Administrator role) +- `camille.nicole@contoso.com` - Camille Nicole (Project Manager role) +- `floris.kregel@contoso.com` - Floris Kregel (Team Lead role) +- `ni.kang@contoso.com` - Ni Kang (Employee role) + +**Project**: + +- "ContosoDashboard Development" with 3 sample tasks in various states + +## Application Pages + +| Page | Route | Description | Auth Required | +|------|-------|-------------|---------------| +| Login | `/login` | User selection for mock auth | No | +| Dashboard | `/` | Summary, announcements, quick actions | Yes | +| Tasks | `/tasks` | View and manage your tasks | Yes | +| Projects | `/projects` | View your projects | Yes | +| Project Details | `/projects/{id}` | Detailed project view | Yes (member only) | +| Team | `/team` | View team members | Yes | +| Notifications | `/notifications` | Manage notifications | Yes | +| Profile | `/profile` | Edit your profile | Yes | +| Logout | `/logout` | End session and clear cookies | Yes | + +## Key Functionalities + +### Dashboard (Home Page) + +- Summary cards with real-time metrics +- Active announcements display +- Quick action links +- Recent notifications feed + +### Task Management + +- Filter by status, priority, and project +- Quick status updates via dropdown +- Priority-based color coding +- Overdue task highlighting + +### Project Management + +- Project cards with progress bars +- Completion percentage calculation +- Team member visibility +- Status badges + +### User Profile + +- Profile information editing +- Availability status management +- Notification preferences +- Display initials when no photo is set + +## Troubleshooting + +### Can't Login + +- Ensure database is created (run `dotnet run` to auto-create) +- Check that seeded users exist in database +- Clear browser cookies and try again + +### Redirected to Login After Login + +- Check browser cookies are enabled +- Clear browser cache and cookies +- Try incognito/private mode + +### Can't Access a Page + +- Verify you're logged in (user name shown in top-right) +- Check if your role has permission for that resource +- Verify you're a member of the project/task you're trying to access + +### Database Issues + +**Option 1: Recreate via LocalDB** + +```powershell +sqllocaldb stop mssqllocaldb +sqllocaldb delete mssqllocaldb +# Then run the application - database will be recreated automatically +``` + +**Option 2: Using EF Tools** + +- Delete database: `dotnet ef database drop --force` +- Recreate: Run application (auto-creates with seed data) + +**Note**: The application uses `EnsureCreated()` for development, so just running `dotnet run` will automatically create and seed the database if it doesn't exist. + +## Security Concepts and Patterns + +This training application demonstrates the following security concepts and patterns: + +1. **Authentication Patterns** - How to implement and configure authentication +2. **Authorization Enforcement** - Using attributes and policies +3. **Claims-Based Identity** - Working with user claims +4. **IDOR Prevention** - Service-level authorization checks +5. **Security Best Practices** - Defense in depth, least privilege +6. **ASP.NET Core Security** - Industry-standard patterns and middleware + +## Known Limitations (Training Context) + +This is a **training application**, not production code. Known limitations include: + +- **Mock authentication**: No real passwords - anyone can select any user account +- **No rate limiting**: Vulnerable to brute force attacks and denial of service +- **No audit logging**: Security events (login, failed auth, data changes) are not logged +- **Simplified input validation**: Production apps need more comprehensive validation +- **No session timeout warnings**: Users aren't warned before session expiration +- **CSP includes unsafe directives**: `'unsafe-inline'` and `'unsafe-eval'` required for Blazor Server but not ideal for security +- **No email verification**: User emails are not validated +- **No account lockout**: Failed login attempts don't trigger account locks + +These limitations are **intentional** for training purposes to keep the application simple and self-contained. Production applications must address all of these security concerns. + +## Code Quality Features + +The application demonstrates good coding practices: + +- Database indexes on frequently queried fields for performance +- Async/await pattern throughout for non-blocking operations +- Entity Framework Core with eager loading (`.Include()`) to prevent N+1 query problems +- Clean separation of concerns (Models, Services, Data, Pages) +- Dependency injection for loose coupling and testability diff --git a/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/StakeholderDocs/document-upload-and-management-feature.md b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/StakeholderDocs/document-upload-and-management-feature.md new file mode 100644 index 0000000..a89c972 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/sdd-implement-contoso-dashboard-feature/StakeholderDocs/document-upload-and-management-feature.md @@ -0,0 +1,186 @@ +# Document Upload and Management Feature - Requirements + +## Overview + +Contoso Corporation needs to add document upload and management capabilities to the ContosoDashboard application. This feature will enable employees to upload work-related documents, organize them by category and project, and share them with team members. + +## Business Need + +Currently, Contoso employees store work documents in various locations (local drives, email attachments, shared drives), leading to: + +- Difficulty locating important documents when needed +- Security risks from uncontrolled document sharing +- Lack of visibility into which documents are associated with specific projects or tasks + +The document upload and management feature addresses these issues by providing a centralized, secure location for work-related documents within the dashboard application that employees already use daily. + +## Target Users + +All Contoso employees who use the ContosoDashboard application will have access to document management features, with permissions based on their existing roles: + +- **Employees**: Upload personal documents and documents for projects they're assigned to +- **Team Leads**: Upload documents and view/manage documents uploaded by their team members +- **Project Managers**: Upload documents and manage all documents associated with their projects +- **Administrators**: Full access to all documents for audit and compliance purposes + +## Core Requirements + +### 1. Document Upload + +**File Selection and Upload** + +- Users must be able to select one or more files from their computer to upload +- Supported file types: PDF, Microsoft Office documents (Word, Excel, PowerPoint), text files, and images (JPEG, PNG) +- Maximum file size: 25 MB per file +- Users should see a progress indicator during upload +- System should display success or error messages after upload completes + +**Document Metadata** + +- When uploading, users must provide: + - Document title (required) + - Description (optional) + - Category selection from predefined list (required): Project Documents, Team Resources, Personal Files, Reports, Presentations, Other + - Associated project (optional - if the document relates to a specific project) + - Tags for easier searching (optional - users can add custom tags) +- System should automatically capture: + - Upload date and time + - Uploaded by (user name) + - File size + - File type + +**Validation and Security** + +- System must scan uploaded files for viruses and malware before storage +- System must reject files that exceed size limits with clear error messages +- System must reject unsupported file types +- Uploaded files must be stored securely with encryption at rest + +### 2. Document Organization and Browsing + +**My Documents View** + +- Users must be able to view a list of all documents they have uploaded +- The view should display: document title, category, upload date, file size, associated project +- Users should be able to sort documents by: title, upload date, category, file size +- Users should be able to filter documents by: category, associated project, date range + +**Project Documents View** + +- When viewing a specific project, users should see all documents associated with that project +- All project team members should be able to view and download project documents +- Project Managers should be able to upload documents to their projects + +**Search** + +- Users should be able to search for documents by: title, description, tags, uploader name, associated project +- Search should return results within 2 seconds +- Users should only see documents they have permission to access in search results + +### 3. Document Access and Management + +**Download and Preview** + +- Users must be able to download any document they have access to +- For common file types (PDF, images), users should be able to preview documents in the browser without downloading + +**Edit Metadata** + +- Users who uploaded a document should be able to edit the document metadata (title, description, category, tags) +- Users should be able to replace a document file with an updated version + +**Delete Documents** + +- Users should be able to delete documents they uploaded +- Project Managers can delete any document in their projects +- Deleted documents should be permanently removed after user confirmation + +**Share Documents** + +- Document owners should be able to share documents with specific users or teams +- Users who receive shared documents should be notified via in-app notification +- Shared documents should appear in recipients' "Shared with Me" section + +### 4. Integration with Existing Features + +**Task Integration** + +- When viewing a task, users should be able to see and attach related documents +- Users should be able to upload a document directly from a task detail page +- Documents attached to tasks should automatically be associated with the task's project + +**Dashboard Integration** + +- Add a "Recent Documents" widget to the dashboard home page showing the last 5 documents uploaded by the user +- Add document count to the dashboard summary cards + +**Notifications** + +- Users should receive notifications when someone shares a document with them +- Users should receive notifications when a new document is added to one of their projects + +### 5. Performance Requirements + +- Document upload should complete within 30 seconds for files up to 25 MB (on typical network) +- Document list pages should load within 2 seconds for up to 500 documents +- Document search should return results within 2 seconds +- Document preview should load within 3 seconds + +### 6. Reporting and Audit + +**Activity Tracking** + +- System should log all document-related activities: uploads, downloads, deletions, share actions +- Administrators should be able to generate reports showing: + - Most uploaded document types + - Most active uploaders + - Document access patterns + +## User Experience Goals + +- **Simplicity**: Uploading a document should require no more than 3 clicks +- **Speed**: Common operations (upload, download, search) should feel instant +- **Clarity**: Users should always know what happens to uploaded files +- **Confidence**: Users should trust that their documents are secure and won't be lost + +## Success Metrics + +The feature will be considered successful if, within 3 months of launch: + +- 70% of active dashboard users have uploaded at least one document +- Average time to locate a document is reduced to under 30 seconds +- 90% of uploaded documents are properly categorized +- Zero security incidents related to document access + +## Technical Constraints + +- Must integrate with existing Azure infrastructure (Azure Blob Storage for file storage) +- Must work within current application architecture (no major rewrites) +- Must comply with existing security policies and authentication mechanisms (Microsoft Entra ID) +- Development timeline: Feature should be production-ready within 8-10 weeks + +## Assumptions + +- Users have reliable internet connections for file uploads/downloads +- Most documents will be under 10 MB in size +- Users are familiar with basic file management concepts +- Azure Blob Storage is approved and available for use + +## Out of Scope + +The following features are NOT included in this initial release: + +- Real-time collaborative editing of documents +- Version history and rollback capabilities +- Advanced document workflows (approval processes, document routing) +- Integration with external systems (SharePoint, OneDrive) +- Mobile app support (initial release is web-only) +- Document templates or document generation features +- Storage quotas and quota management +- Soft delete/trash functionality with recovery + +These may be considered for future enhancements based on user feedback and business needs. + +## Next Steps + +Once approved, these requirements will be used to create detailed specifications using the Spec-Driven Development methodology with GitHub Spec Kit. diff --git a/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/ECommercePricingEngine/ECommercePricingDemo.cs b/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/ECommercePricingEngine/ECommercePricingDemo.cs new file mode 100644 index 0000000..8e36856 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/ECommercePricingEngine/ECommercePricingDemo.cs @@ -0,0 +1,694 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ECommercePricing +{ + public enum MembershipLevel { Guest, Silver, Gold, Premium } + public enum SeasonalEvent { None, BlackFriday, CyberMonday, HolidayWeek, NewYear, BackToSchool } + public enum RegionType { Domestic, International, PremiumZone } + public enum PaymentMethod { CreditCard, DebitCard, PayPal, BankTransfer, Cryptocurrency } + + public class User + { + public MembershipLevel Membership { get; set; } + public bool IsFirstTimeBuyer { get; set; } + public int YearsAsMember { get; set; } + public decimal LifetimeSpent { get; set; } + public bool HasActiveSubscription { get; set; } + public bool IsStudent { get; set; } + public bool IsEmployee { get; set; } + public bool IsCorporateAccount { get; set; } + } + + public class Coupon + { + public string? Code { get; set; } + public bool IsValid { get; set; } + public bool IsExpired { get; set; } + public string? Type { get; set; } // "percent" or "shipping" + public decimal Value { get; set; } // e.g., 10 for 10% off + } + + public class Item + { + public string? Name { get; set; } + public string? Category { get; set; } // e.g., "Electronics", "Clothing" + public decimal Price { get; set; } + } + + public class Order + { + public List Items { get; set; } = new List(); + public bool IsDomestic { get; set; } + public RegionType ShippingRegion { get; set; } + public Coupon? Coupon { get; set; } + public SeasonalEvent ActiveEvent { get; set; } + public PaymentMethod PaymentMethod { get; set; } + public bool HasExpressShipping { get; set; } + public bool IsPreOrder { get; set; } + public DateTime OrderTime { get; set; } + public bool IsBulkOrder { get; set; } + public bool HasGiftWrap { get; set; } + + public decimal GetSubtotal() => Items.Sum(i => i.Price); + public decimal GetSubtotalForCategory(string category) => + Items.Where(i => i.Category == category).Sum(i => i.Price); + public bool ContainsCategory(string category) => + Items.Any(i => i.Category == category); + public int GetCategoryItemCount(string category) => + Items.Count(i => i.Category == category); + public bool IsHighValueOrder() => GetSubtotal() > 1000m; + public bool HasMixedCategories() => Items.Select(i => i.Category).Distinct().Count() >= 3; + } + + public class PricingEngine + { + // Security: Constants for validation bounds + private const decimal MAX_DISCOUNT_PERCENT = 95m; // Maximum 95% discount + private const decimal MIN_FINAL_PRICE = 0.01m; // Minimum $0.01 final price + private const decimal MAX_ORDER_VALUE = 1_000_000m; // Maximum $1M order value + + public static void CalculateFinalPrice(User user, Order order) + { + // Security: Input validation to prevent null reference attacks + if (user == null) + { + throw new ArgumentNullException(nameof(user), "User cannot be null"); + } + + if (order == null) + { + throw new ArgumentNullException(nameof(order), "Order cannot be null"); + } + + if (!IsValidOrder(order)) + { + Console.WriteLine("Error: Invalid order data detected. Pricing calculation aborted."); + return; + } + + decimal baseTotal = order.GetSubtotal(); + + // Security: Validate base total is within reasonable bounds + if (baseTotal <= 0 || baseTotal > MAX_ORDER_VALUE) + { + Console.WriteLine($"Error: Order total ${baseTotal:F2} is outside valid range ($0.01 - ${MAX_ORDER_VALUE:N0})"); + return; + } + + decimal discountPercent = 0m; + decimal shippingCost = CalculateBaseShipping(order); + var appliedDiscounts = new List(); + + // 1. Membership-based discounts: Primary customer tier evaluation + if (user.Membership == MembershipLevel.Premium) + { + discountPercent = SafeAddDiscount(discountPercent, 15, "Premium membership (15%)", appliedDiscounts); + + // 2. Premium high-value threshold: Escalating discounts for premium members + if (baseTotal > 10000) + { + discountPercent = SafeAddDiscount(discountPercent, 10, "Ultra high-value bonus (10%)", appliedDiscounts); + + // 3. Seasonal event multiplier: Premium seasonal benefits + if (order.ActiveEvent == SeasonalEvent.BlackFriday || order.ActiveEvent == SeasonalEvent.CyberMonday) + { + discountPercent = SafeAddDiscount(discountPercent, 8, "Premium seasonal bonus (8%)", appliedDiscounts); + + // 4. Corporate account benefits: B2B premium advantages + if (user.IsCorporateAccount) + { + discountPercent = SafeAddDiscount(discountPercent, 5, "Corporate account bonus (5%)", appliedDiscounts); + + // 5. Subscription service benefits: Recurring revenue incentives + if (user.HasActiveSubscription) + { + discountPercent = SafeAddDiscount(discountPercent, 3, "Subscription service bonus (3%)", appliedDiscounts); + + // 6. Loyalty tenure reward: Long-term premium customer benefits + if (user.YearsAsMember >= 5) + { + discountPercent = SafeAddDiscount(discountPercent, 5, "Veteran premium member (5%)", appliedDiscounts); + + // 7. Lifetime spending tier: Ultimate premium benefits + if (user.LifetimeSpent > 50000) + { + discountPercent = SafeAddDiscount(discountPercent, 7, "VIP status (7%)", appliedDiscounts); + + // 8. Express shipping optimization: Premium logistics benefits + if (order.HasExpressShipping) + { + discountPercent = SafeAddDiscount(discountPercent, 2, "Express shipping loyalty bonus (2%)", appliedDiscounts); + } + } + } + } + } + } + } + else if (baseTotal > 5000) + { + discountPercent = SafeAddDiscount(discountPercent, 5, "High-value bonus (5%)", appliedDiscounts); + } + } + else if (user.Membership == MembershipLevel.Gold) + { + discountPercent = SafeAddDiscount(discountPercent, 12, "Gold membership (12%)", appliedDiscounts); + + // 2. Gold seasonal benefits: Mid-tier seasonal advantages + if (order.ActiveEvent != SeasonalEvent.None) + { + discountPercent = SafeAddDiscount(discountPercent, 6, "Gold seasonal bonus (6%)", appliedDiscounts); + + // 3. Gold volume threshold: Quantity-based gold benefits + if (order.Items.Count >= 15) + { + discountPercent = SafeAddDiscount(discountPercent, 4, "Gold bulk bonus (4%)", appliedDiscounts); + + // 4. Category diversity bonus: Multi-category gold rewards + if (order.HasMixedCategories()) + { + discountPercent = SafeAddDiscount(discountPercent, 3, "Category diversity bonus (3%)", appliedDiscounts); + + // 5. Employee discount stacking: Staff gold benefits + if (user.IsEmployee) + { + discountPercent = SafeAddDiscount(discountPercent, 10, "Employee gold discount (10%)", appliedDiscounts); + + // 6. Pre-order benefits: Early access inventory rewards + if (order.IsPreOrder) + { + discountPercent = SafeAddDiscount(discountPercent, 5, "Pre-order employee bonus (5%)", appliedDiscounts); + + // 7. Payment method optimization: Financial processing benefits + if (order.PaymentMethod == PaymentMethod.BankTransfer || order.PaymentMethod == PaymentMethod.Cryptocurrency) + { + discountPercent = SafeAddDiscount(discountPercent, 3, "Alternative payment bonus (3%)", appliedDiscounts); + } + } + } + } + } + } + } + else if (user.Membership == MembershipLevel.Silver) + { + discountPercent = SafeAddDiscount(discountPercent, 8, "Silver membership (8%)", appliedDiscounts); + + // 2. Silver student benefits: Educational discounts + if (user.IsStudent) + { + discountPercent = SafeAddDiscount(discountPercent, 5, "Student silver bonus (5%)", appliedDiscounts); + + // 3. Back-to-school special: Seasonal student benefits + if (order.ActiveEvent == SeasonalEvent.BackToSchool) + { + discountPercent = SafeAddDiscount(discountPercent, 7, "Back-to-school bonus (7%)", appliedDiscounts); + + // 4. Student bulk purchase: Educational volume discounts + if (order.Items.Count >= 8) + { + discountPercent = SafeAddDiscount(discountPercent, 4, "Student bulk discount (4%)", appliedDiscounts); + + // 5. Student electronics focus: Technology education discounts + if (SafeGetCategoryPercentage(order, "Electronics") > 0.6m) + { + discountPercent = SafeAddDiscount(discountPercent, 6, "Student tech focus bonus (6%)", appliedDiscounts); + + // 6. Gift wrap service: Student presentation benefits + if (order.HasGiftWrap) + { + discountPercent = SafeAddDiscount(discountPercent, 2, "Gift presentation bonus (2%)", appliedDiscounts); + + // 7. Express delivery educational: Time-sensitive learning benefits + if (order.HasExpressShipping) + { + discountPercent = SafeAddDiscount(discountPercent, 3, "Express education bonus (3%)", appliedDiscounts); + } + } + } + } + } + } + } + else if (user.IsFirstTimeBuyer) + { + discountPercent = SafeAddDiscount(discountPercent, 10, "First-time buyer (10%)", appliedDiscounts); + + // 2. New customer seasonal welcome: Event-based new customer benefits + if (order.ActiveEvent != SeasonalEvent.None) + { + discountPercent = SafeAddDiscount(discountPercent, 5, "Seasonal welcome bonus (5%)", appliedDiscounts); + + // 3. New customer volume commitment: Encouraging larger first orders + if (order.Items.Count >= 5) + { + discountPercent = SafeAddDiscount(discountPercent, 4, "First-order volume bonus (4%)", appliedDiscounts); + + // 4. Premium payment method: Financial service onboarding + if (order.PaymentMethod == PaymentMethod.PayPal || order.PaymentMethod == PaymentMethod.CreditCard) + { + discountPercent = SafeAddDiscount(discountPercent, 3, "Premium payment newcomer bonus (3%)", appliedDiscounts); + + // 5. High-value first purchase: Premium new customer treatment + if (order.IsHighValueOrder()) + { + discountPercent = SafeAddDiscount(discountPercent, 6, "High-value newcomer bonus (6%)", appliedDiscounts); + + // 6. Premium zone shipping: Geographic expansion incentives + if (order.ShippingRegion == RegionType.PremiumZone) + { + discountPercent = SafeAddDiscount(discountPercent, 4, "Premium zone newcomer bonus (4%)", appliedDiscounts); + + // 7. Express shipping trial: Premium service introduction + if (order.HasExpressShipping) + { + discountPercent = SafeAddDiscount(discountPercent, 3, "Express shipping trial bonus (3%)", appliedDiscounts); + } + } + } + } + } + } + } + + // 1. Coupon validation and application: Secondary discount layer + if (order.Coupon != null) + { + // 2. Valid coupon: Apply coupon benefits with membership multipliers + if (order.Coupon.IsValid) + { + // 3. Percentage discount coupon: Membership-enhanced coupon benefits + if (order.Coupon.Type == "percent") + { + decimal couponValue = Math.Max(0, Math.Min(50, order.Coupon.Value)); // Security: Cap coupon at 50% + + // 4. Membership coupon enhancement: Tier-based coupon boosts + if (user.Membership == MembershipLevel.Premium) + { + couponValue = Math.Min(50, couponValue * 1.3m); // 30% coupon boost for Premium, capped at 50% + appliedDiscounts.Add($"Premium-enhanced coupon {order.Coupon.Code} ({couponValue:F1}%)"); + + // 5. Seasonal coupon stacking: Event-based premium coupon benefits + if (order.ActiveEvent == SeasonalEvent.BlackFriday) + { + couponValue = Math.Min(55, couponValue + 5); // Black Friday premium coupon boost, capped at 55% + appliedDiscounts.Add("Black Friday premium coupon boost (5%)"); + + // 6. Corporate payment optimization: B2B financial processing benefits + if (user.IsCorporateAccount && order.PaymentMethod == PaymentMethod.BankTransfer) + { + couponValue = Math.Min(60, couponValue * 1.15m); // 15% corporate payment multiplier, capped at 60% + appliedDiscounts.Add($"Corporate payment multiplier (total: {couponValue:F1}%)"); + + // 7. Bulk order corporate: Large-scale business benefits + if (order.IsBulkOrder) + { + couponValue = Math.Min(65, couponValue + 2); // Bulk corporate bonus, capped at 65% + appliedDiscounts.Add("Bulk corporate bonus (2%)"); + } + } + } + } + else if (user.Membership == MembershipLevel.Gold) + { + couponValue = Math.Min(40, couponValue * 1.2m); // 20% coupon boost for Gold, capped at 40% + appliedDiscounts.Add($"Gold-enhanced coupon {order.Coupon.Code} ({couponValue:F1}%)"); + } + else + { + appliedDiscounts.Add($"Coupon {order.Coupon.Code} ({couponValue}%)"); + } + + discountPercent = SafeAddDiscount(discountPercent, couponValue, "", appliedDiscounts, false); + } + // 3. Free shipping coupon: Enhanced shipping benefits + else if (order.Coupon.Type == "shipping") + { + if (order.IsDomestic || user.Membership == MembershipLevel.Premium) + { + shippingCost = 0; + appliedDiscounts.Add($"Free shipping coupon {order.Coupon.Code}"); + } + } + } + // 2. Expired coupon: Handle invalid coupon state + else if (order.Coupon.IsExpired) + { + appliedDiscounts.Add($"Coupon {order.Coupon.Code} expired - no discount"); + Console.WriteLine("Coupon expired. No discount applied."); + } + } + + // 1. Bulk purchase incentive: Volume-based discount with category considerations + if (order.Items.Count >= 20) + { + discountPercent = SafeAddDiscount(discountPercent, 8, "Major bulk purchase (8%)", appliedDiscounts); + } + else if (order.Items.Count >= 10) + { + discountPercent = SafeAddDiscount(discountPercent, 5, "Bulk purchase (5%)", appliedDiscounts); + } + + // Security: Final discount validation + discountPercent = Math.Min(discountPercent, MAX_DISCOUNT_PERCENT); + + // Apply final calculations with category-specific rules + var finalCalculation = ApplyCategorySpecificDiscounts(baseTotal, discountPercent, order); + decimal finalPrice = Math.Max(MIN_FINAL_PRICE, finalCalculation.finalPrice + shippingCost); + + // Display results + Console.WriteLine($"Base Total: ${baseTotal:F2}"); + Console.WriteLine($"Applied Discounts: {string.Join(", ", appliedDiscounts)}"); + Console.WriteLine($"Total Discount: {discountPercent:F1}% (Electronics capped at 15%)"); + Console.WriteLine($"Shipping Cost: ${shippingCost:F2}"); + Console.WriteLine($"Final Price: ${finalPrice:F2}"); + } + + /// + /// Security: Safe discount addition with bounds checking + /// + private static decimal SafeAddDiscount(decimal currentDiscount, decimal additionalDiscount, + string description, List appliedDiscounts, bool addDescription = true) + { + if (additionalDiscount <= 0) return currentDiscount; + + decimal newTotal = currentDiscount + additionalDiscount; + if (newTotal > MAX_DISCOUNT_PERCENT) + { + additionalDiscount = MAX_DISCOUNT_PERCENT - currentDiscount; + newTotal = MAX_DISCOUNT_PERCENT; + } + + if (addDescription && !string.IsNullOrEmpty(description)) + { + appliedDiscounts.Add(description); + } + + return newTotal; + } + + /// + /// Security: Safe category percentage calculation with division by zero protection + /// + private static decimal SafeGetCategoryPercentage(Order order, string category) + { + decimal total = order.GetSubtotal(); + if (total <= 0) return 0; + + return order.GetSubtotalForCategory(category) / total; + } + + /// + /// Security: Validates order data to prevent malicious inputs + /// + private static bool IsValidOrder(Order order) + { + if (order.Items == null || order.Items.Count == 0) + return false; + + foreach (var item in order.Items) + { + if (item == null || string.IsNullOrWhiteSpace(item.Name) || + string.IsNullOrWhiteSpace(item.Category) || item.Price < 0 || item.Price > 100000) + return false; + } + + return true; + } + + private static decimal CalculateBaseShipping(Order order) + { + return order.ShippingRegion switch + { + RegionType.Domestic => 10m, + RegionType.International => 25m, + RegionType.PremiumZone => 35m, + _ => order.IsDomestic ? 10m : 25m + }; + } + + private static (decimal finalPrice, decimal appliedDiscount) ApplyCategorySpecificDiscounts(decimal baseTotal, decimal discountPercent, Order order) + { + // 1. Category-specific discount application: Enhanced margin protection + decimal electronicsSubtotal = order.GetSubtotalForCategory("Electronics"); + decimal clothingSubtotal = order.GetSubtotalForCategory("Clothing"); + decimal accessoriesSubtotal = order.GetSubtotalForCategory("Accessories"); + decimal otherSubtotal = baseTotal - electronicsSubtotal - clothingSubtotal - accessoriesSubtotal; + + // 2. Electronics discount cap: Limit electronics discount to 15% maximum + decimal electronicsDiscount = Math.Min(discountPercent, 15); + decimal discountedElectronics = electronicsSubtotal * (1 - electronicsDiscount / 100); + + // 2. Clothing discount cap: Seasonal fashion considerations + decimal clothingDiscount = order.ActiveEvent == SeasonalEvent.BackToSchool ? + Math.Min(discountPercent, 25) : Math.Min(discountPercent, 20); + decimal discountedClothing = clothingSubtotal * (1 - clothingDiscount / 100); + + // 2. Accessories full discount: No restrictions on accessories + decimal discountedAccessories = accessoriesSubtotal * (1 - discountPercent / 100); + + // 2. Other categories: Apply full discount percentage + decimal discountedOther = otherSubtotal * (1 - discountPercent / 100); + + decimal finalPrice = discountedElectronics + discountedClothing + discountedAccessories + discountedOther; + return (finalPrice, baseTotal - finalPrice); + } + } + + class Program + { + static void Main(string[] args) + { + // Create test data collections + var users = CreateTestUsers(); + var coupons = CreateTestCoupons(); + var orders = CreateTestOrders(); + + // Test different pricing scenarios + TestPricingScenarios(users, coupons, orders); + + // Run security tests to demonstrate security measures + SecurityTest.RunSecurityTests(); + } + + static List CreateTestUsers() + { + return new List + { + // Basic users + new User { Membership = MembershipLevel.Guest, IsFirstTimeBuyer = true, YearsAsMember = 0, LifetimeSpent = 0, HasActiveSubscription = false, IsStudent = false, IsEmployee = false, IsCorporateAccount = false }, + new User { Membership = MembershipLevel.Guest, IsFirstTimeBuyer = false, YearsAsMember = 0, LifetimeSpent = 200, HasActiveSubscription = false, IsStudent = true, IsEmployee = false, IsCorporateAccount = false }, + + // Silver members + new User { Membership = MembershipLevel.Silver, IsFirstTimeBuyer = false, YearsAsMember = 1, LifetimeSpent = 1500, HasActiveSubscription = false, IsStudent = true, IsEmployee = false, IsCorporateAccount = false }, + new User { Membership = MembershipLevel.Silver, IsFirstTimeBuyer = false, YearsAsMember = 2, LifetimeSpent = 3000, HasActiveSubscription = true, IsStudent = false, IsEmployee = false, IsCorporateAccount = false }, + + // Gold members + new User { Membership = MembershipLevel.Gold, IsFirstTimeBuyer = false, YearsAsMember = 3, LifetimeSpent = 8000, HasActiveSubscription = false, IsStudent = false, IsEmployee = true, IsCorporateAccount = false }, + new User { Membership = MembershipLevel.Gold, IsFirstTimeBuyer = false, YearsAsMember = 4, LifetimeSpent = 15000, HasActiveSubscription = true, IsStudent = false, IsEmployee = false, IsCorporateAccount = true }, + + // Premium members + new User { Membership = MembershipLevel.Premium, IsFirstTimeBuyer = false, YearsAsMember = 6, LifetimeSpent = 75000, HasActiveSubscription = true, IsStudent = false, IsEmployee = false, IsCorporateAccount = true }, + new User { Membership = MembershipLevel.Premium, IsFirstTimeBuyer = false, YearsAsMember = 8, LifetimeSpent = 120000, HasActiveSubscription = true, IsStudent = false, IsEmployee = true, IsCorporateAccount = false } + }; + } + + static List CreateTestCoupons() + { + return new List + { + null, // No coupon + new Coupon { Code = "SAVE15", IsValid = true, IsExpired = false, Type = "percent", Value = 15 }, + new Coupon { Code = "FLASHSALE25", IsValid = true, IsExpired = false, Type = "percent", Value = 25 }, + new Coupon { Code = "FREESHIP", IsValid = true, IsExpired = false, Type = "shipping", Value = 0 }, + new Coupon { Code = "EXPIRED20", IsValid = false, IsExpired = true, Type = "percent", Value = 20 } + }; + } + + static List CreateTestOrders() + { + // Complex high-value order - triggers deep nesting + var complexOrder = new Order + { + IsDomestic = true, + ShippingRegion = RegionType.PremiumZone, + ActiveEvent = SeasonalEvent.BlackFriday, + PaymentMethod = PaymentMethod.BankTransfer, + HasExpressShipping = true, + IsPreOrder = true, + IsBulkOrder = true, + HasGiftWrap = false, + OrderTime = DateTime.Now, + Items = new List + { + // Electronics (triggers electronics specialist logic) + new Item { Name = "Gaming Laptop", Category = "Electronics", Price = 3500 }, + new Item { Name = "4K Monitor", Category = "Electronics", Price = 1200 }, + new Item { Name = "Mechanical Keyboard", Category = "Electronics", Price = 300 }, + new Item { Name = "Gaming Mouse", Category = "Electronics", Price = 150 }, + new Item { Name = "VR Headset", Category = "Electronics", Price = 800 }, + new Item { Name = "Tablet", Category = "Electronics", Price = 600 }, + + // Clothing + new Item { Name = "Designer Jacket", Category = "Clothing", Price = 800 }, + new Item { Name = "Premium Jeans", Category = "Clothing", Price = 200 }, + new Item { Name = "Casual Shirt", Category = "Clothing", Price = 80 }, + new Item { Name = "Winter Boots", Category = "Clothing", Price = 250 }, + new Item { Name = "Formal Suit", Category = "Clothing", Price = 600 }, + new Item { Name = "Sports Wear", Category = "Clothing", Price = 120 }, + new Item { Name = "Evening Dress", Category = "Clothing", Price = 400 }, + new Item { Name = "Casual Sneakers", Category = "Clothing", Price = 150 }, + new Item { Name = "Winter Coat", Category = "Clothing", Price = 350 }, + + // Accessories (3+ categories for diversity bonus) + new Item { Name = "Luxury Watch", Category = "Accessories", Price = 2000 }, + new Item { Name = "Designer Bag", Category = "Accessories", Price = 500 }, + new Item { Name = "Gold Necklace", Category = "Accessories", Price = 800 }, + new Item { Name = "Sunglasses", Category = "Accessories", Price = 200 }, + new Item { Name = "Leather Wallet", Category = "Accessories", Price = 100 }, + new Item { Name = "Smart Ring", Category = "Accessories", Price = 300 } + } + }; + + // Student order - triggers student-specific logic + var studentOrder = new Order + { + IsDomestic = true, + ShippingRegion = RegionType.Domestic, + ActiveEvent = SeasonalEvent.BackToSchool, + PaymentMethod = PaymentMethod.PayPal, + HasExpressShipping = true, + IsPreOrder = false, + IsBulkOrder = false, + HasGiftWrap = true, + OrderTime = DateTime.Now, + Items = new List + { + // Electronics-focused for student tech bonus + new Item { Name = "Laptop", Category = "Electronics", Price = 1200 }, + new Item { Name = "External Monitor", Category = "Electronics", Price = 300 }, + new Item { Name = "Wireless Mouse", Category = "Electronics", Price = 50 }, + new Item { Name = "Keyboard", Category = "Electronics", Price = 80 }, + new Item { Name = "Webcam", Category = "Electronics", Price = 100 }, + new Item { Name = "Headphones", Category = "Electronics", Price = 150 }, + new Item { Name = "External Drive", Category = "Electronics", Price = 120 }, + new Item { Name = "Tablet", Category = "Electronics", Price = 400 }, + + // Some non-electronics + new Item { Name = "Backpack", Category = "Accessories", Price = 80 }, + new Item { Name = "Notebook", Category = "Accessories", Price = 15 } + } + }; + + // First-time buyer order - triggers newcomer logic + var newcomerOrder = new Order + { + IsDomestic = false, + ShippingRegion = RegionType.PremiumZone, + ActiveEvent = SeasonalEvent.NewYear, + PaymentMethod = PaymentMethod.CreditCard, + HasExpressShipping = true, + IsPreOrder = false, + IsBulkOrder = false, + HasGiftWrap = false, + OrderTime = DateTime.Now, + Items = new List + { + new Item { Name = "Smartphone", Category = "Electronics", Price = 800 }, + new Item { Name = "Case", Category = "Accessories", Price = 30 }, + new Item { Name = "Charger", Category = "Electronics", Price = 50 }, + new Item { Name = "Screen Protector", Category = "Accessories", Price = 20 }, + new Item { Name = "Wireless Earbuds", Category = "Electronics", Price = 200 }, + new Item { Name = "Power Bank", Category = "Electronics", Price = 60 } + } + }; + + return new List { complexOrder, studentOrder, newcomerOrder }; + } + + static void TestPricingScenarios(List users, List coupons, List orders) + { + var scenarioCount = 1; + + // Test key complex scenarios instead of all combinations + var keyScenarios = new[] + { + // Complex Premium VIP scenario - should hit nesting level 8 + (users.First(u => u.Membership == MembershipLevel.Premium && u.LifetimeSpent > 100000), + coupons.First(c => c?.Code == "FLASHSALE25"), + orders[0]), // Complex order + + // Gold employee scenario - should hit nesting level 7 + (users.First(u => u.Membership == MembershipLevel.Gold && u.IsEmployee), + coupons.First(c => c?.Code == "SAVE15"), + orders[0]), // Complex order with pre-order + + // Student Silver scenario - should hit nesting level 7 + (users.First(u => u.Membership == MembershipLevel.Silver && u.IsStudent), + coupons.First(c => c?.Code == "SAVE15"), + orders[1]), // Student order + + // First-time buyer complex scenario - should hit nesting level 7 + (users.First(u => u.IsFirstTimeBuyer), + coupons.First(c => c?.Code == "FLASHSALE25"), + orders[2]), // Newcomer order + + // Premium with expired coupon + (users.First(u => u.Membership == MembershipLevel.Premium && u.LifetimeSpent > 100000), + coupons.First(c => c?.Code == "EXPIRED20"), + orders[0]), + + // No coupon scenarios + (users.First(u => u.Membership == MembershipLevel.Gold && u.IsEmployee), + null, + orders[0]) + }; + + foreach (var (user, coupon, order) in keyScenarios) + { + // Clone order and assign coupon to avoid modifying original + var testOrder = new Order + { + IsDomestic = order.IsDomestic, + ShippingRegion = order.ShippingRegion, + ActiveEvent = order.ActiveEvent, + PaymentMethod = order.PaymentMethod, + HasExpressShipping = order.HasExpressShipping, + IsPreOrder = order.IsPreOrder, + IsBulkOrder = order.IsBulkOrder, + HasGiftWrap = order.HasGiftWrap, + OrderTime = order.OrderTime, + Coupon = coupon, + Items = order.Items + }; + + Console.WriteLine($"\n=== COMPLEX SCENARIO {scenarioCount++} ==="); + Console.WriteLine($"User: {user.Membership} membership, {user.YearsAsMember} years, Lifetime: ${user.LifetimeSpent:F0}"); + Console.WriteLine($" First-time: {user.IsFirstTimeBuyer}, Student: {user.IsStudent}, Employee: {user.IsEmployee}"); + Console.WriteLine($" Corporate: {user.IsCorporateAccount}, Subscription: {user.HasActiveSubscription}"); + Console.WriteLine($"Order: {testOrder.Items.Count} items, Total: ${testOrder.GetSubtotal():F2}"); + Console.WriteLine($" Event: {testOrder.ActiveEvent}, Payment: {testOrder.PaymentMethod}, Express: {testOrder.HasExpressShipping}"); + Console.WriteLine($" Pre-order: {testOrder.IsPreOrder}, Bulk: {testOrder.IsBulkOrder}, Gift Wrap: {testOrder.HasGiftWrap}"); + Console.WriteLine($" Region: {testOrder.ShippingRegion}"); + Console.WriteLine($"Electronics: ${testOrder.GetSubtotalForCategory("Electronics"):F2}, " + + $"Clothing: ${testOrder.GetSubtotalForCategory("Clothing"):F2}, " + + $"Accessories: ${testOrder.GetSubtotalForCategory("Accessories"):F2}"); + + if (coupon != null) + { + Console.WriteLine($"Coupon: {coupon.Code} ({coupon.Type}, {coupon.Value}%, Valid: {coupon.IsValid}, Expired: {coupon.IsExpired})"); + } + else + { + Console.WriteLine("Coupon: None"); + } + + Console.WriteLine("--- COMPLEX PRICING CALCULATION ---"); + PricingEngine.CalculateFinalPrice(user, testOrder); + Console.WriteLine(new string('=', 60)); + } + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/ECommercePricingEngine/ECommercePricingEngine.csproj b/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/ECommercePricingEngine/ECommercePricingEngine.csproj new file mode 100644 index 0000000..fd4bd08 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/ECommercePricingEngine/ECommercePricingEngine.csproj @@ -0,0 +1,10 @@ + + + + Exe + net9.0 + enable + enable + + + diff --git a/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/ECommercePricingEngine/Output-ECommercePricingEngine.txt b/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/ECommercePricingEngine/Output-ECommercePricingEngine.txt new file mode 100644 index 0000000..d017a27 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/ECommercePricingEngine/Output-ECommercePricingEngine.txt @@ -0,0 +1,137 @@ + +=== COMPLEX SCENARIO 1 === +User: Premium membership, 8 years, Lifetime: $120000 + First-time: False, Student: False, Employee: True + Corporate: False, Subscription: True +Order: 21 items, Total: $13400.00 + Event: BlackFriday, Payment: BankTransfer, Express: True + Pre-order: True, Bulk: True, Gift Wrap: False + Region: PremiumZone +Electronics: $6550.00, Clothing: $2950.00, Accessories: $3900.00 +Coupon: FLASHSALE25 (percent, 25%, Valid: True, Expired: False) +--- COMPLEX PRICING CALCULATION --- +Base Total: $13400.00 +Applied Discounts: Premium membership (15%), Ultra high-value bonus (10%), Premium seasonal bonus (8%), Premium-enhanced coupon FLASHSALE25 (32.5%), Black Friday premium coupon boost (5%), Major bulk purchase (8%) +Total Discount: 78.5% (Electronics capped at 15%) +Shipping Cost: $35.00 +Final Price: $8801.00 +============================================================ + +=== COMPLEX SCENARIO 2 === +User: Gold membership, 3 years, Lifetime: $8000 + First-time: False, Student: False, Employee: True + Corporate: False, Subscription: False +Order: 21 items, Total: $13400.00 + Event: BlackFriday, Payment: BankTransfer, Express: True + Pre-order: True, Bulk: True, Gift Wrap: False + Region: PremiumZone +Electronics: $6550.00, Clothing: $2950.00, Accessories: $3900.00 +Coupon: SAVE15 (percent, 15%, Valid: True, Expired: False) +--- COMPLEX PRICING CALCULATION --- +Base Total: $13400.00 +Applied Discounts: Gold membership (12%), Gold seasonal bonus (6%), Gold bulk bonus (4%), Category diversity bonus (3%), Employee gold discount (10%), Pre-order employee bonus (5%), Alternative payment bonus (3%), Gold-enhanced coupon SAVE15 (18.0%), Major bulk purchase (8%) +Total Discount: 69.0% (Electronics capped at 15%) +Shipping Cost: $35.00 +Final Price: $9171.50 +============================================================ + +=== COMPLEX SCENARIO 3 === +User: Silver membership, 1 years, Lifetime: $1500 + First-time: False, Student: True, Employee: False + Corporate: False, Subscription: False +Order: 10 items, Total: $2495.00 + Event: BackToSchool, Payment: PayPal, Express: True + Pre-order: False, Bulk: False, Gift Wrap: True + Region: Domestic +Electronics: $2400.00, Clothing: $0.00, Accessories: $95.00 +Coupon: SAVE15 (percent, 15%, Valid: True, Expired: False) +--- COMPLEX PRICING CALCULATION --- +Base Total: $2495.00 +Applied Discounts: Silver membership (8%), Student silver bonus (5%), Back-to-school bonus (7%), Student bulk discount (4%), Student tech focus bonus (6%), Gift presentation bonus (2%), Express education bonus (3%), Coupon SAVE15 (15%), Bulk purchase (5%) +Total Discount: 55.0% (Electronics capped at 15%) +Shipping Cost: $10.00 +Final Price: $2092.75 +============================================================ + +=== COMPLEX SCENARIO 4 === +User: Guest membership, 0 years, Lifetime: $0 + First-time: True, Student: False, Employee: False + Corporate: False, Subscription: False +Order: 6 items, Total: $1160.00 + Event: NewYear, Payment: CreditCard, Express: True + Pre-order: False, Bulk: False, Gift Wrap: False + Region: PremiumZone +Electronics: $1110.00, Clothing: $0.00, Accessories: $50.00 +Coupon: FLASHSALE25 (percent, 25%, Valid: True, Expired: False) +--- COMPLEX PRICING CALCULATION --- +Base Total: $1160.00 +Applied Discounts: First-time buyer (10%), Seasonal welcome bonus (5%), First-order volume bonus (4%), Premium payment newcomer bonus (3%), High-value newcomer bonus (6%), Premium zone newcomer bonus (4%), Express shipping trial bonus (3%), Coupon FLASHSALE25 (25%) +Total Discount: 60.0% (Electronics capped at 15%) +Shipping Cost: $35.00 +Final Price: $998.50 +============================================================ + +=== COMPLEX SCENARIO 5 === +User: Premium membership, 8 years, Lifetime: $120000 + First-time: False, Student: False, Employee: True + Corporate: False, Subscription: True +Order: 21 items, Total: $13400.00 + Event: BlackFriday, Payment: BankTransfer, Express: True + Pre-order: True, Bulk: True, Gift Wrap: False + Region: PremiumZone +Electronics: $6550.00, Clothing: $2950.00, Accessories: $3900.00 +Coupon: EXPIRED20 (percent, 20%, Valid: False, Expired: True) +--- COMPLEX PRICING CALCULATION --- +Coupon expired. No discount applied. +Base Total: $13400.00 +Applied Discounts: Premium membership (15%), Ultra high-value bonus (10%), Premium seasonal bonus (8%), Coupon EXPIRED20 expired - no discount, Major bulk purchase (8%) +Total Discount: 41.0% (Electronics capped at 15%) +Shipping Cost: $35.00 +Final Price: $10263.50 +============================================================ + +=== COMPLEX SCENARIO 6 === +User: Gold membership, 3 years, Lifetime: $8000 + First-time: False, Student: False, Employee: True + Corporate: False, Subscription: False +Order: 21 items, Total: $13400.00 + Event: BlackFriday, Payment: BankTransfer, Express: True + Pre-order: True, Bulk: True, Gift Wrap: False + Region: PremiumZone +Electronics: $6550.00, Clothing: $2950.00, Accessories: $3900.00 +Coupon: None +--- COMPLEX PRICING CALCULATION --- +Base Total: $13400.00 +Applied Discounts: Gold membership (12%), Gold seasonal bonus (6%), Gold bulk bonus (4%), Category diversity bonus (3%), Employee gold discount (10%), Pre-order employee bonus (5%), Alternative payment bonus (3%), Major bulk purchase (8%) +Total Discount: 51.0% (Electronics capped at 15%) +Shipping Cost: $35.00 +Final Price: $9873.50 +============================================================ + +=== SECURITY TESTING === + +--- Test 1: Null User Protection --- +Exception thrown: 'System.ArgumentNullException' in ECommercePricingEngine.dll +✓ Security measure working: User cannot be null (Parameter 'user') + +--- Test 2: Null Order Protection --- +Exception thrown: 'System.ArgumentNullException' in ECommercePricingEngine.dll +✓ Security measure working: Order cannot be null (Parameter 'order') + +--- Test 3: Invalid Order Data (Negative Prices) --- +Testing with negative price item... +Error: Invalid order data detected. Pricing calculation aborted. + +--- Test 4: Empty Order --- +Testing with empty order... +Error: Invalid order data detected. Pricing calculation aborted. + +--- Test 5: Extreme Coupon Value Protection --- +Testing with 99% coupon (should be capped at 50%)... +Base Total: $100.00 +Applied Discounts: Silver membership (8%), Coupon EXTREME99 (50%) +Total Discount: 58.0% (Electronics capped at 15%) +Shipping Cost: $10.00 +Final Price: $95.00 + +=== SECURITY TESTING COMPLETE === diff --git a/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/ECommercePricingEngine/SecurityTest.cs b/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/ECommercePricingEngine/SecurityTest.cs new file mode 100644 index 0000000..63e65e0 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/ECommercePricingEngine/SecurityTest.cs @@ -0,0 +1,120 @@ +using System; + +namespace ECommercePricing +{ + /// + /// Security testing class to demonstrate the security measures in action + /// + public class SecurityTest + { + public static void RunSecurityTests() + { + Console.WriteLine("\n=== SECURITY TESTING ==="); + + // Test 1: Null user protection + Console.WriteLine("\n--- Test 1: Null User Protection ---"); + try + { + var validOrder = CreateValidOrder(); + PricingEngine.CalculateFinalPrice(null, validOrder); + } + catch (ArgumentNullException ex) + { + Console.WriteLine($"✓ Security measure working: {ex.Message}"); + } + + // Test 2: Null order protection + Console.WriteLine("\n--- Test 2: Null Order Protection ---"); + try + { + var validUser = CreateValidUser(); + PricingEngine.CalculateFinalPrice(validUser, null); + } + catch (ArgumentNullException ex) + { + Console.WriteLine($"✓ Security measure working: {ex.Message}"); + } + + // Test 3: Invalid order data (negative prices) + Console.WriteLine("\n--- Test 3: Invalid Order Data (Negative Prices) ---"); + var userForInvalidTest = CreateValidUser(); + var invalidOrder = new Order + { + IsDomestic = true, + ShippingRegion = RegionType.Domestic, + ActiveEvent = SeasonalEvent.None, + PaymentMethod = PaymentMethod.CreditCard, + Items = new List + { + new Item { Name = "Invalid Item", Category = "Electronics", Price = -100 } // Negative price + } + }; + Console.WriteLine("Testing with negative price item..."); + PricingEngine.CalculateFinalPrice(userForInvalidTest, invalidOrder); + + // Test 4: Empty order + Console.WriteLine("\n--- Test 4: Empty Order ---"); + var emptyOrder = new Order + { + IsDomestic = true, + ShippingRegion = RegionType.Domestic, + ActiveEvent = SeasonalEvent.None, + PaymentMethod = PaymentMethod.CreditCard, + Items = new List() // Empty items list + }; + Console.WriteLine("Testing with empty order..."); + PricingEngine.CalculateFinalPrice(userForInvalidTest, emptyOrder); + + // Test 5: Extreme coupon value (should be capped) + Console.WriteLine("\n--- Test 5: Extreme Coupon Value Protection ---"); + var extremeCouponOrder = CreateValidOrder(); + extremeCouponOrder.Coupon = new Coupon + { + Code = "EXTREME99", + IsValid = true, + IsExpired = false, + Type = "percent", + Value = 99 // Extreme 99% discount + }; + Console.WriteLine("Testing with 99% coupon (should be capped at 50%)..."); + PricingEngine.CalculateFinalPrice(userForInvalidTest, extremeCouponOrder); + + Console.WriteLine("\n=== SECURITY TESTING COMPLETE ===\n"); + } + + private static User CreateValidUser() + { + return new User + { + Membership = MembershipLevel.Silver, + IsFirstTimeBuyer = false, + YearsAsMember = 2, + LifetimeSpent = 1000, + HasActiveSubscription = false, + IsStudent = false, + IsEmployee = false, + IsCorporateAccount = false + }; + } + + private static Order CreateValidOrder() + { + return new Order + { + IsDomestic = true, + ShippingRegion = RegionType.Domestic, + ActiveEvent = SeasonalEvent.None, + PaymentMethod = PaymentMethod.CreditCard, + HasExpressShipping = false, + IsPreOrder = false, + IsBulkOrder = false, + HasGiftWrap = false, + OrderTime = DateTime.Now, + Items = new List + { + new Item { Name = "Test Item", Category = "Electronics", Price = 100 } + } + }; + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/LoanApprovalWorkflow/LoanApprovalDemo.cs b/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/LoanApprovalWorkflow/LoanApprovalDemo.cs new file mode 100644 index 0000000..37a56ec --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/LoanApprovalWorkflow/LoanApprovalDemo.cs @@ -0,0 +1,660 @@ +using System; +using System.Collections.Generic; + +namespace LoanApprovalSystem +{ + public enum ApprovalStatus + { + Approved, + ConditionallyApproved, + Declined + } + + public class LoanDecision + { + public ApprovalStatus Status { get; set; } + public double InterestRate { get; set; } + public double ApprovedAmount { get; set; } + public string? Notes { get; set; } + } + + public enum EmploymentType { Salaried, SelfEmployed, Contract, Retired, Unemployed } + public enum LoanPurpose { HomePurchase, Refinance, HomeEquity, Personal, Business } + public enum PropertyType { PrimaryResidence, SecondHome, Investment, Commercial } + + public class Applicant + { + public int CreditScore { get; set; } + public double AnnualIncome { get; set; } + public double VerifiedIncome { get; set; } // Income after verification + public int EmploymentYears { get; set; } + public EmploymentType EmploymentStatus { get; set; } + public double DebtToIncomeRatio { get; set; } + public double RequestedLoanAmount { get; set; } + public double PropertyValue { get; set; } // Appraised value + public double DownPayment { get; set; } + public double LiquidAssets { get; set; } // Cash reserves + public int CreditHistoryLength { get; set; } // Years of credit history + public bool HasBankruptcyHistory { get; set; } + public int MonthsSinceBankruptcy { get; set; } + public bool HasForeclosureHistory { get; set; } + public int MonthsSinceForeclosure { get; set; } + public LoanPurpose PurposeOfLoan { get; set; } + public PropertyType PropertyCategory { get; set; } + public bool IsVeteran { get; set; } + public bool IsFirstTimeHomebuyer { get; set; } + public double MonthlyDebtPayments { get; set; } + public int NumberOfInquiries { get; set; } // Recent credit inquiries + } + + public class LoanEvaluator + { + public LoanDecision Evaluate(Applicant applicant) + { + // Security: Input validation to prevent null reference and invalid data attacks + if (applicant == null) + { + throw new ArgumentNullException(nameof(applicant), "Applicant cannot be null"); + } + + if (!IsValidApplicantData(applicant)) + { + return new LoanDecision + { + Status = ApprovalStatus.Declined, + Notes = "Application declined due to invalid or incomplete financial data" + }; + } + + var decision = new LoanDecision(); + + // Security: Safe financial ratio calculations with bounds checking + double loanToValueRatio = SafeDivide(applicant.RequestedLoanAmount, applicant.PropertyValue); + double monthlyIncome = SafeDivide(applicant.VerifiedIncome, 12); + double monthlyPayment = CalculateMonthlyPayment(applicant.RequestedLoanAmount, 4.5); + double paymentToIncomeRatio = SafeDivide(monthlyPayment, monthlyIncome); + double liquidityRatio = SafeDivide(applicant.LiquidAssets, applicant.RequestedLoanAmount); + + // Level 1: Credit Score Primary Assessment - Foundation of loan approval + if (applicant.CreditScore >= 740) + { + // Level 2: Income Verification and Stability - High credit tier analysis + if (applicant.VerifiedIncome >= applicant.AnnualIncome * 0.95) // Income verification within 5% + { + // Level 3: Employment Stability - Career consistency evaluation + if (applicant.EmploymentStatus == EmploymentType.Salaried && applicant.EmploymentYears >= 2) + { + // Level 4: Debt Service Coverage - Financial capacity analysis + if (applicant.DebtToIncomeRatio <= 0.36 && paymentToIncomeRatio <= 0.28) + { + // Level 5: Loan-to-Value Assessment - Risk mitigation analysis + if (loanToValueRatio <= 0.80) + { + // Level 6: Liquidity and Reserve Analysis - Financial cushion evaluation + if (liquidityRatio >= 0.03) // 3% of loan amount in liquid assets + { + // Level 7: Credit History Depth - Long-term creditworthiness + if (applicant.CreditHistoryLength >= 7) + { + // Level 8: Loan Purpose and Property Type - Risk categorization + if (applicant.PurposeOfLoan == LoanPurpose.HomePurchase && + applicant.PropertyCategory == PropertyType.PrimaryResidence) + { + // Prime borrower - best terms + decision.Status = ApprovalStatus.Approved; + decision.InterestRate = 3.25; + decision.ApprovedAmount = applicant.RequestedLoanAmount; + decision.Notes = "Prime borrower approved at best available rate."; + } + else if (applicant.PropertyCategory == PropertyType.SecondHome) + { + // Second home premium + decision.Status = ApprovalStatus.Approved; + decision.InterestRate = 3.75; + decision.ApprovedAmount = applicant.RequestedLoanAmount; + decision.Notes = "Approved for second home with rate premium."; + } + else + { + // Investment property higher rates + decision.Status = ApprovalStatus.Approved; + decision.InterestRate = 4.25; + decision.ApprovedAmount = applicant.RequestedLoanAmount; + decision.Notes = "Investment property approved with higher rate."; + } + } + else + { + // Shorter credit history penalty + decision.Status = ApprovalStatus.Approved; + decision.InterestRate = 3.75; + decision.ApprovedAmount = applicant.RequestedLoanAmount; + decision.Notes = "Approved with rate adjustment for limited credit history."; + } + } + else + { + // Insufficient liquid reserves + decision.Status = ApprovalStatus.ConditionallyApproved; + decision.InterestRate = 4.0; + decision.ApprovedAmount = applicant.RequestedLoanAmount; + decision.Notes = "Conditional approval pending additional reserves requirement."; + } + } + else if (loanToValueRatio <= 0.90) + { + // Higher LTV requires mortgage insurance + decision.Status = ApprovalStatus.Approved; + decision.InterestRate = 4.0; + decision.ApprovedAmount = applicant.RequestedLoanAmount; + decision.Notes = "Approved with mortgage insurance required for high LTV."; + } + else + { + // LTV too high - reduce loan amount + decision.Status = ApprovalStatus.Approved; + decision.InterestRate = 4.25; + decision.ApprovedAmount = applicant.PropertyValue * 0.90; + decision.Notes = "Approved with reduced amount due to LTV limits."; + } + } + else if (applicant.DebtToIncomeRatio <= 0.43) // Qualified mortgage limits + { + // Higher DTI but within QM guidelines + decision.Status = ApprovalStatus.Approved; + decision.InterestRate = 4.5; + decision.ApprovedAmount = applicant.RequestedLoanAmount * 0.85; + decision.Notes = "Approved with rate premium for elevated debt ratios."; + } + else + { + // DTI exceeds qualified mortgage guidelines + decision.Status = ApprovalStatus.ConditionallyApproved; + decision.InterestRate = 5.0; + decision.ApprovedAmount = applicant.RequestedLoanAmount * 0.75; + decision.Notes = "Conditional approval requiring debt reduction plan."; + } + } + else if (applicant.EmploymentStatus == EmploymentType.SelfEmployed && applicant.EmploymentYears >= 2) + { + // Self-employed borrowers require additional documentation + decision.Status = ApprovalStatus.ConditionallyApproved; + decision.InterestRate = 4.25; + decision.ApprovedAmount = applicant.RequestedLoanAmount * 0.90; + decision.Notes = "Conditional approval pending additional income documentation for self-employed."; + } + else + { + // Employment stability concerns + decision.Status = ApprovalStatus.ConditionallyApproved; + decision.InterestRate = 4.75; + decision.ApprovedAmount = applicant.RequestedLoanAmount * 0.80; + decision.Notes = "Conditional approval due to employment history concerns."; + } + } + else + { + // Income verification discrepancy + decision.Status = ApprovalStatus.ConditionallyApproved; + decision.InterestRate = 4.5; + decision.ApprovedAmount = Math.Min(applicant.RequestedLoanAmount, applicant.VerifiedIncome * 4); + decision.Notes = "Conditional approval pending income re-verification."; + } + } + else if (applicant.CreditScore >= 620) // FHA minimum + { + // Level 2: Government Program Eligibility - Alternative approval paths + if (applicant.IsFirstTimeHomebuyer || applicant.IsVeteran) + { + // Level 3: Program-specific DTI allowances - Enhanced qualification criteria + if (applicant.DebtToIncomeRatio <= 0.41) // FHA/VA allowances + { + // Level 4: Down Payment and LTV Analysis - Program requirements + if ((applicant.IsVeteran && loanToValueRatio <= 1.0) || + (applicant.IsFirstTimeHomebuyer && applicant.DownPayment >= applicant.PropertyValue * 0.035)) + { + // Level 5: Credit Event Recovery Analysis - Past financial difficulties + if (!applicant.HasBankruptcyHistory && !applicant.HasForeclosureHistory) + { + // Level 6: Recent Credit Activity - Current credit management + if (applicant.NumberOfInquiries <= 6) // Reasonable shopping activity + { + // Government program approval + decision.Status = ApprovalStatus.Approved; + decision.InterestRate = applicant.IsVeteran ? 3.5 : 4.0; + decision.ApprovedAmount = applicant.RequestedLoanAmount; + decision.Notes = applicant.IsVeteran ? + "VA loan approved with government backing." : + "FHA loan approved for first-time homebuyer."; + } + else + { + // Too much recent credit activity + decision.Status = ApprovalStatus.ConditionallyApproved; + decision.InterestRate = 4.5; + decision.ApprovedAmount = applicant.RequestedLoanAmount; + decision.Notes = "Conditional approval pending explanation of recent credit inquiries."; + } + } + else if (applicant.HasBankruptcyHistory && applicant.MonthsSinceBankruptcy >= 24) + { + // Bankruptcy recovery period met + decision.Status = ApprovalStatus.Approved; + decision.InterestRate = 5.0; + decision.ApprovedAmount = applicant.RequestedLoanAmount * 0.90; + decision.Notes = "Approved with rate premium due to bankruptcy history."; + } + else if (applicant.HasForeclosureHistory && applicant.MonthsSinceForeclosure >= 36) + { + // Foreclosure waiting period met + decision.Status = ApprovalStatus.Approved; + decision.InterestRate = 5.25; + decision.ApprovedAmount = applicant.RequestedLoanAmount * 0.85; + decision.Notes = "Approved with premium for foreclosure history."; + } + else + { + // Credit events too recent + decision.Status = ApprovalStatus.Declined; + decision.Notes = "Declined due to recent bankruptcy or foreclosure."; + } + } + else + { + // Insufficient down payment + decision.Status = ApprovalStatus.ConditionallyApproved; + decision.InterestRate = 4.75; + decision.ApprovedAmount = applicant.PropertyValue * 0.965; // Maximum FHA + decision.Notes = "Conditional approval requiring increased down payment."; + } + } + else + { + // DTI too high even for government programs + decision.Status = ApprovalStatus.Declined; + decision.Notes = "Declined due to debt-to-income ratio exceeding program limits."; + } + } + else + { + // Non-government conventional loan at lower credit tier + if (applicant.DebtToIncomeRatio <= 0.38 && loanToValueRatio <= 0.75) + { + decision.Status = ApprovalStatus.Approved; + decision.InterestRate = 5.5; + decision.ApprovedAmount = Math.Min(applicant.RequestedLoanAmount, 400000); + decision.Notes = "Approved with conservative terms for credit profile."; + } + else + { + decision.Status = ApprovalStatus.Declined; + decision.Notes = "Declined due to credit score and debt ratio combination."; + } + } + } + else + { + // Credit score below lending thresholds + decision.Status = ApprovalStatus.Declined; + decision.Notes = "Declined due to credit score below minimum lending standards."; + } + + return decision; + } + + /// + /// Security: Validates applicant data to prevent malicious or invalid inputs + /// + private bool IsValidApplicantData(Applicant applicant) + { + // Validate credit score range (300-850 is typical FICO range) + if (applicant.CreditScore < 300 || applicant.CreditScore > 850) + return false; + + // Validate financial amounts are positive and reasonable + if (applicant.AnnualIncome <= 0 || applicant.AnnualIncome > 10_000_000) + return false; + + if (applicant.VerifiedIncome <= 0 || applicant.VerifiedIncome > 10_000_000) + return false; + + if (applicant.RequestedLoanAmount <= 0 || applicant.RequestedLoanAmount > 50_000_000) + return false; + + if (applicant.PropertyValue <= 0 || applicant.PropertyValue > 100_000_000) + return false; + + // Validate ratios are within reasonable bounds + if (applicant.DebtToIncomeRatio < 0 || applicant.DebtToIncomeRatio > 1.0) + return false; + + // Validate employment years is reasonable + if (applicant.EmploymentYears < 0 || applicant.EmploymentYears > 50) + return false; + + // Validate credit history length + if (applicant.CreditHistoryLength < 0 || applicant.CreditHistoryLength > 50) + return false; + + // Validate down payment doesn't exceed property value + if (applicant.DownPayment < 0 || applicant.DownPayment > applicant.PropertyValue) + return false; + + return true; + } + + /// + /// Security: Safe division operation to prevent division by zero errors + /// + private double SafeDivide(double numerator, double denominator) + { + if (Math.Abs(denominator) < 0.01) // Effectively zero + return 0; // Return 0 instead of throwing exception for demo purposes + + return numerator / denominator; + } + + /// + /// Security: Enhanced payment calculation with input validation and overflow protection + /// + private double CalculateMonthlyPayment(double loanAmount, double annualRate) + { + // Input validation + if (loanAmount <= 0 || annualRate < 0 || annualRate > 30) // Max 30% interest rate + { + throw new ArgumentException("Invalid loan amount or interest rate"); + } + + // Handle zero interest rate case + if (Math.Abs(annualRate) < 0.001) + { + return loanAmount / 360; // Simple division for 0% interest + } + + double monthlyRate = annualRate / 100 / 12; + int numberOfPayments = 360; // 30 years + + try + { + double factor = Math.Pow(1 + monthlyRate, numberOfPayments); + return loanAmount * (monthlyRate * factor) / (factor - 1); + } + catch (OverflowException) + { + // Handle mathematical overflow + throw new ArgumentException("Loan calculation resulted in mathematical overflow"); + } + } + } + + class Program + { + static void Main(string[] args) + { + var applicants = CreateTestApplicants(); + var evaluator = new LoanEvaluator(); + + Console.WriteLine("=== Loan Approval Workflow Test Results ===\n"); + + for (int i = 0; i < applicants.Count; i++) + { + var applicant = applicants[i]; + var decision = evaluator.Evaluate(applicant); + + Console.WriteLine($"Applicant {i + 1}:"); + Console.WriteLine($" Credit Score: {applicant.CreditScore}"); + // Security: In production, consider masking/logging sensitive financial data appropriately + Console.WriteLine($" Annual Income: ${applicant.AnnualIncome:N0}"); + Console.WriteLine($" Verified Income: ${applicant.VerifiedIncome:N0}"); + Console.WriteLine($" Employment: {applicant.EmploymentStatus} ({applicant.EmploymentYears} years)"); + Console.WriteLine($" Debt-to-Income Ratio: {applicant.DebtToIncomeRatio:P1}"); + Console.WriteLine($" Requested Amount: ${applicant.RequestedLoanAmount:N0}"); + Console.WriteLine($" Property Value: ${applicant.PropertyValue:N0}"); + Console.WriteLine($" Down Payment: ${applicant.DownPayment:N0}"); + Console.WriteLine($" Liquid Assets: ${applicant.LiquidAssets:N0}"); + Console.WriteLine($" Credit History: {applicant.CreditHistoryLength} years"); + Console.WriteLine($" Loan Purpose: {applicant.PurposeOfLoan}"); + Console.WriteLine($" Property Type: {applicant.PropertyCategory}"); + Console.WriteLine($" Veteran: {applicant.IsVeteran}"); + Console.WriteLine($" First-Time Homebuyer: {applicant.IsFirstTimeHomebuyer}"); + Console.WriteLine(); + Console.WriteLine($" RESULT:"); + Console.WriteLine($" Status: {decision.Status}"); + Console.WriteLine($" Interest Rate: {decision.InterestRate}%"); + Console.WriteLine($" Approved Amount: ${decision.ApprovedAmount:N0}"); + Console.WriteLine($" Notes: {decision.Notes}"); + Console.WriteLine(new string('-', 60)); + Console.WriteLine(); + } + + // Run security tests to demonstrate security measures + SecurityTest.RunSecurityTests(); + } + + /// + /// Creates a collection of test applicants demonstrating realistic loan approval scenarios + /// + /// List of applicants with varying financial profiles + private static List CreateTestApplicants() + { + return new List + { + // Scenario 1: Prime borrower - Excellent credit, stable employment, primary residence + // Expected: Approved at 3.25% - best available rate + new Applicant + { + CreditScore = 780, + AnnualIncome = 120000, + VerifiedIncome = 118000, // 98% verification + EmploymentYears = 8, + EmploymentStatus = EmploymentType.Salaried, + DebtToIncomeRatio = 0.25, + RequestedLoanAmount = 400000, + PropertyValue = 500000, // LTV = 80% + DownPayment = 100000, + LiquidAssets = 25000, // 6.25% of loan amount + CreditHistoryLength = 12, + HasBankruptcyHistory = false, + MonthsSinceBankruptcy = 0, + HasForeclosureHistory = false, + MonthsSinceForeclosure = 0, + PurposeOfLoan = LoanPurpose.HomePurchase, + PropertyCategory = PropertyType.PrimaryResidence, + IsVeteran = false, + IsFirstTimeHomebuyer = false, + MonthlyDebtPayments = 2500, + NumberOfInquiries = 2 + }, + + // Scenario 2: Second home purchase - High credit score, investment property + // Expected: Approved at 3.75% with second home premium + new Applicant + { + CreditScore = 760, + AnnualIncome = 150000, + VerifiedIncome = 145000, + EmploymentYears = 6, + EmploymentStatus = EmploymentType.Salaried, + DebtToIncomeRatio = 0.32, + RequestedLoanAmount = 320000, + PropertyValue = 400000, // LTV = 80% + DownPayment = 80000, + LiquidAssets = 15000, + CreditHistoryLength = 10, + HasBankruptcyHistory = false, + MonthsSinceBankruptcy = 0, + HasForeclosureHistory = false, + MonthsSinceForeclosure = 0, + PurposeOfLoan = LoanPurpose.HomePurchase, + PropertyCategory = PropertyType.SecondHome, + IsVeteran = false, + IsFirstTimeHomebuyer = false, + MonthlyDebtPayments = 4000, + NumberOfInquiries = 3 + }, + + // Scenario 3: VA loan candidate - Veteran with good credit + // Expected: Approved at 3.5% with VA loan benefits + new Applicant + { + CreditScore = 680, + AnnualIncome = 75000, + VerifiedIncome = 73000, + EmploymentYears = 4, + EmploymentStatus = EmploymentType.Salaried, + DebtToIncomeRatio = 0.38, + RequestedLoanAmount = 280000, + PropertyValue = 280000, // LTV = 100% (VA allows) + DownPayment = 0, // VA loan - no down payment required + LiquidAssets = 12000, + CreditHistoryLength = 8, + HasBankruptcyHistory = false, + MonthsSinceBankruptcy = 0, + HasForeclosureHistory = false, + MonthsSinceForeclosure = 0, + PurposeOfLoan = LoanPurpose.HomePurchase, + PropertyCategory = PropertyType.PrimaryResidence, + IsVeteran = true, + IsFirstTimeHomebuyer = false, + MonthlyDebtPayments = 2375, + NumberOfInquiries = 4 + }, + + // Scenario 4: Self-employed borrower - Good credit but employment complexity + // Expected: Conditional approval pending additional documentation + new Applicant + { + CreditScore = 750, + AnnualIncome = 95000, + VerifiedIncome = 85000, // Self-employed income harder to verify + EmploymentYears = 3, + EmploymentStatus = EmploymentType.SelfEmployed, + DebtToIncomeRatio = 0.30, + RequestedLoanAmount = 350000, + PropertyValue = 450000, // LTV = 78% + DownPayment = 100000, + LiquidAssets = 20000, + CreditHistoryLength = 9, + HasBankruptcyHistory = false, + MonthsSinceBankruptcy = 0, + HasForeclosureHistory = false, + MonthsSinceForeclosure = 0, + PurposeOfLoan = LoanPurpose.HomePurchase, + PropertyCategory = PropertyType.PrimaryResidence, + IsVeteran = false, + IsFirstTimeHomebuyer = false, + MonthlyDebtPayments = 2375, + NumberOfInquiries = 5 + }, + + // Scenario 5: First-time homebuyer with FHA loan - Lower credit, minimal down payment + // Expected: Approved at 4.0% with FHA program + new Applicant + { + CreditScore = 640, + AnnualIncome = 65000, + VerifiedIncome = 64000, + EmploymentYears = 3, + EmploymentStatus = EmploymentType.Salaried, + DebtToIncomeRatio = 0.40, + RequestedLoanAmount = 240000, + PropertyValue = 250000, // LTV = 96% + DownPayment = 10000, // 4% down payment + LiquidAssets = 8000, + CreditHistoryLength = 6, + HasBankruptcyHistory = false, + MonthsSinceBankruptcy = 0, + HasForeclosureHistory = false, + MonthsSinceForeclosure = 0, + PurposeOfLoan = LoanPurpose.HomePurchase, + PropertyCategory = PropertyType.PrimaryResidence, + IsVeteran = false, + IsFirstTimeHomebuyer = true, + MonthlyDebtPayments = 2167, + NumberOfInquiries = 6 + }, + + // Scenario 6: Bankruptcy recovery - Good recovery but recent bankruptcy + // Expected: Approved with rate premium due to credit event + new Applicant + { + CreditScore = 650, + AnnualIncome = 80000, + VerifiedIncome = 79000, + EmploymentYears = 5, + EmploymentStatus = EmploymentType.Salaried, + DebtToIncomeRatio = 0.35, + RequestedLoanAmount = 200000, + PropertyValue = 250000, // LTV = 80% + DownPayment = 50000, + LiquidAssets = 15000, + CreditHistoryLength = 4, // Credit rebuilding after bankruptcy + HasBankruptcyHistory = true, + MonthsSinceBankruptcy = 30, // 2.5 years since bankruptcy + HasForeclosureHistory = false, + MonthsSinceForeclosure = 0, + PurposeOfLoan = LoanPurpose.HomePurchase, + PropertyCategory = PropertyType.PrimaryResidence, + IsVeteran = true, // Eligible for VA loan + IsFirstTimeHomebuyer = false, + MonthlyDebtPayments = 2333, + NumberOfInquiries = 8 + }, + + // Scenario 7: High DTI but strong credit - Debt consolidation opportunity + // Expected: Conditional approval requiring debt reduction + new Applicant + { + CreditScore = 720, + AnnualIncome = 90000, + VerifiedIncome = 88000, + EmploymentYears = 4, + EmploymentStatus = EmploymentType.Salaried, + DebtToIncomeRatio = 0.48, // Above qualified mortgage limits + RequestedLoanAmount = 300000, + PropertyValue = 380000, // LTV = 79% + DownPayment = 80000, + LiquidAssets = 12000, + CreditHistoryLength = 8, + HasBankruptcyHistory = false, + MonthsSinceBankruptcy = 0, + HasForeclosureHistory = false, + MonthsSinceForeclosure = 0, + PurposeOfLoan = LoanPurpose.Refinance, + PropertyCategory = PropertyType.PrimaryResidence, + IsVeteran = false, + IsFirstTimeHomebuyer = false, + MonthlyDebtPayments = 3600, + NumberOfInquiries = 12 + }, + + // Scenario 8: Below minimum credit score - Declined + // Expected: Declined due to credit score below lending standards + new Applicant + { + CreditScore = 580, // Below FHA minimum + AnnualIncome = 55000, + VerifiedIncome = 54000, + EmploymentYears = 2, + EmploymentStatus = EmploymentType.Salaried, + DebtToIncomeRatio = 0.42, + RequestedLoanAmount = 180000, + PropertyValue = 200000, // LTV = 90% + DownPayment = 20000, + LiquidAssets = 5000, + CreditHistoryLength = 3, + HasBankruptcyHistory = false, + MonthsSinceBankruptcy = 0, + HasForeclosureHistory = true, + MonthsSinceForeclosure = 18, // Too recent + PurposeOfLoan = LoanPurpose.HomePurchase, + PropertyCategory = PropertyType.PrimaryResidence, + IsVeteran = false, + IsFirstTimeHomebuyer = true, + MonthlyDebtPayments = 1925, + NumberOfInquiries = 15 + } + }; + } + } +} diff --git a/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/LoanApprovalWorkflow/LoanApprovalWorkflow.csproj b/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/LoanApprovalWorkflow/LoanApprovalWorkflow.csproj new file mode 100644 index 0000000..fd4bd08 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/LoanApprovalWorkflow/LoanApprovalWorkflow.csproj @@ -0,0 +1,10 @@ + + + + Exe + net9.0 + enable + enable + + + diff --git a/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/LoanApprovalWorkflow/Output-LoanApprovalWorkflow.txt b/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/LoanApprovalWorkflow/Output-LoanApprovalWorkflow.txt new file mode 100644 index 0000000..0a047cf --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/LoanApprovalWorkflow/Output-LoanApprovalWorkflow.txt @@ -0,0 +1,226 @@ +=== Loan Approval Workflow Test Results === + +Applicant 1: + Credit Score: 780 + Annual Income: $120,000 + Verified Income: $118,000 + Employment: Salaried (8 years) + Debt-to-Income Ratio: 25.0% + Requested Amount: $400,000 + Property Value: $500,000 + Down Payment: $100,000 + Liquid Assets: $25,000 + Credit History: 12 years + Loan Purpose: HomePurchase + Property Type: PrimaryResidence + Veteran: False + First-Time Homebuyer: False + + RESULT: + Status: Approved + Interest Rate: 3.25% + Approved Amount: $400,000 + Notes: Prime borrower approved at best available rate. +------------------------------------------------------------ + +Applicant 2: + Credit Score: 760 + Annual Income: $150,000 + Verified Income: $145,000 + Employment: Salaried (6 years) + Debt-to-Income Ratio: 32.0% + Requested Amount: $320,000 + Property Value: $400,000 + Down Payment: $80,000 + Liquid Assets: $15,000 + Credit History: 10 years + Loan Purpose: HomePurchase + Property Type: SecondHome + Veteran: False + First-Time Homebuyer: False + + RESULT: + Status: Approved + Interest Rate: 3.75% + Approved Amount: $320,000 + Notes: Approved for second home with rate premium. +------------------------------------------------------------ + +Applicant 3: + Credit Score: 680 + Annual Income: $75,000 + Verified Income: $73,000 + Employment: Salaried (4 years) + Debt-to-Income Ratio: 38.0% + Requested Amount: $280,000 + Property Value: $280,000 + Down Payment: $0 + Liquid Assets: $12,000 + Credit History: 8 years + Loan Purpose: HomePurchase + Property Type: PrimaryResidence + Veteran: True + First-Time Homebuyer: False + + RESULT: + Status: Approved + Interest Rate: 3.5% + Approved Amount: $280,000 + Notes: VA loan approved with government backing. +------------------------------------------------------------ + +Applicant 4: + Credit Score: 750 + Annual Income: $95,000 + Verified Income: $85,000 + Employment: SelfEmployed (3 years) + Debt-to-Income Ratio: 30.0% + Requested Amount: $350,000 + Property Value: $450,000 + Down Payment: $100,000 + Liquid Assets: $20,000 + Credit History: 9 years + Loan Purpose: HomePurchase + Property Type: PrimaryResidence + Veteran: False + First-Time Homebuyer: False + + RESULT: + Status: ConditionallyApproved + Interest Rate: 4.5% + Approved Amount: $340,000 + Notes: Conditional approval pending income re-verification. +------------------------------------------------------------ + +Applicant 5: + Credit Score: 640 + Annual Income: $65,000 + Verified Income: $64,000 + Employment: Salaried (3 years) + Debt-to-Income Ratio: 40.0% + Requested Amount: $240,000 + Property Value: $250,000 + Down Payment: $10,000 + Liquid Assets: $8,000 + Credit History: 6 years + Loan Purpose: HomePurchase + Property Type: PrimaryResidence + Veteran: False + First-Time Homebuyer: True + + RESULT: + Status: Approved + Interest Rate: 4% + Approved Amount: $240,000 + Notes: FHA loan approved for first-time homebuyer. +------------------------------------------------------------ + +Applicant 6: + Credit Score: 650 + Annual Income: $80,000 + Verified Income: $79,000 + Employment: Salaried (5 years) + Debt-to-Income Ratio: 35.0% + Requested Amount: $200,000 + Property Value: $250,000 + Down Payment: $50,000 + Liquid Assets: $15,000 + Credit History: 4 years + Loan Purpose: HomePurchase + Property Type: PrimaryResidence + Veteran: True + First-Time Homebuyer: False + + RESULT: + Status: Approved + Interest Rate: 5% + Approved Amount: $180,000 + Notes: Approved with rate premium due to bankruptcy history. +------------------------------------------------------------ + +Applicant 7: + Credit Score: 720 + Annual Income: $90,000 + Verified Income: $88,000 + Employment: Salaried (4 years) + Debt-to-Income Ratio: 48.0% + Requested Amount: $300,000 + Property Value: $380,000 + Down Payment: $80,000 + Liquid Assets: $12,000 + Credit History: 8 years + Loan Purpose: Refinance + Property Type: PrimaryResidence + Veteran: False + First-Time Homebuyer: False + + RESULT: + Status: Declined + Interest Rate: 0% + Approved Amount: $0 + Notes: Declined due to credit score and debt ratio combination. +------------------------------------------------------------ + +Applicant 8: + Credit Score: 580 + Annual Income: $55,000 + Verified Income: $54,000 + Employment: Salaried (2 years) + Debt-to-Income Ratio: 42.0% + Requested Amount: $180,000 + Property Value: $200,000 + Down Payment: $20,000 + Liquid Assets: $5,000 + Credit History: 3 years + Loan Purpose: HomePurchase + Property Type: PrimaryResidence + Veteran: False + First-Time Homebuyer: True + + RESULT: + Status: Declined + Interest Rate: 0% + Approved Amount: $0 + Notes: Declined due to credit score below minimum lending standards. +------------------------------------------------------------ + + +=== LOAN APPROVAL SECURITY TESTING === + +--- Test 1: Null Applicant Protection --- +√ Security measure working: Applicant cannot be null (Parameter 'applicant') + +--- Test 2: Invalid Credit Score Bounds --- +Testing with credit score of 900 (above valid range)... +Result: Declined - Application declined due to invalid or incomplete financial data + +--- Test 3: Negative Financial Values --- +Testing with negative annual income... +Result: Declined - Application declined due to invalid or incomplete financial data + +--- Test 4: Extreme Loan Amount --- +Testing with $100M loan request (above reasonable limits)... +Result: Declined - Application declined due to invalid or incomplete financial data + +--- Test 5: Invalid Debt-to-Income Ratio --- +Testing with 150% debt-to-income ratio... +Result: Declined - Application declined due to invalid or incomplete financial data + +--- Test 6: Down Payment Exceeding Property Value --- +Testing with down payment exceeding property value... +Result: Declined - Application declined due to invalid or incomplete financial data + +--- Test 7: Extreme Employment Years --- +Testing with 75 years of employment... +Result: Declined - Application declined due to invalid or incomplete financial data + +--- Test 8: Valid Applicant (Security Baseline) --- +Testing with valid applicant data... +Result: Declined - Rate: 0% - Amount: $0 +Notes: Declined due to credit score and debt ratio combination. + +--- Test 9: Zero Property Value (Division Protection) --- +Testing with zero property value (tests safe division)... +Result: Declined - Application declined due to invalid or incomplete financial data + +=== LOAN APPROVAL SECURITY TESTING COMPLETE === \ No newline at end of file diff --git a/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/LoanApprovalWorkflow/SecurityTest.cs b/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/LoanApprovalWorkflow/SecurityTest.cs new file mode 100644 index 0000000..8d49740 --- /dev/null +++ b/DownloadableCodeProjects/standalone-lab-projects/simplify-complex-conditionals/LoanApprovalWorkflow/SecurityTest.cs @@ -0,0 +1,128 @@ +using System; + +namespace LoanApprovalSystem +{ + /// + /// Security testing class to demonstrate the security measures in the loan approval system + /// + public class SecurityTest + { + public static void RunSecurityTests() + { + Console.WriteLine("\n=== LOAN APPROVAL SECURITY TESTING ==="); + + var evaluator = new LoanEvaluator(); + + // Test 1: Null applicant protection + Console.WriteLine("\n--- Test 1: Null Applicant Protection ---"); + try + { + #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type - testing security + var result = evaluator.Evaluate(null); + #pragma warning restore CS8625 + } + catch (ArgumentNullException ex) + { + Console.WriteLine($"✓ Security measure working: {ex.Message}"); + } + + // Test 2: Invalid credit score bounds + Console.WriteLine("\n--- Test 2: Invalid Credit Score Bounds ---"); + var invalidCreditApplicant = CreateValidApplicant(); + invalidCreditApplicant.CreditScore = 900; // Above valid range (300-850) + Console.WriteLine("Testing with credit score of 900 (above valid range)..."); + var decision = evaluator.Evaluate(invalidCreditApplicant); + Console.WriteLine($"Result: {decision.Status} - {decision.Notes}"); + + // Test 3: Negative financial values + Console.WriteLine("\n--- Test 3: Negative Financial Values ---"); + var negativeIncomeApplicant = CreateValidApplicant(); + negativeIncomeApplicant.AnnualIncome = -50000; // Negative income + Console.WriteLine("Testing with negative annual income..."); + decision = evaluator.Evaluate(negativeIncomeApplicant); + Console.WriteLine($"Result: {decision.Status} - {decision.Notes}"); + + // Test 4: Extreme loan amount + Console.WriteLine("\n--- Test 4: Extreme Loan Amount ---"); + var extremeLoanApplicant = CreateValidApplicant(); + extremeLoanApplicant.RequestedLoanAmount = 100_000_000; // $100M loan + Console.WriteLine("Testing with $100M loan request (above reasonable limits)..."); + decision = evaluator.Evaluate(extremeLoanApplicant); + Console.WriteLine($"Result: {decision.Status} - {decision.Notes}"); + + // Test 5: Invalid debt-to-income ratio + Console.WriteLine("\n--- Test 5: Invalid Debt-to-Income Ratio ---"); + var invalidDTIApplicant = CreateValidApplicant(); + invalidDTIApplicant.DebtToIncomeRatio = 1.5; // 150% DTI (impossible) + Console.WriteLine("Testing with 150% debt-to-income ratio..."); + decision = evaluator.Evaluate(invalidDTIApplicant); + Console.WriteLine($"Result: {decision.Status} - {decision.Notes}"); + + // Test 6: Down payment exceeding property value + Console.WriteLine("\n--- Test 6: Down Payment Exceeding Property Value ---"); + var invalidDownPaymentApplicant = CreateValidApplicant(); + invalidDownPaymentApplicant.PropertyValue = 300000; + invalidDownPaymentApplicant.DownPayment = 400000; // Down payment > property value + Console.WriteLine("Testing with down payment exceeding property value..."); + decision = evaluator.Evaluate(invalidDownPaymentApplicant); + Console.WriteLine($"Result: {decision.Status} - {decision.Notes}"); + + // Test 7: Extreme employment years + Console.WriteLine("\n--- Test 7: Extreme Employment Years ---"); + var extremeEmploymentApplicant = CreateValidApplicant(); + extremeEmploymentApplicant.EmploymentYears = 75; // 75 years of employment + Console.WriteLine("Testing with 75 years of employment..."); + decision = evaluator.Evaluate(extremeEmploymentApplicant); + Console.WriteLine($"Result: {decision.Status} - {decision.Notes}"); + + // Test 8: Valid applicant (should pass all security checks) + Console.WriteLine("\n--- Test 8: Valid Applicant (Security Baseline) ---"); + var validApplicant = CreateValidApplicant(); + Console.WriteLine("Testing with valid applicant data..."); + decision = evaluator.Evaluate(validApplicant); + Console.WriteLine($"Result: {decision.Status} - Rate: {decision.InterestRate}% - Amount: ${decision.ApprovedAmount:N0}"); + Console.WriteLine($"Notes: {decision.Notes}"); + + // Test 9: Edge case - Zero property value (division by zero protection) + Console.WriteLine("\n--- Test 9: Zero Property Value (Division Protection) ---"); + var zeroPropertyApplicant = CreateValidApplicant(); + zeroPropertyApplicant.PropertyValue = 0; // Should trigger safe division + Console.WriteLine("Testing with zero property value (tests safe division)..."); + decision = evaluator.Evaluate(zeroPropertyApplicant); + Console.WriteLine($"Result: {decision.Status} - {decision.Notes}"); + + Console.WriteLine("\n=== LOAN APPROVAL SECURITY TESTING COMPLETE ===\n"); + } + + /// + /// Creates a valid baseline applicant for security testing + /// + private static Applicant CreateValidApplicant() + { + return new Applicant + { + CreditScore = 720, + AnnualIncome = 80000, + VerifiedIncome = 78000, + EmploymentYears = 5, + EmploymentStatus = EmploymentType.Salaried, + DebtToIncomeRatio = 0.30, + RequestedLoanAmount = 300000, + PropertyValue = 375000, + DownPayment = 75000, + LiquidAssets = 15000, + CreditHistoryLength = 8, + HasBankruptcyHistory = false, + MonthsSinceBankruptcy = 0, + HasForeclosureHistory = false, + MonthsSinceForeclosure = 0, + PurposeOfLoan = LoanPurpose.HomePurchase, + PropertyCategory = PropertyType.PrimaryResidence, + IsVeteran = false, + IsFirstTimeHomebuyer = false, + MonthlyDebtPayments = 2000, + NumberOfInquiries = 3 + }; + } + } +} diff --git a/Instructions/Concepts/How to scope vibe coding lab exercise.md b/Instructions/Concepts/How to scope vibe coding lab exercise.md new file mode 100644 index 0000000..e4690b7 --- /dev/null +++ b/Instructions/Concepts/How to scope vibe coding lab exercise.md @@ -0,0 +1,75 @@ +--- +lab: + title: How to scope vibe coding lab exercise + description: Vibe coding lab exercises should be scoped to focus on specific tasks and outcomes, ensuring that participants can effectively learn and apply the concepts within a limited timeframe. + duration: 5 minutes + level: 200 +--- + +# How to scope vibe coding lab exercise + +Vibe coding lab exercises should be scoped to focus on specific tasks and outcomes, ensuring that participants can effectively learn and apply the concepts within a limited timeframe. + +## Vibe coding project types + +The coding projects for a lab exercise need to be carefully scoped, either narrowly or specifically to accommodate 20-30+ minute exercises. + +1. Start from scratch projects (implement basics, stub features, page navigation, but little or no app data) + + 1. Create a prototype eCommerce app + + Example prompt: + + - I need to create a prototype eCommerce app using JavaScript, HTML, and CSS. + - The prototype eCommerce app must provide a dynamic user interface that automatically scales to accommodate viewing on desktop and phone devices. + - The eCommerce app should include the following pages: products list, product details, shopping cart, and checkout. Each page should provide basic functionality and forward/back navigation between pages. + - Use a simple dataset of 10 fruit products. Include: product name, description, price per unit (where unit could be the number of items or ounces, pounds, etc.), and a simple image that represents the product. + - The products list page should display a list of products with basic information such as product name, price per unit, and image. The products list page should also provide a way to select a quantity of a product, and an option to add selected items to the shopping cart. + - The product details page should display detailed information about a selected product, including product name, description, price per unit, and image. The product details page should also provide a way to navigate back to the products list page. + - The shopping cart page should display a list of products added to the cart, including product name, quantity, and total price. The shopping cart page should also provide a way to update the quantity of products in the cart, and remove products from the cart. + - The checkout page should display a summary of the products in the cart, including product name, quantity, and total price. The checkout page should also provide a way to confirm the order. + - The prototype eCommerce app should provide basic navigation between pages. Create a collapsible navigation bar on the left side of the products list, product details, shopping cart, and checkout pages. The navigation bar should allow users to navigate between the products list, product details, shopping cart, and checkout pages. + - The prototype eCommerce app should have basic styling to make it visually appealing, but it does not need to be fully responsive or polished. + - The prototype eCommerce app Will not include any backend functionality, such as user authentication, payment processing, or database integration. It will be a static prototype that demonstrates the basic concepts of an eCommerce app. + + 1. Create a prototype for an AI enhanced app (using Python and Hugging Face models) + +1. Add a new feature to a project + + 1. Add a new feature to an existing app + - For example, add a search feature to the eCommerce app. + - The search feature should allow users to search for products by name or description. + - The search results should display a list of products that match the search criteria, including product name, price per unit, and image. + - The search feature should be accessible from the products list page and should provide a way to navigate back to the products list page. + + 1. Add a new page to an existing app + + - For example, add a contact us page to the eCommerce app. + - The contact us page should include a form that allows users to submit their name, email address, and message. + - The contact us page should also provide a way to navigate back to the products list page. + +1. Change a tool, framework, or technology projects + + 1. Change the front-end framework of an existing app + + - For example, change the front-end framework of the eCommerce app from JavaScript to React or Angular. + - The new front-end framework should provide the same functionality as the original app, including product listing, product details, shopping cart, and checkout. + - The new front-end framework should also provide a dynamic user interface that automatically scales to accommodate viewing on desktop and phone devices. + + 1. Change the back-end technology of an existing app + + - For example, change the back-end technology of the eCommerce app from Node.js to Python Flask or Django. + - The new back-end technology should provide the same functionality as the original app, including user authentication, payment processing, and database integration. + +1. Code review and improvement projects + + 1. Add logging to a project + 1. Add unit tests to a project + +1. Coding language conversion projects + + Convert and existing app to a different language or framework. + + For example convert a Python app to a Java app or a C# app. + +1. Redesign the architecture projects (too big) diff --git a/Instructions/Concepts/Sample PRDs.md b/Instructions/Concepts/Sample PRDs.md new file mode 100644 index 0000000..236ee37 --- /dev/null +++ b/Instructions/Concepts/Sample PRDs.md @@ -0,0 +1,514 @@ +# Sample Product Requirements Documents (PRDs) for Vibe Coding + +This document includes a collection of sample PRDs to illustrate how to effectively use the PRD template for vibe coding projects. + +## PRD Template + +Product Requirements Document (PRD) template. + +```md +# Product Requirements Document (PRD) Template for GitHub Copilot Agent Workflows + +## 1. Project Summary +### Instructions: +Provide a brief overview of the product, including its purpose, target audience, and key goals. + +### Example: +**Product:** Daily Mood Tracker Web App +**Purpose:** To help users log their daily mood, view mood trends, and optionally share mood data with a therapist. +**Target Audience:** Individuals seeking to track their mental health and therapists who monitor their patients' mood patterns. +**Goals:** +- Enable users to log their mood quickly and easily. +- Provide visualizations of mood trends over time. +- Allow users to share mood data with their therapist. + +## 2. Problem Overview +### Instructions: +Describe the current pain points or inefficiencies that the product aims to address. Include assumptions and constraints. + +### Example: +**Current Pain Points:** +- Users lack a simple and effective way to track their mood daily. +- Therapists need a reliable method to monitor their patients' mood patterns remotely. +**Assumptions:** +- Users have access to a smartphone or computer. +- Therapists are willing to use digital tools for monitoring. +**Constraints:** +- The app must be user-friendly and accessible. +- Data privacy and security must be ensured. + +## 3. Scope +### Instructions: +Define what is in scope and what is explicitly out of scope for the MVP. + +### Example: +**In Scope:** +- Mood logging functionality. +- Mood trend visualizations. +- Data sharing with therapists. +**Out of Scope:** +- Advanced mood analysis algorithms. +- Integration with other health tracking apps. + +## 4. Use Cases & Scenarios +### Instructions: +Provide real-world examples of how users will interact with the product. Include user personas and workflows. + +### Example: +**Use Case 1:** +- **Persona:** Jane, a 30-year-old professional experiencing stress. +- **Scenario:** Jane logs her mood daily using the app and views her mood trends to identify patterns. +**Use Case 2:** +- **Persona:** Dr. Smith, a therapist. +- **Scenario:** Dr. Smith reviews mood data shared by his patients to monitor their progress. + +## 5. Requirements +### Instructions: +List the functional and non-functional requirements. Include user stories and acceptance criteria. Add mockups or wireframes if available. + +### Example: +**Functional Requirements:** +- **User Story:** As a user, I want to log my mood daily so that I can track my mental health. +- **Acceptance Criteria:** The mood logging feature should allow users to select their mood from predefined options and add notes. +**Non-functional Requirements:** +- The app should be responsive and work on both mobile and desktop devices. +- Data should be encrypted to ensure privacy. + +## 6. Dependencies +### Instructions: +Identify cross-team or cross-system dependencies. List required technologies, APIs, or services. + +### Example: +**Dependencies:** +- Integration with a secure database for storing mood data. +- Use of charting libraries for visualizing mood trends. + +## 7. Success Metrics +### Instructions: +Define how success will be measured. Include adoption, performance, and satisfaction metrics. + +### Example: +**Success Metrics:** +- Number of daily active users. +- User satisfaction ratings. +- Therapist adoption rate. + +## 8. Competitive Analysis +### Instructions: +Provide an overview of similar products in the market. Highlight strengths, weaknesses, and differentiators. + +### Example: +**Competitive Analysis:** +- **Product A:** Offers mood tracking but lacks data sharing with therapists. +- **Product B:** Provides advanced mood analysis but is complex to use. +**Differentiators:** Our app focuses on simplicity and therapist integration. + +## 9. Product Roadmap +### Instructions: +Outline the timeline for MVP, V1.0, V2.0, etc. Include preview phases. + +### Example: +**Product Roadmap:** +- **MVP:** Mood logging, trend visualizations, data sharing (Q1 2023) +- **V1.0:** Enhanced visualizations, user feedback integration (Q2 2023) +- **V2.0:** Advanced mood analysis, integration with health apps (Q3 2023) + +## 10. Risks & Challenges +### Instructions: +Identify technical, legal, operational, or market risks. Provide mitigation strategies. + +### Example: +**Risks & Challenges:** +- **Technical Risk:** Ensuring data privacy and security. +- **Mitigation:** Implement robust encryption and security protocols. +- **Market Risk:** User adoption. +- **Mitigation:** Conduct user testing and iterate based on feedback. + +## 11. Open Questions +### Instructions: +List unresolved issues or decisions pending input. + +### Example: +**Open Questions:** +- Should the app include mood prediction features? +- What additional mood tracking options should be provided? + +## 12. Supporting Documentation +### Instructions: +Provide links to research, design docs, or related specs. + +### Example: +**Supporting Documentation:** +- [User Research Report](link) +- [Design Mockups](link) + +## 13. Sign-Off +### Instructions: +Include stakeholder approvals and version history. + +### Example: +**Sign-Off:** +- **Version 1.0:** Approved by Product Manager, Lead Developer, and UX Designer. + +``` + +## PRD Samples + +This section includes sample PRDs to illustrate how to effectively use the PRD template for vibe coding + +### Sample 1 - Pet Adoption Web App Product Requirements Document (PRD) + +```md +# Product Requirements Document (PRD) Template + +## Executive Summary +### What is the product? +The product is a web application for pet adoption. It supports users who want to give up their pets for adoption and users who want to adopt pets. The app allows users to browse available pets without logging in, but requires an account and authentication for adopting or donating pets. + +### What problem does it solve? +The app addresses the need for a streamlined and user-friendly platform for pet adoption, making it easier for pet owners to find new homes for their pets and for potential adopters to find pets that match their preferences. + +### Who are the users and what are their goals? +- **Pet Owners**: Users who need to give up their pets for adoption. +- **Potential Adopters**: Users who are looking to adopt a pet. +- **General Users**: Users who want to browse available pets without logging in. + +## Problem Overview +### Description of the current pain points or inefficiencies +Pet adoption processes can be cumbersome and inefficient, with limited online platforms that provide comprehensive information about pets available for adoption. Users often struggle to find detailed information about pets, including their medical history and care requirements. + +### Assumptions and constraints +- Only pets that can be found in a pet store (dogs, cats, hamsters, snakes, turtles) are accepted. +- The business does not support fish or birds. +- Users must provide credentials to donate or adopt pets. +- Users can browse pets without logging in. + +### Current vs. future state comparison +- **Current State**: Limited online platforms with incomplete information about pets. +- **Future State**: A comprehensive web app with detailed pet listings, user authentication, and streamlined adoption processes. + +## Scope +### What’s in scope and what’s explicitly out of scope +#### In Scope +- User authentication for donating and adopting pets. +- Browsing pets without logging in. +- Detailed pet listings including statistics, history with previous owner, medical history, care requirements, and images. +- Support for pets commonly found in pet stores (dogs, cats, hamsters, snakes, turtles). + +#### Out of Scope +- Support for fish or birds. +- Advanced features such as pet training or veterinary services. + +### MVP definition: the smallest set of features that deliver value +- User authentication for donating and adopting pets. +- Browsing pets without logging in. +- Detailed pet listings with basic statistics, history, medical info, care needs, and images. + +## Use Cases & Scenarios +### Real-world examples of how users will interact with the product +#### Use Case 1: Browsing Pets +- **Scenario**: A user visits the app to browse available pets without logging in. +- **Steps**: + 1. User navigates to the homepage. + 2. User selects the "Browse Pets" option. + 3. User views a list of available pets with basic information and images. + +#### Use Case 2: Donating a Pet +- **Scenario**: A pet owner wants to give up their pet for adoption. +- **Steps**: + 1. User logs in or creates an account. + 2. User selects the "Donate a Pet" option. + 3. User fills out a form with pet details (statistics, history, medical info, care needs, images). + 4. User submits the form, and the pet listing is created. + +#### Use Case 3: Adopting a Pet +- **Scenario**: A potential adopter wants to adopt a pet. +- **Steps**: + 1. User logs in or creates an account. + 2. User browses available pets and selects a pet for adoption. + 3. User fills out an adoption form and submits it. + 4. User receives confirmation and instructions for completing the adoption process. + +## Requirements +### Functional Requirements: user stories and acceptance criteria +#### User Story 1: As a user, I want to browse available pets without logging in. +- **Acceptance Criteria**: + - Users can view a list of available pets with basic information and images. + - Users can filter pets by type (dog, cat, hamster, snake, turtle). + +#### User Story 2: As a pet owner, I want to donate my pet for adoption. +- **Acceptance Criteria**: + - Users must log in or create an account to donate a pet. + - Users can fill out a form with pet details (statistics, history, medical info, care needs, images). + - The pet listing is created and visible to other users. + +#### User Story 3: As a potential adopter, I want to adopt a pet. +- **Acceptance Criteria**: + - Users must log in or create an account to adopt a pet. + - Users can fill out an adoption form and submit it. + - Users receive confirmation and instructions for completing the adoption process. + +### Non-functional Requirements: performance, scalability, security, etc. +- The app must handle concurrent users efficiently. +- User data must be securely stored and transmitted. +- The app must be responsive and accessible on various devices. + +## Dependencies +### Cross-team or cross-system dependencies +- Integration with authentication services (e.g., OAuth). +- Integration with image storage services (e.g., AWS S3). +- Dependencies on frontend and backend frameworks (e.g., React, Node.js). + +## Success Metrics +### How will success be measured? (e.g., adoption, performance, satisfaction) +- **Adoption**: Number of registered users and active users. +- **Performance**: Page load times and server response times. +- **Satisfaction**: User feedback and ratings. + +### ROI or impact metrics +- **Adoption Rate**: Percentage of users who complete the donation or adoption process. +- **User Engagement**: Average time spent browsing pets. + +## Competitive Analysis +### Overview of similar products in the market +- Comparison of features, strengths, and weaknesses of existing pet adoption platforms. + +## Product Roadmap +### Timeline for MVP, V1.0, V2.0, etc. +- **MVP**: Basic pet browsing, donation, and adoption features. +- **V1.0**: Enhanced pet listings, user profiles, and search functionality. +- **V2.0**: Advanced features such as pet training resources and veterinary services. + +### Preview phases (dogfood, private/public preview) +- **Dogfood**: Internal testing with team members. +- **Private Preview**: Limited release to selected users. +- **Public Preview**: Open release to all users. + +## Risks & Challenges +### Technical, legal, operational, or market risks +- **Technical Risks**: Scalability and performance issues. +- **Legal Risks**: Compliance with data protection regulations. +- **Operational Risks**: Ensuring user adoption and engagement. + +### Mitigation strategies +- Implement robust testing and monitoring. +- Ensure compliance with legal requirements. +- Develop user engagement strategies. + +## Open Questions +### Unresolved issues or decisions pending input +- What additional pet types should be considered for future versions? +- How can we enhance user engagement and satisfaction? + +## Supporting Documentation +### Links to research, design docs, or related specs +- Research on pet adoption trends and user preferences. +- Design mockups and wireframes. + +## Sign-Off +### Stakeholder approvals and version history +- Approval from product manager, development team, and key stakeholders. +- Version history and change log. +``` + +### SAMPLE 2 - from Wendy on the DevDiv team (Copilot Custom Instructions for MyTodoListApp) + +```md +## Project Overview + +This workspace contains a .NET Aspire solution for a Todo List application, including: + +- **MyTodoApi**: Minimal API backend for todo items, using Entity Framework Core and SQLite. +- **MyNewTodoListApp**: Blazor WebAssembly frontend for managing todo items, consuming the API. +- **BlazorApp1**: Additional Blazor app (sample or test). +- **Aspire.AppHost**: Orchestrator for running the solution locally with Aspire. +- **Aspire.ServiceDefaults**: Shared service configuration for Aspire projects. + +## Key Technologies + +- .NET Aspire (for orchestration and service defaults) +- Blazor WebAssembly (frontend) +- ASP.NET Core Minimal API (backend) +- Entity Framework Core with SQLite (data persistence) + +## Main Features + +- CRUD operations for todo items +- API endpoints for todo management +- Blazor UI for interacting with todos +- Local development orchestration with Aspire + +## File/Folder Highlights + +- **MyTodoApi/**: API project, contains `Program.cs`, `TodoStore.cs`, `ApplicationDbContext.cs`, and `Models/`. +- **MyNewTodoListApp/**: Blazor WASM frontend, contains `Program.cs`, `ToDoClient.cs`, `Pages/`, and `Shared/`. +- **Aspire.AppHost/**: Aspire orchestration, contains `Program.cs` and `appsettings.json`. + +## Coding/Response Guidelines + +- Prefer .NET 9 idioms and minimal API patterns. +- Use dependency injection and configuration best practices. +- For Blazor, use component-based design and leverage `@inject` for services. +- When suggesting code, reference the relevant project and file. +- When discussing API endpoints, refer to **MyTodoApi**. +- When discussing UI, refer to **MyNewTodoListApp**. +- For orchestration or service config, refer to **Aspire.AppHost** or **Aspire.ServiceDefaults**. + +## Example Use Cases + +- Add a new todo item via the Blazor frontend. +- Retrieve all todos from the API. +- Update or delete a todo item. +- Run the solution locally using Aspire. +``` + +### SAMPLE 3 - for MyMoodTrackerApp + +```md + +## Project Overview + +Daily Mood Tracker: A web application for users to log their daily mood and view mood trends. + +Scenario: A wellness startup wants a prototype for users to log their mood and see trends. + +## Key Features +- User authentication (optional) +- Mood logging + - Mood selection UI (e.g., emojis or dropdown) + - Text input for notes + - Submit entries (tagged with entry date and time) +- Mood trend visualization + - e.g., charts based on date range, weekday, or time of day + - View past entries +- Local storage - Entity Framework Core with SQLite (data persistence) +- CRUD operations for mood entries + +## Project Structure + +- MyMoodTrackerApi/ + - Minimal API backend for mood entries + - Entity Framework Core with SQLite + - Contains Program.cs, MoodStore.cs, ApplicationDbContext.cs, Models/ + - Endpoints for mood entry management +- MyMoodTrackerApp/ + - Blazor WebAssembly frontend for mood logging and visualization + - Contains Program.cs, MoodClient.cs, Pages/, Shared/ + - UI for mood entry submission and trend visualization + - Uses HttpClient to interact with the API + +``` + +### SAMPLE 4 - for MyMoodTrackerApp + +```md + +1. Project Summary + + Product: Daily Mood Tracker Web App + + Purpose: To help users log their daily mood, view mood trends. + Target Audience: Individuals seeking to track their mental health. + + Goals: + + - Enable users to log their mood quickly and easily. + - Provide visualizations of mood trends over time. + +2. Problem Overview + + Current Pain Points: + + - Users lack a simple and effective way to track their mood daily. + + Assumptions: + + - Users have access to a smartphone or computer. + + Constraints: + + - The app must be user-friendly and accessible. + - Data privacy and security must be ensured. + +3. Scope + + In Scope: + + - Mood logging functionality. + - Mood trend visualizations. + + Out of Scope: + + - Data sharing with therapists. + - Advanced mood analysis algorithms. + - Integration with other health tracking apps. + +4. Use Cases & Scenarios + + Use Case 1: + + - Persona: A 30-year-old professional experiencing stress. + - Scenario: The user logs their mood daily using the app and views their mood trends to identify patterns. + +5. Requirements + + Functional Requirements: + + - User Story: As a user, I want to log my mood one or more times daily so that I can track my mental health. + - Acceptance Criteria: The mood logging feature should allow users to select their mood from predefined options and add notes. The app should store the date/time stamp with notes for charting. + + Non-functional Requirements: + + - The app should be responsive and work on both mobile and desktop devices. + - Data should be encrypted to ensure privacy. + +6. Dependencies + + Dependencies: + + - Integration with a secure SQLite database for storing mood data. + - Use of charting libraries for visualizing mood trends. + +7. Success Metrics + + Success Metrics: + + - Number of daily active users. + - User satisfaction ratings. + +8. Competitive Analysis + + Competitive Analysis: + + - Product A: Offers mood entry but poor tracking. + - Product B: Provides advanced mood analysis but is complex to use. + + Differentiators: Our app focuses on simplicity and mood analysis tools. + +9. Product Roadmap + + Product Roadmap: + + - MVP: Mood logging, trend visualizations (Q1 2023) + - V1.0: Enhanced visualizations, user feedback integration (Q2 2023) + - V2.0: Advanced mood analysis, integration with health apps (Q3 2023) + +10. Risks & Challenges + + Risks & Challenges: + + - Technical Risk: Ensuring data privacy and security. + - Mitigation: Implement robust encryption and security protocols. + - Market Risk: User adoption. + - Mitigation: Conduct user testing and iterate based on feedback. + +11. Open Questions + + Open Questions: + + - Should the app include mood prediction features? + - What additional mood tracking options should be provided? + +12. Supporting Documentation + +``` diff --git a/Instructions/Demos/DEMO_deploying_an_arm_template.md b/Instructions/Demos/DEMO_deploying_an_arm_template.md deleted file mode 100644 index 54ddee1..0000000 --- a/Instructions/Demos/DEMO_deploying_an_arm_template.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -demo: - title: 'Demo: Deploying an ARM Template' - module: 'Module 1: Exploring Azure Resource Manager' ---- - -# Demo: Deploying an ARM Template - -## Instructions - -1. Quisque dictum convallis metus, vitae vestibulum turpis dapibus non. - - 1. Suspendisse commodo tempor convallis. - - 1. Nunc eget quam facilisis, imperdiet felis ut, blandit nibh. - - 1. Phasellus pulvinar ornare sem, ut imperdiet justo volutpat et. - -1. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. - -1. Vestibulum hendrerit orci urna, non aliquet eros eleifend vitae. - -1. Curabitur nibh dui, vestibulum cursus neque commodo, aliquet accumsan risus. - - ``` - Sed at malesuada orci, eu volutpat ex - ``` - -1. In ac odio vulputate, faucibus lorem at, sagittis felis. - -1. Fusce tincidunt sapien nec dolor congue facilisis lacinia quis urna. - - > **Note**: Ut feugiat est id ultrices gravida. - -1. Phasellus urna lacus, luctus at suscipit vitae, maximus ac nisl. - - - Morbi in tortor finibus, tempus dolor a, cursus lorem. - - - Maecenas id risus pharetra, viverra elit quis, lacinia odio. - - - Etiam rutrum pretium enim. - -1. Curabitur in pretium urna, nec ullamcorper diam. diff --git a/Instructions/Labs/LAB_01_deploying_arm_templates.md b/Instructions/Labs/LAB_01_deploying_arm_templates.md deleted file mode 100644 index 0d80038..0000000 --- a/Instructions/Labs/LAB_01_deploying_arm_templates.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -lab: - title: 'Lab: Deploying Azure Resource Manager templates' - module: 'Module 1: Exploring Azure Resource Manager' ---- - -# Lab: Deploying Azure Resource Manager templates -# Student lab manual - -## Lab scenario - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus lobortis, erat vel egestas faucibus, dui magna semper velit, id congue sapien lectus id turpis. Nam egestas tempus enim. Ut venenatis vehicula ex, id rutrum odio lacinia at. Donec congue, tortor sed fermentum imperdiet, mauris mi auctor dui, ac cursus ex augue a odio. Aliquam erat volutpat. Vivamus faucibus fringilla augue in dignissim. Quisque sit amet nulla id risus gravida auctor. Ut in est varius, cursus odio rhoncus, placerat erat. Suspendisse nec metus est. - -## Objectives - -After you complete this lab, you will be able to: - -- Cras tincidunt massa et nunc vulputate, eget vestibulum massa tincidunt. - -- Maecenas suscipit at nisl vitae malesuada. - -- Suspendisse eu arcu id velit consequat venenatis. - -## Lab Setup - - - **Estimated Time**: 00 minutes - -## Instructions - -### Before you start - -#### Setup Task - -1. Integer dolor purus, gravida eu sem id, efficitur aliquet neque. - -1. Suspendisse viverra mauris in metus laoreet consectetur. - -1. Sed diam risus, convallis quis condimentum at, egestas malesuada libero. - -### Exercise 0: - -#### Task 0: - -1. Quisque dictum convallis metus, vitae vestibulum turpis dapibus non. - - 1. Suspendisse commodo tempor convallis. - - 1. Nunc eget quam facilisis, imperdiet felis ut, blandit nibh. - - 1. Phasellus pulvinar ornare sem, ut imperdiet justo volutpat et. - -1. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. - -1. Vestibulum hendrerit orci urna, non aliquet eros eleifend vitae. - -1. Curabitur nibh dui, vestibulum cursus neque commodo, aliquet accumsan risus. - - ``` - Sed at malesuada orci, eu volutpat ex - ``` - -1. In ac odio vulputate, faucibus lorem at, sagittis felis. - -1. Fusce tincidunt sapien nec dolor congue facilisis lacinia quis urna. - - > **Note**: Ut feugiat est id ultrices gravida. - -1. Phasellus urna lacus, luctus at suscipit vitae, maximus ac nisl. - - - Morbi in tortor finibus, tempus dolor a, cursus lorem. - - - Maecenas id risus pharetra, viverra elit quis, lacinia odio. - - - Etiam rutrum pretium enim. - -1. Curabitur in pretium urna, nec ullamcorper diam. - -#### Review - -Maecenas fringilla ac purus non tincidunt. Aenean pellentesque velit id suscipit tempus. Cras at ullamcorper odio. diff --git a/Instructions/Labs/LAB_AK_00_configure_github_copilot_sdk_lab.md b/Instructions/Labs/LAB_AK_00_configure_github_copilot_sdk_lab.md new file mode 100644 index 0000000..69d2cf0 --- /dev/null +++ b/Instructions/Labs/LAB_AK_00_configure_github_copilot_sdk_lab.md @@ -0,0 +1,128 @@ +--- +lab: + title: Prepare - Configure your GitHub Copilot SDK lab environment + description: Review the lab requirements and configure resources for the GitHub Copilot SDK exercises. + duration: 30 minutes + level: 200 + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Configure your GitHub Copilot SDK lab environment + +Before you begin a GitHub Copilot SDK lab exercise, you need to ensure that your development environment includes the required tools and resources. + +Your lab environment must include the following resources: + +- Git version 2.48 or later. +- The .NET SDK version 8.0 or later. +- Access to a GitHub account with GitHub Copilot enabled. +- Visual Studio Code with the C# Dev Kit and GitHub Copilot Chat extensions. +- GitHub Copilot CLI installed and authenticated with your GitHub account. + +## Install the Git, .NET, Visual Studio Code, and GitHub resources + +The GitHub Copilot SDK lab exercises use GitHub Copilot in Visual Studio Code as the primary AI coding assistant. To use GitHub Copilot, you need access to a GitHub account with a GitHub Copilot subscription. GitHub requires Git for version control operations. + +The lab application that you'll be working on was built using C# (ASP.NET Core 8.0 and Blazor). The data access layer of the lab application uses Entity Framework Core and SQLite. The lab application is available in a GitHub repository that you clone to your lab environment during the lab exercise. + +Complete the following steps to ensure that the required Git, .NET, Visual Studio Code, and GitHub resources are available. + +1. Ensure that Git version 2.48 or later is installed in your lab environment. + + Run the following command in a terminal window to check the installed version of Git: + + ```bash + git --version + ``` + + If you're running Windows and you want to update Git, you can use the following command: + + ```bash + git update-git-for-windows + ``` + + If necessary, you can download Git using the following URL: Download Git. + +1. Ensure that Git is configured to use your name and email address. + + If required, you can use the following commands to set your Git user name and email address. + + > **NOTE**: Update the following commands with your information before you run the commands. + + ```bash + git config --global user.name "Julie Miller" + ``` + + ```bash + git config --global user.email julie.miller@example.com + ``` + +1. Ensure that the .NET 8.0 SDK, or a later version, is installed in your lab environment. + + The starter application for this lab was developed using .NET 8.0. However, you can update the projects to use the latest LTS or STS version of the .NET SDK if you don't have .NET 8 installed. + + Run the following command in a terminal window to check the installed versions of the .NET SDK: + + ```dotnetcli + dotnet --list-sdks + ``` + + If necessary, you can download the .NET SDK using the following URL: Download .NET SDK. + +1. Ensure that the .NET SDK is configured to use the official NuGet.org repository as a source for downloading and restoring packages. + + For example, open a terminal window and then run the following command: + + ```bash + dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org + ``` + +1. Ensure that Visual Studio Code and the C# Dev Kit extension are installed in your lab environment. + + If necessary, you can download Visual Studio Code using the following URL: Download Visual Studio Code + + You can install the C# Dev Kit extension using the Extensions view in Visual Studio Code. + +1. Ensure that you have access to a GitHub account and GitHub Copilot subscription. + + You can log in to your GitHub account using the following URL: GitHub login. + + If you don't have a GitHub account, you can create an individual account from the GitHub login page. On the login page, select **Create an account**. + + Open the settings/profile page of your GitHub account and verify that you have access to a GitHub Copilot subscription. If you have an active subscription for GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise that you can use for training, you can use your existing GitHub Copilot subscription to complete the GitHub Copilot exercises. + + If you have an individual GitHub account, but you don't have a GitHub Copilot subscription, you can set up a GitHub Copilot Free plan from Visual Studio Code during a training exercise. + + > **IMPORTANT**: The GitHub Copilot Free plan is a limited version of GitHub Copilot, allowing up to 2,000 code completions and 50 chats or premium requests per month. If you use a GitHub Copilot Free plan outside training exercises, you may exceed the plan's resource limits before completing the training. The GitHub Copilot Free plan is not available for GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscriptions. + +1. Ensure that GitHub Copilot Chat is accessible in your Visual Studio Code environment. + + You can install the GitHub Copilot Chat extension using the Extensions view in Visual Studio Code. + +## Install the lab application dependencies + +The GitHub Copilot SDK uses the engine behind GitHub Copilot CLI for AI code generation and chat interactions. To ensure that the lab environment is properly configured for the GitHub Copilot SDK exercises, you need to install and authenticate the GitHub Copilot CLI. + +For more information about the GitHub Copilot CLI, see the official documentation: About GitHub Copilot CLI. + +Complete the following steps to ensure that the GitHub Copilot CLI is installed and configured in your lab environment. + +1. To open the official installation instructions for the GitHub Copilot CLI, use the following link: + + Install GitHub Copilot CLI + + The GitHub Copilot CLI is available for Windows, macOS, and Linux. Follow the instructions to install the GitHub Copilot CLI for your operating system. + +1. After installing the GitHub Copilot CLI, run the following command to authenticate the CLI with your GitHub account: + + ```bash + copilot login + ``` + + This command will open a browser window where you can log in to your GitHub account and authorize the GitHub Copilot CLI to access your account. + + > **NOTE**: The authorization code is displayed in the terminal window after you run the `copilot login` command. If the browser window doesn't open automatically, you can copy and paste the authorization code into the browser to complete the authentication process. + + After authenticating, you can start using the GitHub Copilot SDK in your lab exercises. The GitHub Copilot CLI will be used by the GitHub Copilot SDK to generate AI code completions and chat responses in Visual Studio Code. diff --git a/Instructions/Labs/LAB_AK_00_configure_github_dev_kit_lab.md b/Instructions/Labs/LAB_AK_00_configure_github_dev_kit_lab.md new file mode 100644 index 0000000..8ebc510 --- /dev/null +++ b/Instructions/Labs/LAB_AK_00_configure_github_dev_kit_lab.md @@ -0,0 +1,254 @@ +--- +lab: + title: Prepare - Configure your GitHub Spec Kit lab environment + description: Review the lab requirements and configure resources for the GitHub Spec Kit exercises. + duration: 40 minutes + level: 200 + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Configure your GitHub Spec Kit lab environment + +Before you begin the Spec-Driven Development with GitHub Dev Kit lab exercise, you need to ensure that your development environment includes the required tools and resources. + +Your lab environment must include the following resources: + +- Git version 2.48 or later. +- The .NET SDK version 8.0 or later. +- Access to a GitHub account with GitHub Copilot enabled. +- Visual Studio Code with the C# Dev Kit and GitHub Copilot Chat extensions. +- SQL Server LocalDB or SQLite. +- Python version 3.11 or later. +- The uv package manager. + +## Install the GitHub, .NET, and Visual Studio Code resources + +The "Spec-Driven Development with GitHub Dev Kit" lab exercise uses GitHub Copilot in Visual Studio Code as the primary AI assistant. To use GitHub Copilot, you need access to a GitHub account with a GitHub Copilot subscription. GitHub requires Git for version control operations. The lab application that you'll be working on was built using .NET (ASP.NET Core 8.0 and Blazor). + +Complete the following steps to ensure that the required GitHub, .NET, and Visual Studio Code tools and resources are available. + +1. Ensure that Git version 2.48 or later is installed in your lab environment. + + Run the following command in a terminal window to check the installed version of Git: + + ```bash + git --version + ``` + + If you're running Windows and you want to update Git, you can use the following command: + + ```bash + git update-git-for-windows + ``` + + If necessary, you can download Git using the following URL: Download Git. + +1. Ensure that Git is configured to use your name and email address. + + If required, you can use the following commands to set your Git user name and email address. + + > **NOTE**: Update the following commands with your information before you run the commands. + + ```bash + git config --global user.name "Julie Miller" + ``` + + ```bash + git config --global user.email julie.miller@example.com + ``` + +1. Ensure that the .NET 8.0 SDK, or a later version, is installed in your lab environment. + + Installing the latest LTS or STS version of the .NET SDK is recommended, however, you can use .NET 8.0 to complete this exercise. + + Run the following command in a terminal window to check the installed version of the .NET SDK: + + ```dotnetcli + dotnet --version + ``` + + If necessary, you can download the .NET SDK using the following URL: Download .NET SDK. + +1. Ensure that the .NET SDK is configured to use the official NuGet.org repository as a source for downloading and restoring packages. + + For example, open a terminal window and then run the following command: + + ```bash + dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org + ``` + +1. Ensure that Visual Studio Code and the C# Dev Kit extension are installed in your lab environment. + + If necessary, you can download Visual Studio Code using the following URL: Download Visual Studio Code + + You can install the C# Dev Kit extension using the Extensions view in Visual Studio Code. + +1. Ensure that you have access to a GitHub account and GitHub Copilot subscription. + + You can log in to your GitHub account using the following URL: GitHub login. + + If you don't have a GitHub account, you can create an individual account from the GitHub login page. On the login page, select **Create an account**. + + Open the settings/profile page of your GitHub account and verify that you have access to a GitHub Copilot subscription. If you have an active subscription for GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise that you can use for training, you can use your existing GitHub Copilot subscription to complete the GitHub Copilot exercises. + + If you have an individual GitHub account, but you don't have a GitHub Copilot subscription, you can set up a GitHub Copilot Free plan from Visual Studio Code during a training exercise. + + > **IMPORTANT**: The GitHub Copilot Free plan is a limited version of GitHub Copilot, allowing up to 2,000 code completions and 50 chats or premium requests per month. If you use a GitHub Copilot Free plan outside training exercises, you may exceed the plan's resource limits before completing the training. The GitHub Copilot Free plan is not available for GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscriptions. + +1. Ensure that GitHub Copilot Chat is accessible in your Visual Studio Code environment. + + You can install the GitHub Copilot Chat extension using the Extensions view in Visual Studio Code. + +## Install the lab application dependencies + +The application that you're working on during the lab uses either a SQL Server LocalDB database or a SQLite database to store application data. SQL Server LocalDB is a lightweight version of SQL Server that's ideal for development and testing. SQLite is a self-contained, serverless database engine that's easy to set up and use. + +Complete the following steps to ensure that SQL Server LocalDB is installed in your lab environment. + +1. Check to see if SQL Server LocalDB is installed in your lab environment. + + Run the following command in a terminal window to check for LocalDB installation: + + ```powershell + sqllocaldb info + ``` + + Expected output: List of LocalDB instances or an empty list if none exist. For example: + + ```output + MSSQLLocalDB + ``` + + If the command fails or LocalDB is not installed, use the following steps to install SQL Server 2019 LocalDB. Otherwise, skip to the "Install the GitHub Spec Kit tools and resources" section. + +1. To download the SQL Server 2019 Express edition installer file, open the following link in a browser: SQL Server 2019 Express download + +1. After the download is complete, open the SQL Server 2019 installer file (for example, **SQL2019-SSEI-Expr.exe**). + +1. On the SQL Server 2019 installation wizard, select **Download Media**. + +1. Under **Specify SQL Server installer download**, select the **LocalDB** package, and then select the **Download** button. + +1. When you see the **Download successful** message, select the **Open folder** button. + +1. Run the SQL Server LocalDB installer file (for example, **SqlLocalDB.msi**), and then follow the prompts to complete the installation. + +1. To verify the installation, open PowerShell or Command Prompt, and then run the following command: + + ```powershell + sqllocaldb info + ``` + + You should see a list of LocalDB instances (or an empty list if none exist yet). For example: + + ```output + MSSQLLocalDB + ``` + + If you need to create the default instance of MSSQLLocalDB, run the following commands: + + ```powershell + sqllocaldb create MSSQLLocalDB + sqllocaldb start MSSQLLocalDB + ``` + +1. To download SQLite, follow the instructions at the following URL: Download SQLite. + +## Install the GitHub Spec Kit tools and resources + +The GitHub Spec Kit's command-line interface (CLI) tool is Python-based and requires Python 3.11 or later. The uv package manager is used to install and manage the GitHub Spec Kit CLI tool. + +Complete the following steps to install and configure the GitHub Spec Kit tools and resources in your lab environment. + +1. Ensure that Python 3.11 or later is installed in your lab environment. + + GitHub Spec Kit's CLI tool is Python-based and requires Python 3.11+. + + To check the installed Python version, run the following command: + + ```powershell + python --version + ``` + + Required output: **Python 3.11.0** or later. + + If you need to install Python, you can download the installer from the following URL: python.org. + + If you're in a corporate environment, you can also use your organization's software distribution system. + +1. Ensure that the uv package manager is installed in your lab environment. + + ```powershell + uv --version + ``` + + You should see output similar to the following sample: + + ```output + uv 0.9.17 (2b5d65e61 2025-12-09) + ``` + + To install uv using Windows PowerShell, run the following command: + + ```powershell + powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" + ``` + + GitHub Spec Kit uses uv for CLI installation and management. + + You can find more installation instructions at the following URL: docs.astral.sh/uv. + +1. To ensure that uv is in your environment PATH, restart your terminal window, and then run the following commands: + + ```powershell + cd C:\ + uv --version + ``` + + You should see output similar to the following sample: + + ```output + uv 0.9.17 (2b5d65e61 2025-12-09) + ``` + +1. Open a terminal window. + + You can use a Command Prompt, PowerShell, or Terminal window. + +1. To install GitHub Spec Kit's Specify CLI tool, run the following PowerShell command: + + ```powershell + uv tool install specify-cli --from git+https://github.com/github/spec-kit.git + ``` + + This command installs the latest version directly from the GitHub repository and makes the *specify* command available system-wide. + + The specify command-line tool is used to initialize projects for spec-driven development. + +1. To ensure that the *specify* command is in your environment PATH, restart your terminal window, and then run the following command: + + ```powershell + specify version + ``` + + After a short delay, you should see output that's similar to the following sample: + + ```output + CLI Version 0.0.22 + Template Version 0.0.90 + Released 2025-12-04 + Python 3.14.0 + Platform Windows + Architecture AMD64 + OS Version 10.0.26200 + ``` + + Troubleshooting installation issues: + + - Command not found: If the *specify* command isn't recognized after installation, the *uv* tools directory might not be in your PATH. To verify the installation, run *uv tool list* command. You might need to restart your terminal or manually add the tools directory to your PATH. + + - In corporate environments with SSL interception, you might need to configure certificates. Contact your IT department for assistance. + +Your GitHub Spec Kit development environment is now configured and ready. diff --git a/Instructions/Labs/LAB_AK_00_configure_lab_environment.md b/Instructions/Labs/LAB_AK_00_configure_lab_environment.md new file mode 100644 index 0000000..73afb8f --- /dev/null +++ b/Instructions/Labs/LAB_AK_00_configure_lab_environment.md @@ -0,0 +1,60 @@ +--- +lab: + title: Prepare - Configure your lab environment for GitHub Copilot exercises + description: Review lab requirements and configure resources before starting GitHub Copilot exercises. + duration: 15 minutes + level: 200 + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Configure your lab environment for GitHub Copilot exercises + +Your lab environment must be configured for C# development using Visual Studio Code and GitHub Copilot. Access to a GitHub account with GitHub Copilot enabled is required. + +Complete the following steps to verify that your lab environment is configured correctly: + +1. Verify that Git version 2.48 or later is installed in your lab environment. + + Run the following command in a terminal window to check the installed version of Git: + + ```bash + git --version + ``` + + If you're running Windows and you want to update Git, you can use the following command: + + ```bash + git update-git-for-windows + ``` + + If necessary, you can download Git using the following URL: Download Git. + +1. Verify that the latest LTS or STS version of the .NET SDK is installed in your lab environment. + + Run the following command in a terminal window to check the installed version of the .NET SDK: + + ```dotnetcli + dotnet --version + ``` + + If necessary, you can download the .NET SDK using the following URL: Download .NET SDK. + +1. Verify that Visual Studio Code and the C# Dev Kit extension are installed in your lab environment. + + If necessary, you can download Visual Studio Code using the following URL: Download Visual Studio Code + + You can install the C# Dev Kit extension using the Extensions view in Visual Studio Code. + +1. Verify that you have access to a GitHub account and GitHub Copilot subscription. + + You can log in to your GitHub account using the following URL: GitHub login. + + If you don't have a GitHub account, you can create an individual account from the GitHub login page. On the login page, select **Create an account**. + + Open the settings/profile page of your GitHub account and verify that you have access to a GitHub Copilot subscription. If you have an active subscription for GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise that you can use for training, you can use your existing GitHub Copilot subscription to complete the GitHub Copilot exercises. + + If you have an individual GitHub account, but you don't have a GitHub Copilot subscription, you can set up a GitHub Copilot Free plan from Visual Studio Code during a training exercise. + + > **IMPORTANT**: The GitHub Copilot Free plan is a limited version of GitHub Copilot, allowing up to 2,000 code completions and 50 chats or premium requests per month. If you use a GitHub Copilot Free plan outside training exercises, you may exceed the plan's resource limits before completing the training. The GitHub Copilot Free plan is not available for GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscriptions. diff --git a/Instructions/Labs/LAB_AK_00_configure_lab_environment_py.md b/Instructions/Labs/LAB_AK_00_configure_lab_environment_py.md new file mode 100644 index 0000000..036e45d --- /dev/null +++ b/Instructions/Labs/LAB_AK_00_configure_lab_environment_py.md @@ -0,0 +1,60 @@ +--- +lab: + title: Prepare - Configure your lab environment for GitHub Copilot exercises (Python) + description: Review lab requirements and configure resources before starting GitHub Copilot exercises. + duration: 20 minutes + level: 200 + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Configure your lab environment for GitHub Copilot exercises + +Your lab environment must be configured for Python development using Visual Studio Code and GitHub Copilot. Access to a GitHub account with GitHub Copilot enabled is required. + +Complete the following steps to verify that your lab environment is configured correctly: + +1. Verify that Git version 2.48 or later is installed in your lab environment. + + Run the following command in a terminal window to check the installed version of Git: + + ```bash + git --version + ``` + + If you're running Windows and you want to update Git, you can use the following command: + + ```bash + git update-git-for-windows + ``` + + If necessary, you can download Git using the following URL: Download Git. + +1. Verify that the latest version of Python is installed in your lab environment. + + Run the following command in a terminal window to check the installed version of Python: + + ```bash + python3 --version + ``` + + If necessary, follow the steps to Configure Python in Visual Studio Code using the following URL: Getting Started with Python in VS Code. + +1. Verify that Visual Studio Code and the Python extension are installed in your lab environment. + + If necessary, you can download Visual Studio Code using the following URL: Download Visual Studio Code + + You can install the Python extension using the Extensions view in Visual Studio Code. + +1. Verify that you have access to a GitHub account and GitHub Copilot subscription. + + You can log in to your GitHub account using the following URL: GitHub login. + + If you don't have a GitHub account, you can create an individual account from the GitHub login page. On the login page, select **Create an account**. + + Open the settings/profile page of your GitHub account and verify that you have access to a GitHub Copilot subscription. If you have an active subscription for GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise that you can use for training, you can use your existing GitHub Copilot subscription to complete the GitHub Copilot exercises. + + If you have an individual GitHub account, but you don't have a GitHub Copilot subscription, you can set up a GitHub Copilot Free plan from Visual Studio Code during a training exercise. + + > **IMPORTANT**: The GitHub Copilot Free plan is a limited version of GitHub Copilot, allowing up to 2,000 code completions and 50 chats or premium requests per month. If you use a GitHub Copilot Free plan outside training exercises, you may exceed the plan's resource limits before completing the training. The GitHub Copilot Free plan is not available for GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscriptions. diff --git a/Instructions/Labs/LAB_AK_00_enable_github_copilot_in_visual_studio_code.md b/Instructions/Labs/LAB_AK_00_enable_github_copilot_in_visual_studio_code.md new file mode 100644 index 0000000..3d4c32d --- /dev/null +++ b/Instructions/Labs/LAB_AK_00_enable_github_copilot_in_visual_studio_code.md @@ -0,0 +1,42 @@ +--- +lab: + title: Prepare - Enable GitHub Copilot in Visual Studio Code + description: Complete the steps required to enable GitHub Copilot in Visual Studio Code. + duration: 10 minutes + level: 200 + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Enable GitHub Copilot in Visual Studio Code + +GitHub offers three GitHub Copilot plans for individual developers and two plans for organizations and enterprises. The plans are designed to meet the needs of individual developers, teams, and organizations. The GitHub Copilot Free plan is available to individual GitHub users, while the paid plans are available to individuals and organizations that require additional features and capabilities. + +Complete the following steps to enable GitHub Copilot in Visual Studio Code: + +1. Open Visual Studio Code. + +1. Ensure that the latest version of Visual Studio Code is installed. + + To check to updates, select **Manage** (the gear icon) in the lower left corner of the Visual Studio Code window, and then select **Check for Updates**. + +1. On the Visual Studio Code Status Bar, to activate GitHub Copilot, hover the mouse pointer over the Copilot icon, and then select **Set up Copilot**. + + ![Screenshot showing the GitHub Copilot Settings button.](./Media/m00-github-copilot-setup.png) + +1. On the **Sign in to use Copilot for free** page, select **Sign in**. + + The GitHub account sign in page opens in your default web browser. + +1. On the GitHub sign in page, enter the GitHub account credentials that you'll be using for this exercise, and then select **Sign in**. + +1. Follow the online instructions to authenticate your account and authorize access in Visual Studio Code. + + You'll be directed back to Visual Studio Code when the authentication/authorization process is complete. + +1. To verify that GitHub Copilot is activated, open Visual Studio Code's **Extensions** view. + + You should see the GitHub Copilot and GitHub Copilot Chat extensions listed in the **Installed** section of the Extensions view. + + ![Screenshot showing GitHub Copilot the Visual Studio Code Extensions view.](./Media/m00-github-copilot-extensions-vscode.png) diff --git a/Instructions/Labs/LAB_AK_01_deploying_arm_templates.md b/Instructions/Labs/LAB_AK_01_deploying_arm_templates.md deleted file mode 100644 index 5e25394..0000000 --- a/Instructions/Labs/LAB_AK_01_deploying_arm_templates.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -lab: - title: 'Lab: Deploying Azure Resource Manager templates' - type: 'Answer Key' - module: 'Module 1: Exploring Azure Resource Manager' ---- - -# Lab: Deploying Azure Resource Manager templates -# Student lab answer key - -## Instructions - -### Before you start - -#### Setup Task - -1. Integer dolor purus, gravida eu sem id, efficitur aliquet neque. - -1. Suspendisse viverra mauris in metus laoreet consectetur. - -1. Sed diam risus, convallis quis condimentum at, egestas malesuada libero. - -### Exercise 0: - -#### Task 0: - -1. Quisque dictum convallis metus, vitae vestibulum turpis dapibus non. - - 1. Suspendisse commodo tempor convallis. - - 1. Nunc eget quam facilisis, imperdiet felis ut, blandit nibh. - - 1. Phasellus pulvinar ornare sem, ut imperdiet justo volutpat et. - -1. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. - -1. Vestibulum hendrerit orci urna, non aliquet eros eleifend vitae. - -1. Curabitur nibh dui, vestibulum cursus neque commodo, aliquet accumsan risus. - - ``` - Sed at malesuada orci, eu volutpat ex - ``` - -1. In ac odio vulputate, faucibus lorem at, sagittis felis. - -1. Fusce tincidunt sapien nec dolor congue facilisis lacinia quis urna. - - > **Note**: Ut feugiat est id ultrices gravida. - -1. Phasellus urna lacus, luctus at suscipit vitae, maximus ac nisl. - - - Morbi in tortor finibus, tempus dolor a, cursus lorem. - - - Maecenas id risus pharetra, viverra elit quis, lacinia odio. - - - Etiam rutrum pretium enim. - -1. Curabitur in pretium urna, nec ullamcorper diam. - -#### Review - -Maecenas fringilla ac purus non tincidunt. Aenean pellentesque velit id suscipit tempus. Cras at ullamcorper odio. diff --git a/Instructions/Labs/LAB_AK_01_examine_settings_interface.md b/Instructions/Labs/LAB_AK_01_examine_settings_interface.md new file mode 100644 index 0000000..9c33d0a --- /dev/null +++ b/Instructions/Labs/LAB_AK_01_examine_settings_interface.md @@ -0,0 +1,514 @@ +--- +lab: + title: Exercise - Examine GitHub Copilot settings and user interface features + description: Learn how to configure GitHub Copilot settings and how to access GitHub Copilot features in Visual Studio Code. + duration: 25 minutes + level: 200 + islab: true + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Examine GitHub Copilot settings and user interface features + +Visual Studio Code provides a seamless and customizable GitHub Copilot experience for developers. In this exercise you examine GitHub Copilot settings and explore the GitHub Copilot user interface in Visual Studio Code. + +This exercise should take approximately **25** minutes to complete. + +> **IMPORTANT**: To complete this exercise, you must provide your own GitHub account and GitHub Copilot subscription. If you don't have a GitHub account, you can sign up for a free individual account and use a GitHub Copilot Free plan to complete the exercise. If you have access to a GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscription from within your lab environment, you can use your existing GitHub Copilot subscription to complete this exercise. + +## Before you start + +Your lab environment must include the following: + +- Git 2.48 or later +- Either .NET or Python: + + - **.NET SDK 9.0** or later with Visual Studio Code with the **C# Dev Kit** extension. + - **Python 3.10** or later with Visual Studio Code with the **Python** extension + +- Access to a GitHub account with GitHub Copilot enabled. + +If you're using a local PC as a lab environment for this exercise: + +- For help configuring your local PC as your lab environment, open the following link in a browser: Configure your lab environment resources. + +- For help enabling your GitHub Copilot subscription in Visual Studio Code, open the following link in a browser: Enable GitHub Copilot within Visual Studio Code. + +If you're using a hosted lab environment for this exercise: + +- For help enabling your GitHub Copilot subscription in Visual Studio Code, paste the following URL into a browser's site navigation bar: Enable GitHub Copilot within Visual Studio Code. + +- To ensure that the .NET SDK is configured to use the official NuGet.org repository as a source for downloading and restoring packages: + + Open a command terminal and then run the following command: + + ```bash + + dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org + + ``` + +- To configure the hosted lab environment for Python, follow these steps: + + 1. To determine the version of Python installed in the hosted environment, run the following command: + + ```bash + python --version + ``` + + If necessary, use the following steps at the following URL to Configure Python in Visual Studio Code: Getting Started with Python in VS Code. + + 1. Install the Python extension using the Extensions view in Visual Studio Code. + +## Exercise scenario + +You're a developer working in the IT department of your local community. The backend systems that support the public library were lost in a fire. Your team needs to develop a temporary solution to help the library staff manage their operations until the system can be replaced. Your team chose GitHub Copilot to accelerate the development process. + +This exercise includes the following tasks: + +1. Examine GitHub Copilot settings in Visual Studio Code. +1. Explore the GitHub Copilot user interface in Visual Studio Code. + +## Examine GitHub Copilot settings in Visual Studio Code + +GitHub Copilot settings are split between your GitHub account and the Visual Studio Code environment. In Visual Studio Code, settings enable you to configure the behavior of GitHub Copilot. Starting with VS Code 1.116, GitHub Copilot is a built-in extension, so you no longer need to install a separate extension to use its features. In your GitHub account, settings enable you to manage your GitHub Copilot subscription, configure the retention of prompts and suggestions, and allow or block suggestions matching public code. + +> **NOTE**: The GitHub Copilot settings described in this section are specific to the Visual Studio Code environment. If you use GitHub Copilot in other environments, such as GitHub Codespaces or JetBrains IDEs, there may be other settings that are specific to those environments. Visual Studio Code's settings editor organizes Copilot settings across several categories, including Code Editing, Chat, Agent, and Inline Chat settings. + +### Enable or disable GitHub Copilot features in Visual Studio Code + +GitHub Copilot in Visual Studio Code is enabled by default when you activate a plan. However, you can temporarily disable GitHub Copilot features if you need to, and then re-enable them when you're ready. + +Use the following steps to complete this section of the exercise: + +1. Open a new instance of Visual Studio Code. + +1. On the bottom status bar, select the GitHub Copilot icon. + + The Copilot status menu opens with options to manage GitHub Copilot features, including the ability to enable or disable inline suggestions and next edit suggestions (NES). + +1. Notice that the Copilot status menu includes options to enable/disable **Inline Suggestions** and **Next edit suggestions**. + + You can disable inline suggestions globally or for the current file's language. You can also use the **Snooze** button to temporarily pause inline suggestions for five-minute increments, and then select **Cancel Snooze** to resume them. + +1. To open the Chat menu, select the **More Actions** dropdown to the right of the Chat button in the title bar. + + The Chat menu includes options to open the Chat view, Quick Chat, and Inline Chat interfaces, as well as options to manage settings for each of those interfaces. + +1. On the Chat menu, select **Configure Inline Suggestions**. + +1. Notice that the Configure Inline Suggestions menu includes an option to **Edit Settings** + + Selecting the Edit Settings option opens the Visual Studio Code settings editor in a view that's filtered for GitHub Copilot. This provides a comprehensive interface for managing GitHub Copilot settings. + +1. To open Visual Studio Code's Extensions view, select the Extensions icon on the left menu bar. + +1. In the Extensions view, enter **GitHub Copilot Chat** in the search bar. + +1. To open the GitHub Copilot Chat settings menu, select the gear icon on the GitHub Copilot Chat extension. + +1. Notice the options to Enable AI Features and Disable AI Features. + +The GitHub Copilot Chat settings menu includes options to enable or disable GitHub Copilot features for specific languages. For example, you can disable GitHub Copilot for Markdown files if you don't want suggestions when working on documentation. + +If you want to test the enable/disable options, you can select the disable option. However, be sure to re-enable GitHub Copilot before you continue with this exercise. + +### Examine settings for the GitHub Copilot Chat extension + +Default settings are configured for you when you activate GitHub Copilot in Visual Studio Code. The settings are organized under the Extensions label, which includes settings for GitHub Copilot Chat. You can customize settings for GitHub Copilot using Visual Studio Code's settings tab. + +Use the following steps to complete this section of the exercise: + +1. On Visual Studio Code's top menu bar, open the Chat menu (next to the Chat button in the title bar). + + The Chat menu includes a **Configure Inline Suggestions** option that provides the option to **Edit Settings** associated with GitHub Copilot. + +1. On the Chat menu, select **Configure Inline Suggestions**, and then select **Edit Settings**. + +1. Take a moment to review how the GitHub Copilot settings are organized. + + Notice that the left side of the settings editor shows the settings organized into sections, including a section for Extensions. The extensions section should include the GitHub Copilot Chat extension. The right side of the screen shows the settings for the selected section (all settings are displayed when no section is selected). You can use the search bar at the top of the settings editor to filter settings by keyword. + +1. Under the Extensions label, select **GitHub Copilot Chat**. + + Notice that the settings list is now filtered for GitHub Copilot Chat settings only. + + The GitHub Copilot Chat extension has a long list of available settings and it's updated regularly. The GitHub Copilot Chat extension also includes preview and experimental settings that are subject to change and could be discontinued. The preview and experimental settings are included at the end of the list and they're tagged as either **Preview** or **Experimental**. + +1. Take a couple of minutes to review the settings for GitHub Copilot Chat. + + We recommend keeping the default settings during this training. This helps to ensure that you have the expected experience when working through the exercises. When you have completed the training, you can experiment with these settings to customize your experience. + +1. In the search bar at the top of the settings editor, type **GitHub Copilot Enable**. + + You should now see the setting that can be used to enable or disable GitHub Copilot completions for specified languages. + +1. In the list of languages, select **markdown**. + + The default value for Markdown is set to **false**. This means that GitHub Copilot completions are disabled for Markdown files. + +1. To enable GitHub Copilot for Markdown files, select **Edit Item** (pencil icon), select **false**, change the value to **true**, and then select **OK**. + + You can now use GitHub Copilot to help you author or update Markdown files. For example, GitHub Copilot can generate code completion suggestions when you're working on project documentation. + +1. Close the Visual Studio Code settings editor. + +## Explore the GitHub Copilot user interface in Visual Studio Code + +Visual Studio Code seamlessly integrates GitHub Copilot's AI features into your development environment. + +GitHub Copilot's features are organized into the following categories: + +- Agents: An agent is an AI assistant that works autonomously to complete a coding task. Give it a high-level goal, and it breaks the goal into steps, edits files across your project, runs commands, and self-corrects when something goes wrong. Agents can run locally, in the background, or in the cloud. + +- Natural language chat: Visual Studio Code works with GitHub Copilot to provide multiple chat interfaces: Chat view, Quick Chat, and Inline Chat. The Chat view supports three modes (Ask, Plan, and Agent) that you can switch between during a conversation. + +- Inline suggestions: GitHub Copilot provides two kinds of inline suggestions as you type. Ghost text suggestions provide dimmed code suggestions at your current cursor location. Next edit suggestions (NES) predict both the location and content of your next edit based on changes you're already making. + +- Smart actions: GitHub Copilot automates common tasks with Smart actions to eliminate repetitive prompt writing. + +GitHub Copilot's productivity features are easy to access and fit seamlessly into your workflow without interrupting your coding experience. + +### Explore the Chat view features + +Visual Studio Code's Chat view provides a comprehensive interface for interacting with GitHub Copilot. The Chat view is a unified experience that supports Ask, Plan, and Agent modes, so you can switch between asking questions, planning a task, and running an agent all within the same conversation. The Chat view also provides features for managing your chat sessions, such as saving and loading chat history, adding context to your chats, and selecting different models for generating responses. + +Use the following steps to complete this section of the exercise: + +1. To toggle the Chat view from open to closed, select the **Chat** button (or press **Ctrl+Alt+I**). + + The Chat button (labeled **Toggle Chat** in the user interface) is located at the top of the Visual Studio Code window, just to the right of the search textbox. + +1. To toggle the Chat view from closed to open, select the **Chat** button again. + + The default location for the Chat view is the Secondary Side Bar on the right side of the Visual Studio Code window. A **Views and More Actions** button (three dots on the top menu of the Chat view) can be used to open a context menu with options for moving the Chat view to different locations, opening the Chat view in an editor tab, or opening it in a separate window. + +1. Take a few minutes to examine the Chat view interface. + + Starting from the top and moving down, the Chat view includes the following interface elements: + + - Chat view toolbar: The Chat view toolbar is located in the top right corner of the Chat view. You can use the toolbar to manage the chat history, start a new chat, open the Chat view in another location, or hide the Chat view. Hover your mouse pointer over the toolbar button icons to see a description. + + - Chat response area: The Chat response area is the space below the Chat view toolbar where GitHub Copilot displays responses. Responses include code suggestions, explanations, interactive elements, and other information related to your prompt. + + - Prompt textbox: The prompt textbox is where you enter your prompts. You can use this text box to ask GitHub Copilot questions about your codebase, request code suggestions, or ask for help with specific tasks. + + - Add Context button: The Add Context button is located in bottom section of the Chat view. You can use this button search for resources that add context to Chat session. The resources can be anything from internal project files to public repositories on GitHub that are external to your organization. + + - Set Agent menu: The Set Agent dropdown menu is located to the right of the Start Voice Chat button. Based on your specific needs, you can choose between different modes of chat: + + - **Ask**: Use this mode to ask GitHub Copilot questions about your codebase. You can use Ask mode to explain code, suggest changes, or provide information about the codebase. + - **Plan**: Use this mode to plan code changes in your workspace before implementing them. When you select the Plan mode, GitHub Copilot provides a structured response that breaks down the task into smaller steps, helping you understand the overall approach before any code changes are made. The Plan mode is usually reserved for highly complex tasks. + - **Agent**: Use this mode to run GitHub Copilot as an agent. You can use GitHub Copilot to run commands, execute code, or perform other tasks in your workspace. + + - Pick Model menu: The Pick Model menu is located to the right of the Chat Mode menu. You can use this button to select the model that GitHub Copilot uses to generate responses. Model selections may be limited based on your GitHub Copilot subscription, your GitHub Copilot settings, and the models available in your region. + + - Configure Tools button: The Configure Tools button is located to the right of the Pick Model menu. You can use this button to manage tools that GitHub Copilot can use in Agent mode. For example, you can use the Configure Tools menu to connect GitHub Copilot to your codebase, terminal, or other resources that an agent might need to access when performing a task. + + - Start Voice Chat button: The microphone button in the Chat input area starts a voice chat session with GitHub Copilot. Voice Chat requires the **VS Code Speech** extension to be installed (the microphone icon does not appear if the VS Code Speech extension isn't installed). When a voice session is active, your spoken input is transcribed and submitted as a chat prompt. + + - Send button: The Send button is located to the right of the Pick Model menu. You can use this button to submit your prompt to GitHub Copilot and receive a response. The menu includes several options for how your prompt is submitted. + + - Set Session Target menu: The Set Session Target menu is located in the bottom left corner of the Chat view. You can use this menu to select where the agent runs. For example, you can select to run the agent locally in the VS Code editor, run the agent in the background using Copilot CLI, run the agent remotely in the cloud, or use a third-party agent harness and SDK such as Anthropic's Claude. + + - Set Permissions button: The Set Permissions button is located to the right of the Delegate Session button. You can use this button to manage permissions for the current chat session. For example, you can use this menu to allow or restrict GitHub Copilot's access to your codebase, terminal, or other resources. + +1. In the Chat view, select the **Ask** agent and set the model to **Auto**. + + The Ask agent is designed for asking questions and generating code suggestions. You can use the Ask agent to explain code, suggest changes, or provide information about your codebase. + +1. Use the Prompt textbox to enter the following prompt, and then submit the prompt: + + **For C#:** + + ```text + Create a C# console app that prints Hello World to the console. + ``` + + **Or for Python:** + + ```text + Create a Python console app that prints Hello World to the console. + ``` + +1. Take a minute to review the response. + + Notice that GitHub Copilot's response provides instructions for creating a console app project, but doesn't offer to complete the task on your behalf. + +1. To create the console app project, follow the instructions provided in the Chat view response. + + For example: + + Open Visual Studio Code's integrated terminal, and then run the following commands: + + **For C#:** + + ```bash + dotnet new console -n HelloWorldApp + code HelloWorldApp + ``` + + **Or for Python:** + + ```bash + mkdir HelloWorldApp + cd HelloWorldApp + echo "print('Hello World')" > Program.py + code . + ``` + +### Explore the Quick Chat features + +The Quick Chat window is a simplified interface for interacting with GitHub Copilot. It provides a quick way to ask questions, request code suggestions, or get help with specific tasks without leaving the code editor. + +Use the following steps to complete this section of the exercise: + +1. Open Visual Studio Code's Chat menu. + + The Chat menu includes options such as: + + - Open Chat **Ctrl+Alt+I** + - Open Inline Chat **Ctrl+I** + - Open Quick Chat **Ctrl+Shift+Alt+L** + +1. On the Chat menu, select **Open Quick Chat**. + + By default, the Quick Chat window opens at the top center of the Visual Studio Code window. + +1. Notice that the Quick Chat window provides a subset of the options provided by the Chat view. + +1. Use the Quick Chat window to submit the following prompt: + + ```text + Tell me about the Program.cs file + ``` + +1. Take a moment to review the response. + + As long as you haven't opened the Program.cs file in the editor or added it to the Quick Chat context, GitHub Copilot is unable to provide specific information about the file. It may ask you to add the file to the context. + +1. To add your Program.cs file to the Quick Chat context, drag-and-drop the Program.cs file from the Explorer view to the very top of the Quick Chat window. + +1. Notice that the Quick Chat window now includes **Program.cs** just below the prompt textbox. + + > **TIP**: Adding context to the Chat helps GitHub Copilot provide more relevant responses. When adding project files to the Chat context, it's often easier to use a drag-and-drop operation rather than the Add Context button. + +1. Scroll to the top of the Quick Chat window and resubmit the same prompt: + + ```text + Tell me about the Program.cs file + ``` + +1. Notice that the new response provides a detailed description of your Program.cs file, including what it does and how to run it. + +1. In the top-right corner of the Quick Chat window, select **Open in Chat View**. + + Notice that the Quick Chat window closes and the Chat view opens with responses that appeared the Quick Chat window. If the Chat view doesn't display the Quick Chat session, use the Copilot Chat menu to open the Quick Chat window, and then select **Open in Chat View**. + + Switching to the Chat view is useful when you need to extend and manage a chat session that started in the Quick Chat window. + + > **TIP**: The Quick Chat window is great for quick questions and simple tasks. However, if you want a more dedicated Chat environment, you should use the Chat view. + +### Review the Inline Chat features + +Inline Chat lets you interact with GitHub Copilot directly in the code editor without switching to the Chat view. When you have a file open in the editor, you can press **Ctrl+I** to open a chat prompt at your cursor position, describe a change, and VS Code shows the suggested edits as a diff inline in the editor. Use the **Keep** or **Undo** controls to accept or reject the changes. + +When a file belongs to an active chat editing session, pressing **Ctrl+I** opens "Ask in Chat" in the Chat view instead of regular inline chat. The editor context menu also shows **Ask in Chat** instead of **Open Inline Chat** for these files. This routes your prompt into the existing session so it can use the full conversation context. You can disable this behavior by setting `inlineChat.askInChat` to `false`. + +To configure the default model for inline chat, use the `inlineChat.defaultModel` setting. An experimental `inlineChat.affordance` setting controls whether a visual hint appears in the editor when you select text, providing quick access to inline chat for the selection. + +You can also add a code selection or file to an existing Chat session without opening Inline Chat. Right-click selected code in the editor and choose **Add Selection to Chat**, or right-click a file in the Explorer and choose **Add File to Chat**. This attaches the code or file as context in the Chat view, so you can ask questions or request edits that reference it. + +### Compare the Chat view's Ask and Agent modes + +The Chat view has three modes: **Ask**, **Plan**, and **Agent**. The Ask mode is designed for asking questions and generating code suggestions. The Agent mode is designed for autonomous coding tasks where Copilot can search your workspace, edit files, run terminal commands, and use tools. The Plan mode is designed for planning complex tasks by breaking them down into smaller steps. + +> **NOTE**: Edit mode is deprecated as of VS Code 1.110 and will be removed in a future release. Agent mode provides a superset of Edit mode's capabilities. The following steps use Ask and Agent modes instead. + +Use the following steps to complete this section of the exercise: + +1. Open the Program.cs file in the code editor, and then replace the existing code with the following code snippet: + + ```csharp + + using System; + + namespace HelloWorldApp; + + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World"); + } + } + + ``` + +1. Ensure that you have the Chat view open and that the **Ask** agent is selected. + +1. Select the following code: + + ```csharp + + static void Main(string[] args) + { + Console.WriteLine("Hello World"); + } + + ``` + + Notice that the Chat view context is updated to specify the selected code lines in the Program.cs file. + +1. In the Chat view, enter the following prompt: + + ```text + + Refactor the selected code to display "Generate equations for addition and subtraction:" + + ``` + +1. Take a minute to review response displayed in the Chat view. + + Notice that the response includes a code snippet. When you hover the mouse pointer over the code, the Chat view displays buttons in the upper right corner of the code that provide the following options: + + - **Apply to Program.cs**: Use the Apply to Program.cs option to apply the suggested code update to the Program.cs file. + - **Insert at Cursor**: Use the Insert at Cursor option to insert the suggested code update at the current cursor position in the editor. + - **Copy**: Use the Copy option to copy the suggested code update to the clipboard. + + You could use one of these options to apply the suggested code update, but for this exercise, you use Agent mode to apply the update directly. + +1. In the Chat view, open the Set Agent dropdown, and then select **Agent**. + +1. Submit the following prompt: + + ```text + + Refactor the selected code to display "Generate equations for addition and subtraction:" + + ``` + +1. Take a minute to review the updates suggested in the code editor. + + Notice the following: + + - In Agent mode, Copilot applies edits directly to your files. The code editor displays a *Diff-style* view that shows the changes, similar to the Diff view used in GitHub pull requests. + - The code editor displays **Keep** and **Undo** buttons that you can use to apply or reject the changes made to the code. + - The code editor displays additional buttons that can be used to manage the suggested edits. + + In addition to the edit controls displayed in the editor tab, the Chat view displays a **Keep** button that you can use to apply all edits and an **Undo** button to cancel the edits, and an abbreviated description of the suggested update. + +1. In the Chat view, select **Keep** to apply all suggested code updates. + +> **NOTE**: In Agent mode, Copilot may also run terminal commands, search your workspace for context, and use other tools autonomously. By default, Copilot asks for your approval before running terminal commands. To conserve GitHub Copilot resources, this exercise uses a simple prompt that only requires file edits. + +### Explore code completion and next edit suggestions + +GitHub Copilot provides two kinds of inline suggestions as you type: + +- **Ghost text suggestions (code completions)**: Dimmed text appears at your current cursor position suggesting the completion of the current line or a block of new code. +- **Next edit suggestions (NES)**: Copilot predicts both the location and content of your next edit based on the changes you're already making. A gutter arrow appears to indicate where the next suggested edit is located. + +Both features are enabled by default and can be configured via the GitHub Copilot status menu in the Status Bar. + +#### Explore ghost text code completions + +Use the following steps to complete this section of the exercise: + +1. With the Program.cs file open in the code editor, position the cursor at the end of the Console.WriteLine statement. + +1. To generate a code completion suggestion, press **Enter**. + + After a moment, GitHub Copilot generates a code completion suggestion (ghost text) based on the context of the code in the editor. + +1. To accept the code completion suggestion, press **Tab**. + + The code in the editor is updated to include the suggested code lines. + + When you accept a code completion suggestion, GitHub Copilot may suggest additional code lines. When this happens, you can press the **Tab** key to accept the suggestion, press the **Esc** key to reject the suggestion, or enter your own code to override the suggestion. + +#### Explore next edit suggestions (NES) + +Next edit suggestions predict your next code edit based on changes you've already made. For example, if you rename a variable on one line, NES suggests updating the same variable name on other lines. + +Use the following steps to complete this section of the exercise: + +1. Ensure that you have the Program.cs file open in the code editor. + +1. Replace the existing Program class with the following code snippet: + + ```csharp + + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Generate equations for addition and subtraction:"); + for (int i = 0; i < 5; i++) + { + int num1 = new Random().Next(1, 100); + int num2 = new Random().Next(1, 100); + Console.WriteLine($"{num1} + {num2} = {num1 + num2}"); + Console.WriteLine($"{num1} - {num2} = {num1 - num2}"); + } + + } + } + + ``` + +1. In the code editor, select the variable name **num1**. + +1. To change the variable name, type **position1** + +1. Look for the next edit suggestions. + + You should see next edit suggestions to update the variable name **num1** to **position1** on the other lines of code in the Main method. You may also see next edit suggestions to change the **num2** variable name to **position2**. The suggestions may not appear on all lines at once. Instead, they may appear one at a time as you make changes to the code. + +1. Look for an arrow indicator in the left gutter of the editor. + +1. To see a list of options for the next edit suggestions, hover the mouse pointer over the arrow. + + You should see options that include accepting or rejecting the next suggestion(s). + +1. Press **Tab** to "Go To / Accept" the suggested edits. + + NES helps you stay in the flow of coding by predicting related changes you need to make. Suggestions might span a single symbol, an entire line, or multiple lines, depending on the scope of the change. + +### Access Smart Actions + +Smart Actions are a set of predefined actions that are available from the Copilot context menu. You can use Smart Actions to quickly perform common tasks in Visual Studio Code without having to write prompts. + +Use the following steps to complete this section of the exercise: + +1. Ensure that you have the Program.cs file open in the code editor. + +1. In the code editor, select the Main method, right-click the selected code, and then select **Explain**. + +1. Take a minute to consider the results of the smart action. + + Notice that the Explain smart action constructs a prompt that's based on the code selection and submits the prompt in the Chat view. + + The explanation includes a detailed description of the selected code, and may include suggested updates. For example, the explanation may include a suggestion to update the use of the `Random` class. + + The Explain smart action isn't designed as a code review tool. You could make updates based on its suggestions, but there is another option. The **Review** smart action. + +1. Right-click the selected code again, and then select **Review**. + + You can use the Review smart action to get suggestions for improving your code, such as suggestions for improving code quality, security, performance, and adherence to best practices. + + Even if the Explain smart action didn't catch the issue above, the Review smart action should. + +1. Review the suggested updates provided by the Review smart action. + + The Review smart action should recommend a single instance of the `Random` class that's reused across loop iterations, which is a best practice for generating random numbers in .NET. It may also suggest improved variable names that replace `position1` and `position2`, since the current names might not be descriptive in the current context. + +## Summary + +In this exercise, you examined GitHub Copilot settings and explored the GitHub Copilot user interface in Visual Studio Code. You configured Copilot settings and explored the Chat view, including Ask mode for questions and Agent mode for autonomous code edits. You used Quick Chat for lightweight interactions and reviewed how Inline Chat integrates with the editor. You also used ghost text code completions and next edit suggestions (NES) to accelerate coding, and applied Smart Actions such as Explain and Review to analyze and improve your code. + +## Clean up + +Now that you've finished the exercise, take a minute to ensure that you haven't made changes to your GitHub account or GitHub Copilot subscription that you don't want to keep. If you made any changes, revert them now. diff --git a/Instructions/Labs/LAB_AK_02_analyze_document_code.md b/Instructions/Labs/LAB_AK_02_analyze_document_code.md new file mode 100644 index 0000000..b2803a3 --- /dev/null +++ b/Instructions/Labs/LAB_AK_02_analyze_document_code.md @@ -0,0 +1,329 @@ +--- +lab: + title: Exercise - Analyze and document code using GitHub Copilot + description: Learn how to analyze new or unfamiliar code and how to generate documentation using GitHub Copilot in Visual Studio Code. + duration: 20 minutes + level: 200 + islab: true + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Analyze and document code using GitHub Copilot + +GitHub Copilot can help you understand and document a codebase by generating explanations and documentation. In this exercise, you use GitHub Copilot to analyze a codebase and generate documentation for the project. + +This exercise should take approximately **20** minutes to complete. + +> **IMPORTANT**: To complete this exercise, you must provide your own GitHub account and GitHub Copilot subscription. If you don't have a GitHub account, you can sign up for a free individual account and use a GitHub Copilot Free plan to complete the exercise. If you have access to a GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscription from within your lab environment, you can use your existing GitHub Copilot subscription to complete this exercise. + +## Before you start + +Your lab environment must include the following: Git 2.48 or later, .NET SDK 9.0 or later, Visual Studio Code with the C# Dev Kit extension, and access to a GitHub account with GitHub Copilot enabled. + +If you're using a local PC as a lab environment for this exercise: + +- For help configuring your local PC as your lab environment, open the following link in a browser: Configure your lab environment resources. + +- For help enabling your GitHub Copilot subscription in Visual Studio Code, open the following link in a browser: Enable GitHub Copilot within Visual Studio Code. + +If you're using a hosted lab environment for this exercise: + +- For help enabling your GitHub Copilot subscription in Visual Studio Code, paste the following URL into a browser's site navigation bar: Enable GitHub Copilot within Visual Studio Code. + +- To ensure that the .NET SDK is configured to use the official NuGet.org repository as a source for downloading and restoring packages: + + Open a command terminal and then run the following command: + + ```bash + + dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org + + ``` + +## Exercise scenario + +You're a developer working in the IT department of your local community. The backend systems that support the public library were lost in a fire. Your team needs to develop a temporary solution to help the library staff manage their operations until the system can be replaced. Your team chose GitHub Copilot to accelerate the development process. + +Your colleague has developed an initial version of the library application, but due to time constraints, they haven't had a chance to document the code. You need to analyze the codebase and create documentation for the project. + +This exercise includes the following tasks: + +- Set up the library application in Visual Studio Code. +- Use GitHub Copilot to explain the library application codebase. +- Use GitHub Copilot to create a README.md file for the library application. + +## Set up the library application in Visual Studio Code + +Your colleague has developed an initial version of the library application and has made it available as a .zip file. You need to download the zip file, extract the code files, and then open the solution in Visual Studio Code. + +Use the following steps to set up the library application: + +1. Open a browser window in your lab environment. + +1. To download a zip file containing the library application, paste the following URL into your browser's address bar: [GitHub Copilot lab - Analyze and document code](https://github.com/MicrosoftLearning/mslearn-github-copilot-dev/raw/refs/heads/main/DownloadableCodeProjects/Downloads/AZ2007LabAppM2.zip) + + The zip file named AZ2007LabAppM2.zip will be downloaded to your lab environment. + +1. Extract the files from the **AZ2007LabAppM2.zip** file. + + For example: + + 1. Navigate to the downloads folder in your lab environment. + + 1. Right-click **AZ2007LabAppM2.zip**, and then select **Extract all**. + + 1. Select **Show extracted files when complete**, and then select **Extract**. + +1. Open the extracted files folder, then copy the **AccelerateDevGHCopilot** folder to a location that's easy to access, such as your Windows Desktop folder. + +1. Open the **AccelerateDevGHCopilot** folder in Visual Studio Code. + + For example: + + 1. Open Visual Studio Code in your lab environment. + + 1. In Visual Studio Code, on the **File** menu, select **Open Folder**. + + 1. Navigate to the Windows Desktop folder, select **AccelerateDevGHCopilot** and then select **Select Folder**. + +1. In the Visual Studio Code SOLUTION EXPLORER view, expand the solution to show the following solution structure: + + - AccelerateDevGHCopilot\ + - src\ + - Library.ApplicationCore\ + - Library.Console\ + - Library.Infrastructure\ + - tests\ + - UnitTests\ + +1. Ensure that the solution builds successfully. + + For example, in the SOLUTION EXPLORER view, right-click **AccelerateDevGHCopilot**, and then select **Build**. + + You may see some Warnings, but there shouldn't be any Errors reported. + +## Use GitHub Copilot to explain the library application codebase + +GitHub Copilot can help you to understand an unfamiliar codebase by generating explanations at the solution, file, and code line levels. + +### Analyze code using prompts in the Chat view + +GitHub Copilot's Chat view includes a chat-based interface that allows you to interact with GitHub Copilot using natural language prompts. When evaluating an existing codebase for the first time, you can create prompts that generate an explanation at the workspace or project level, or at the code block or code line level. To assist you in specifying the context of your prompt, GitHub Copilot provides chat participants, chat variables, and slash commands. + +- Use chat participants in your prompts to invoke a domain-specific expert. Experts provide the most accurate responses. +- Use chat variables in your prompts to include specific context. Context helps GitHub Copilot generate more relevant responses. +- Use slash commands in your prompts to invoke specific actions or to set the intent of your prompt. + +Use the following steps to complete this section of the exercise: + +1. Ensure that the AccelerateDevGHCopilot solution is open in Visual Studio Code. + +1. Open GitHub Copilot's Chat view. + + To open the Chat view, select the **Toggle Chat** button at the top of the Visual Studio Code window. + + You can also open the Chat view using the **Ctrl+Alt+I** keyboard shortcut. + +1. Set the chat mode to **Ask** and select the **Auto** model. + + The Set Mode and Pick Model menus are in the bottom-left corner of the Chat view. + + > **NOTE**: You can use a different model if your plan allows it, but responses may differ from those shown in this exercise. Free-plan users have a limited number of monthly chat requests, so each prompt counts against your quota. + +1. In the Chat view, enter a prompt that uses the **#codebase** chat variable to include the full context of the codebase when generating a description of your code. + + For example, enter the following prompt in the Chat view: + + ```plaintext + #codebase describe this project + ``` + + Use chat variables, such as **#codebase**, to include specific context in your prompt. Context helps GitHub Copilot generate more relevant responses. + +1. Take a minute to compare GitHub Copilot's response with the actual project files. + + You should see a response that describes all of the projects included in the solution: + + - **Library.ApplicationCore** + - **Library.Console** + - **Library.Infrastructure** + - **UnitTests** + +1. Use the SOLUTION EXPLORER view to expand the project folders. + +1. Take a moment to review the project files. + +1. Enter a prompt in the Chat view that asks how to publish your codebase to a private GitHub repository. + + For example, enter the following prompt in the Chat view: + + ```plaintext + @github #codebase What's the easiest way to publish my current codebase to a private GitHub repo from within Visual Studio Code? + ``` + + Use chat participants, such as **@github**, to assign a domain expert for your prompt. Domain experts help GitHub Copilot generate accurate responses. + + > **NOTE**: GitHub Copilot considers your chat history and the code files you have open in Visual Studio Code when constructing a context for your prompt and generating a response. + +1. Open the **Program.cs** file and examine the code. + +1. Enter a prompt in the Chat view that generates an explanation of the Program.cs file. + + For example, enter the following prompt in the Chat view: + + ```plaintext + /explain #codebase Explain the Program.cs file + ``` + + Use slash commands, such as **/explain**, to specify the intent of your prompt. Communicating your intent helps GitHub Copilot understand the type of response that it needs to generate. The list of available slash commands may vary depending on your environment and the context of your chat. + +1. Take a minute to review the detailed response generated by GitHub Copilot. + + You should see a response that includes an overview and a breakdown that explains how the file is used within the application. + +1. Close the Program.cs file. + +### Improve chat responses by adding context + +GitHub Copilot uses context to generate relevant responses. + +Opening files in the code editor is one way to establish context, but you can also add files to the Chat context using drag-and-drop operations or by using the **Add Context** button in the Chat view. + +Use the following steps to complete this section of the exercise: + +1. Expand the **Library.Infrastructure** project, and then expand the **Data** folder. + +1. Use a drag-and-drop operation to add the following files from the SOLUTION EXPLORER view to the Chat context: **JsonData.cs**, **JsonLoanRepository.cs**, and **JsonPatronRepository.cs**. + + GitHub Copilot uses the Chat context to understand the code files that are relevant to your prompt. You can add files to the Chat context using drag-and-drop operations, or you can use the **Add Context** button in the Chat view. + + Instead of adding individual files manually, you can let Copilot find the right files from your codebase. This approach can be useful when you don't know which files are relevant to your question, but it does slow down the response time. To let Copilot find the right files automatically, add #codebase in your prompt. + +1. Enter a prompt in the Chat view that generates an explanation of the data access classes. + + For example, enter the following prompt in the Chat view: + + ```plaintext + /explain Explain how the data access classes work + ``` + +1. Take a couple minutes to read through the response. + + You should see a response that describes each of the data access classes (**JsonData**, **JsonLoanRepository**, and **JsonPatronRepository**) and how they work together to manage data access in the application. Key methods, such as **LoadData**, **SaveLoans**, and **SavePatrons**, should be mentioned in the response. + +1. Take a minute to examine the JSON data files that are used to simulate library records. + + The JSON data files are located in the **src/Library.Console/Json** folder. + + The data files use ID properties to link entities. For example, a **Loan** object has a **PatronId** property that links to a **Patron** object with the same ID. The JSON files contain data for authors, books, book items, patrons, and loans. + + > **NOTE**: Notice that Author names, book titles, and patron names have been anonymized for the purposes of this training. + +### Build and run the application + +Running the application helps you understand the user interface, key features of the application, and how app components interact. + +Use the following steps to complete this section of the exercise: + +1. Ensure that you have **SOLUTION EXPLORER** open in the Explorer view. + + The SOLUTION EXPLORER is a dedicated view added to Visual Studio Code's primary sidebar by the C# Dev Kit extension. It provides a structured, solution-centric view of your .NET application that's similar to the Solution Explorer in the Visual Studio IDE. It's more informative and organized than the plain folder/file tree that the built-in Explorer view. + +1. To run the application, right-click **Library.Console**, select **Debug**, and then select **Start New Instance**. + + If the **Debug** and **Start New Instance** options aren't displayed, ensure that you're using the Solution Explorer view and not the Explorer view. + + The following steps guide you through a simple use case. + +1. When prompted for a patron name, type **One** and then press Enter. + + You should see a list of patrons that match the search query. + + > **NOTE**: The application uses a case-sensitive search process. + +1. At the "Input Options" prompt, type **2** and then press Enter. + + Entering **2** selects the second patron in the list. + + You should see the patron's name and membership status followed by book loan details. + +1. At the "Input Options" prompt, type **1** and then press Enter. + + Entering **1** selects the first book in the list. + + You should see book details listed, including the due date and return status. + +1. At the "Input Options" prompt, type **r** and then press Enter. + + Entering **r** returns the book. + +1. Verify that the message "Book was successfully returned." is displayed. + + The message "Book was successfully returned." should be followed by the book details. Returned books are marked with **Returned: True**. + +1. To begin a new search, type **s** and then press Enter. + +1. When prompted for a patron name, type **One** and then press Enter. + +1. At the "Input Options" prompt, type **2** and then press Enter. + +1. Verify that first book loan is marked **Returned: True**. + +1. At the "Input Options" prompt, type **q** and then press Enter. + +1. Stop the debug session. + +## Create the project documentation for the README file + +Readme files provide project contributors and stakeholders with essential information about a code repository. They help users understand the purpose of the project, how to use it, and how to contribute. A well-structured README file can significantly improve the usability and maintainability of a project. + +You need a README file that includes the following sections: + +- **Project Title**: A brief, clear title for the project. +- **Description**: A detailed explanation of what the project is and what it does. +- **Project Structure**: A breakdown of the project structure, including key folders and files. +- **Key Classes and Interfaces**: A list of key classes and interfaces in the project. +- **Usage**: Instructions on how to use the project, often including code examples. +- **License**: The license that the project is under. + +In this section of the exercise, you'll use GitHub Copilot to create project documentation and add it to a **README.md** file. + +Use the following steps to complete this section of the exercise: + +1. Add a new file named **README.md** to the root folder of the **AccelerateDevGHCopilot** solution. + +1. Open the Chat view, and select the **Ask** agent mode. + +1. To generate project documentation for your README file, enter the following prompt: + + ```plaintext + + #codebase I need you to generate the contents of a README.md file that I can use for the current code repository. Use "Library App" as the project title. The README file should include the following sections: Description, Project Structure, Key Classes and Interfaces, Usage, License. Format all sections as raw markdown. Use a bullet list with indents to represent the project structure. Do not include ".gitignore" or the ".github", "bin", and "obj" folders. I want add the suggested content to the README.md file that's open in the editor. + + ``` + + > **NOTE**: Using multiple prompts, one for each section of the README file would produce more detailed results. A single prompt is used in this exercise to simplify the process. + +1. Review the response to ensure each section is formatted as markdown. + + You can update sections individually to provide more detailed information or if they aren't formatted correctly. You can also copy GitHub Copilot's response to the README file and then make corrections directly in the markdown file. + +1. Copy the suggested documentation, and then paste it into the README.md file. + + To copy the entire response, scroll to the bottom of the response, right-click in the empty space to the right of the "thumbs-up" icon, and then select **Copy** + +1. Format the README.md file as needed. + + You can update the settings for GitHub Copilot to enable markdown formatting, then use GitHub Copilot to help you update sections of the README.md file. + + When you're finished, you should have a README.md file that includes each of the specified sections, with an appropriate level of detail. + +## Summary + +In this exercise, you learned how to use GitHub Copilot to analyze and document a codebase. Using the Chat view in Ask mode, you used chat participants, chat variables, and slash commands to generate explanations for the project structure, key classes, and data access classes. You improved response relevance by adding files to the Chat context using drag-and-drop. You also ran the application to understand its behavior, and used GitHub Copilot to generate content for a README.md file. + +## Clean up + +Now that you've finished the exercise, take a minute to ensure that you haven't made changes to your GitHub account or GitHub Copilot subscription that you don't want to keep. If you made any changes, revert them now. diff --git a/Instructions/Labs/LAB_AK_02_analyze_document_code_py.md b/Instructions/Labs/LAB_AK_02_analyze_document_code_py.md new file mode 100644 index 0000000..1853997 --- /dev/null +++ b/Instructions/Labs/LAB_AK_02_analyze_document_code_py.md @@ -0,0 +1,354 @@ +--- +lab: + title: Exercise - Analyze and document code using GitHub Copilot (Python) + description: Learn how to analyze new or unfamiliar code and how to generate documentation using GitHub Copilot in Visual Studio Code. + duration: 20 minutes + level: 200 + islab: true + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Analyze and document code using GitHub Copilot + +GitHub Copilot can help you understand and document a codebase by generating explanations and documentation. In this exercise, you use GitHub Copilot to analyze a codebase and generate documentation for the project. + +This exercise should take approximately **20** minutes to complete. + +> **IMPORTANT**: To complete this exercise, you must provide your own GitHub account and GitHub Copilot subscription. If you don't have a GitHub account, you can sign up for a free individual account and use a GitHub Copilot Free plan to complete the exercise. If you have access to a GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscription from within your lab environment, you can use your existing GitHub Copilot subscription to complete this exercise. + +## Before you start + +Your lab environment must include the following: Git 2.48 or later, Python 3.10 or later, Visual Studio Code with the Python extension form Microsoft, and access to a GitHub account with GitHub Copilot enabled. + +If you're using a local PC as a lab environment for this exercise: + +- For help configuring your local PC as your lab environment, open the following link in a browser: Configure your lab environment resources. + +- For help enabling your GitHub Copilot subscription in Visual Studio Code, open the following link in a browser: Enable GitHub Copilot within Visual Studio Code. + +If you're using a hosted lab environment for this exercise: + +- For help enabling your GitHub Copilot subscription in Visual Studio Code, paste the following URL into a browser's site navigation bar: Enable GitHub Copilot within Visual Studio Code. + +- Open a command terminal and then run the following commands: + + To ensure that Visual Studio Code is configured to use the correct version of Python, verify your Python installation is version 3.10 or later: + + ```bash + python --version + ``` + + To ensure that Git is configured to use your name and email address, update the following commands with your information, and then run the commands: + + ```bash + + git config --global user.name "John Doe" + + ``` + + ```bash + + git config --global user.email johndoe@example.com + + ``` + +## Exercise scenario + +You're a developer working in the IT department of your local community. The backend systems that support the public library were lost in a fire. Your team needs to develop a temporary project to help the library staff manage their operations until the system can be replaced. Your team chose GitHub Copilot to accelerate the development process. + +Your colleague has developed an initial version of the library application, but due to time constraints, they haven't had a chance to document the code. You need to analyze the codebase and create documentation for the project. + +This exercise includes the following tasks: + +- Set up the library application in Visual Studio Code. +- Use GitHub Copilot to explain the library application codebase. +- Use GitHub Copilot to create a README.md file for the library application. + +## Set up the library application in Visual Studio Code + +Your colleague has developed an initial version of the library application and has made it available as a .zip file. You need to download the zip file, extract the code files, and then open the project in Visual Studio Code. + +Use the following steps to set up the library application: + +1. Open a browser window in your lab environment. + +1. To download a zip file containing the library application, paste the following URL into your browser's address bar: [GitHub Copilot lab - Analyze and document code](https://github.com/MicrosoftLearning/mslearn-github-copilot-dev/raw/refs/heads/main/DownloadableCodeProjects/Downloads/AZ2007LabAppM2Python.zip) + + The zip file named AZ2007LabAppM2Python.zip will be downloaded to your lab environment. + +1. Extract the files from the **AZ2007LabAppM2Python.zip** file. + + For example: + + 1. Navigate to the downloads folder in your lab environment. + + 1. Right-click **AZ2007LabAppM2Python.zip**, and then select **Extract all**. + + 1. Select **Show extracted files when complete**, and then select **Extract**. + +1. Open the extracted files folder, then copy the **AccelerateDevGHCopilot** folder to a location that's easy to access, such as your Windows Desktop folder. + +1. Open the **AccelerateDevGHCopilot** folder in Visual Studio Code. + + For example: + + 1. Open Visual Studio Code in your lab environment. + + 1. In Visual Studio Code, on the **File** menu, select **Open Folder**. + + 1. Navigate to the Windows Desktop folder, select **AccelerateDevGHCopilot** and then select **Select Folder**. + +1. In the Visual Studio Code EXPLORER view, verify the following project structure: + + - AccelerateDevGHCopilot/library + ├── application_core + ├── console + ├── infrastructure + └── tests + +1. Ensure that the application runs successfully. + + For example, open a terminal in Visual Studio Code, navigate to the **AccelerateDevGHCopilot/library** directory, and run the following command: + + ```bash + python -m unittest discover tests + ``` + + You'll see some Warnings, but there shouldn't be any Errors. + +## Use GitHub Copilot to explain the library application codebase + +GitHub Copilot can help you to understand an unfamiliar codebase by generating explanations at the project, file, and code line levels. + +### Analyze code using prompts in the Chat view + +GitHub Copilot's Chat view includes a chat-based interface that allows you to interact with GitHub Copilot using natural language prompts. When evaluating an existing codebase for the first time, you can create prompts that generate an explanation at the workspace or project level, or at the code block or code line level. To assist you in specifying the context of your prompt, GitHub Copilot provides chat participants, chat variables, and slash commands. + +- Chat participants are used to scope your prompt to a specific domain. +- Chat variables are used to include specific context in your prompt. +- Slash commands are used to avoid writing complex prompts for common scenarios. + +Use the following steps to complete this section of the exercise: + +1. Ensure that the AccelerateDevGHCopilot/library project is open in Visual Studio Code. + +1. Open GitHub Copilot's Chat view. + + To open the Chat view, select the **Toggle Chat** button at the top of the Visual Studio Code window. + + ![Screenshot showing the GitHub Copilot status menu.](./Media/m02-github-copilot-toggle-chat.png) + + You can also open the Chat view using the **Ctrl+Alt+I** keyboard shortcut. + +1. Set the chat mode to **Ask** and select the **Auto** model. + + The Set Mode and Pick Model menus are in the bottom-left corner of the Chat view. + + > **NOTE**: You can use a different model if your plan allows it, but responses may differ from those shown in this exercise. Free-plan users have a limited number of monthly chat requests, so each prompt counts against your quota. + +1. In the Chat view, enter a prompt that uses the **#codebase** chat variable to include the full context of the codebase when generating a description of your code. + + For example, enter the following prompt in the Chat view: + + ```plaintext + #codebase describe this project + ``` + + Use chat variables, such as **#codebase**, to include specific context in your prompt. Context helps GitHub Copilot generate more relevant responses. + +1. Take a minute to compare GitHub Copilot's response with the actual project files. + + You should see a response that describes each of the projects in the project: + + - **application_core** + - **infrastructure** + - **console** + - **tests** + +1. Use the EXPLORER view to expand the project folders. + +1. Locate and then open the **console_app.py** file. + + The **console_app.py** file is located in the **application _core\console** folder. + +1. Take a moment to review the code file. + +1. Enter a prompt in the Chat view that generates a description of the **console_app.py** class. + + For example, enter the following prompt in the Chat view: + + ```plaintext + @workspace #usages How is the ConsoleApp class used? + ``` + + Use chat variables, such as **#usages**, to include specific context in your prompt. To see a list of the chat variables, type **#** in the chat prompt box. + + > **NOTE**: GitHub Copilot considers your chat history and the code files you have open in Visual Studio Code when constructing a context for your prompt and generating a response. + +1. Take a minute to verify the accuracy of GitHub Copilot's response. + + You should see a response that the describes where the **ConsoleApp** class is defined and how it's used in the codebase. The **console_app.py** and **main.py** files are referenced in the response, along with line numbers + +1. Open the **main.py** file from the root of the **application _core\console** folder and examine the code. + +1. Enter a prompt in the Chat view that generates an explanation of the **main.py** file. + + For example, enter the following prompt in the Chat view: + + ```plaintext + @workspace /explain Explain the main.py file + ``` + + Use Slash commands, such as **/explain**, to avoid writing complex prompts for common scenarios. To see a list of all available slash commands, type **/** in the chat prompt box. Available slash commands may vary, depending on your environment and the context of your chat. + +1. Take a minute to review the detailed response generated by GitHub Copilot. + + You should see a response that includes an overview and a breakdown that explains how the file is used within the application. + +1. Close the **main.py** file. + +### Improve chat responses by adding context + +GitHub Copilot uses context to generate more relevant responses. + +Opening files in the code editor is one way to establish context, but you can also add files to the Chat context using drag-and-drop operations or by using the **Attach Context** button in the Chat view. + +Use the following steps to complete this section of the exercise: + +1. Expand the **Infrastructure** folder. + +1. Use a drag-and-drop operation to add the following files from the EXPLORER view to the Chat context: **json_data.py**, **json_loan_repository.py**, and **json_patron_repository.py**. + + GitHub Copilot uses the Chat Context to understand the code files that are relevant to your prompt. You can add files to the Chat context using drag-and-drop operations, or you can use the **Attach Context** button in the Chat view. + + Instead of adding individual files manually, you can let Copilot find the right files from your codebase automatically. This can be useful when you don't know which files are relevant to your question. + + To let Copilot find the right files automatically, add #codebase in your prompt or select Codebase from the list of context types. + +1. Enter a prompt in the Chat view that generates an explanation of the data access classes. + + For example, enter the following prompt in the Chat view: + + ```plaintext + @workspace /explain Explain how the data access classes work + ``` + +1. Take a couple minutes to read through the response. + + You should see a response that describes each of the data access classes (**json_data.py**, **json_loan_repository.py**, and **json_patron_repository.py**) and how they work together to manage data access in the application. Key methods, such as **load_data**, **save_loans**, and **save_patrons**, should be mentioned in the response. + +1. Take a minute to examine the JSON data files that are used to simulate library records. + + The JSON data files are located in the **infrastructure/json** folder. + + The data files use ID properties to link entities. For example, a **loan** object has a **patron_id** property that links to a **patron** object with the same ID. The JSON files contain data for authors, books, book items, patrons, and loans. + + > **NOTE**: Notice that Author names, book titles, and patron names have been anonymized for the purposes of this training. + +### Build and run the application + +Running the application helps you understand the user interface, key features of the application, and how app components interact. + +Use the following steps to complete this section of the exercise: + +1. Ensure that you have the **Explorer** view open. + +1. To run the application in Visual Studio Code using Python, open **console/main.py** in the editor, press **CTRL+Shift+D** to open the Run and Debug panel, choose **Python: Current File** or another debug configuration, and then **F5** to start debugging. + + The following steps guide you through a simple use case. + +1. When prompted for a patron name, type **One** and then press Enter. + + You should see a list of patrons that match the search query. + + > **NOTE**: The application uses a case-sensitive search process. + +1. At the "Input Options" prompt, type **2** and then press Enter. + + Entering **2** selects the second patron in the list. + + You should see the patron's name and membership status followed by book loan details. + +1. At the "Input Options" prompt, type **1** and then press Enter. + + Entering **1** selects the first book in the list. + + You should see book details listed, including the due date and return status. + +1. At the "Input Options" prompt, type **r** and then press Enter. + + Entering **r** returns the book. + +1. Verify that the message "Book was successfully returned." is displayed. + + The message "Book was successfully returned." should be followed by the book details. Returned books are marked with **Returned: True**. + +1. To begin a new search, type **s** and then press Enter. + +1. When prompted for a patron name, type **One** and then press Enter. + +1. At the "Input Options" prompt, type **2** and then press Enter. + +1. Verify that first book loan is marked **Returned: True**. + +1. At the "Input Options" prompt, type **q** and then press Enter. + +1. Stop the debug session. + +## Create the project documentation for the README file + +Readme files provide project contributors and stakeholders with essential information about a code repository. They help users understand the purpose of the project, how to use it, and how to contribute. A well-structured README file can significantly improve the usability and maintainability of a project. + +You need a README file that includes the following sections: + +- **Project Title**: A brief, clear title for the project. +- **Description**: A detailed explanation of what the project is and what it does. +- **Project Structure**: A breakdown of the project structure, including key folders and files. +- **Key Classes and Interfaces**: A list of key classes and interfaces in the project. +- **Usage**: Instructions on how to use the project, often including code examples. +- **License**: The license that the project is under. + +In this section of the exercise, you'll use GitHub Copilot to create project documentation and add it to a **README.md** file. + +Use the following steps to complete this section of the exercise: + +1. Add a new file named **README.md** to the root folder of the **AccelerateDevGHCopilot** project. + +1. Open the Chat view. + +1. To generate project documentation for your README file, enter the following prompt: + + ```plaintext + @workspace Generate the contents of a README.md file for a code repository. + Use "Library App" as the project title. The README file should include the + following sections: Description, Project Structure, Key Classes and Interfaces, + Usage, License. Format all sections as raw markdown. Use a bullet list with + indents to represent the project structure. Do not include ".gitignore", ". + pyc", or the ".github", "__pycache__" folders. + ``` + + > **NOTE**: Using multiple prompts, one for each section of the README file would produce more detailed results. A single prompt is used in this exercise to simplify the process. + +1. Review the response to ensure each section is formatted as markdown. + + You can update sections individually to provide more detailed information or if they aren't formatted correctly. You can also copy GitHub Copilot's response to the README file and then make corrections directly in the markdown file. + +1. Copy the suggested documentation, and then paste it into the README.md file. + + To copy the entire response, scroll to the bottom of the response, right-click in the empty space to the right of the "thumbs-up" icon, and then select **Copy** + +1. Format the README.md file as needed. + + You can update the settings for GitHub Copilot to enable markdown formatting, then use GitHub Copilot to help you update sections of the README.md file. + + When you're finished, you should have a README.md file that includes each of the specified sections, with an appropriate level of detail. + +## Summary + +In this exercise, you learned how to use GitHub Copilot to analyze and document a codebase. You used GitHub Copilot to generate explanations for the project structure, key classes, and data access classes. You also used GitHub Copilot to create a README.md file for the project. + +## Clean up + +Now that you've finished the exercise, take a minute to ensure that you haven't made changes to your GitHub account or GitHub Copilot subscription that you don't want to keep. If you made any changes, revert them now. diff --git a/Instructions/Labs/LAB_AK_03_develop_code_features.md b/Instructions/Labs/LAB_AK_03_develop_code_features.md new file mode 100644 index 0000000..c172da4 --- /dev/null +++ b/Instructions/Labs/LAB_AK_03_develop_code_features.md @@ -0,0 +1,944 @@ +--- +lab: + title: Exercise - Develop new code features using GitHub Copilot + description: Learn how to accelerate the development of new code features using GitHub Copilot in Visual Studio Code. + duration: 30 minutes + level: 200 + islab: true + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Develop new code features using GitHub Copilot + +GitHub Copilot's code completion and interactive chat features help developers write code faster and with fewer errors. It provides suggestions for code snippets, functions, and even entire classes based on the context of the code being written. In this exercise, you use GitHub Copilot to accelerate the development of new code features in Visual Studio Code. + +This exercise should take approximately **30** minutes to complete. + +> **IMPORTANT**: To complete this exercise, you must provide your own GitHub account and GitHub Copilot subscription. If you don't have a GitHub account, you can sign up for a free individual account and use a GitHub Copilot Free plan to complete the exercise. If you have access to a GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscription from within your lab environment, you can use your existing GitHub Copilot subscription to complete this exercise. + +## Before you start + +Your lab environment must include the following: Git 2.48 or later, .NET SDK 9.0 or later, Visual Studio Code with the C# Dev Kit extension, and access to a GitHub account with GitHub Copilot enabled. + +If you're using a local PC as a lab environment for this exercise: + +- For help configuring your local PC as your lab environment, open the following link in a browser: Configure your lab environment resources. + +- For help enabling your GitHub Copilot subscription in Visual Studio Code, open the following link in a browser: Enable GitHub Copilot within Visual Studio Code. + +If you're using a hosted lab environment for this exercise: + +- For help enabling your GitHub Copilot subscription in Visual Studio Code, paste the following URL into a browser's site navigation bar: Enable GitHub Copilot within Visual Studio Code. + +- To ensure that the .NET SDK is configured to use the official NuGet.org repository as a source for downloading and restoring packages: + + Open a command terminal and then run the following command: + + ```bash + + dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org + + ``` + +- To ensure that Git is configured to use your name and email address: + + Update the following commands with your information, and then run the commands: + + ```bash + + git config --global user.name "John Doe" + + ``` + + ```bash + + git config --global user.email johndoe@example.com + + ``` + +## Exercise scenario + +You're a developer working in the IT department of your local community. The backend systems that support the public library were lost in a fire. Your team needs to develop a temporary solution to help the library staff manage their operations until the system can be replaced. Your team chose GitHub Copilot to accelerate the development process. + +An initial version of your library application was tested by end users and several additional features are requested. Your team agreed to work on the following features: + +- Book availability: Enable a librarian to determine the availability status of a book. This feature should display a message indicating that a book is available for loan or the return due date if the book is currently on loan to another patron. + +- Book loans: Enable a librarian to loan a book to a patron (if the book is available). This feature should display the option for a patron to receive a book on loan, update Loans.json with the new loan, and display updated loan details for the patron. + +- Book reservations: Enable a librarian to reserve a book for a patron (unless the book is already reserved). This feature should implement a new book reservation process. This feature may require creating a new Reservations.json file along with the new classes and interfaces required to support the reservation process. + +Each team member will work on one of the new features and then regroup. You'll work on the feature to determine the availability status of a book. Your coworker will work on the feature to loan a book to a patron. The final feature, to reserve a book for a patron, will be developed after the other two features are completed. + +This exercise includes the following tasks: + +1. Set up the library application in Visual Studio Code. + +1. Use Visual Studio Code to create a GitHub repository for the library application. + +1. Create a "book availability" branch in the code repository. + +1. Develop a new "book availability" feature. + + - Use GitHub Copilot suggestions to help implement the code more quickly and accurately. + - Sync your code updates to the "book availability" branch of your remote repository. + +1. Merge your "book availability" updates into the main branch of the repository. + +## Set up the library application in Visual Studio Code + +You need to download the existing application, extract the code files, and then open the solution in Visual Studio Code. + +Use the following steps to set up the library application: + +1. Open a browser window in your lab environment. + +1. To download a zip file containing the library application, paste the following URL into your browser's address bar: [GitHub Copilot lab - develop code features](https://github.com/MicrosoftLearning/mslearn-github-copilot-dev/raw/refs/heads/main/DownloadableCodeProjects/Downloads/AZ2007LabAppM3.zip) + + The zip file is named **AZ2007LabAppM3.zip**. + +1. Extract the files from the **AZ2007LabAppM3.zip** file. + + For example: + + 1. Navigate to the downloads folder in your lab environment. + + 1. Right-click **AZ2007LabAppM3.zip**, and then select **Extract all**. + + 1. Select **Show extracted files when complete**, and then select **Extract**. + +1. Open the extracted files folder, then copy the **AccelerateDevGHCopilot** folder to a location that's easy to access, such as your Windows Desktop folder. + +1. Open the **AccelerateDevGHCopilot** folder in Visual Studio Code. + + For example: + + 1. Open Visual Studio Code in your lab environment. + + 1. In Visual Studio Code, on the **File** menu, select **Open Folder**. + + 1. Navigate to the Windows Desktop folder, select **AccelerateDevGHCopilot** and then select **Select Folder**. + +1. In the Visual Studio Code SOLUTION EXPLORER view, verify the following solution structure: + + - AccelerateDevGHCopilot\ + - src\ + - Library.ApplicationCore\ + - Library.Console\ + - Library.Infrastructure\ + - tests\ + - UnitTests\ + +1. Ensure that the solution builds successfully. + + For example, in the SOLUTION EXPLORER view, right-click **AccelerateDevGHCopilot**, and then select **Build**. + + You'll see some Warnings, but there shouldn't be any Errors reported. + +## Create the GitHub repository for your code + +Creating the GitHub repository for your code will enable you to share your work with others and collaborate on the project. + +> **NOTE**: You use your own GitHub account to create a private GitHub repository for the library application. + +Use the following steps to complete this section of the exercise: + +1. Open a browser window and navigate to the GitHub login page. + + The GitHub login page is: [https://github.com/login](https://github.com/login). + +1. Sign in to your GitHub account. + + Your GitHub account must include a GitHub Copilot subscription. + +1. Open your GitHub account menu, and then select **Repositories**. + +1. Switch to the Visual Studio Code window. + +1. In Visual Studio Code, open the Source Control view. + +1. Select **Publish to GitHub**. + +1. Name for the repository **AccelerateDevGHCopilot**. + + > **NOTE**: If you're not signed in to GitHub in Visual Studio Code, you'll be prompted to sign in. Once you're signed in, authorize Visual Studio Code with the requested permissions. + +1. Select **Publish to GitHub private repository**. + +1. Notice that Visual Studio Code displays status messages during the publish process. + + When the publish process is finished, you'll see a message informing you that your code was successfully published to the GitHub repository that you specified. + +1. Switch to the browser window for your GitHub account. + +1. Open the new AccelerateDevGHCopilot repository in your GitHub account. + + If you don't see your AccelerateDevGHCopilot repository, refresh the page. If you still don't see the repository, try the following steps: + + 1. Switch to Visual Studio Code. + 1. Open your notifications (a notification was generated when the new repository was published). + 1. Select **Open on GitHub** to open your repository. + +## Create a new branch in the repository + +Before you start developing the new "book availability" feature, you need to create a new branch in the repository. This enables you to work on the new feature without affecting the main branch of the repository. You can merge the new feature into the main branch when the code is ready. + +Use the following steps to complete this section of the exercise: + +1. Ensure that you have the AccelerateDevGHCopilot solution open in Visual Studio Code. + +1. Select the Source Control view and ensure that the local repository is synchronized with the remote repository (Pull or Sync). + +1. In the bottom-left corner of the window, select **main**. + +1. To create a new branch, type **book availability** and then select **+ Create new branch**. + +1. To push the new branch to the remote repository, select **Publish Branch**. + + You don't need to create a Pull Request at this point. You can merge the new feature into the main branch when the code is ready. + +## Develop a new "book availability" feature + +In this section of the exercise, you use GitHub Copilot to develop a new feature for the library application. The requested feature will enable a librarian to check whether a book is available for loan, a common scenario that isn't currently supported by your library application. + +To implement the book availability feature, you'll need to complete the following updates: + +- Add a new **SearchBooks** action to the **CommonActions** enum in CommonActions.cs. + +- Update the **WriteInputOptions** method in ConsoleApp.cs. + + - Add support for the new **CommonActions.SearchBooks** option. + - Display the option to check if a book is available for loan. + +- Update the **ReadInputOptions** method in ConsoleApp.cs. + + - Add support for the new **CommonActions.SearchBooks** option. + +- Update the **PatronDetails** method in ConsoleApp.cs. + + - Add **CommonActions.SearchBooks** to **options** before calling **ReadInputOptions**. + - Add an **else if** to handle the **SearchBooks** action. + - The **else if** block should call a new method named **SearchBooks**. + +- Create a new **SearchBooks** method in ConsoleApp.cs. + + - The **SearchBooks** method should read a user provided book title. + - Check if a book is available for loan, and display a message stating either: + + - "**book.title** is available for loan", or + - "**book.title** is on loan to another patron. The return due date is **loan.DueDate**." + +GitHub Copilot can help you implement the code updates needed to complete the new feature. + +The Chat view provides an easy way to develop new code features using GitHub Copilot. You can use the Chat view to ask questions about you codebase or development environment, to request code update suggestions, and get explanations for the code generated by GitHub Copilot. + +### Implement "book availability" updates using GitHub Copilot + +GitHub Copilot provides several options for developing new code features. When you want to stay in the editor, you can use code completion suggestions (ghost test suggestions), next edit suggestions, and the inline chat feature. When you want to interact with the AI AI-interactions, you can use agents in the Chat view. The Chat view provides three default agent modes (Ask, Plan, and Agent). Each mode supports a specific purpose: + +- Ask: The Ask mode works best for answering questions about your codebase, coding, and general technology concepts. Use Ask mode when you want to understand how something works, explore ideas, or get help with coding tasks. +- Plan: The Plan mode is optimized for creating a structured implementation plan for a coding task. Use the plan agent when you want to break down a complex feature or change into smaller, manageable steps before implementation. +- Agent: The Agent mode is optimized for complex coding tasks based on high-level requirements that might require running terminal commands and tools. The AI operates autonomously, determining the relevant context and files to edit, planning the work needed, and iterating to resolve problems as they arise. + +In this task, you use inline chat and agent modes to implement the "book availability" feature. + +Use the following steps to complete this section of the exercise: + +1. Open the SOLUTION EXPLORER view. + +1. Expand the **Library.Console** project. + +1. Open the CommonActions.cs file, and then select the **CommonActions** enum. + + You need to add a new **SearchBooks** action to **CommonActions**. + +1. Set the chat mode to **Ask** and select the **Auto** model. + + The Set Mode and Pick Model menus are in the bottom-left corner of the Chat view. + + > **NOTE**: You can use a different model if your plan allows it, but responses may differ from those shown in this exercise. Free-plan users have a limited number of monthly chat requests, so each prompt counts against your quota. + +1. In the Chat view, enter the following prompt: + + ```plaintext + How should I update the selected code to include a new `SearchBooks` action? + ``` + + GitHub Copilot should suggest a code update that adds the new **SearchBooks** action to the **CommonActions** enum. + +1. Review the suggested update. + + Your updated code should look similar to the following code snippet: + + ```csharp + + public enum CommonActions + { + Repeat = 0, + Select = 1, + Quit = 2, + SearchPatrons = 4, + RenewPatronMembership = 8, + ReturnLoanedBook = 16, + ExtendLoanedBook = 32, + SearchBooks = 64 + } + + ``` + +1. In the Chat view, hover the mouse pointer over the suggested code, and then select **Apply in Editor**. + + > **NOTE**: If prompted, select **Active editor src\\Library.Console\\CommonActions.cs**. + + The suggested code should be visible in the code editor, with options to **Keep** or **Undo**. + +1. To accept the suggested edits, select **Keep**. + + The **Ask** mode enables you to explore potential updates outside the code editor, but you can still apply the suggestions. It's a good option for exploring different approaches for implementing a new feature. Working outside the editor keeps your codebase clean until you're ready to implement the changes. + +1. Open the ConsoleApp.cs file. + +1. Find and then select the **WriteInputOptions** method. + + You need to add support for the new **CommonActions.SearchBooks** option. If the **SearchBooks** option is flagged, display the option to check if a book is available for loan. + +1. Open the inline chat (**Ctrl+I**) and then enter the following prompt: + + ```plaintext + Update selection to include an option for the `CommonActions.SearchBooks` action. Use the letter "b" and the message "to check for book availability". + ``` + + GitHub Copilot should suggest a code update that adds a new **if** block for the **SearchBooks** action. + +1. Review the suggested update and then select **Keep**. + + The updated method should be similar to the following code snippet: + + ```csharp + + static void WriteInputOptions(CommonActions options) + { + Console.WriteLine("Input Options:"); + if (options.HasFlag(CommonActions.ReturnLoanedBook)) + { + Console.WriteLine(" - \"r\" to mark as returned"); + } + if (options.HasFlag(CommonActions.ExtendLoanedBook)) + { + Console.WriteLine(" - \"e\" to extend the book loan"); + } + if (options.HasFlag(CommonActions.RenewPatronMembership)) + { + Console.WriteLine(" - \"m\" to extend patron's membership"); + } + if (options.HasFlag(CommonActions.SearchPatrons)) + { + Console.WriteLine(" - \"s\" for new search"); + } + if (options.HasFlag(CommonActions.SearchBooks)) + { + Console.WriteLine(" - \"b\" to check for book availability"); + } + if (options.HasFlag(CommonActions.Quit)) + { + Console.WriteLine(" - \"q\" to quit"); + } + if (options.HasFlag(CommonActions.Select)) + { + Console.WriteLine("Or type a number to select a list item."); + } + } + + ``` + +1. Scroll up slightly to find and then select the **ReadInputOptions** method. + + Once again, you need to add support for the new **CommonActions.SearchBooks** option. Include a case that handles the user selecting the **SearchBooks** action. + +1. Open the inline chat and then enter the following prompt: + + ```plaintext + Update selection to include an option for the `CommonActions.SearchBooks` action. + ``` + + GitHub Copilot should suggest an update that adds a new **case** that handles the user selecting the **SearchBooks** action. + +1. Review the suggested update and then select **Keep**. + + The suggested update should be similar to the following code snippet: + + ```csharp + + static CommonActions ReadInputOptions(CommonActions options, out int optionNumber) + { + CommonActions action; + optionNumber = 0; + do + { + Console.WriteLine(); + WriteInputOptions(options); + string? userInput = Console.ReadLine(); + + action = userInput switch + { + "q" when options.HasFlag(CommonActions.Quit) => CommonActions.Quit, + "s" when options.HasFlag(CommonActions.SearchPatrons) => CommonActions.SearchPatrons, + "b" when options.HasFlag(CommonActions.SearchBooks) => CommonActions.SearchBooks, + "m" when options.HasFlag(CommonActions.RenewPatronMembership) => CommonActions.RenewPatronMembership, + "e" when options.HasFlag(CommonActions.ExtendLoanedBook) => CommonActions.ExtendLoanedBook, + "r" when options.HasFlag(CommonActions.ReturnLoanedBook) => CommonActions.ReturnLoanedBook, + _ when int.TryParse(userInput, out optionNumber) => CommonActions.Select, + _ => CommonActions.Repeat + }; + + if (action == CommonActions.Repeat) + { + Console.WriteLine("Invalid input. Please try again."); + } + } while (action == CommonActions.Repeat); + return action; + } + + ``` + +1. Scroll down to find and then select the **PatronDetails** method. + + There are two things that you need to accomplish: + + - You need to add **CommonActions.SearchBooks** to **options** before calling **ReadInputOptions**. + - You also need to add an **else if** to handle the **SearchBooks** action. The **else if** block should call a new method named **SearchBooks**. + + You can address both requirements with the same prompt. + +1. In the Chat view, switch to the **Agent* mode. + +1. In the Chat view, enter the following prompt: + + ```plaintext + Update the selected PatronDetail method to add `CommonActions.SearchBooks` to `options` before calling `ReadInputOptions`. Add an `else if` block to handle the `SearchBooks` action. The `else if` block should call a new method named `SearchBooks`. + ``` + + GitHub Copilot should suggest a code update that adds **CommonActions.SearchBooks** to **options** before calling **ReadInputOptions**. + +1. Review the suggested updates. + + The updated PatronDetails method should be similar to the following code snippet: + + ```csharp + + async Task PatronDetails() + { + Console.WriteLine($"Name: {selectedPatronDetails.Name}"); + Console.WriteLine($"Membership Expiration: {selectedPatronDetails.MembershipEnd}"); + Console.WriteLine(); + Console.WriteLine("Book Loans:"); + int loanNumber = 1; + foreach (Loan loan in selectedPatronDetails.Loans) + { + Console.WriteLine($"{loanNumber}) {loan.BookItem!.Book!.Title} - Due: {loan.DueDate} - Returned: {(loan.ReturnDate != null).ToString()}"); + loanNumber++; + } + + CommonActions options = CommonActions.SearchPatrons | CommonActions.Quit | CommonActions.Select | CommonActions.RenewPatronMembership | CommonActions.SearchBooks; + CommonActions action = ReadInputOptions(options, out int selectedLoanNumber); + + if (action == CommonActions.Select) + { + if (selectedLoanNumber >= 1 && selectedLoanNumber <= selectedPatronDetails.Loans.Count()) + { + var selectedLoan = selectedPatronDetails.Loans.ElementAt(selectedLoanNumber - 1); + selectedLoanDetails = selectedPatronDetails.Loans.Where(l => l.Id == selectedLoan.Id).Single(); + return ConsoleState.LoanDetails; + } + else + { + Console.WriteLine("Invalid book loan number. Please try again."); + return ConsoleState.PatronDetails; + } + } + else if (action == CommonActions.Quit) + { + return ConsoleState.Quit; + } + else if (action == CommonActions.SearchPatrons) + { + return ConsoleState.PatronSearch; + } + else if (action == CommonActions.RenewPatronMembership) + { + var status = await _patronService.RenewMembership(selectedPatronDetails.Id); + Console.WriteLine(EnumHelper.GetDescription(status)); + // reloading after renewing membership + selectedPatronDetails = (await _patronRepository.GetPatron(selectedPatronDetails.Id))!; + return ConsoleState.PatronDetails; + } + else if (action == CommonActions.SearchBooks) + { + return await SearchBooks(); + } + + throw new InvalidOperationException("An input option is not handled."); + } + + ``` + + > **NOTE**: The suggested code updates may include stub code for the **SearchBooks** method. You can accept that code. You'll implement the **SearchBooks** method in the next section. + +1. In the Chat view, to accept all the suggested updates, select **Keep**. + + Selecting **Keep** in the Chat view applies all the suggested updates to the code editor. You can also select **Keep** for each individual update in the code editor if you want to review and apply them one at a time. + +### Implement a SearchBooks method using the Chat view + +There's one step remaining to implement the "book availability" updates, create the **SearchBooks** method. The **SearchBooks** method will read a user provided book title, check if a book is available for loan, and display a message indicating the book's availability status. You'll use the Chat view to evaluate the requirements and implement the **SearchBooks** method. + +GitHub Copilot's Chat view provides a conversational and interactive environment that isn't available when using inline chat. You can use the Chat view to ask questions, request code suggestions, and get explanations for the code generated by GitHub Copilot. The Chat view supports the following three modes: + +You'll be using the Agent mode to implement the **SearchBooks** method. + +Use the following steps to complete this section of the exercise: + +1. Take a minute to consider the process requirements for the **SearchBooks** method. + + What's the process that the method needs to complete? What's the return type for this method? Does it require parameters? + + The **SearchBooks** method should implement the following process: + + 1. Prompt the user for a book title. + 1. Read the user provided book title. + 1. Check if a book is available for loan. + 1. Display a message stating one of the following options: + + - "**{book.title}** is available for loan" + - "**{book.title}** is on loan to another patron. The return due date is **{loan.DueDate}**." + + To build the message options, your code will need to access the following JSON files: + + - **Books.json** is needed to find the matching **Title** and **BookId**. + - **Loans.json** is needed to find the **ReturnDate** and **DueDate** for the matching **BookItemId**. The **BookItemId** is the same as the **BookId** in **Books.json**. + +1. Ensure that you have the following **SearchBooks** method created in the ConsoleApp.cs file: + + ```csharp + + async Task SearchBooks() + { + + return ConsoleState.PatronDetails; + } + + ``` + + > **NOTE**: Be sure to remove any code comments created by GitHub Copilot. Unnecessary and inaccurate comments can negatively influence GitHub Copilot's suggestions. + +1. Select the **SearchBooks** method. + +1. In the Chat view, enter the following prompt: + + ```plaintext + I need to update the selected SearchBooks method to obtain a book title. The code should prompt the user to "Enter a book title to search for", and then read the user input and ensure the book title isn't null. + ``` + +1. Review the suggested update. + + The suggested update should be similar to the following code snippet: + + ```csharp + + async Task SearchBooks() + { + string? bookTitle = null; + while (string.IsNullOrWhiteSpace(bookTitle)) + { + Console.Write("Enter a book title to search for: "); + bookTitle = Console.ReadLine(); + } + + // Placeholder for further logic to search for the book + Console.WriteLine($"Searching for the book titled \"{bookTitle}\"..."); + + return ConsoleState.PatronDetails; + } + + ``` + +1. In the code editor, to accept the suggested code, select **Keep**. + +1. In the Chat view, use the **Set Agent** dropdown to select **Ask** mode. + +1. Select the **SearchBooks** method. + +1. Use drag-and-drop operations to add the following files to the Chat context: + + (from the Library.Console project) + - ConsoleApp.cs + - Program.cs + - Books.json + - Loans.json + - BookItems.json + + (from the Library.Infrastructure project) + - JsonData.cs + - JsonLoanRepository.cs + +1. Enter the following prompt: + + ```plaintext + Explain how to update the `SearchBooks` method and ConsoleApps class to find a matching book title in the `Books.json` file. Use the user supplied book title to find a matching book. If a book is found, use Loans.json to check if the book is available for loan. If the book has been returned, display a message stating "`book.title` is available for loan". If the book is on loan, display a message stating "`book.title` is on loan to another patron. The return due date is `loan.DueDate`". + ``` + +1. Take a minute to review the suggested updates. + + Notice the following: + + - The response includes a detailed explanation of the suggested updates. + - The ConsoleApp class requires a JsonData object to access Books.json and Loans.json. The suggested code uses dependency injection to pass the JsonData object to the ConsoleApp class constructor. + - The SearchBooks method must be updated with logic that determines the availability of the book title and displays an appropriate message. The suggested code uses LINQ queries to check the loan status. + - The Program.cs file must be updated to pass the JsonData object to the ConsoleApp class. + + The response should include the following information: + + ```plaintext + + 1. Dependency Injection: + + Added JsonData as a dependency to the ConsoleApp constructor to access the JSON data files. + + 2. Book Search: + + Used FirstOrDefault to find a book in Books.json that matches the user-supplied title (case-insensitive). + + 3. Book Item Lookup: + + Retrieved the corresponding BookItem using the BookId from BookItems.json. + + 4. Loan Check: + + Checked Loans.json for an active loan (i.e., a loan with a null ReturnDate) for the BookItem. + + 5. Output Messages: + + Displayed whether the book is available for loan or currently on loan with the due date. + + Integration in Program.cs + + Ensure that JsonData is registered in the dependency injection container in Program.cs: + + ``` + + You can use the Chat view's **Ask** mode to analyze code updates, then use the **Agent** mode to implement the code updates. + +1. Use GitHub Copilot's response to construct a new prompt. + + For example, you can use the explanation section to create the following prompt: + + ```plaintext + + Add JsonData as a dependency to the ConsoleApp constructor to access the JSON data files. Use FirstOrDefault to find a book in Books.json that matches the user-supplied title (case-insensitive). Retrieve the corresponding BookItem using the BookId from BookItems.json. Check Loans.json for an active loan (loan.ReturnDate == null) for the BookItem. Display whether the book is available for loan or currently on loan with the due date. Ensure that JsonData is registered in the dependency injection container in Program.cs. + + ``` + + You can adjust the prompt to achieve specific requirements. For example, if you want a specific message displayed to the end user, you could add your requirement to the prompt: + + ```plaintext + + Add JsonData as a dependency to the ConsoleApp constructor to access the JSON data files. Use FirstOrDefault to find a book in Books.json that matches the user-supplied title (case-insensitive). Retrieve the corresponding BookItem using the BookId from BookItems.json. Check Loans.json for an active loan (loan.ReturnDate == null) for the BookItem. Display whether the book is available for loan or currently on loan with the due date. Ensure that JsonData is registered in the dependency injection container in Program.cs. If the book has been returned, display a message stating "`book.title` is available for loan". If the book is on loan, display a message stating "`book.title` is on loan to another patron. The return due date is `loan.DueDate`". + + ``` + +1. In the Chat view, use the **Set Agent** dropdown to select **Agent** mode. + +1. Use drag-and-drop operations to add the following files to the Chat context: + + (from the Library.Console project) + - ConsoleApp.cs + - Program.cs + - Books.json + - Loans.json + - BookItems.json + + (from the Library.Infrastructure project) + - JsonData.cs + - JsonLoanRepository.cs + +1. Select the **SearchBooks** method. + +1. Enter the following prompt: + + ```plaintext + + Add JsonData as a dependency to the ConsoleApp constructor to access the JSON data files. Use FirstOrDefault to find a book in Books.json that matches the user-supplied title (case-insensitive). Retrieve the corresponding BookItem using the BookId from BookItems.json. Check Loans.json for an active loan (loan.ReturnDate == null) for the BookItem. Display whether the book is available for loan or currently on loan with the due date. Ensure that JsonData is registered in the dependency injection container in Program.cs. If the book has been returned, display a message stating "`book.title` is available for loan". If the book is on loan, display a message stating "`book.title` is on loan to another patron. The return due date is `loan.DueDate`". + + ``` + +1. Take a minute to review the suggested updates in the ConsoleApp.cs file. + + You can use the **Go to Previous Chat Edit** and **Go to Next Chat Edit** buttons to navigate through the suggested code updates, or you can scroll manually through the file. + + **ConsoleApp.cs** + + Code updates that add the **JsonData** dependency to the **ConsoleApp** constructor can be found near the top of the ConsoleApp class. + + ```csharp + + JsonData _jsonData; + + public ConsoleApp(ILoanService loanService, IPatronService patronService, IPatronRepository patronRepository, ILoanRepository loanRepository, JsonData jsonData) + { + _patronRepository = patronRepository; + _loanRepository = loanRepository; + _loanService = loanService; + _patronService = patronService; + _jsonData = jsonData; + } + + ``` + + Code updates that check if a book is available for loan can be found in the **SearchBooks** method. + + ```csharp + + async Task SearchBooks() + { + string? bookTitle = null; + while (string.IsNullOrWhiteSpace(bookTitle)) + { + Console.Write("Enter a book title to search for: "); + bookTitle = Console.ReadLine(); + } + + await _jsonData.EnsureDataLoaded(); + + var book = _jsonData.Books!.FirstOrDefault(b => string.Equals(b.Title, bookTitle, StringComparison.OrdinalIgnoreCase)); + if (book == null) + { + Console.WriteLine($"No book found with the title \"{bookTitle}\"."); + return ConsoleState.PatronDetails; + } + + var bookItem = _jsonData.BookItems!.FirstOrDefault(bi => bi.BookId == book.Id); + if (bookItem == null) + { + Console.WriteLine($"No book item found for the title \"{book.Title}\"."); + return ConsoleState.PatronDetails; + } + + var loan = _jsonData.Loans!.FirstOrDefault(l => l.BookItemId == bookItem.Id && l.ReturnDate == null); + if (loan == null) + { + Console.WriteLine($"\"{book.Title}\" is available for loan."); + } + else + { + Console.WriteLine($"\"{book.Title}\" is on loan to another patron. The return due date is {loan.DueDate}."); + } + + return ConsoleState.PatronDetails; + } + + ``` + + **Program.cs** + + Code to register JsonData and ConsoleApp for dependency injection was already available in the Program.cs file. + + ```csharp + + services.AddSingleton(); + services.AddSingleton(); + + ``` + +1. In the Chat view, to keep all edits, select **Keep**. + + Always review GitHub Copilot suggestions before accepting updates. + + If you're unsure about the suggested updates, you can accept changes and then ask GitHub Copilot for an explanation. You can revert the edits if you decide against the updates. + + > **NOTE**: If GitHub Copilot suggests formatting the return date using a culture-specific format, ensure that a `using System.Globalization;` statement is added to top of the ConsoleApp.cs file. + +1. Ensure that you've accepted updates in both the ConsoleApp.cs and Program.cs files. + +1. Scroll to the top of the ConsoleApp.cs file and locate the following code line: + + ```csharp + + JsonData _jsonData; + + ``` + +1. If JsonData isn't recognized in the ConsoleApp class, add the following using statement above the ConsoleApp class. + + ```csharp + + using Library.Infrastructure.Data; + + ``` + +1. Open Visual Studio Code's SOLUTION EXPLORER view. + +1. Build the solution and ensure that no errors were introduced by your code updates. + + You'll see Warning messages, but there shouldn't be any errors. + + To build the solution using the SOLUTION EXPLORER view, right-click **AccelerateDevGHCopilot**, and then select **Build**. + +## Merge your "book availability" updates into the main branch of the repository + +It's important to test your code before merging it into the main branch of the repository. Testing ensures that your code works as expected and doesn't introduce any new issues. In this exercise, you'll use manual testing to verify that the "book availability" feature works as expected. + +In this section of the exercise, you complete the following tasks: + +1. Test the "book availability" feature. +1. Sync your changes with the remote repository. +1. Create a pull request to merge your changes into the main branch of the repository. + +### Test the "book availability" feature + +Manual testing can be used to verify that the new feature works as expected. Using a data source that can be verified is important. In this case, you use the **Books.json** and **Loans.json** files to verify that the new feature reports the availability status of a book correctly. + +Use the following steps to complete this section of the exercise: + +1. To run the application, right-click **Library.Console**, select **Debug**, and then select **Start New Instance**. + +1. When prompted for a patron name, type **One** and then press Enter. + + You should see a list of patrons that match the search query. + +1. At the "Input Options" prompt, type **2** and then press Enter. + + Entering **2** selects the second patron in the list. + + You should see the patron's name and membership status followed by book loan details. + +1. At the "Input Options" prompt, type **b** and then press Enter. + + Entering **b** selects the option to search for a book's availability status. + + You should see a prompt to enter a book title. + +1. Type **Book One** and then press Enter. + + In the original data that you downloaded, **Book One** is currently on loan to **Patron Forty-Nine**, so it shouldn't be available. + +1. Verify that the application displays a message indicating that the book is on loan to another patron. + +1. At the "Input Options" prompt, type **b** and then press Enter. + +1. Type **Book Nineteen** and then press Enter. + +1. Verify that the application displays a message indicating that the book is available for loan. + +1. At the "Input Options" prompt, type **q** and then press Enter. + +1. Stop the debug session. + +1. Use the EXPLORER view to locate and then open the **Loans.json** file. + + The Loans.json file is used to track the loan status of each book. You can use the Loans.json file to verify that the availability status for Book One and Book Nineteen is correct. + + The updated Loans.json file should be located in either the **Library.Console\bin\Debug\net8.0\Json** folder or **Library.Console\Json** folder. + + - If you're using the Visual Studio Code debugger to run the app, the updated Loans.json file should be located in the **Library.Console\bin\Debug\net8.0\Json** folder. + + - If you're using a **dotnet run** command from the **AccelerateDevGHCopilot\src\Library.Console** folder to run the app, the updated Loans.json file should be located in the **Library.Console\Json** folder. + +1. Verify that loan ID 37 and loan ID 46 are both for Book One (**"BookItemId": 1**). + + The loan IDs are listed sequentially in the Loans.json file. + + - Loan ID 37 should have a **ReturnDate** value of **2024-01-17**, indicating that the book was returned on that date. + - Loan ID 46 should have a **ReturnDate** value **null**, indicating that the book is currently on loan (loaned on **2024-07-09** but not returned). + + The **ReturnDate** value is used to determine whether the book is currently on loan. If the **ReturnDate** value is **null**, the book is currently on loan. + +1. Verify that loan ID 34 is for Book Nineteen (**"BookItemId": 19**) and that the **ReturnDate** has been assigned a value. + +### Sync your changes with the remote repository + +1. Select the Source Control view. + +1. Ensure that the files you updated are listed under **Changes**. + + You should see the CommonActions.cs and ConsoleApp.cs files listed under **Changes**. The Program.cs file may also be listed. + +1. Use GitHub Copilot to generate a message for the **Commit**. + + ![Screenshot showing the Generate Commit Message with Copilot button.](./Media/m03-github-copilot-commit-message.png) + +1. To stage and commit your changes, select **Commit** and then select **Yes**. + +1. To synchronize changes to the remote repository, select **Sync Changes**. + +### Create a pull request to merge your changes into the main branch + +You've implemented the feature that enables a librarian to determine the availability status of a book. Now you need to merge your changes into the main branch of the repository. You can create a pull request to merge your changes into the main branch. + +Use the following steps to complete this section of the exercise: + +1. Open your GitHub repository in a web browser. + + To open your GitHub repository from Visual Studio Code: + + 1. In the bottom-left corner of of the Visual Studio Code window, select **book-availability**. + 1. On the context menu, to the right of the **book-availability** branch, select the **Open in GitHub** icon. + +1. On your GitHub repository page, select the **Compare & pull request** button. + +1. Ensure that **Base** specifies **main**, **compare** specifies **book-availability**, and **Able to merge** is checked. + +1. Under **Add a description**, select the Copilot Actions button (the GitHub Copilot icon), and then select the option to generate a summary. + + > **NOTE**: The GitHub Copilot Free plan doesn't support the pull request summary feature at this time. + + If you're using the GitHub Copilot Free plan, you can write your own summary, or use the summary below to complete the pull request. + + ```plaintext + + This pull request introduces a new feature to the library console application: the ability to search for books and check their availability. It also includes updates to dependency injection and the CommonActions enumeration to support this functionality. Below are the most important changes grouped by theme. + + New Feature: Book Search + + Added a new SearchBooks action to the CommonActions enumeration (src/Library.Console/CommonActions.cs). + + Updated PatronDetails method to handle the SearchBooks action, including a new SearchBooks method that allows users to search for a book by title and check its availability (src/Library.Console/ConsoleApp.cs). + + Modified ReadInputOptions and WriteInputOptions methods to include the new SearchBooks option (src/Library.Console/ConsoleApp.cs). + + Dependency Injection Updates + + Added JsonData as a dependency in the ConsoleApp constructor and ensured it is registered in the DI container before ConsoleApp (src/Library.Console/ConsoleApp.cs, src/Library.Console/Program.cs). + + ``` + +1. Once the summary is generated, select **Preview**. + +1. Take a minute to review the summary. + + The pull request summary generated by GitHub Copilot should be similar to the following example: + + ![Screenshot showing a pull request summary generated using a GitHub Copilot Enterprise account.](./Media/m03-github-copilot-pull-request-summary.png) + +1. Select **Create pull request**. + +1. If all checks pass and there are no conflicts with the base branch, select **Merge pull request**, and then select **Confirm merge**. + + Notice that you can delete the **book-availability** branch after merging the changes. To delete the branch, select **Delete branch**. + +1. Switch back to the Visual Studio Code window. + +1. Switch to the **main** branch of the repository. + +1. Open the Source Control view, and then **Pull** the changes from the remote repository. + +1. Verify that the book-availability feature is available in the **main** branch. + +## Summary + +In this exercise, you learned how to use GitHub Copilot to develop a new code feature for a C# application. You published the project to a private GitHub repository from within Visual Studio Code, created a feature branch, and used inline chat, Ask mode, and Agent mode to implement the "book availability" feature. You tested your changes, then used GitHub Copilot to generate a commit message and a pull request summary before merging the feature branch into main. + +## Clean up + +Now that you've finished the exercise, take a minute to ensure that you haven't made changes to your GitHub account or GitHub Copilot subscription that you don't want to keep. If you made any changes, revert them now. diff --git a/Instructions/Labs/LAB_AK_03_develop_code_features_py.md b/Instructions/Labs/LAB_AK_03_develop_code_features_py.md new file mode 100644 index 0000000..5b0be09 --- /dev/null +++ b/Instructions/Labs/LAB_AK_03_develop_code_features_py.md @@ -0,0 +1,1075 @@ +--- +lab: + title: Exercise - Develop new code features using GitHub Copilot (Python) + description: Learn how to accelerate the development of new code features using GitHub Copilot in Visual Studio Code. + duration: 30 minutes + level: 200 + islab: true + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Develop new code features using GitHub Copilot + +GitHub Copilot's code completion and interactive chat features help developers write code faster and with fewer errors. It provides suggestions for code snippets, functions, and even entire classes based on the context of the code being written. In this exercise, you use GitHub Copilot to accelerate the development of new code features in Visual Studio Code. + +This exercise should take approximately **30** minutes to complete. + +> **IMPORTANT**: To complete this exercise, you must provide your own GitHub account and GitHub Copilot subscription. If you don't have a GitHub account, you can sign up for a free individual account and use a GitHub Copilot Free plan to complete the exercise. If you have access to a GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscription from within your lab environment, you can use your existing GitHub Copilot subscription to complete this exercise. + +## Before you start + +Your lab environment must include the following: Git 2.48 or later, Python 3.10 or later, Visual Studio Code with the Python extension form Microsoft, and access to a GitHub account with GitHub Copilot enabled. + +If you're using a local PC as a lab environment for this exercise: + +- For help configuring your local PC as your lab environment, open the following link in a browser: Configure your lab environment resources. + +- For help enabling your GitHub Copilot subscription in Visual Studio Code, open the following link in a browser: Enable GitHub Copilot within Visual Studio Code. + +If you're using a hosted lab environment for this exercise: + +- For help enabling your GitHub Copilot subscription in Visual Studio Code, paste the following URL into a browser's site navigation bar: Enable GitHub Copilot within Visual Studio Code. + +- Open a command terminal and then run the following commands: + + To ensure that Visual Studio Code is configured to use the correct version of Python, verify your Python installation is version 3.10 or later: + + ```bash + python --version + ``` + + To ensure that Git is configured to use your name and email address, update the following commands with your information, and then run the commands: + + ```bash + + git config --global user.name "John Doe" + + ``` + + ```bash + + git config --global user.email johndoe@example.com + + ``` + +## Exercise scenario + +You're a developer working in the IT department of your local community. The backend systems that support the public library were lost in a fire. Your team needs to develop a temporary project to help the library staff manage their operations until the system can be replaced. Your team chose GitHub Copilot to accelerate the development process. + +An initial version of your library application was tested by end users and several additional features are requested. Your team agreed to work on the following features: + +- Book availability: Enable a librarian to determine the availability status of a book. This feature should display a message indicating that a book is available for loan or the return due date if the book is currently on loan to another patron. + +- Book loans: Enable a librarian to loan a book to a patron (if the book is available). This feature should display the option for a patron to receive a book on loan, update Loans.json with the new loan, and display updated loan details for the patron. + +- Book reservations: Enable a librarian to reserve a book for a patron (unless the book is already reserved). This feature should implement a new book reservation process. This feature may require creating a new Reservations.json file along with the new classes and interfaces required to support the reservation process. + +Each team member will work on one of the new features and then regroup. You'll work on the feature to determine the availability status of a book. Your coworker will work on the feature to loan a book to a patron. The final feature, to reserve a book for a patron, will be developed after the other two features are completed. + +This exercise includes the following tasks: + +1. Set up the Library application in Visual Studio Code. + +1. Use Visual Studio Code to create a GitHub repository for the Library application. + +1. Create a "book availability" branch in the code repository. + +1. Develop a new "book availability" feature. + + - Use GitHub Copilot suggestions to help implement the code more quickly and accurately. + - Sync your code updates to the "book availability" branch of your remote repository. + +1. Merge your "book availability" updates into the main branch of the repository. + +## Set up the Library application in Visual Studio Code + +You need to download the existing application, extract the code files, and then open the project in Visual Studio Code. + +Use the following steps to set up the Library application: + +1. Open a browser window in your lab environment. + +1. To download a zip file containing the Library application, paste the following URL into your browser's address bar: [GitHub Copilot lab - develop code features](https://github.com/MicrosoftLearning/mslearn-github-copilot-dev/raw/refs/heads/main/DownloadableCodeProjects/Downloads/AZ2007LabAppM3Python.zip) + + The zip file is named **AZ2007LabAppM3Python.zip**. + +1. Extract the files from the **AZ2007LabAppM3Python.zip** file. + + For example: + + 1. Navigate to the downloads folder in your lab environment. + + 1. Right-click **AZ2007LabAppM3Python.zip**, and then select **Extract all**. + + 1. Select **Show extracted files when complete**, and then select **Extract**. + +1. Open the extracted files folder, then copy the **AccelerateDevGHCopilot** folder to a location that's easy to access, such as your Windows Desktop folder. + +1. Open the **AccelerateDevGHCopilot** folder in Visual Studio Code. + + For example: + + 1. Open Visual Studio Code in your lab environment. + + 1. In Visual Studio Code, on the **File** menu, select **Open Folder**. + + 1. Navigate to the Windows Desktop folder, select **AccelerateDevGHCopilot** and then select **Select Folder**. + +1. In the Visual Studio Code EXPLORER view, verify the following project structure: + + - AccelerateDevGHCopilot/library + ├── application_core + ├── console + ├── infrastructure + └── tests + └── readme.md + +1. Ensure that the application runs successfully. + + For example, open a terminal in Visual Studio Code, navigate to the **AccelerateDevGHCopilot/library** directory, and run the following command: + + ```bash + python -m unittest discover -v tests + ``` + + You'll see some Warnings, but there shouldn't be any Errors. + +## Create the GitHub repository for your code + +Creating the GitHub repository for your code will enable you to share your work with others and collaborate on the project. + +> **NOTE**: You use your own GitHub account to create a private GitHub repository for the library application. + +Use the following steps to complete this section of the exercise: + +1. Open a browser window and navigate to your GitHub account. + + The GitHub login page is: [https://github.com/login](https://github.com/login). + +1. Sign in to your GitHub account. + +1. Open your GitHub account menu, and then select **Your repositories**. + +1. Switch to the Visual Studio Code window. + +1. In Visual Studio Code, open the Source Control view. + +1. Select **Publish to GitHub**. + +1. Name for the repository **AccelerateDevGHCopilot**. + + > **NOTE**: If you're not signed in to GitHub in Visual Studio Code, you'll be prompted to sign in. Once you're signed in, authorize Visual Studio Code with the requested permissions. + +1. Select **Publish to GitHub private repository**. + +1. Notice that Visual Studio Code displays status messages during the publish process. + + When the publish process is finished, you'll see a message informing you that your code was successfully published to the GitHub repository that you specified. + +1. Switch to the browser window for your GitHub account. + +1. Open the new AccelerateDevGHCopilot repository in your GitHub account. + + If you don't see your AccelerateDevGHCopilot repository, refresh the page. If you still don't see the repository, try the following steps: + + 1. Switch to Visual Studio Code. + 1. Open your notifications (a notification was generated when the new repository was published). + 1. Select **Open on GitHub** to open your repository. + +## Create a new branch in the repository + +Before you start developing the new "book availability" feature, you need to create a new branch in the repository. This enables you to work on the new feature without affecting the main branch of the repository. You can merge the new feature into the main branch when the code is ready. + +Use the following steps to complete this section of the exercise: + +1. Ensure that you have the AccelerateDevGHCopilot project open in Visual Studio Code. + +1. Select the Source Control view and ensure that the local repository is synchronized with the remote repository (Pull or Sync). + +1. In the bottom-left corner of the window, select **main**. + +1. To create a new branch, type **book availability** and then select **+ Create new branch**. + +1. To push the new branch to the remote repository, select **Publish Branch**. + +## Use GitHub Copilot to Develop a new "book availability" feature + +In this section of the exercise, you use GitHub Copilot to develop a new feature for the Library application. The requested feature will enable a librarian to check whether a book is available for loan, a common scenario that isn't currently supported by your current Library application. + +To implement the book availability feature, you'll need to complete the following updates: + +- Add a new **SEARCH_BOOKS** action to the **CommonActions** enum in **library/console/common_actions.py**. + +- Update the **WriteInputOptions** method in **library/console/console_app.py**. + + - Add support for the new **CommonActions.SEARCH_BOOKS** option. + - Display the option to check if a book is available for loan. + +- Update the **ReadInputOptions** method in **library/console/console_app.py**. + + - Add support for the new **CommonActions.SEARCH_BOOKS** option. + +- Update the **PatronDetails** method in **library/console/console_app.py**. + + - Add **CommonActions.SEARCH_BOOKS** to **options** before calling **ReadInputOptions**. + - Add an **else if** to handle the **SEARCH_BOOKS** action. + - The **else if** block should call a new method named **SEARCH_BOOKS**. + +- Create a new **SEARCH_BOOKS** method in **library/console/console_app.py**. + + - The **SEARCH_BOOKS** method should read a user provided book title. + - Check if a book is available for loan, and display a message stating either: + + - "**book.title** is available for loan", or + - "**book.title** is on loan to another patron. The return due date is **loan.DueDate**." + +GitHub Copilot Chat can help you implement the code updates needed to complete the new feature. + +- You can use inline chat sessions to implement smaller, more discreet code updates based on your requirements. +- You can use the Chat view to work on larger code updates that may require a more conversational and iterative approach. + +### Implement "book availability" updates using Copilot inline chat + +**Copilot Inline Chat** sessions allow you to interact with GitHub Copilot directly in your code editor. You can use inline chat to ask questions, request code suggestions, and get explanations for the code generated by GitHub Copilot. + +Use the following steps to complete this section of the exercise: + +1. Open the EXPLORER view. + +1. Expand the **library/console** project. + +1. Open the **console/common_actions.py** file, and then **select** the **CommonActions** class. + + You need to add a new **SEARCH_BOOKS** action to **CommonActions**. + +1. Open the inline chat: Hover on the selection, right click and a menu opens, choose **"Copilot"**, and select **Editor Inline Chat**. + +1. Enter the following prompt: + + ```plaintext + Update selection to include a new `SEARCH_BOOKS` action. + ``` + + GitHub Copilot should suggest a code update that adds the new **SEARCH_BOOK** action to the **CommonActions** class. + +1. Review the suggested update and then select **Accept**. + + Your updated code should look similar to the following code snippet: + + ```python + + class CommonActions(Flag): + REPEAT = 0 + SELECT = auto() + QUIT = auto() + SEARCH_PATRONS = auto() + SEARCH_BOOKS = auto() # added + RENEW_PATRON_MEMBERSHIP = auto() + RETURN_LOANED_BOOK = auto() + EXTEND_LOANED_BOOK = auto() + ``` + + Notice the addition of `SEARCH_BOOKS = auto()` to `CommonActions`. + +1. Open the **library/console/console_app.py** file. + +1. Find and then select the `write_input_options` method in the `ConsoleApp` class. Hover on the selection, right click and a menu opens, choose **"Copilot"**, and select **Editor Inline Chat**. + + You need to add support for the new `CommonActions.SEARCH_BOOKS` option. If the `SEARCH_BOOKS` option is present, display the option to check if a book is available for loan. + +1. Open the inline chat and then enter the following prompt: + + ```plaintext + Update selection to include an option for the `CommonActions.SEARCH_BOOKS` action. Use the letter "b" and the message "to check for book availability". + ``` + + GitHub Copilot should suggest a code update that adds a new `if` block for the `SEARCH_BOOKS` action. + +1. Review the suggested update and then select **Accept**. + + The suggested update should be similar to the following code snippet: + + ```python + def write_input_options(self, options): + print("Input Options:") + if options & CommonActions.RETURN_LOANED_BOOK: + print(' - "r" to mark as returned') + if options & CommonActions.EXTEND_LOANED_BOOK: + print(' - "e" to extend the book loan') + if options & CommonActions.RENEW_PATRON_MEMBERSHIP: + print(' - "m" to extend patron\'s membership') + if options & CommonActions.SEARCH_PATRONS: + print(' - "s" for new search') + if options & CommonActions.SEARCH_BOOKS: + print(' - "b" to check for book availability') + if options & CommonActions.QUIT: + print(' - "q" to quit') + if options & CommonActions.SELECT: + print(' - type a number to select a list item.') + ``` + +1. Scroll down to find and then select the `_handle_patron_details_selection` method (or the input handling section) in the **library/console/console_app.py** file. + + Once again, you need to add support for the new `CommonActions.SEARCH_BOOKS` option. Include a case that handles the user selecting the `SEARCH_BOOKS` action (for example, when the user enters "b"). + +1. Open the inline chat and then enter the following prompt: + + ```plaintext + Update selection to include an option for the `CommonActions.SEARCH_BOOKS` action. + ``` + + GitHub Copilot should suggest an update that adds a new `elif` block that handles the user selecting the `SEARCH_BOOKS` action. + +1. Review the suggested update and then select **Accept**. + + The suggested update should be similar to the following code snippet: + + ```python + def _handle_patron_details_selection(self, selection, patron, valid_loans): + # ...existing code... + + elif selection == 'b': + # Placeholder for book search functionality + print("Book search functionality is not implemented yet.") + return ConsoleState.PATRON_DETAILS + elif selection.isdigit(): + idx = int(selection) + if 1 <= idx <= len(valid_loans): + self.selected_loan_details = valid_loans[idx - 1][1] + return ConsoleState.LOAN_DETAILS + print("Invalid selection. Please enter a number shown in the list above.") + return ConsoleState.PATRON_DETAILS + else: + print("Invalid input. Please enter a number, 'm', 's', 'b', or 'q'.") + return ConsoleState.PATRON_DETAILS + ``` + +#### Use Copilot Inline Chat to share a code selection to GitHub Copilot Chat + +1. Ensure that GitHub Copilot Chat is open in **Ask mode**. + +1. There are two things that you need to accomplish: + + - You need to add `CommonActions.SEARCH_BOOKS` to `options` before calling `_get_patron_details_input`. + - You also need to add an `if` or `elif` block to handle the `"b"` selection for the `SEARCH_BOOKS` action. The block should call a new method named `search_books`. + + You can address both requirements with the same prompt. + +1. Locate and then **select** the `patron_details` method in the **library/console/console_app.py** file. + +1. With the `patron_details` method still selected, hover on the selection. **Right click** and a menu opens, choose **"Copilot"** and select **"Add Selection to Chat"**. + +1. Enter the following prompt: + + ```plaintext + @workspace Update selection to add `CommonActions.SEARCH_BOOKS` to `options` before calling `_get_patron_details_input`. Add an `if` or `elif` block to handle the `"b"` selection for the `SEARCH_BOOKS` action. The block should call a new method named `search_books`. + ``` + + GitHub Copilot Chat should suggest a code update that adds `CommonActions.SEARCH_BOOKS` to `options` before calling `_get_patron_details_input`. For the supplied code, select the "Apply in Editor" icon. + + ![Screenshot showing a GitHub Copilot Ask mode - Apply in Editor icon.](./Media/m03-github-copilot-chat-view-response-ask-mode.png) + +1. Review the suggested update and then select **Accept**. + ```python + def patron_details(self) -> ConsoleState: + patron = self.selected_patron_details + print(f"\nName: {patron.name}") + print(f"Membership Expiration: {patron.membership_end}") + loans = self._loan_repository.get_loans_by_patron_id(patron.id) + print("\nBook Loans History:") + + valid_loans = self._print_loans(loans) + + if valid_loans: + options = ( + CommonActions.RENEW_PATRON_MEMBERSHIP + | CommonActions.SEARCH_PATRONS + | CommonActions.QUIT + | CommonActions.SELECT + | CommonActions.SEARCH_BOOKS # Added SEARCH_BOOKS to options + ) + selection = self._get_patron_details_input(options) + return self._handle_patron_details_selection(selection, patron, valid_loans) + else: + print("No valid loans for this patron.") + options = ( + CommonActions.SEARCH_PATRONS + | CommonActions.QUIT + | CommonActions.SEARCH_BOOKS # Added SEARCH_BOOKS to options + ) + selection = self._get_patron_details_input(options) + return self._handle_no_loans_selection(selection) + + def _handle_patron_details_selection(self, selection, patron, valid_loans): + if selection == 'q': + return ConsoleState.QUIT + elif selection == 's': + return ConsoleState.PATRON_SEARCH + elif selection == 'm': + status = self._patron_service.renew_membership(patron.id) + print(status) + self.selected_patron_details = self._patron_repository.get_patron(patron.id) + return ConsoleState.PATRON_DETAILS + elif selection == 'b': + return self.search_books() # Call the new search_books method + elif selection.isdigit(): + idx = int(selection) + if 1 <= idx <= len(valid_loans): + self.selected_loan_details = valid_loans[idx - 1][1] + return ConsoleState.LOAN_DETAILS + print("Invalid selection. Please enter a number shown in the list above.") + return ConsoleState.PATRON_DETAILS + else: + print("Invalid input. Please enter a number, 'm', 's', 'b', or 'q'.") + return ConsoleState.PATRON_DETAILS + + def _handle_no_loans_selection(self, selection): + if selection == 'q': + return ConsoleState.QUIT + elif selection == 's': + return ConsoleState.PATRON_SEARCH + elif selection == 'b': + return self.search_books() # Handle SEARCH_BOOKS when no loans + else: + print("Invalid input.") + return ConsoleState.PATRON_DETAILS + + def search_books(self) -> ConsoleState: + print("Book search functionality is not implemented yet.") + return ConsoleState.PATRON_DETAILS + + ``` + + + > **NOTE**: The code suggested by Inline chat may include stub code for the **search_books()** method as in the previous code sample. You can accept that code stub, but you'll implement the **search_books** method in the next section. + +### Implement a SEARCH_BOOKS method using Copilot Chat Ask mode + +There's one step remaining to implement the "book availability" updates, create the **search_books** method. The **search_books** method will read a user provided book title, check if a book is available for loan, and display a message indicating the book's availability status. You'll use the Chat view to evaluate the requirements and implement the **search_books** method. + +GitHub Copilot's Chat view provides a conversational and interactive environment that isn't available when using inline chat. You can use the Chat view to ask questions, request code suggestions, and get explanations for the code generated by GitHub Copilot. The Chat view supports the following three modes: + +- Ask mode: Ask mode is used to gain a better understanding of your codebase, brainstorm ideas, and help with coding tasks. The code suggestions generated in Ask mode can be implemented directly into your codebase or copied to the clipboard. +- Edit mode: Edit mode is used to make changes to your code, such as refactoring or adding new features. Edit mode can make edits across multiple files in your project. +- Agent mode: Agent mode is used to define a high-level task and to start an agentic code editing session to accomplish that task. In agent mode, Copilot autonomously plans the work needed and determines the relevant files and context. The agent can make changes to your code, run tests, and even deploy your application. + +You'll be using the **Inline Chat**, **Ask** and **Edit** modes to implement the **search_books** method. + +Use the following steps to complete this section of the exercise: + +1. Take a minute to consider the process requirements for the **search_books** method. + + What's the process that the method needs to complete? What's the return type for this method? Does it require parameters? + + The **search_books** method should implement the following process: + + 1. Prompt the user for a book title. + 1. Read the user provided book title. + 1. Check if a book is available for loan. + 1. Display a message stating one of the following options: + + - "**{book.title}** is available for loan" + - "**{book.title}** is on loan to another patron. The return due date is **{loan.due_date}**." + + To build the message options, your code will need to access the following JSON files: + + - **Books.json** is needed to find the matching **Title** and **Id**. + - **BookItems.json** is needed to find the **BookId** for each physical copy of a book (where **BookId** matches the **Id** in **Books.json**). + - **Loans.json** is needed to find the **ReturnDate** and **DueDate** for the matching **BookItemId** (where **BookItemId** matches the **Id** in **BookItems.json**). + +> **Note:** +> The **BookItemId** in **Loans.json** refers to the **Id** in **BookItems.json**. +> The **BookId** in **BookItems.json** refers to the **Id** in **Books.json**. + +1. Ensure that you have the following **search_books** method created in the **console_app.py** file: + + ```python + + def search_books(self) -> ConsoleState: + print("Book search functionality is not implemented yet.") + return ConsoleState.PATRON_DETAILS + + ``` + + > **NOTE**: Be sure to remove any code comments created by GitHub Copilot. Unnecessary and inaccurate comments can negatively influence GitHub Copilot's suggestions. + +1. Open the **console_app.py** in VSCode, select the **search_books** method. + +1. Set the chat mode to **Ask** and select the **Auto** model. + + The Set Mode and Pick Model menus are in the bottom-left corner of the Chat view. + + > **NOTE**: You can use a different model if your plan allows it, but responses may differ from those shown in this exercise. Free-plan users have a limited number of monthly chat requests, so each prompt counts against your quota. + +1. In the Chat view, enter the following prompt: + + ```plaintext + @workspace Update selection to obtain a book title. Prompt the user to "Enter a book title to search for". Read the user input and ensure the book title isn't null. + ``` + +1. Review the suggested update. + + The suggested update should be similar to the following code snippet: + + ```python + + def search_books(self) -> ConsoleState: + while True: + book_title = input("Enter a book title to search for: ").strip() + if not book_title: + print("No input provided. Please enter a book title.") + else: + # Placeholder for future book search logic + print(f"Searching for book titled: {book_title}") + break + return ConsoleState.PATRON_DETAILS + + ``` + +1. Hover the mouse pointer over the suggested code, and then select **Apply to library\console\console_app.py**. + + The suggested code should be visible in the code editor, with options to **Keep** or **Undo**. + +1. In the code editor, to accept the suggested code, select **Keep**. + +1. Select the **search_books** method. + +1. Use drag-and-drop operations to add the following files to the Chat context: + + - library/console/**console_app.py** (current file selected) + - library/console/**main.py** + - library/infrastructure/Json/**Books.json** + - library/infrastructure/Json/**Loans.json** + - library/infrastructure/Json/**BookItems.json** + - library/infrastructure/**json_data.py** + - library/infrastructure/**json_loan_repository.py** + +1. Enter the following prompt: + + ```plaintext + @workspace provide a simple text overview plan of how to update the search_books method and ConsoleApps class to find a matching book title in the Books.json file. Use the user supplied book title to find a matching book. If a book is found, use Loans.json to check if the book is available for loan. If the book has been returned, display a message stating "book.title is available for loan". If the book is on loan, display a message stating "book.title is on loan to another patron. The return due date is loan.DueDate". Don't provide code yet. List the general change recommendations for each high level item as a numbered list of paragraphs. + ``` + +1. Take a minute to review the suggested updates. + + The response should include the following information: + + ```plaintext + + 1. Update the ConsoleApp class to access book data: + Add a reference to the book repository or the loaded books data from JsonData within the ConsoleApp class. This will allow the search_books method to access the list of all books and perform searches based on user input. + + 2. Modify the search_books method to search for a book: + Update the search_books method to use the user-supplied book title to search for a matching book in the loaded books data (from Books.json). The search should be case-insensitive and match the title exactly or partially, depending on requirements. + + 3. Check book availability using loan data: + Once a matching book is found, use the book's ID to find all related book items (copies) and then check the Loans.json data to determine if any copies are currently on loan (i.e., have a loan record with a null ReturnDate). If all copies are returned, the book is available for loan. + + 4. Display book availability status to the user: + If at least one copy of the book is available (not currently on loan), display a message such as "book.title is available for loan." If all copies are on loan, display a message such as "book.title is on loan to another patron. The return due date is loan.DueDate," using the due date from the active loan. + + 5. Handle no match or multiple matches: + If no book matches the search, display an appropriate message to the user. Optionally, if multiple books match, prompt the user to refine their search or select from a list. + + 6. Keep user in the book search loop: + After displaying the result, allow the user to search for another book or return to the previous menu, maintaining a smooth user experience. + + ``` + + > **NOTE**: you may have received code updates in the GitHub Copilot response. + > The code updates will be addressed in the next step. + + You can also use the Chat view's **Ask** mode to analyze code updates, then use the **Edit** mode to implement the code updates. + +1. Review sample prompts (you will prompt later). + + ```plaintext + + Update the search_books method and ConsoleApp class so that when a user enters a book title, the app searches + Books.json for a matching title (case-insensitive, partial match allowed). If a match is found, check all + related BookItem records and their loan status in Loans.json. If any copy is not currently on loan (no active + loan or has a ReturnDate), display "book.title is available for loan". If all copies are on loan, display + "book.title is on loan to another patron. The return due date is loan.DueDate" (show the soonest due date). + Integrate this logic into the user flow and ensure clear user messaging. + ``` + + You can adjust the prompt to achieve specific requirements by asking Copilot to generate you a prompt from the Copilot feedback. A Copilot generated example prompt follows for review: + + ```plaintext + + Update the ConsoleApp class and its search_books method to implement the following: + + 1. Add access to the loaded books data (from JsonData or a book repository) in ConsoleApp. + 2. In search_books, use the user-supplied book title to search for a matching book in Books.json (case-insensitive, partial or exact match). + 3. If a book is found, use its ID to find all related book items (copies) and check Loans.json to see if any copies are currently on loan (ReturnDate is null). + 4. If at least one copy is available, display: "book.title is available for loan." If all are on loan, display: "book.title is on loan to another patron. The return due date is loan.DueDate." + 5. If no book matches, inform the user. If multiple books match, prompt for refinement or selection. + 6. After displaying the result, allow the user to search again or return to the previous menu. + + Please provide the complete implementation for this book search and availability feature. + ``` + +### Implement search_books method using Copilot Chat Edit mode + +1. To switch the Chat view to the Edit mode, select **Set Mode**, and then select **Edit**. + + When prompted to start a new session, select **Yes**. + +1. Use drag-and-drop operations to add the following files to the Chat context using **Chat Edit mode**: + + - library/console/**console_app.py** + - library/console/**main.py** + - library/infrastructure/**json_data.py** + - library/infrastructure/Json/**Books.json** + - library/infrastructure/Json/**Loans.json** + - library/infrastructure/Json/**BookItems.json** + +1. Select the **search_books** method from **console_app.py**. + +1. Enter the following prompt: + + ```plaintext + + @workspace Update the ConsoleApp class so it can access the loaded books data from JsonData or a book repository, and update main.py to instantiate ConsoleApp with the loaded JsonData instance by passing json_data=json_data. In the search_books method, prompt the user for a book title and search for a matching book in Books.json using a case-insensitive, partial or exact match. If a book is found, use its ID to find all related book items (copies) and check Loans.json to determine if any copies are currently on loan (ReturnDate is null). If at least one copy is available, display a message stating the book is available for loan; if all are on loan, display a message with the book title and the due date of the loan. If no book matches, inform the user, and if multiple books match, prompt for refinement or selection. After displaying the result, allow the user to search again or return to the previous menu. + ``` + +1. Take a minute to review the suggested updates in the console_app.py file. + + You can use **Previous** and **Next** to navigate through the suggested code updates, or you can scroll manually through the file. + + **console_app.py** + + Code updates that add the **json_data** dependency to the **console_app** constructor can be found near the top of the ConsoleApp class. + + ```python + class ConsoleApp: + def **init**( + self, + loan_service: ILoanService, + patron_service: IPatronService, + patron_repository: IPatronRepository, + loan_repository: ILoanRepository, + json_data=None, # <-- Add json_data for direct access to books/items + book_repository=None # <-- Optionally allow a book repo + ): + self._current_state: ConsoleState = ConsoleState.PATRON_SEARCH + self.matching_patrons = [] + self.selected_patron_details = None + self.selected_loan_details = None + self._patron_repository = patron_repository + self._loan_repository = loan_repository + self._loan_service = loan_service + self._patron_service = patron_service + self._json_data = json_data # <-- store json_data + self._book_repository = book_repository + ``` + + Code completes the `search_books` method to implement functionality in the stub code to check if to use **json_data** to find a book by title, retrieve its BookItem, check for an active loan, and display availability or loan status as requested. + + ```python + def search_books(self) -> ConsoleState: + while True: + book_title = input("Enter a book title to search for: ").strip() + if not book_title: + print("No book title provided. Please try again.") + continue + + # Case-insensitive, partial or exact match + books = self._json_data.books + matches = [b for b in books if book_title.lower() in b.title.lower()] + + if not matches: + print("No matching books found.") + again = input("Search again? (y/n): ").strip().lower() + if again == 'y': + continue + else: + return ConsoleState.PATRON_DETAILS + + if len(matches) == 1: + book = matches[0] + else: + print("\nMultiple books found:") + for idx, b in enumerate(matches, 1): + print(f"{idx}) {b.title}") + selection = input("Select a book by number or 'r' to refine search: ").strip().lower() + if selection == 'r': + continue + if not selection.isdigit() or not (1 <= int(selection) <= len(matches)): + print("Invalid selection.") + continue + book = matches[int(selection) - 1] + + # Find all book items (copies) for this book + book_items = [bi for bi in self._json_data.book_items if bi.book_id == book.id] + if not book_items: + print("No copies of this book are in the library.") + again = input("Search again? (y/n): ").strip().lower() + if again == 'y': + continue + else: + return ConsoleState.PATRON_DETAILS + + # Find all loans for these book items + loans = self._json_data.loans + on_loan = [] + available = [] + for item in book_items: + # Find latest loan for this item (if any) + item_loans = [l for l in loans if l.book_item_id == item.id] + if item_loans: + # Get the most recent loan (by LoanDate) + latest_loan = max(item_loans, key=lambda l: l.loan_date or l.due_date or l.return_date or 0) + if latest_loan.return_date is None: + on_loan.append(latest_loan) + else: + available.append(item) + else: + available.append(item) + + if available: + print(f"Book '{book.title}' is available for loan.") + else: + # All copies are on loan, show due dates + due_dates = [l.due_date for l in on_loan if l.due_date] + if due_dates: + next_due = min(due_dates) + print(f"All copies of '{book.title}' are currently on loan. Next due date: {next_due}") + else: + print(f"All copies of '{book.title}' are currently on loan.") + + again = input("Search for another book? (y/n): ").strip().lower() + if again == 'y': + continue + else: + return ConsoleState.PATRON_DETAILS + ``` + + **main.py** + + Modified instantiation of `ConsoleApp` to pass in `json_data` so the app can access the JSON data files. + + ```python + # ...existing code... + app = ConsoleApp( + loan_service=loan_service, + patron_service=patron_service, + patron_repository=patron_repo, + loan_repository=loan_repo, + json_data=json_data # <-- Pass json_data so ConsoleApp can access books/items/loans + ) + # ...existing code... + + ``` + +1. In the Chat view, to keep all edits, select **Keep**. + + Always review GitHub Copilot suggestions before accepting updates. + + If you're unsure about the suggested updates, you can accept changes and then ask GitHub Copilot for an explanation. You can revert the edits if you decide against the updates. + + > **NOTE**: If GitHub Copilot suggests formatting the return date using a specific locale or format, ensure you use Python's datetime.strftime() method to format the date as needed. + +1. Ensure that you've accepted updates in both the console_app.py and main.py files. + +1. Open Visual Studio Code's EXPLORER view. + +1. Run your tests or start the application to ensure that no errors were introduced by your code updates. + + You may see warning messages, but there shouldn't be any errors. + + To run the tests, open a terminal in Visual Studio Code, navigate to the AccelerateDevGHCopilot/library directory, and run: + + ```bash + python -m unittest discover tests + ``` + +## Merge your "book availability" updates into the main branch of the repository + +It's important to test your code before merging it into the main branch of the repository. Testing ensures that your code works as expected and doesn't introduce any new issues. In this exercise, you'll use manual testing to verify that the "book availability" feature works as expected. + +In this section of the exercise, you complete the following tasks: + +1. Test the "book availability" feature. +1. Sync your changes with the remote repository. +1. Create a pull request to merge your changes into the main branch of the repository. + +### Test the "book availability" feature + +Manual testing can be used to verify that the new feature works as expected. Using a data source that can be verified is important. In this case, you use the **Books.json** and **Loans.json** files to verify that the new feature reports the availability status of a book correctly. + +Use the following steps to complete this section of the exercise: + +1. To run the application, open a terminal in Visual Studio Code, navigate to the `AccelerateDevGHCopilot/library` directory, and run: + +```bash +python console/main.py +``` + +1. When prompted for a patron name, type **One** and then press Enter. + + You should see a list of patrons that match the search query. + +1. At the "Input Options" prompt, type **2** (selection "Patron One") and then press Enter. + + - Entering **2** selects the second patron in the list. + - You should see the patron's name and membership status followed by book loan details. + +1. To Choose "Book One" which shows "Returned: False" enter: **1**. + + - Notice the state is **Returned: False** + - Or find a patron with a book that has status **"Returned: False"**. Note the name of the book the book. + +1. To return the book choose: **r** + + - Notice the state is updated to **Returned: True** + +1. Check books for "Patron One" again: + + - To search for a patron choose: **s** + - Repeat steps **Search for a patron:** for **Patron One**. + - To Choose "Book One" which shows "Returned: True" enter: **1** + + Notice that the Books title is shown but there is no availability information. + +1. Check for book availability and check out an available book. + + - To check for book availability: **b** + - Search for an available book, enter: **One** + + Notice that the Books title is shown and availability shown as "Book One is available for loan." Also notice that there is no option to checkout "Book One." + +1. Your are asked "Search for another book? (y/n)" enter: **n** + +1. Stop the application by entering "q" for quit. + +**There is a problem with the application**. Book availability is being reported but there is no option to checkout a book. + +### Fix checkout book bug with Agent Mode + +Copilot is an AI tool, and like people, it can make mistakes, so you need to check the results. To fix this bug you will use Agent mode. + +1. Open Github Copilot Chat and select "**Agent mode**." +1. To add the the manual testing from the terminal to the chat: + + - In the Chat window select **Add Context** in the Copilot Chat (ensure it's Agent mode!) + - Type "Tools" and then type "Tools" and select with mouse or with "Enter" key. + - Type "terminalSelection" and select "**terminalLastCommand** the terminal's last run command" + +1. Add files to Chat content by opening the files listed in the editor in VSCode. Then in Copilot chat window select **Add Context** and select **Open Editors**: + + - **console_app.py** (contains the main application logic and user interaction) + - **loan_service.py** (logic for loans) + - **json_data.py** (handles loading/saving of books, book items, patrons, authors, loans) + - **json_loan_repository.py** (handles retrieving, adding, and updating loan records) + - **Books.json** (sample book data) + - **BookItems.json** (sample book item/copy data) + - **Patrons.json** (sample patron data) + - **Loans.json** (sample loan/checkout data) + - **Authors.json** (sample author data) + +1. Review and enter the prompt to address the bug (Ensure Copilot Chat displays "**Agent**"): + + ```plaintext + @workspace My goal is to make it possible for a patron to check out an available book directly from the console app menu. Right now, when I search for a book and it is available, the app only tells me "...is available for loan" but doesn’t let me check it out. Please help me update the code so that when a book is available, the app prompts me to check it out, and if I choose yes, it creates a new loan for the current patron and book. Make sure the checkout works smoothly in the menu flow. For the loan_service add checkout functionality and ensure the console app calls this method. Only use the existing enum and menu option names as defined in the codebase. Only use enum values and menu option names that are already defined in the codebase. Do not invent, rename, or add new enum values or menu options. + ``` + + You should notice that the Agent reads specific lines of several files. It makes updates and then checks it and makes further updates. + +1. Review the agent mode updates in **console-app.py** towards then bottom of the `search_books` method, that should be similar to the following code: + + ```python + def search_books(self) -> ConsoleState: + # ...existing code... + + if available: + print(f"Book '{book.title}' is available for loan.") + # Prompt to check out + checkout = input("Would you like to check out this book? (y/n): ").strip().lower() + if checkout == 'y': + if not self.selected_patron_details: + print("No patron selected. Please select a patron first.") + return ConsoleState.PATRON_SEARCH + # Use the first available copy + book_item = available[0] + # Create a new loan using the loan service + status = self._loan_service.create_loan( + patron_id=self.selected_patron_details.id, + book_item_id=book_item.id + ) + print(status) + # Reload loans to reflect the new loan + if hasattr(self._json_data, 'load_data'): + self._json_data.load_data() + print(f"Book '{book.title}' checked out to {self.selected_patron_details.name}.") + return ConsoleState.PATRON_DETAILS + # ...existing code... + ``` + +1. Review the updates to **loan_service.create_loan**. + +```python + def create_loan(self, patron_id: int, book_item_id: int): + """Create a new loan for the given patron and book item, and add it to the repository.""" + from ..entities.loan import Loan + from datetime import datetime, timedelta + # Check if the book item is already on loan + all_loans = getattr(self._loan_repository, 'get_all_loans', lambda: [])() + for loan in all_loans: + if loan.book_item_id == book_item_id and loan.return_date is None: + return "This copy is already on loan." + # Generate a new loan ID + max_id = max((l.id for l in all_loans if hasattr(l, 'id')), default=0) + new_id = max_id + 1 + now = datetime.now() + due = now + timedelta(days=14) + new_loan = Loan( + id=new_id, + book_item_id=book_item_id, + patron_id=patron_id, + loan_date=now, + due_date=due, + return_date=None + ) + self._loan_repository.add_loan(new_loan) + return f"Loan created. Due date: {due.date()}" + +``` + +1. Notice the updates of ConsoleApp.search_books and LoanService.: + + The search_books and loan_service methods are now updated so that when a book is available, the app prompts the user to check it out. If the user chooses yes, a new loan is created for the current patron and the selected book item, and the menu flow returns to the patron details. The checkout process is now integrated smoothly into the menu flow. + +1. Accept the changes. + +1. Retest the previous test steps and **complete a book check out** to ensure the fix is correct. If issues remain continue with troubleshooting with the Agent or Ask mode as needed. + +### Sync your changes with the remote repository + +1. Select the Source Control view. + +1. Ensure that the files you updated are listed under **Changes**. + + You should see the common_actions.py and console_app.py files listed under **Changes**. The main.py file may also be listed. + +1. Use GitHub Copilot to generate a message for the **Commit**. + + ![Screenshot showing the Generate Commit Message with Copilot button.](./Media/m03-github-copilot-commit-message-python.png) + +1. To stage and commit your changes, select **Commit** and then select **Yes**. + +1. To synchronize changes to the remote repository, select **Sync Changes**. + +### Create a pull request to merge your changes into the main branch + +You've implemented the feature that enables a librarian to determine the availability status of a book. Now you need to merge your changes into the main branch of the repository. You can create a pull request to merge your changes into the main branch. + +Use the following steps to complete this section of the exercise: + +1. Open your GitHub repository in a web browser. + + To open your GitHub repository from Visual Studio Code: + + 1. In the bottom-left corner of of the Visual Studio Code window, select **book-availability**. + 1. On the context menu, to the right of the **book-availability** branch, select the **Open in GitHub** icon. + +1. On your GitHub repository page, select the **Compare & pull request** tab. + +1. Ensure that **Base** specifies **main**, **compare** specifies **book-availability**, and **Able to merge** is checked. + +1. Under **Add a description**, select the Copilot Actions button (the GitHub Copilot icon), and then select the option to generate a summary. + + > **NOTE**: The GitHub Copilot Free plan doesn't support the pull request summary feature at this time. + + If you're using the GitHub Copilot Free plan, you can write your own summary, or use the summary below to complete the pull request. + + ```plaintext + + This pull request introduces a new feature to the library console application: the ability to search for books and check their availability. It also includes updates to dependency injection and the CommonActions enumeration to support this functionality. Below are the most important changes grouped by theme. + + New Feature: Book Search + + Added a new SEARCH_BOOKS action to the CommonActions enumeration (library\console\common_actions.py). + + Updated PatronDetails method to handle the SEARCH_BOOKS action, including a new SEARCH_BOOKS method that allows users to search for a book by title and check its availability (library\console\console_app.py). + + Modified ReadInputOptions and WriteInputOptions methods to include the new SEARCH_BOOKS option (library/console/console_app.py). + + Dependency Injection Updates + + Added JsonData as a dependency in the ConsoleApp constructor and ensured it is registered in the DI container before ConsoleApp (library/console/console_app.py, library/console/main.py). + + ``` + +1. Once the summary is generated, select **Preview**. + +1. Take a minute to review the summary. + + The pull request summary generated by GitHub Copilot should be similar to the following example: + + ![Screenshot showing a pull request summary generated using a GitHub Copilot Enterprise account.](./Media/m03-github-copilot-pull-request-summary.png) + +1. Select **Create pull request**. + +1. If all checks pass and there are no conflicts with the base branch, select **Merge pull request**, and then select **Confirm merge**. + + Notice that you can delete the **book-availability** branch after merging the changes. To delete the branch, select **Delete branch**. + +1. Switch back to the Visual Studio Code window. + +1. Switch to the **main** branch of the repository. + +1. Open the Source Control view, and then **Pull** the changes from the remote repository. + +1. Verify that the book-availability feature is available in the **main** branch. + +## Summary + +In this exercise, you learned how to use GitHub Copilot to develop a new code feature for a Python application. You developed the feature in a new branch using GitHub Copilot's inline chat and Chat view, tested you code, and then merged your changes into the main branch of the repository. You also used GitHub Copilot to generate a commit message and a pull request summary. + +## Clean up + +Now that you've finished the exercise, take a minute to ensure that you haven't made changes to your GitHub account or GitHub Copilot subscription that you don't want to keep. If you made any changes, revert them now. diff --git a/Instructions/Labs/LAB_AK_04_develop_unit_tests_pytest.md b/Instructions/Labs/LAB_AK_04_develop_unit_tests_pytest.md new file mode 100644 index 0000000..26a6163 --- /dev/null +++ b/Instructions/Labs/LAB_AK_04_develop_unit_tests_pytest.md @@ -0,0 +1,765 @@ +--- +lab: + title: Exercise - Develop unit tests using GitHub Copilot (Python) + description: Learn how to accelerate the development of unit tests using GitHub in Visual Studio Code. + duration: 25 minutes + level: 200 + islab: true + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Develop unit tests using GitHub Copilot + +The large language models behind GitHub Copilot are trained on a variety of code testing frameworks and scenarios. GitHub Copilot is a great tool for generating test cases, test methods, test assertions and mocks, and test data. In this exercise, you use GitHub Copilot to accelerate the development of unit tests for a Python application. + +This exercise should take approximately **25** minutes to complete. + +> **IMPORTANT**: To complete this exercise, you must provide your own GitHub account and GitHub Copilot subscription. If you don't have a GitHub account, you can sign up for a free individual account and use a GitHub Copilot Free plan to complete the exercise. If you have access to a GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscription from within your lab environment, you can use your existing GitHub Copilot subscription to complete this exercise. + +## Before you start + +Your lab environment must include the following: Git 2.48 or later, Python 3.10 or later, Visual Studio Code with the Python extension form Microsoft, and access to a GitHub account with GitHub Copilot enabled. + +If you're using a local PC as a lab environment for this exercise: + +- For help configuring your local PC as your lab environment, open the following link in a browser: Configure your lab environment resources. + +- For help enabling your GitHub Copilot subscription in Visual Studio Code, open the following link in a browser: Enable GitHub Copilot within Visual Studio Code. + +If you're using a hosted lab environment for this exercise: + +- For help enabling your GitHub Copilot subscription in Visual Studio Code, paste the following URL into a browser's site navigation bar: Enable GitHub Copilot within Visual Studio Code. + +- Open a command terminal and then run the following commands: + + To ensure that Visual Studio Code is configured to use the correct version of Python, verify your Python installation is version 3.10 or later: + + ```bash + python --version + ``` + + To ensure that Git is configured to use your name and email address, update the following commands with your information, and then run the commands: + + ```bash + + git config --global user.name "John Doe" + + ``` + + ```bash + + git config --global user.email johndoe@example.com + + ``` + +## Exercise scenario + +You're a developer working in the IT department of your local community. The backend systems that support the public library were lost in a fire. Your team needs to develop a temporary project to help the library staff manage their operations until the system can be replaced. Your team chose GitHub Copilot to accelerate the development process. + +You have an initial version of the library application that includes a unit test project named UnitTests. You need to accelerate the development of additional unit tests using GitHub Copilot. + +This exercise includes the following tasks: + +1. Set up the library application in Visual Studio Code. + +1. Examine the approach to unit testing implemented by the UnitTests project. + +1. Extend the UnitTests project to begin testing the data access classes in the library\infrastructure project. + +## Set up the library application in Visual Studio Code + +You need to download the existing application, extract the code files, and then open the project in Visual Studio Code. + +Use the following steps to set up the library application: + +1. Open a browser window in your lab environment. + +1. To download a zip file containing the library application, paste the following URL into your browser's address bar: [GitHub Copilot lab - develop unit tests](https://github.com/MicrosoftLearning/mslearn-github-copilot-dev/raw/refs/heads/main/DownloadableCodeProjects/Downloads/AZ2007LabAppM4Python.zip) + + The zip file is named **AZ2007LabAppM4Python.zip**. + +1. Extract the files from the **AZ2007LabAppM4Python.zip** file. + + For example: + + 1. Navigate to the downloads folder in your lab environment. + + 1. Right-click **AZ2007LabAppM4Python.zip**, and then select **Extract all**. + + 1. Select **Show extracted files when complete**, and then select **Extract**. + +1. Open the extracted files folder, then copy the **AccelerateDevGHCopilot** folder to a location that's easy to access, such as your Windows Desktop folder. + +1. Open the **AccelerateDevGHCopilot** folder in Visual Studio Code. + + For example: + + 1. Open Visual Studio Code in your lab environment. + + 1. In Visual Studio Code, on the **File** menu, select **Open Folder**. + + 1. Navigate to the Windows Desktop folder, select **AccelerateDevGHCopilot** and then select **Select Folder**. + +1. In the Visual Studio Code EXPLORER view, verify the following project structure: + + - AccelerateDevGHCopilot/library + ├── application_core + ├── console + ├── infrastructure + └── tests + +## Examine the approach to unit testing implemented by the tests directory + +In this section of the exercise, you use GitHub Copilot to examine the unit testing approach implemented by the UnitTests project. + +Use the following steps to complete this section of the exercise: + +1. Expand the **tests** folder in the EXPLORER view. + + Your existing codebase includes a UnitTests project that implements the following folder structure: + + ─ tests + ├─ test_patron_service.py + + The test project structure mirrors and supports 2 of the **services** portion of the **application_core** project. + + ─ application_core + └─ services + ├─ loan_service.py + ├─ patron_service.py + +1. Open the Chat view. + +1. Set the chat mode to **Ask** and select the **Auto** model. + + The Set Mode and Pick Model menus are in the bottom-left corner of the Chat view. + + > **NOTE**: You can use a different model if your plan allows it, but responses may differ from those shown in this exercise. Free-plan users have a limited number of monthly chat requests, so each prompt counts against your quota. + +1. Add the following files to the Chat context: + + (under library\application_core\services\ ) + + - loan_service.py + - patron_service.py + + (under library\tests\ ) + + - test_patron_service.py + - test_loan_service.py + +1. To learn about the existing unit test project, enter the following prompt: + + ```plaintext + @workspace Explain the approach to unit testing that's been implemented in this workspace. + ``` + + This prompt asks GitHub Copilot to explain the approach to unit testing in the context of the workspace. Adding relevant files to the Chat context improves the response. + +1. Take a minute to review GitHub Copilot's response. + + > You should see a description that's similar to the following example: + > + > This workspace uses a standard Python unit testing approach with the `unittest` framework and mocking via `unittest.mock.MagicMock`. The main features are: + > + > - **Test Classes**: Each service (e.g., `LoanService`, `PatronService`) has a corresponding test class (e.g., `TestLoanService`, `PatronServiceTest`). + > - **Setup**: The `setUp` method creates a mock repository and injects it into the service under test, isolating business logic from data access. + > - **Test Methods**: Each method tests a specific behavior, such as successful membership renewal or handling missing data. + > - **Assertions**: Tests use assertions like `self.assertEqual` to verify expected outcomes. + > - **Test Data**: Test objects (e.g., `Patron`, `Loan`) are created with relevant attributes for each scenario. + > - **Test Discovery**: Each test file includes: + > + > ```python + > if __name__ == "__main__": + > unittest.main() + > ``` + > + > - **Mocking**: Repository methods are mocked to control return values and isolate logic. + > + > See examples in `library2/tests/test_patron_service.py` and `library2/tests/test_loan_service.py`. + +1. To analyze the benefits of the current approach, enter the following prompt: + + ```plaintext + @workspace What are the benefits of this approach to unit testing? + ``` + +1. Take a minute to review GitHub Copilot's response. + + You can continue the conversation and ask for more details about specific aspects of the unit testing approach. For example, you can ask about the benefits of using test factories, mocking dependencies, or the overall structure of the tests. + +1. Take a minute to consider how you would extend the existing approach to test the library\infrastructure project. + + The library\infrastructure project contains data access classes that interact with the file system to load and save data. + +1. Add the following files to the Chat context: + + - library\application_core\services\loan_service.py + - library\application_core\services\patron_service.py + + - library\application_core\enums\loan_extension_status.py + - library\application_core\enums\loan_return_status.py + - library\application_core\enums\membership_renewal_status.py + + - library\tests\test_patron_service.py + - library\tests\test_loan_service.py + +1. To ask GitHub Copilot for help extending the UnitTests project, enter the following prompt: + + ```plaintext + @workspace How can I extend the `tests` project to begin testing methods in the `library\infrastructure` project? + Provide a process overview. + ``` + + This prompt asks GitHub Copilot to provide a process overview for extending the UnitTests project to begin testing methods in the library\infrastructure project. Several files are included in the prompt to provide context for the response. + +1. Take a minute to review the response from GitHub Copilot. + + >You should see an explanation that's similar to the following example: + > + >To extend the `tests` project to cover methods in the `library4Test/infrastructure` project, follow this process: + > + >1. **Identify Target Methods** + > Review the code in `infrastructure/` (e.g., `json_data.py`, `json_loan_repository.py`, `json_patron_repository.py`) and select the methods you want to test. + > + >2. **Create New Test Files** + > In the `tests/` directory, create new test files such as `test_json_data.py`, `test_json_loan_repository.py`, etc., following the naming convention used for existing tests. + > + >3. **Import Classes/Functions** + > In each new test file, import the relevant classes or functions from the `infrastructure` module. + > + >4. **Write Test Classes and Methods** + > - Use `unittest.TestCase` as the base class. + > - Implement `setUp` if you need to initialize objects or test data. + > - Write test methods for each behavior or edge case you want to verify. + > + >5. **Mock External Dependencies** + > If the infrastructure code interacts with files or external systems, use `unittest.mock` to mock those dependencies, ensuring tests are isolated and repeatable. + > + >6. **Use Assertions** + > Use assertions (`self.assertEqual`, `self.assertTrue`, etc.) to check that the methods behave as expected. + > + >7. **Run and Refine Tests** + > Run your tests using the command line or your IDE’s test runner. Refine your tests and code as needed based on the results. + > + >**Example Skeleton:** + > + >```python + >import unittest + >from infrastructure.json_data import JsonData # Adjust import as needed + > + >class TestJsonData(unittest.TestCase): + > def test_some_method(self): + > # Arrange + > # Act + > # Assert + > pass + > + >if __name__ == "__main__": + > unittest.main() + >``` + > + >**Summary:** + > + >- Add new test files in `tests/` for each infrastructure module. + >- Import and test methods using `unittest`. + >- Mock external dependencies as needed. + >- Use assertions to verify behavior. + >  + +Your results will likely differ slightly, examine them closely. + + > **TIP**: Use GitHub Copilot's **Ask** mode to investigate your approach to testing. Use the responses to plan, develop, or extend your unit tests. + +## Extend the UnitTests project to begin testing data access classes + +The **library\infrastructure** files contain data access classes that interact with the file system to load and save data. The project includes the following classes: + +- JsonData (`json_data.py`): A class that loads and saves JSON data. +- JsonLoanRepository (`json_loan_repository.py`): A class that implements the ILoanRepository interface and uses the JsonData class to load and save loan data. +- JsonPatronRepository (`json_patron_repository.py`): A class that implements the IPatronRepository interface and uses the JsonData class to load and save patron data. + +### Use Agent mode to create a new test class + +You can use the Chat view's Agent mode when you have a specific task in mind and want to enable Copilot to autonomously edit your code. For example, you can use Agent mode to create and edit files, or to invoke tools to accomplish tasks. In Agent mode, GitHub Copilot can autonomously plan the work needed and determine the relevant files and context. It then makes edits to your codebase and invokes tools to accomplish the request you made. + +> **NOTE**: The Agent mode is only available in Visual Studio Code. If you're using GitHub Copilot in a different environment, you can use the Chat mode to accomplish similar tasks. + +In this section of the exercise, you use GitHub Copilot's Agent mode to create a new test class for the GetLoan method of the JsonLoanRepository class. + +Use the following steps to complete this section of the exercise: + +1. In the Chat view, select the **Set Mode** button, and then select **Agent**. + + > **IMPORTANT**: When you use the Chat view in agent mode, GitHub Copilot may make multiple premium requests to complete a single task. Premium requests can be used by user-initiated prompts and follow-up actions Copilot takes on your behalf. The total number of premium requests used is based on the complexity of the task, the number of steps involved, and the model selected. + +1. To start an automated task that creates a test class for the JsonLoanRepository.get_loan method (infrastructure\json_loan_repository.py), enter the following prompt: + + ```plaintext + + Add a `test_json_loan_repository.py` file to the **library\tests** directory. Create a class named `TestJsonLoanRepository`. + In the `TestJsonLoneRepository` class create a stub class named `get_loan`. Add a reference to classes tested. + + ``` + + This prompt asks GitHub Copilot to create a new class file in the tests project folder. + + - tests\ + - test_json_loan_repository.py + + The prompt also asks GitHub Copilot to add a reference to **library\infrastructure**. + +1. Take a minute to review the response from GitHub Copilot. + + Notice the following updates in the Chat view and code editor: + + - The agent displays status messages as it completes the requested tasks. The first task will be to create the **test_json_loan_repository.py** file. The agent may pause and ask you for confirmation before creating the file. + + ![Screenshot showing the Chat view in Agent mode.](./Media/m04-github-copilot-agent-mode-terminal-command-mkdir-py.png) + + - The **test_json_loan_repository.py** file is open in the code editor with edits similar to the following update: + + ```python + + import unittest + from infrastructure.json_loan_repository import JsonLoanRepository + from infrastructure.json_data import JsonData + from application_core.entities.loan import Loan + + class TestJsonLoanRepository(unittest.TestCase): + def get_loan(self): + # Stub for get_loan test + pass + + if __name__ == "__main__": + unittest.main() + + ``` + +1. If the agent pauses the task and asks you for permission to run a make directory command in the terminal, select **Keep** or **Continue**. + + When you select **Keep** or **Continue**, GitHub Copilot completes the following actions: + + - A new file named **test_json_loan_repository.py** is created in the **tests** folder. + +1. Take a moment to review the updates. + + You should see the following updates in the editor: + + - The **tests** folder now includes **test_json_loan_repository.py** with a reference to **infrastructure.json_loan_repository**. + +1. In the Chat view, to accept all changes, select **Keep**. + +1. In the EXPLORER view, expand the **library\tests** folder. + + You should see the following folder structure: + + tests + ├─ test_json_loan_repository.py + ├─ test_loan_service.py + └─ test_patron_service.py + +### Prepare to create unit tests for the GetLoan method + +In this section of the exercise, you use GitHub Copilot's Edit mode to create unit tests for the **GetLoan** method in the **JsonLoanRepository** class (**json_loan_repository.py**). + +Use the following steps to complete this section of the exercise: + +1. In the Chat view, select the **Set Mode** button, and then select **Edit**. + + Use the Edit mode to update selected files. Responses are displayed as code suggestions in the code editor. + +1. Open the **json_loan_repository.py** file from the **library\infrastructure** folder. + +1. Take a minute to review the **json_loan_repository.py** file. + + ```python + import json + from datetime import datetime + from application_core.interfaces.iloan_repository import ILoanRepository + from application_core.entities.loan import Loan + from .json_data import JsonData + from typing import Optional + + class JsonLoanRepository(ILoanRepository): + def __init__(self, json_data: JsonData): + self._json_data = json_data + + def get_loan(self, loan_id: int) -> Optional[Loan]: + for loan in self._json_data.loans: + if loan.id == loan_id: + return loan + return None + + def update_loan(self, loan: Loan) -> None: + for idx in range(len(self._json_data.loans)): + if self._json_data.loans[idx].id == loan.id: + self._json_data.loans[idx] = loan + self._json_data.save_loans(self._json_data.loans) + return + + def add_loan(self, loan: Loan) -> None: + self._json_data.loans.append(loan) + self._json_data.save_loans(self._json_data.loans) + self._json_data.load_data() + + def get_loans_by_patron_id(self, patron_id: int): + result = [] + for loan in self._json_data.loans: + if loan.patron_id == patron_id: + result.append(loan) + return result + + def get_all_loans(self): + return self._json_data.loans + + def get_overdue_loans(self, current_date): + overdue = [] + for loan in self._json_data.loans: + if loan.return_date is None and loan.due_date < current_date: + overdue.append(loan) + return overdue + + def sort_loans_by_due_date(self): + # Manual bubble sort for demonstration + n = len(self._json_data.loans) + for i in range(n): + for j in range(0, n - i - 1): + if self._json_data.loans[j].due_date > self._json_data.loans[j + 1].due_date: + self._json_data.loans[j], self._json_data.loans[j + 1] = self._json_data.loans[j + 1], self._json_data.loans[j] + return self._json_data.loans + + + ``` + +1. Notice the following Methods in **`JsonLoanRepository`**: + + - `__init__(self, json_data: JsonData)`: Initializes repository with a `JsonData` object. + - `get_loan(self, loan_id: int) -> Optional[Loan]`: Retrieves a loan by its ID. + - `update_loan(self, loan: Loan) -> None`: Updates an existing loan and saves changes. + - `add_loan(self, loan: Loan) -> None`: Adds a new loan, saves, and reloads data. + - `get_loans_by_patron_id(self, patron_id: int)`: Gets all loans for a specific patron. + - `get_all_loans(self)`: Returns all loans. + - `get_overdue_loans(self, current_date)`: Returns loans overdue as of `current_date`. + - `sort_loans_by_due_date(self)`: Sorts loans by due date using bubble sort. + - Also, `get_loans_by_patron_id` and `get_overdue_loans`) return lists of loans, and that `sort_loans_by_due_date` sorts in place and returns the sorted list. + +1. Use of objects to load and save data: + + - Use a `JsonData` object (`self._json_data`) to access and modify the in-memory list of loans. + - Persists changes by calling `self._json_data.save_loans(self._json_data.loans)`. + - After adding a loan, calls `self._json_data.load_data()` to refresh in-memory data from storage. + +1. Take a minute to consider the JsonLoanRepository Class: Field and Constructor Requirements + +**Field:** + +- `self._json_data`: + An instance of `JsonData`. This field holds the in-memory list of loans and provides methods to load and save loan data to persistent storage (such as a JSON file). + +**Constructor:** + +- `__init__(self, json_data: JsonData)`: + The constructor requires a `JsonData` object as a parameter. This object is assigned to `self._json_data` and is used by all repository methods to access and persist loan data. + +**How Methods Use the Field:** + +- All methods (`get_loan`, `update_loan`, `add_loan`, `get_loans_by_patron_id`, `get_all_loans`, `get_overdue_loans`, `sort_loans_by_due_date`) interact with the loan data through `self._json_data.loans`. +- Methods that modify data (`update_loan`, `add_loan`) call `self._json_data.save_loans()` to persist changes. +- `add_loan` also calls `self._json_data.load_data()` to refresh the in-memory data after saving. + +The **JsonLoanRepository.get_loan** method receives a `loan_id` parameter when called. The method searches through `self._json_data.loans` for a loan with a matching ID. If a matching loan is found, it returns the populated `Loan` object. If no matching loan is found, it returns `None`. + +For unit testing `get_loan`: + +- You can use a mock loan repository object to test the case where a matching ID is found. Load the mock with the loan you want to find, and use a test class to mock the ILoanRepository interface and instantiate a mock repository. +- Similarly, you can use a mock patron repository object to test the scenario where a specific patron exists. Populate the mock with the patron you want to retrieve, and use a test class to mock the IPatronRepository interface and instantiate a mock repository. This approach allows you to simulate both successful retrieval and not-found cases for different repository types. +- You can use a real `JsonLoanRepository` object to test the case where no matching ID is found. Specify a loan ID that you know isn't present (e.g., a value over 100). +- You'll need a `JsonData` object to create a real `JsonLoanRepository`. If your test project doesn't have access to the production `JsonData`, create a test instance or mock as needed. + +### Use the Edit mode to create unit tests + +**Files to include for context in tests:** + +- From the **application_core/entities** folder. + - `loan.py` (Loan entity) +- From the **application_core/services** folder. + - `loan_service.py` +- From the **infrastructure** folder. + - `json_data.py` + - `json_loan_repository.py` +- From the **tests** folder. + - `test_json_loan_repository.py` + +1. Enter the following Edit prompt: + + ```plaintext + + #Codebase Create fields and a class constructor for the `test_json_loan_repository.py` file to support unit tests for the get_loan method in the JsonLoanRepository class. Add private fields: _mock_loan_repository, _json_loan_repository, and _json_data. Initialize these fields in the class constructor or in a setUp (for unittest) or setup_method (for pytest). Use a setup method or fixture to populate _json_data with test data for instantiating JsonLoanRepository. + ``` + + This prompt asks GitHub Copilot to suggest fields and a class constructor. + +1. Take a minute to review the response from GitHub Copilot. + + You may see a code suggestion that's similar to the following code snippet: + + ```python + + import unittest + from unittest.mock import MagicMock + from infrastructure.json_loan_repository import JsonLoanRepository + from infrastructure.json_data import JsonData + from application_core.entities.loan import Loan + from application_core.entities.book_item import BookItem + from application_core.entities.patron import Patron + from datetime import datetime, timedelta + + class TestJsonLoanRepository(unittest.TestCase): + def setUp(self): + # Create a mock JsonData with minimal test data + self._json_data = MagicMock(spec=JsonData) + # Create a test loan and supporting objects + test_patron = Patron(id=1, name="Test Patron", membership_end=datetime.now()+timedelta(days=30), membership_start=datetime.now()-timedelta(days=365)) + test_book_item = BookItem(id=1, book_id=1, acquisition_date=datetime.now()-timedelta(days=100)) + test_loan = Loan(id=1, book_item_id=1, patron_id=1, patron=test_patron, loan_date=datetime.now()-timedelta(days=10), due_date=datetime.now()+timedelta(days=4), return_date=None, book_item=test_book_item) + self._json_data.loans = [test_loan] + self._json_loan_repository = JsonLoanRepository(self._json_data) + self._mock_loan_repository = MagicMock() + + def get_loan(self): + # Stub for get_loan test + pass + + if __name__ == "__main__": + unittest.main() + + ``` + +1. In the Chat view (Edit mode), to accept all updates, select **Keep**. + +1. Add the following files to the Chat context: + + - library/ + - application_core/entities/**loan.py** + - infrastructure/**json_data.py** + - infrastructure/**json_loan_repository.py** + - tests/**test_json_loan_repository.py** + +1. Select the contents of the **test_json_loan_repository.py** file, and then enter the following prompt in the Chat view: + + ```plaintext + @workspace Update the `test_json_loan_repository` test file and address the following: + - Use import system paths to the relative library root that match this project’s folder structure. + - Ensure the Loan class is imported correctly. + - Confirm datetime and timedelta are imported if used. + - Add any missing methods as stubs for later completion to DummyJsonData that are required by JsonLoanRepository. + - Remove any unused variables. + - Remove any unnecessary or incorrect imports. + - Use unittest standards + ``` + + This prompt asks GitHub Copilot to suggest a unit test for the `JsonLoanRepository.get_loan` method in Python. The unit test should verify the scenario where a loan ID exists in the test data. The test should use a `DummyJsonData` instance to provide sample loans, create a `JsonLoanRepository` with this data, call `get_loan` with a valid loan ID, and use assertions to check that the returned loan has the expected ID. The loan ID should be present in the `DummyJsonData.loans` list. + +1. Take a minute to review the `test_patron_service.py` updates suggested by GitHub Copilot. + + You should see a code suggestion that's similar to the following code snippet: + + ```python + + import sys + import unittest + from pathlib import Path + sys.path.append(str(Path(**file**).resolve().parent.parent)) + from infrastructure.json_loan_repository import JsonLoanRepository + from application_core.entities.loan import Loan + from application_core.entities.book_item import BookItem + from application_core.entities.patron import Patron + from datetime import datetime, timedelta + + class DummyJsonData: + def **init**(self): + self.loans = [] + self.save_loans_called = False + + def save_loans(self, loans): + self.save_loans_called = True + + def load_data(self): + pass + + class TestJsonLoanRepository(unittest.TestCase): + def setUp(self): + self._json_data = DummyJsonData() + test_patron = Patron(id=1, name="Test Patron", membership_end=datetime.now()+timedelta(days=30), membership_start=datetime.now()-timedelta(days=365)) + test_book_item = BookItem(id=1, book_id=1, acquisition_date=datetime.now()-timedelta(days=100)) + test_loan = Loan(id=1, book_item_id=1, patron_id=1, patron=test_patron, loan_date=datetime.now()-timedelta(days=10), due_date=datetime.now()+timedelta(days=4), return_date=None, book_item=test_book_item) + self._json_data.loans = [test_loan] + self._json_loan_repository = JsonLoanRepository(self._json_data) + + def test_get_loan(self): + loan = self._json_loan_repository.get_loan(1) + self.assertIsNotNone(loan) + self.assertEqual(loan.id, 1) + + def test_get_loan_not_found(self): + loan = self._json_loan_repository.get_loan(999) + self.assertIsNone(loan) + + if **name** == "**main**": + unittest.main() + + ``` + +1. You should notice that the test file now uses a minimal `DummyJsonData` class with only the required methods, corrects all import paths, removes unused variables and imports, and ensures the Loan class and datetime utilities are imported properly. + +1. In the Chat view, to accept all updates, select **Keep**. + +1. Run unittest tests on portions of the **AccelerateDevGitHubCopilot** project to ensure there are no obvious errors. In the terminal at the \library prompt enter: + + ```plaintext + python -m unittest discover -v tests + ``` + +1. Compare to running pytest (expect same result with different output formats). + + ```plaintext + pytest tests -v + ``` + +1. manually test: + + ```plaintext + python console\main.py + ``` + + A basic test is to: + 1. search for user name, enter: **one** + 1. select a listed patron, enter: **1** + 1. select "b", enter: **b** + 1. search for a book: **Twenty** + 1. checkout book, enter: **y** + 1. quit, enter: **q** + +### Use Copilot Inline editor Chat to create unit tests + +1. Use the inline editor chat feature in **test_json_loan_repository.py** to create a test for the case where the loan ID isn't found. + + Select **`class TestJsonLoanRepository`**: and prompt with: + + ```plaintext + I need to ensure 2 test cases are created for this class. Identify or create one test cases for where the loan `Id` is found, and one when loan `Id` isn't found. No more than 2 more basic test cases are needed. + ``` + + Accept the suggestions to create a new test method. + +1. Take a minute to review the new unit text. + + You should see a suggested unit text that's similar to the following code snippets: + + ```python + + class TestJsonLoanRepository(unittest.TestCase): + # ...existing code... + + def test_get_loan_found(self): + # Test case where loan with id=1 exists + found_loan = self._json_loan_repository.get_loan(1) + self.assertIsNotNone(found_loan) + self.assertEqual(found_loan.id, 1) + + def test_get_loan_not_found_again(self): + # Test case where loan with id=2 does not exist + not_found_loan = self._json_loan_repository.get_loan(2) + self.assertIsNone(not_found_loan) + + ``` + + >**NOTE** Coplot Inline Chat might also crate some mocking data, depending on the implementation. + +### Enable Pytest + +Compared to Unittest, Pytest some advantages such as concise syntax, features like fixtures and parameterization, and better failure reporting. Pytest makes tests easier to write and maintain and Pytest runs Unittest test cases. + +1. Pytest is enabled from the install of the Visual Studio Microsoft Python extension, install if needed. + +1. Select the flask icon ![Screenshot showing the test flask icon.](./Media/m04-pytest-flask-py.png) on the toolbar once tests are discovered. If the icon isn't present review the previous instructions + +1. Choose "Configure Python Tests" or if previously configured: + +1. if the tests haven't been configured or are testing the correct project go to the next step. If you need to change the test project continue with: + - `Ctrl+Shift+P` to open the Command Palette. + - enter **"Python: Configure Tests"**. + - Select "pytest." + - Select the directory for your python code. + - Select the play icon to run tests. + +1. Select Pytest from the options. + +1. Choose the folder containing your test code (`library\`) + +1. Select the play icon to run tests. + ![Screenshot showing the pytest results.](./Media/m04-pytest-configure-results-py.png) + +1. Optionally, run the ptytest command from the `library` path in the terminal passing the path to the **tests** folder and the `-v` "verbose" command `-vv` command (or `-vv` "very verbose"): + + ```plaintext + pytest tests -v + ``` + +>**NOTE** Further assistance with PyTest configuration is available in the articles: Visual Studio Microsoft Python extension and Python testing in Visual Studio Code. + +The previous steps & command will run your Unittest (and Pytest test) cases. + + >**NOTE**: Although the code was run using **Pytest** the report states that the **unittest** framework was used because there are no Pytest specific test formatted items created at this point in the lab. **Pytest** also runs all **unitest** formatted tests along with any Pytest test cases. + +### Include Pytest test cases + +Add Pytest-style test functions with GitHub Copilot Chat **Edit mode**, Provide the following files from library\tests: + +- test_json_loan_repository.py +- test_loan_service.py +- test_patron_service.py + +1. Use the following prompt: + + ```plaintext + + Add new Pytest-style test functions to the following files: test_json_loan_repository.py, test_loan_service.py, and test_patron_service.py. + - Do not remove or rewrite existing Unittest-based test classes or methods. + - Import pytest at the top if not already present. + - For each file, add: + - At least one parameterized test using @pytest.mark.parametrize. + - At least one fixture using @pytest.fixture for reusable setup. + - At least one test using pytest.raises for exception/assertion testing. + - Name all new Pytest test functions with the test_ prefix. + - Clearly separate new Pytest functions from existing Unittest classes. + - If a fixture or parameterized test needs a dummy or mock class, define it within the file or reuse an existing one. + - Demonstrate how Pytest makes tests more concise and expressive compared to Unittest. + + ``` + +1. Review the added test cases and **save**. You will test in the next section. + +## Run the unit tests using Pytest + +1. Use the toolbar method, select the flask icon ![Screenshot showing the test flask icon.](./Media/m04-pytest-flask-py.png) on the toolbar. Select the play icon to run tests. + ![Screenshot showing the Pytest results.](./Media/m04-pytest-configure-results-py.png). + +1. To fix test Errors or test failures use the ![Screenshot showing fix test failures icon.](./Media/m04-fix-pytest-test-failure-py.png). Apply the fix provide. + + - If the fix has to be made another file it can easier to select open in chat. and ask Copilot to provide the fit + + ```plaintext + @workspace provide the fix for the test failure + ``` + +## Summary + +In this exercise, you learned how to use GitHub Copilot to accelerate the development of unit tests in a Python application ato use Pytest and run. You used GitHub Copilot's Chat view in Ask mode, Agent mode, and Edit mode. You used Ask mode to examine the existing unit testing approach, Agent mode to create project folders and a new test class, and Edit mode to create unit tests. You also used GitHub Copilot's code completion feature to create a unit test. + +## Clean up + +Now that you've finished the exercise, take a minute to ensure that you haven't made changes to your GitHub account or GitHub Copilot subscription that you don't want to keep. If you made any changes, revert them now. diff --git a/Instructions/Labs/LAB_AK_04_develop_unit_tests_xunit.md b/Instructions/Labs/LAB_AK_04_develop_unit_tests_xunit.md new file mode 100644 index 0000000..46fba6d --- /dev/null +++ b/Instructions/Labs/LAB_AK_04_develop_unit_tests_xunit.md @@ -0,0 +1,764 @@ +--- +lab: + title: Exercise - Develop unit tests using GitHub Copilot + description: Learn how to accelerate the development of unit tests using GitHub in Visual Studio Code. + duration: 25 minutes + level: 200 + islab: true + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Develop unit tests using GitHub Copilot + +The large language models behind GitHub Copilot are trained on a variety of code testing frameworks and scenarios. GitHub Copilot is a great tool for generating test cases, test methods, test assertions and mocks, and test data. In this exercise, you use GitHub Copilot to accelerate the development of unit tests for a C# application. + +This exercise should take approximately **25** minutes to complete. + +> **IMPORTANT**: To complete this exercise, you must provide your own GitHub account and GitHub Copilot subscription. If you don't have a GitHub account, you can sign up for a free individual account and use a GitHub Copilot Free plan to complete the exercise. If you have access to a GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscription from within your lab environment, you can use your existing GitHub Copilot subscription to complete this exercise. + +## Before you start + +Your lab environment must include the following: Git 2.48 or later, .NET SDK 9.0 or later, Visual Studio Code with the C# Dev Kit extension, and access to a GitHub account with GitHub Copilot enabled. + +If you're using a local PC as a lab environment for this exercise: + +- For help configuring your local PC as your lab environment, open the following link in a browser: Configure your lab environment resources. + +- For help enabling your GitHub Copilot subscription in Visual Studio Code, open the following link in a browser: Enable GitHub Copilot within Visual Studio Code. + +If you're using a hosted lab environment for this exercise: + +- For help enabling your GitHub Copilot subscription in Visual Studio Code, paste the following URL into a browser's site navigation bar: Enable GitHub Copilot within Visual Studio Code. + +- To ensure that the .NET SDK is configured to use the official NuGet.org repository as a source for downloading and restoring packages: + + Open a command terminal and then run the following command: + + ```bash + + dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org + + ``` + +## Exercise scenario + +You're a developer working in the IT department of your local community. The backend systems that support the public library were lost in a fire. Your team needs to develop a temporary solution to help the library staff manage their operations until the system can be replaced. Your team chose GitHub Copilot to accelerate the development process. + +You have an initial version of the library application that includes a unit test project named UnitTests. You need to accelerate the development of additional unit tests using GitHub Copilot. + +This exercise includes the following tasks: + +1. Set up the library application in Visual Studio Code. + +1. Examine the approach to unit testing implemented by the UnitTests project. + +1. Extend the UnitTests project to begin testing the data access classes in the Library.Infrastructure project. + +## Set up the library application in Visual Studio Code + +You need to download the existing application, extract the code files, and then open the solution in Visual Studio Code. + +Use the following steps to set up the library application: + +1. Open a browser window in your lab environment. + +1. To download a zip file containing the library application, paste the following URL into your browser's address bar: [GitHub Copilot lab - develop unit tests](https://github.com/MicrosoftLearning/mslearn-github-copilot-dev/raw/refs/heads/main/DownloadableCodeProjects/Downloads/AZ2007LabAppM4.zip) + + The zip file is named **AZ2007LabAppM4.zip**. + +1. Extract the files from the **AZ2007LabAppM4.zip** file. + + For example: + + 1. Navigate to the downloads folder in your lab environment. + + 1. Right-click **AZ2007LabAppM4.zip**, and then select **Extract all**. + + 1. Select **Show extracted files when complete**, and then select **Extract**. + +1. Open the extracted files folder, then copy the **AccelerateDevGHCopilot** folder to a location that's easy to access, such as your Windows Desktop folder. + +1. Open the **AccelerateDevGHCopilot** folder in Visual Studio Code. + + For example: + + 1. Open Visual Studio Code in your lab environment. + + 1. In Visual Studio Code, on the **File** menu, select **Open Folder**. + + 1. Navigate to the Windows Desktop folder, select **AccelerateDevGHCopilot** and then select **Select Folder**. + +1. In the Visual Studio Code SOLUTION EXPLORER view, verify the following solution structure: + + - AccelerateDevGHCopilot\ + - src\ + - Library.ApplicationCore\ + - Library.Console\ + - Library.Infrastructure\ + - tests\ + - UnitTests\ + +1. Ensure that the solution builds successfully. + + For example, in the SOLUTION EXPLORER view, right-click **AccelerateDevGHCopilot**, and then select **Build**. + + You'll see some Warnings, but there shouldn't be any Errors reported. + +## Examine the approach to unit testing implemented by the UnitTests project + +In this section of the exercise, you use GitHub Copilot to examine the unit testing approach implemented by the UnitTests project. + +Use the following steps to complete this section of the exercise: + +1. Expand the **UnitTests** project in the SOLUTION EXPLORER view. + + Your existing codebase includes a UnitTests project that implements the following folder structure: + + - UnitTests\ + - ApplicationCore\ + - LoanService\ + - **ExtendLoan.cs** + - **ReturnLoan.cs** + - PatronService\ + - **RenewMembership.cs** + - LoanFactory.cs + - PatronFactory.cs + + The test project structure mirrors and supports the **Services** portion of the **ApplicationCore** project. + + - ApplicationCore\ + - Services\ + - LoanService.cs: Contains the **ExtendLoan** and **ReturnLoan** methods. + - PatronService.cs: Contains the **RenewMembership** method. + +1. Open the Chat view. + +1. Set the chat mode to **Ask** and select the **Auto** model. + + The Set Mode and Pick Model menus are in the bottom-left corner of the Chat view. + + > **NOTE**: You can use a different model if your plan allows it, but responses may differ from those shown in this exercise. Free-plan users have a limited number of monthly chat requests, so each prompt counts against your quota. + +1. Add the following files to the Chat context: + + (under the Library.ApplicationCore project) + + - LoanService.cs + - PatronService.cs + + (under UnitTests project) + + - ExtendLoan.cs + - ReturnLoan.cs + - RenewMembership.cs + - LoanFactory.cs + - PatronFactory.cs + +1. To learn about the existing unit test project, enter the following prompt: + + ```plaintext + #codebase Explain the approach to unit testing that's been implemented in this workspace. + ``` + + This prompt asks GitHub Copilot to explain the approach to unit testing in the context of the codebase. Adding relevant files to the Chat context improves the response. + +1. Take a minute to review GitHub Copilot's response. + + You should see a description that's similar to the following example: + + ```markdown + + The unit testing approach in this workspace follows a structured and comprehensive methodology to ensure the correctness of the application's core business logic. Here's an overview of the approach: + + 1. **Use of Test Factories** + - Factories like PatronFactory and LoanFactory are used to create test data. These factories provide reusable methods to generate consistent and meaningful test objects, such as patrons with specific membership statuses or loans with different states (e.g., returned, current, expired). + - This approach reduces boilerplate code in test cases and ensures that test data is consistent across different tests. + + 2. **Mocking Dependencies** + - **NSubstitute** is used to mock dependencies like repositories (IPatronRepository, ILoanRepository). This allows tests to isolate the behavior of the services being tested without relying on actual database or external dependencies. + - Mocked methods are configured to return specific data or simulate certain behaviors, enabling precise control over test scenarios. + + 3. **Service-Oriented Testing** + - Unit tests are focused on the core services (PatronService and LoanService), which encapsulate the business logic of the application. + - Each service method is tested for various scenarios, including success cases, edge cases, and error conditions. + + 4. **Comprehensive Test Coverage** + - Tests cover a wide range of scenarios for each service method: + - **PatronService.RenewMembership:** + - Success cases (e.g., renewing with or without loans, expired membership). + - Failure cases (e.g., patron not found, too early to renew, overdue loans). + - **LoanService.ReturnLoan:** + - Success cases (e.g., returning a loan with valid conditions). + - Failure cases (e.g., loan not found, already returned). + - **LoanService.ExtendLoan:** + - Success cases (e.g., extending a valid loan). + - Failure cases (e.g., loan not found, membership expired, loan already returned or expired). + - This ensures that all possible paths in the business logic are tested. + + 5. **Assertions for Expected Behavior** + - Each test uses assertions to validate the expected outcomes: + - **Return values**: Ensuring the correct status is returned (e.g., MembershipRenewalStatus.Success, LoanReturnStatus.LoanNotFound). + - **State changes**: Verifying that object properties (e.g., MembershipEnd, ReturnDate, DueDate) are updated correctly. + - **Error handling**: Ensuring exceptions or errors are handled gracefully. + + 6. **Descriptive Test Names** + - Test methods use descriptive names and Fact attributes with DisplayName to clearly indicate the scenario being tested. This improves readability and makes it easier to understand the purpose of each test. + + 7. **Focus on Business Logic** + - Tests are designed to validate the business rules implemented in the services, such as: + - Not allowing membership renewal if it's too early or if there are overdue loans. + - Preventing loan extensions for expired memberships or already returned loans. + - This ensures that the core functionality of the application behaves as expected under various conditions. + + 8. **Error Simulation** + - Tests simulate error scenarios, such as repository methods returning null or throwing exceptions, to verify that the services handle these cases appropriately. + + 9. **Constants for Configurable Logic** + - Constants like LoanService.ExtendByDays are used in the logic and verified in tests to ensure consistency and maintainability. + + **Summary** + This workspace implements a robust unit testing strategy by leveraging test factories, mocking, and comprehensive test coverage. The focus is on isolating and validating the business logic of core services, ensuring that all possible scenarios are accounted for. This approach enhances the reliability and maintainability of the application. + + ``` + +1. To analyze the benefits of the current approach, enter the following prompt: + + ```plaintext + #codebase What are the benefits of this approach to unit testing? + ``` + +1. Take a minute to review GitHub Copilot's response. + + You can continue the conversation and ask for more details about specific aspects of the unit testing approach. For example, you can ask about the benefits of using test factories, mocking dependencies, or the overall structure of the tests. + +1. Take a minute to consider how you would extend the existing approach to test the Library.Infrastructure project. + + The Library.Infrastructure project contains data access classes that interact with the file system to load and save data. + +1. Add the following files to the Chat context: + + (under the Library.ApplicationCore project) + + - LoanService.cs + - PatronService.cs + + (under UnitTests project) + + - ExtendLoan.cs + - ReturnLoan.cs + - RenewMembership.cs + - LoanFactory.cs + - PatronFactory.cs + +1. To ask GitHub Copilot for help extending the UnitTests project, enter the following prompt: + + ```plaintext + #codebase How can I extend the `UnitTests` project to begin testing methods in the `Library.Infrastructure` project? Provide a process overview. + ``` + + This prompt asks GitHub Copilot to provide a process overview for extending the UnitTests project to begin testing methods in the Library.Infrastructure project. Several files are included in the prompt to provide context for the response. + +1. Take a minute to review the response from GitHub Copilot. + + You should see an explanation that's similar to the following example: + + ```markdown + + 1. Add Project Reference: + + - Open the UnitTests.csproj file located at tests/UnitTests/UnitTests.csproj. + - Add a reference to the Library.Infrastructure project. + + 2. Identify methods to test: + + - Identify the methods in the **Library.Infrastructure** project that need to be tested. For example, if you want to test the **LoanService** class, identify the methods in the **LoanService** class that need to be tested. + + 3. Create Test Classes: + + - Create new test classes in the **tests/UnitTests** directory to test the methods in the **Library.Infrastructure** project. For example, if you want to test **LoanService**, create a file named **LoanServiceTests.cs**. + + 4. Write Unit Tests: + + - In the newly created test classes, write unit tests for the methods in the **Library.Infrastructure** project. Use the xUnit framework for writing tests and NSubstitute for mocking dependencies. + + 5. Run Tests: + + - Use Visual Studio Code's integrated terminal or the Test Explorer to run the tests. + - Ensure that the tests are discovered and executed correctly. + + ``` + + > **TIP**: Use GitHub Copilot's **Ask** mode to investigate your approach to testing. Use the responses to plan, develop, or extend your unit tests. + +## Extend the UnitTests project to begin testing data access classes + +The **Library.Infrastructure** project contains data access classes that interact with the file system to load and save data. The project includes the following classes: + +- JsonData: A class that loads and saves JSON data. +- JsonLoanRepository: A class that implements the ILoanRepository interface and uses the JsonData class to load and save loan data. +- JsonPatronRepository: A class that implements the IPatronRepository interface and uses the JsonData class to load and save patron data. + +### Use Agent mode to create a new test class + +You can use the Chat view's Agent mode when you have a specific task in mind and want to enable Copilot to autonomously edit your code. For example, you can use Agent mode to create and edit files, or to invoke tools to accomplish tasks. In Agent mode, GitHub Copilot can autonomously plan the work needed and determine the relevant files and context. It then makes edits to your codebase and invokes tools to accomplish the request you made. + +In this section of the exercise, you use GitHub Copilot's Agent mode to create a new test class for the GetLoan method of the JsonLoanRepository class. + +Use the following steps to complete this section of the exercise: + +1. In the Chat view, select the **Set Mode** button, and then select **Agent**. + + > **IMPORTANT**: When you use the Chat view in agent mode, GitHub Copilot may make multiple premium requests to complete a single task. Premium requests can be used by user-initiated prompts and follow-up actions Copilot takes on your behalf. The total number of premium requests used is based on the complexity of the task, the number of steps involved, and the model selected. + +1. To start an automated task that creates a test class for the JsonLoanRepository.GetLoan method, enter the following prompt: + + ```plaintext + + Add `Infrastructure\JsonLoanRepository` folders to the UnitTests project. Create a class file named `GetLoan.cs` in the `JsonLoanRepository` folder and create a stub class named `GetLoan`. Add a reference to the Library.Infrastructure project inside UnitTests.csproj. + + ``` + + This prompt asks GitHub Copilot to create a new folder structure and class file in the UnitTests project. + + - UnitTests\ + - Infrastructure\ + - JsonLoanRepository\ + - GetLoan.cs + + The prompt also asks GitHub Copilot to add a reference to the Library.Infrastructure project inside the UnitTests.csproj file. + +1. If prompted with a request for permission, select **Allow in this Session**. + + The **Set Permissions** menu in the Chat view provides options for **Default Approvals** and **Bypass Approvals**. The **Default Approvals** option allows GitHub Copilot to request permission for each action it takes. The **Bypass Approvals** option allows GitHub Copilot to take actions without requesting permission. For this exercise, use **Default Approvals**. + +1. Take a minute to review the response from GitHub Copilot. + + The agent displays status messages in the Chat view as it completes the requested tasks. Notice the following updates in the code editor: + + - The agent creates the folder structure in the UnitTests project for the GetLoan.cs file. + - The UnitTests.csproj file is updated to include a reference to the Library.Infrastructure project. + +1. Take a moment to review the updates. + + You should see the following updates in the editor: + + - The **GetLoan.cs** file is created in the **Infrastructure\JsonLoanRepository** folder. + - The **UnitTests** project now includes a reference to **Library.Infrastructure.csproj**. + +1. In the Chat view, to accept all changes, select **Keep**. + +1. In the SOLUTION EXPLORER view, expand the **Infrastructure\JsonLoanRepository** folder structure. + + You should see the following folder structure: + + - UnitTests\ + - Infrastructure\ + - JsonLoanRepository\ + - GetLoan.cs + +### Use the Agent mode to create unit tests for the GetLoan method + +In this section of the exercise, you use GitHub Copilot's Agent mode to create unit tests for the **GetLoan** method in the **JsonLoanRepository** class. + +Use the following steps to complete this section of the exercise: + +1. Open the **JsonLoanRepository.cs** file. + + **JsonLoanRepository.cs** is located in the **src/Library.Infrastructure/Data/** folder. + +1. Take a minute to review the **JsonLoanRepository.cs** file. + + ```csharp + using Library.ApplicationCore; + using Library.ApplicationCore.Entities; + + namespace Library.Infrastructure.Data; + + public class JsonLoanRepository : ILoanRepository + { + private readonly JsonData _jsonData; + + public JsonLoanRepository(JsonData jsonData) + { + _jsonData = jsonData; + } + + public async Task GetLoan(int id) + { + await _jsonData.EnsureDataLoaded(); + + foreach (Loan loan in _jsonData.Loans!) + { + if (loan.Id == id) + { + Loan populated = _jsonData.GetPopulatedLoan(loan); + return populated; + } + } + return null; + } + + public async Task UpdateLoan(Loan loan) + { + Loan? existingLoan = null; + foreach (Loan l in _jsonData.Loans!) + { + if (l.Id == loan.Id) + { + existingLoan = l; + break; + } + } + + if (existingLoan != null) + { + existingLoan.BookItemId = loan.BookItemId; + existingLoan.PatronId = loan.PatronId; + existingLoan.LoanDate = loan.LoanDate; + existingLoan.DueDate = loan.DueDate; + existingLoan.ReturnDate = loan.ReturnDate; + + await _jsonData.SaveLoans(_jsonData.Loans!); + + await _jsonData.LoadData(); + } + } + } + ``` + +1. Notice the following details about the **JsonLoanRepository** class: + + - The **JsonLoanRepository** class contains two methods: **GetLoan** and **UpdateLoan**. + - The **JsonLoanRepository** class uses a **JsonData** object to load and save loan data. + +1. Take a minute to consider the field and constructor requirements for the **GetLoan** test class. + + The **JsonLoanRepository.GetLoan** method receives a loan ID parameter when it's called. The method uses **_jsonData.EnsureDataLoaded** to get the latest JSON data, and **_jsonData.Loans** to search for a matching loan. If the method finds a matching loan ID, it returns a populated loan object (**populated**). If the method is unable to find a matching loan ID, it returns **null**. + + For the GetLoan unit tests: + + - You can use a mock loan repository object (**_mockLoanRepository**) to help test the case where a matching ID is found. Load the mock with the ID you want to find. The **ReturnLoanTest** class demonstrates how to mock the **ILoanRepository** interface and instantiate a mock loan repository object. + + - You can use a non-mock loan repository object (**_jsonLoanRepository**) to test the case where no matching ID is found. Just specify a loan ID that you know isn't in the file (anything over 100 should work). + + - You'll need a **JsonData** object to create a non-mock **JsonLoanRepository** object. Since the **UnitTests** project doesn't have access to the **JsonData** object created by the **ConsoleApp** project, you'll need to create one using the **IConfiguration** interface. + +1. Open the GetLoan.cs test file and select the GetLoan class. + +1. Add the following files to the Chat context: + + (under the Library.ApplicationCore project) + + - LoanService.cs + + (under the Library.Infrastructure project) + + - JsonData.cs + - JsonLoanRepository.cs + + (under the UnitTests project) + + - ReturnLoan.cs + - LoanFactory.cs + +1. Enter the following prompt: + + ```plaintext + + #codebase Create fields and a class constructor for the `GetLoan.cs` file. The class will be used to create unit tests for the GetLoan method in the `JsonLoanRepository.cs` file. Create the following private readonly fields: `_mockLoanRepository`, `_jsonLoanRepository`, `_configuration`, and `_jsonData`. Instantiate the fields in the `GetLoanTest` constructor. Use `ConfigurationBuilder` to create a `_configuration` object that can be used to instantiate the JsonData object. + + ``` + + This prompt asks GitHub Copilot to suggest fields and a class constructor. + +1. Take a minute to review the response from GitHub Copilot. + + You may see a code suggestion that's similar to the following code snippet: + + ```csharp + using NSubstitute; + using Library.ApplicationCore; + using Library.ApplicationCore.Entities; + using Library.ApplicationCore.Interfaces; + using Library.Infrastructure.Data; + using Microsoft.Extensions.Configuration; + + namespace UnitTests.Infrastructure.JsonLoanRepository; + + public class GetLoanTest + { + private readonly ILoanRepository _mockLoanRepository; + private readonly JsonLoanRepository _jsonLoanRepository; + private readonly IConfiguration _configuration; + private readonly JsonData _jsonData; + + public GetLoanTest() + { + _mockLoanRepository = Substitute.For(); + _configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + _jsonData = new JsonData(_configuration); + _jsonLoanRepository = new JsonLoanRepository(_jsonData); + } + + // Add test methods here + } + ``` + +1. In the Chat view, to accept all updates, select **Keep**. + +1. After accepting the updates, check for the following issues: + + > **NOTE**: Sample code in the following step shows updates that correct the following issues: + + - If there's a conflict between the **UnitTests.Infrastructure.JsonLoanRepository** namespace and the **JsonLoanRepository** type specified in the code, you should update the namespace in GetLoans.cs to eliminate the conflict. Follow the pattern used in the **ReturnLoan.cs** and **RenewMembership.cs** files. + + - If **ILoanRepository** isn't recognized in your code, you may need to add a **using** directive for **Library.ApplicationCore** to the top of the file. + + - If the **_configuration** object isn't instantiated correctly, you may need to update the code line containing **ConfigurationBuilder**. You can simplify the code to use **_configuration = new ConfigurationBuilder().Build();**. + + - If a **using Library.ApplicationCore.Interfaces** is suggested by GitHub Copilot, you can delete it from the top of the file. + +1. Update the **GetLoan.cs** file to address the issues you identified in the previous step. + + You can use the following code snippet as a reference: + + ```csharp + using NSubstitute; + using Library.ApplicationCore; + using Library.ApplicationCore.Entities; + using Library.Infrastructure.Data; + using Microsoft.Extensions.Configuration; + + namespace UnitTests.Infrastructure.JsonLoanRepositoryTests; + + public class GetLoanTest + { + private readonly ILoanRepository _mockLoanRepository; + private readonly JsonLoanRepository _jsonLoanRepository; + private readonly IConfiguration _configuration; + private readonly JsonData _jsonData; + + public GetLoanTest() + { + _mockLoanRepository = Substitute.For(); + _configuration = new ConfigurationBuilder().Build(); + _jsonData = new JsonData(_configuration); + _jsonLoanRepository = new JsonLoanRepository(_jsonData); + } + + } + ``` + +1. Add the following files to the Chat context: + + (under the Library.ApplicationCore project) + + - LoanService.cs + + (under the Library.ApplicationCore project) + + - Loans.json. + + (under the Library.Infrastructure project) + + - JsonData.cs + - JsonLoanRepository.cs + + (under the UnitTests project) + + - ReturnLoan.cs + - LoanFactory.cs + +1. Select the contents of the **GetLoan.cs** file, and then enter the following prompt in the Chat view: + + ```plaintext + #codebase Update the selection to include a unit test for the `JsonLoanRepository.GetLoan` method. The unit test should test the case where a loan ID is found in the data. Use `_mockLoanRepository` to arrange the expected return loan. Use `_jsonLoanRepository` to return an actual loan. Asserts should verify that the return loan ID matches the expected loan ID. Use a loan ID that exists in the `Loans.json` file. + ``` + + This prompt asks GitHub Copilot to suggest a unit test for the **JsonLoanRepository.GetLoan** method. The unit test should test the case where a loan ID is found in the data. The test should use **_mockLoanRepository** to arrange the expected return loan, **_jsonLoanRepository** to return an actual loan, and asserts to verify that the return loan ID matches the expected loan ID. The loan ID should exist in the **Loans.json** file. + +1. Take a minute to review the updates suggested by GitHub Copilot. + + You should see a code suggestion that's similar to the following code snippet: + + ```csharp + using NSubstitute; + using Library.ApplicationCore; + using Library.ApplicationCore.Entities; + using Library.Infrastructure.Data; + using Microsoft.Extensions.Configuration; + using Xunit; + + namespace UnitTests.Infrastructure.JsonLoanRepositoryTests; + + public class GetLoanTest + { + private readonly ILoanRepository _mockLoanRepository; + private readonly JsonLoanRepository _jsonLoanRepository; + private readonly IConfiguration _configuration; + private readonly JsonData _jsonData; + + public GetLoanTest() + { + _mockLoanRepository = Substitute.For(); + _configuration = new ConfigurationBuilder().Build(); + _jsonData = new JsonData(_configuration); + _jsonLoanRepository = new JsonLoanRepository(_jsonData); + } + + [Fact(DisplayName = "JsonLoanRepository.GetLoan: Returns loan when loan ID is found")] + public async Task GetLoan_ReturnsLoanWhenLoanIdIsFound() + { + // Arrange + var loanId = 1; // Use a loan ID that exists in the Loans.json file + var expectedLoan = new Loan + { + Id = loanId, + BookItemId = 17, + PatronId = 22, + LoanDate = DateTime.Parse("2023-12-08T00:40:43.1808862"), + DueDate = DateTime.Parse("2023-12-22T00:40:43.1808862"), + ReturnDate = null + }; + + _mockLoanRepository.GetLoan(loanId).Returns(expectedLoan); + + // Act + var actualLoan = await _jsonLoanRepository.GetLoan(loanId); + + // Assert + Assert.NotNull(actualLoan); + Assert.Equal(expectedLoan.Id, actualLoan?.Id); + } + } + ``` + + > **NOTE**: Ensure that the BookItemId and PatronId values are valid. + +1. In the Chat view, to accept all updates, select **Keep**. + + If the **Loan** class isn't recognized in your code, ensure that you have a **using Library.ApplicationCore.Entities;** statement at the top of the GetLoan.cs file. The **Loan** class is located in the **Library.ApplicationCore.Entities** namespace. + +1. Build the **AccelerateDevGitHubCopilot** solution to ensure there are no errors. + +1. Use GitHub Copilot's ghost text suggestion feature to create a test for the case where the loan ID isn't found. + + Create a blank line after the **GetLoan_ReturnsLoanWhenLoanIdIsFound** method. + + Accept the autocompletion suggestions to create a new test method. + + > **NOTE**: Code completions may appear one line at a time. You may need to press **Tab** or **Enter** several times to get the completed unit test code. + +1. Take a minute to review the new unit text. + + You should see a suggested unit text that's similar to the following code snippets: + + ```csharp + + [Fact(DisplayName = "JsonLoanRepository.GetLoan: Returns null when ID is not found")] + public async Task GetLoan_ReturnsNullWhenIdIsNotFound() + { + // Arrange + var loanId = 999; // Loan ID that does not exist in Loans.json + + _mockLoanRepository.GetLoan(loanId).Returns((Loan?)null); + + // Act + var actualLoan = await _jsonLoanRepository.GetLoan(loanId); + + // Assert + Assert.Null(actualLoan); + } + + ``` + + GitHub Copilot's autocompletion feature may mock an expected loan even though it isn't needed, so you could get the following code snippet: + + ```csharp + [Fact(DisplayName = "JsonLoanRepository.GetLoan: Returns null when loan ID is not found")] + public async Task GetLoan_ReturnsNullWhenLoanIdIsNotFound() + { + // Arrange + var loanId = 999; // Use a loan ID that does not exist in the Loans.json file + var expectedLoan = new Loan { Id = loanId, BookItemId = 101, PatronId = 202, LoanDate = DateTime.Now, DueDate = DateTime.Now.AddDays(14) }; + _mockLoanRepository.GetLoan(loanId).Returns(expectedLoan); + + // Act + var actualLoan = await _jsonLoanRepository.GetLoan(loanId); + + // Assert + Assert.Null(actualLoan); + } + + ``` + + You can delete the code that mocks an expected loan, but you need a loan ID that doesn't exist in the **Loans.json** file. + + Ensure that your "Returns null when loan ID is not found" unit test assigns a **loanId** value that isn't in the data set. + +1. Notice that the unit tests require access to the JSON data files. + + The **JsonLoanRepository.GetLoan** method uses a **JsonData** object to load and save loan data. + + The JSON data files are located in the **Library.Console\Json** folder. You need to update the **UnitTests.csproj** file to include these files in the test project. + +1. Add the following XML snippet to the **UnitTests.csproj** file: + + ```xml + + + Json\%(RecursiveDir)%(FileName)%(Extension) + PreserveNewest + + + ``` + + This ensures that the JSON data files are copied to the output directory when the tests are run. + +## Run the unit tests + +There are several ways to run the unit tests for the **JsonLoanRepository** class. You can use Visual Studio Code's Test Explorer, the integrated terminal, or the **dotnet test** command. + +Use the following steps to complete this section of the exercise: + +1. Ensure that you have the GetLoans.cs file open in editor. + +1. Build the solution and ensure that there are no errors. + + Right-click **AccelerateDevGitHubCopilot** and then select **Build**. + +1. Notice the "green play button" to the left of the test methods. + +1. Open Visual Studio Code's Test Explorer view. + + To open the Test Explorer view, select the beaker-shaped icon on the left-side Activity bar. The Test Explorer is labeled "Testing" in the user interface. + + The Test Explorer is a tree view that shows all the test cases in your workspace. You can run/debug your test cases and view the test results using Test Explorer. + +1. Expand **UnitTests** and the underlying nodes to locate **GetLoanTest**. + +1. Run the **JsonLoanRepository.GetLoan: Returns loan when loan ID is found** test case. + +1. Notice that the test results are displayed in the Test Explorer view and the code Editor. + + You should see a green checkmark that indicates the test passed. + +1. Use the code Editor to run the **JsonLoanRepository.GetLoan: Returns null when loan ID is not found** test case. + + To run the test from the code Editor, select the green play button to the left of the test method. + + Notice that the test results are displayed in the Test Explorer view and the code Editor. + + Ensure that the **JsonLoanRepository.GetLoan: Returns null when loan ID is not found** test passes. You should see a green checkmark to the left of both tests. + +## Summary + +In this exercise, you learned how to use GitHub Copilot to accelerate the development of unit tests in a C# application. You used GitHub Copilot's Chat view in Ask mode and Agent mode. You used Ask mode to examine the existing unit testing approach and plan how to extend it. You used Agent mode to create project folders, a new test class, and unit tests for the GetLoan method. You also used GitHub Copilot's code completion feature to create an additional unit test. + +## Clean up + +Now that you've finished the exercise, take a minute to ensure that you haven't made changes to your GitHub account or GitHub Copilot subscription that you don't want to keep. If you made any changes, revert them now. diff --git a/Instructions/Labs/LAB_AK_05_refactor_improve_existing_code.md b/Instructions/Labs/LAB_AK_05_refactor_improve_existing_code.md new file mode 100644 index 0000000..3faa0b0 --- /dev/null +++ b/Instructions/Labs/LAB_AK_05_refactor_improve_existing_code.md @@ -0,0 +1,1094 @@ +--- +lab: + title: Exercise - Refactor existing code using GitHub Copilot + description: Learn how to refactor and improve existing code sections using GitHub Copilot in Visual Studio Code. + duration: 30 minutes + level: 200 + islab: true + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Refactor existing code using GitHub Copilot + +GitHub Copilot can be used to evaluate your entire codebase and suggest updates that help you to refactor and improve your code. In this exercise, you use GitHub Copilot to refactor specified sections of a C# application while making improvements to code quality, reliability, performance, and security. + +This exercise should take approximately **30** minutes to complete. + +> **IMPORTANT**: To complete this exercise, you must provide your own GitHub account and GitHub Copilot subscription. If you don't have a GitHub account, you can sign up for a free individual account and use a GitHub Copilot Free plan to complete the exercise. If you have access to a GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscription from within your lab environment, you can use your existing GitHub Copilot subscription to complete this exercise. + +## Before you start + +Your lab environment must include the following: Git 2.48 or later, .NET SDK 9.0 or later, Visual Studio Code with the C# Dev Kit extension, and access to a GitHub account with GitHub Copilot enabled. + +If you're using a local PC as a lab environment for this exercise: + +- For help configuring your local PC as your lab environment, open the following link in a browser: Configure your lab environment resources. + +- For help enabling your GitHub Copilot subscription in Visual Studio Code, open the following link in a browser: Enable GitHub Copilot within Visual Studio Code. + +If you're using a hosted lab environment for this exercise: + +- For help enabling your GitHub Copilot subscription in Visual Studio Code, paste the following URL into a browser's site navigation bar: Enable GitHub Copilot within Visual Studio Code. + +- To ensure that the .NET SDK is configured to use the official NuGet.org repository as a source for downloading and restoring packages: + + Open a command terminal and then run the following command: + + ```bash + + dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org + + ``` + +## Exercise scenario + +You're a developer working in the IT department of your local community. The backend systems that support the public library were lost in a fire. Your team needs to develop a temporary solution to help the library staff manage their operations until the system can be replaced. Your team chose GitHub Copilot to accelerate the development process. + +You handed off an initial version of the library application for review. The review team identified opportunities to improve code quality, performance, readability, maintainability, and security. + +The following updates are assigned to you: + +1. Refactor the EnumHelper class to use static dictionaries instead of reflection. + + - Using static dictionaries will improve performance (removes the overhead of reflection). + - Eliminating reflection also improves code readability, maintainability, and security. + +1. Refactor the data access methods to use LINQ (Language Integrated Query) rather than foreach loops. + + - Using LINQ provides a more concise and readable way to query collections, databases, and XML documents. + - Using LINQ can improve code readability, maintainability, and performance. + +This exercise includes the following tasks: + +1. Set up the library application in Visual Studio Code. +1. Analyze and refactor code using the Chat view in Ask and Agent modes. + +## Set up the library application in Visual Studio Code + +You need to download the existing application, extract the code files, and then open the solution in Visual Studio Code. + +Use the following steps to set up the library application: + +1. Open a browser window in your lab environment. + +1. To download a zip file containing the library application, paste the following URL into your browser's address bar: [GitHub Copilot lab - refactor existing code](https://github.com/MicrosoftLearning/mslearn-github-copilot-dev/raw/refs/heads/main/DownloadableCodeProjects/Downloads/AZ2007LabAppM5.zip) + + The zip file is named **AZ2007LabAppM5.zip**. + +1. Extract the files from the **AZ2007LabAppM5.zip** file. + + For example: + + 1. Navigate to the downloads folder in your lab environment. + + 1. Right-click **AZ2007LabAppM5.zip**, and then select **Extract all**. + + 1. Select **Show extracted files when complete**, and then select **Extract**. + +1. Open the extracted files folder, then copy the **AccelerateDevGHCopilot** folder to a location that's easy to access, such as your Windows Desktop folder. + +1. Open the **AccelerateDevGHCopilot** folder in Visual Studio Code. + + For example: + + 1. Open Visual Studio Code in your lab environment. + + 1. In Visual Studio Code, on the **File** menu, select **Open Folder**. + + 1. Navigate to the Windows Desktop folder, select **AccelerateDevGHCopilot** and then select **Select Folder**. + +1. In the Visual Studio Code SOLUTION EXPLORER view, verify the following solution structure: + + - AccelerateDevGHCopilot\ + - src\ + - Library.ApplicationCore\ + - Library.Console\ + - Library.Infrastructure\ + - tests\ + - UnitTests\ + +1. Ensure that the solution builds successfully. + + For example, in the SOLUTION EXPLORER view, right-click **AccelerateDevGHCopilot**, and then select **Build**. + + You'll see some Warnings, but there shouldn't be any Errors reported. + +## Analyze and refactor code using the Chat view in Ask and Agent modes + +Reflection is a powerful coding feature that allows you to inspect and manipulate objects at runtime. However, reflection can be slow and there are potential security risks associated with reflection that should be considered. + +You need to: + +1. Analyze your workspace and investigate how to address your assigned task. +1. Refactor the EnumHelper class to use static dictionaries instead of reflection. + +### Analyze the EnumHelper class using the Chat view in Ask mode + +GitHub Copilot's Chat view has three modes: **Ask**, **Plan**, and **Agent**. Each mode is designed for different types of interactions with GitHub Copilot. + +- Ask: The Ask mode works best for answering questions about your codebase, coding, and general technology concepts. Use Ask mode when you want to understand how something works, explore ideas, or get help with coding tasks. +- Plan: The Plan mode is optimized for creating a structured implementation plan for a coding task. Use the plan agent when you want to break down a complex feature or change into smaller, manageable steps before implementation. +- Agent: The Agent mode is optimized for complex coding tasks based on high-level requirements that might require running terminal commands and tools. The AI operates autonomously, determining the relevant context and files to edit, planning the work needed, and iterating to resolve problems as they arise. + +In this section of the exercise, you use the Chat view in Ask mode to analyze your coding assignment. + +Use the following steps to complete this section of the exercise: + +1. In the SOLUTION EXPLORER view, expand the **Library.ApplicationCore** folder, and then expand the **Enums** folder. + +1. Open the EnumHelper.cs file and review the existing code. + + ```csharp + using System.ComponentModel; + using System.Reflection; + + namespace Library.ApplicationCore.Enums; + + public static class EnumHelper + { + public static string GetDescription(Enum value) + { + if (value == null) + return string.Empty; + + FieldInfo fieldInfo = value.GetType().GetField(value.ToString())!; + + DescriptionAttribute[] attributes = + (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false); + + if (attributes != null && attributes.Length > 0) + { + return attributes[0].Description; + } + else + { + return value.ToString(); + } + } + } + ``` + +1. Open GitHub Copilot's Chat view. + + The Chat view provides a managed conversational interface for interacting with GitHub Copilot. + + You can toggle the Chat view between open and closed using the **Toggle Chat** button, which is located at the top of the Visual Studio Code window, just to the right of the search textbox. You can also use the keyboard shortcut **Ctrl+Alt+I** to toggle the Chat view. + +1. Ensure that the **Ask** mode is selected in the Set Agent dropdown menu. + + The Set Agent dropdown menu is displayed near the bottom-right corner of the Chat view. + +1. Set the chat mode to **Ask** and select the **Auto** model. + + The Set Mode and Pick Model menus are in the bottom-left corner of the Chat view. + + > **NOTE**: You can use a different model if your plan allows it, but responses may differ from those shown in this exercise. Free-plan users have a limited number of monthly chat requests, so each prompt counts against your quota. + +1. Select the code in the EnumHelper.cs file. + +1. Review and then submit the following prompt: + + ```plaintext + #codebase Explain how the GetDescription method uses reflection to assign the return value. + ``` + +1. Take a minute to review the response. + + The **GetDescription** method uses reflection to retrieve the description attribute of an enum parameter named **value**. + + The method checks if the **value** parameter is null. If it is, the method returns an empty string. Otherwise, it uses reflection to get the field information for the enum value and retrieves the attributes of type **DescriptionAttribute**. If any attributes are found, it returns the description; otherwise, it returns the string representation of the enum value. + +1. Review and then submit the following prompt: + + ```plaintext + #codebase Which files in this workspace are used to store the enum values passed to the GetDescription method? + ``` + + The response should tell you to check the Enums folder. The enum values are defined in the **LoanExtensionStatus**, **LoanReturnStatus**, and **MembershipRenewalStatus** files. + +1. Add the following files to the Chat context: + + - EnumHelper.cs + - LoanExtensionStatus.cs + - LoanReturnStatus.cs + - MembershipRenewalStatus.cs + + You can use a drag-and-drop operation to add the files from Visual Studio Code's explorer view to the Chat view. You can also use the **Add Context** button in the Chat view to add files and other resources. + + > **NOTE**: Adding files to the Chat context ensures that GitHub Copilot considers those files when generating a response. The relevance and accuracy of responses increase when GitHub Copilot understands the context associated with your prompts. + +1. Review and then submit the following prompt: + + ```plaintext + + #codebase I need to refactor the `EnumHelper` class and remove any code that uses reflection. Use static dictionaries to supply enum description attributes. Use a separate dictionary for each enum. The dictionaries should use values from the `LoanExtensionStatus.cs`, `LoanReturnStatus.cs`, and `MembershipRenewalStatus.cs` files. Explain how to update the EnumHelper class using dictionaries and show me the updated code. + + ``` + + When writing any prompt, clarity and context are important. Using chat participants, slash commands, and chat variables helps to define context in a way that GitHub Copilot can understand. + + When writing a prompt that asks GitHub Copilot how to solve a problem, start with the problem that you're trying to solve. Use concise sentences to describe details, specify constraints, and identify resources. Finally, be sure to tell GitHub Copilot what to include in the response. + + In this case, your prompt begins with a description of your problem/goal. You tell GitHub Copilot that you need to refactor the **EnumHelper** class and remove any code that uses reflections. You tell GitHub Copilot that you want to use dictionaries to supply enum description attributes. You specify that you want a separate dictionary for each enum, and you tell GitHub Copilot that the enum values should be sourced from the files that you name. You finish by asking GitHub Copilot to explain how to update the **EnumHelper** class, and you ask for a code sample containing the updated code. + +1. Take a minute to review the response provided by GitHub Copilot. + + The response should be similar to the following markdown and code samples: + + ```markdown + + To refactor the EnumHelper class and remove reflection, you can create static dictionaries for each enum type. These dictionaries will map each enum value to its description. The dictionaries will be initialized with the descriptions defined in the Description attributes of the enums. + + Here's how to update the **EnumHelper** class: + + 1. Create a static dictionary for each enum (LoanExtensionStatus, LoanReturnStatus, and MembershipRenewalStatus). + 2. Populate these dictionaries with the enum values as keys and their descriptions as values. + 3. Update the GetDescription method to retrieve the description from the appropriate dictionary based on the enum type. + + Here’s the updated code: + + ``` + + ```csharp + + using System.Collections.Generic; + + namespace Library.ApplicationCore.Enums; + + public static class EnumHelper + { + private static readonly Dictionary LoanExtensionStatusDescriptions = new() + { + { LoanExtensionStatus.Success, "Book loan extension was successful." }, + { LoanExtensionStatus.LoanNotFound, "Loan not found." }, + { LoanExtensionStatus.LoanExpired, "Cannot extend book loan as it already has expired. Return the book instead." }, + { LoanExtensionStatus.MembershipExpired, "Cannot extend book loan due to expired patron's membership." }, + { LoanExtensionStatus.LoanReturned, "Cannot extend book loan as the book is already returned." }, + { LoanExtensionStatus.Error, "Cannot extend book loan due to an error." } + }; + + private static readonly Dictionary LoanReturnStatusDescriptions = new() + { + { LoanReturnStatus.Success, "Book was successfully returned." }, + { LoanReturnStatus.LoanNotFound, "Loan not found." }, + { LoanReturnStatus.AlreadyReturned, "Cannot return book as the book is already returned." }, + { LoanReturnStatus.Error, "Cannot return book due to an error." } + }; + + private static readonly Dictionary MembershipRenewalStatusDescriptions = new() + { + { MembershipRenewalStatus.Success, "Membership renewal was successful." }, + { MembershipRenewalStatus.PatronNotFound, "Patron not found." }, + { MembershipRenewalStatus.TooEarlyToRenew, "It is too early to renew the membership." }, + { MembershipRenewalStatus.LoanNotReturned, "Cannot renew membership due to an outstanding loan." }, + { MembershipRenewalStatus.Error, "Cannot renew membership due to an error." } + }; + + public static string GetDescription(TEnum value) where TEnum : Enum + { + return value switch + { + LoanExtensionStatus loanExtensionStatus => LoanExtensionStatusDescriptions[loanExtensionStatus], + LoanReturnStatus loanReturnStatus => LoanReturnStatusDescriptions[loanReturnStatus], + MembershipRenewalStatus membershipRenewalStatus => MembershipRenewalStatusDescriptions[membershipRenewalStatus], + _ => value.ToString() + }; + } + } + + ``` + + ```markdown + + Explanation: + + 1. Static Dictionaries: Each dictionary is specific to an enum type and maps enum values to their descriptions. + 2. Generic Method: The GetDescription method is now generic and uses a switch expression to determine which dictionary to use based on the enum type. + 3. No Reflection: The use of reflection is completely removed, improving performance and type safety. + + This approach ensures that the descriptions are statically defined and avoids the runtime overhead of reflection. + + ``` + +1. In the Chat view, hover the mouse pointer over the code sample included in the response. + +1. Notice the three buttons that appear in the top-right corner of the code snippet. + +1. Hover the mouse pointer over each of the buttons to see a tooltip that describes the action. + + The first two buttons copy code into the editor. The third button copies code to the clipboard. + +> **NOTE**: You could use the Ask mode to update the **EnumHelper** class. However, the Agent mode refactors your code directly within the code editor and provides more options for accepting updates. + +### Refactor the EnumHelper class using the Chat view in Agent mode + +The Chat view's Agent mode is designed for editing code in your workspace. You can use the Agent mode to refactor code, add comments, or make other changes to your code. + +1. In the Chat view, use the **Set Agent** dropdown menu to select **Agent** mode. + + In **Agent** mode, GitHub Copilot works in both the Chat view and the code editor. The Chat view is used to keep track of the conversation and provide context, while the code editor is used to make direct changes to your code. The Agent mode is generally used when implementing a new feature, fixing a bug, or refactoring code. + +1. Add the following files to the Chat context: + + - EnumHelper.cs + - LoanExtensionStatus.cs + - LoanReturnStatus.cs + - MembershipRenewalStatus.cs + +1. Review and then submit the following prompt: + + ```plaintext + + #codebase I need to refactor the `GetDescription` method in the `EnumHelper` class and remove any code that uses reflection. Use static dictionaries to supply enum description attributes. Use a separate dictionary for each enum. The dictionaries should use values from the `LoanExtensionStatus.cs`, `LoanReturnStatus.cs`, and `MembershipRenewalStatus.cs` files. I want a single GetDescription method that uses pattern matching to determine the type of the enum and retrieve the description from the appropriate dictionary. + + ``` + + This prompt tells GitHub Copilot to refactor the **EnumHelper** class using dictionaries rather than reflection to assign enum description attributes. It specifies that a separate dictionary should be used for each enum, and that the enum values should be sourced from specific files. + +1. Take a minute to review the suggested code updates. + + Review the suggested updates to ensure that the enum values are coming from the **LoanExtensionStatus.cs**, **LoanReturnStatus.cs**, and **MembershipRenewalStatus.cs** files. + + You can open each of the enum files to verify that the enum values in the dictionaries are correct. If you find discrepancies, have GitHub Copilot update the dictionaries for each enum individually. For example, you can use the following prompt for the **LoanExtensionStatus** enum: + + ```plaintext + + #codebase Use the description values in LoanExtensionStatus.cs to update the LoanExtensionStatus dictionary in the EnumHelper class. Provide the updated code for the LoanExtensionStatus dictionary in the EnumHelper class. + + ``` + +1. In the Chat view, to accept all updates, select **Keep**. + + You could also use the Chat Edits toolbar near the bottom of the code editor tab to accept or reject code updates. + +1. Take a minute to review the updated **GetDescription** method. + + GitHub Copilot should have updated the **GetDescription** method to use pattern matching and static dictionaries instead of reflection. The updated method should look similar to one of the following examples: + + ```csharp + + public static string GetDescription(Enum value) + { + if (value == null) + return string.Empty; + + // Use type checks to select the correct dictionary + if (value is LoanExtensionStatus les && LoanExtensionStatusDescriptions.TryGetValue(les, out var lesDesc)) + return lesDesc; + if (value is LoanReturnStatus lrs && LoanReturnStatusDescriptions.TryGetValue(lrs, out var lrsDesc)) + return lrsDesc; + if (value is MembershipRenewalStatus mrs && MembershipRenewalStatusDescriptions.TryGetValue(mrs, out var mrsDesc)) + return mrsDesc; + + return value.ToString(); + } + + ``` + + or + + ```csharp + + public static string GetDescription(TEnum value) where TEnum : Enum + { + return value switch + { + MembershipRenewalStatus status => MembershipRenewalDescriptions.TryGetValue(status, out var description) ? description : status.ToString(), + LoanReturnStatus status => LoanReturnDescriptions.TryGetValue(status, out var description) ? description : status.ToString(), + LoanExtensionStatus status => LoanExtensionDescriptions.TryGetValue(status, out var description) ? description : status.ToString(), + _ => value.ToString() + }; + } + + ``` + + This code uses pattern matching to determine the type of the enum and retrieve the description from the appropriate dictionary. The **switch** statement checks the type of the enum **value** and returns the corresponding description from the dictionary. If the enum value isn't found in the dictionary, the method falls back to calling ToString() on the enum value, which returns the name of the enum member as a string. + + If you have GitHub Copilot refactor the GetDescription method to eliminate the lambda expressions, the underlying logic is easier to follow: + + ```csharp + + public static string GetDescription(TEnum value) where TEnum : Enum + { + switch (value) + { + case MembershipRenewalStatus status: + string membershipDescription; + if (MembershipRenewalDescriptions.TryGetValue(status, out membershipDescription)) + { + return membershipDescription; + } + return status.ToString(); + + case LoanReturnStatus status: + string loanReturnDescription; + if (LoanReturnDescriptions.TryGetValue(status, out loanReturnDescription)) + { + return loanReturnDescription; + } + return status.ToString(); + + case LoanExtensionStatus status: + string loanExtensionDescription; + if (LoanExtensionDescriptions.TryGetValue(status, out loanExtensionDescription)) + { + return loanExtensionDescription; + } + return status.ToString(); + + default: + return value.ToString(); + } + } + + ``` + +1. Build your solution to ensure that there are no errors were introduced. + + You'll see the same warnings that you saw at the start of this exercise, but there shouldn't be any error messages. + +## Refactor code using the Chat view in Agent mode + +LINQ is a powerful feature in C# that allows you to query collections, databases, and XML documents in a uniform way. LINQ provides a more concise and readable way to query data compared to traditional foreach loops. + +This section of the exercise includes using the Agent mode to refactor the JsonData, JsonLoanRepository, and JsonPatronRepository classes to use LINQ instead of foreach loops. + +### Refactor the JsonData class using Agent mode + +The JsonData class includes the following data access methods: GetPopulatedPatron, GetPopulatedLoan, GetPopulatedBookItem, GetPopulatedBook. These methods use foreach loops to iterate over collections and populate objects. You can refactor these methods to use LINQ to improve code readability and maintainability. + +Use the following steps to complete this section of the exercise: + +1. Ensure that the Chat view is open and that the **Agent** mode is selected in the Set Agent dropdown menu. + +1. In the SOLUTION EXPLORER view, expand the **Library.Infrastructure** project, and then expand the **Data** folder. + +1. Open the JsonData.cs file. + +1. Scroll down to locate the **GetPopulatedPatron** method. + + The **GetPopulatedPatron** method is designed to create a fully populated library **Patron** object. It copies the basic properties of the **Patron** and populates its **Loans** collection with detailed **Loan** objects. + +1. Select the **GetPopulatedPatron** method. + + ```csharp + + public Patron GetPopulatedPatron(Patron p) + { + Patron populated = new Patron + { + Id = p.Id, + Name = p.Name, + ImageName = p.ImageName, + MembershipStart = p.MembershipStart, + MembershipEnd = p.MembershipEnd, + Loans = new List() + }; + + foreach (Loan loan in Loans!) + { + if (loan.PatronId == p.Id) + { + populated.Loans.Add(GetPopulatedLoan(loan)); + } + } + + return populated; + } + + ``` + +1. In the Chat view, enter a prompt that refactors the method using LINQ. + + ```plaintext + #selection refactor selection to `return new Patron` using LINQ + ``` + +1. Take a minute to review the suggested update. + + The suggested update should look similar to the following code: + + ```csharp + public Patron GetPopulatedPatron(Patron p) + { + return new Patron + { + Id = p.Id, + Name = p.Name, + ImageName = p.ImageName, + MembershipStart = p.MembershipStart, + MembershipEnd = p.MembershipEnd, + Loans = Loans! + .Where(loan => loan.PatronId == p.Id) + .Select(GetPopulatedLoan) + .ToList() + }; + } + ``` + + Notice that a LINQ query is used to replace the foreach loop. + + The LINQ code uses the object initializer to assign object properties to the new **Patron** object. This removes the requirement for a separate **populated** instance of the **Patron** object. Overall, the updated code is shorter and more readable. + + The code uses the patron **p** to assign some basic properties to the new **Patron** object. Then it populates the **Loans** collection with loans that are associated with the Patron parameter **p**, transforming each loan using the **GetPopulatedLoan** method. + + You can break down the LINQ code line that populates the **Loans** collection: + + - **Loans!**: The **Loans!** expression accesses the **Loans** collection. The **!** operator is a null-forgiving operator, indicating that the developer is confident that **Loans** is not null. You should ensure that **Loans** is properly initialized before calling the **GetPopulatedPatron** method. + + - **.Where(loan => loan.PatronId == p.Id)**: This code filters the loans to include only those that belong to the input patron **p**. + + - **.Select(GetPopulatedLoan)**: This code transforms each filtered loan using the **GetPopulatedLoan** method. + + - **.ToList()**: Converts the result to a **List\**. + +1. To accept the suggested update, select **Keep**. + + You're going to use this same approach to refactor three other methods. + +1. Refactor the **GetPopulatedLoan**, **GetPopulatedBookItem**, and **GetPopulatedBook** methods using the same approach. + + For example, use the following prompts to refactor the three methods: + + For the **GetPopulatedLoan** method: + + ```plaintext + + #selection refactor selection to `return new Loan` using LINQ. Use `GetPopulatedBookItem` for the `BookItem` property. Use `Single` for BookItem and Patron properties. + + ``` + + For the **GetPopulatedBookItem** method: + + ```plaintext + + #selection refactor selection to `return new BookItem` using LINQ. Use `GetPopulatedBook` and `Single` for the `BookItem` property. + + ``` + + For the **GetPopulatedBook** method: + + ```plaintext + + #selection refactor selection to `return new Book` using LINQ. Use `Where` and `Select` for `Author` property. Use `First` author. + + ``` + +1. After accepting the suggested updates, take a minute to review your code changes. + + Your updated code should look similar to the following code: + + ```csharp + public Loan GetPopulatedLoan(Loan l) + { + return new Loan + { + Id = l.Id, + BookItemId = l.BookItemId, + PatronId = l.PatronId, + LoanDate = l.LoanDate, + DueDate = l.DueDate, + ReturnDate = l.ReturnDate, + BookItem = GetPopulatedBookItem(BookItems!.Single(bi => bi.Id == l.BookItemId)), + Patron = Patrons!.Single(p => p.Id == l.PatronId) + }; + } + + public BookItem GetPopulatedBookItem(BookItem bi) + { + return new BookItem + { + Id = bi.Id, + BookId = bi.BookId, + AcquisitionDate = bi.AcquisitionDate, + Condition = bi.Condition, + Book = GetPopulatedBook(Books!.Single(b => b.Id == bi.BookId)) + }; + } + + public Book GetPopulatedBook(Book b) + { + return new Book + { + Id = b.Id, + Title = b.Title, + AuthorId = b.AuthorId, + Genre = b.Genre, + ISBN = b.ISBN, + ImageName = b.ImageName, + Author = Authors!.Where(a => a.Id == b.AuthorId).Select(a => new Author { + Id = a.Id, + Name = a.Name + }).First() + }; + } + ``` + +1. Use the **Explain** smart action to display an explanation of the LINQ queries. + + To open the **Explain** smart action, select code in the editor, right-click the selected code, and then select **Explain**. The **Explain** smart action provides a detailed explanation of the selected code. In this case, th LINQ queries used in the code. + + For example, you can use the **Explain** smart action on the **GetPopulatedBook** method to see an explanation of the LINQ query used to populate the **Author** property of the **Book** object. + + ```csharp + Author = Authors!.Where(a => a.Id == b.AuthorId).Select(a => new Author { + Id = a.Id, + Name = a.Name + }).First() + ``` + + The **Explain** smart action provides a detailed explanation of the LINQ query used to populate the **Author** property of the **Book** object. + + For example, the explanation might look like this: + + ```plaintext + + The active selection is a C# code snippet that assigns a value to the Author property. This value is derived from a collection of Author objects named Authors. The code uses LINQ to filter and transform the data within this collection. + + First, the Authors! expression uses the null-forgiving operator (!) to indicate that Authors is not null, even if the compiler might think otherwise. This is a way to suppress nullable warnings. The Where method is then called on the Authors collection to filter the elements. The lambda expression a => a.Id == b.AuthorId is used to find all Author objects where the Id matches the AuthorId property of another object b. + + After filtering, the Select method is used to project each filtered Author object into a new Author object. This is done by creating a new instance of the Author class and copying the Id and Name properties from the original Author object. + + Finally, the First method is called to retrieve the first element from the resulting sequence. This means that the Author property will be assigned the first Author object that matches the filter criteria and has been projected into a new Author instance. + + This approach ensures that the Author property is set to a new Author object with the same Id and Name as the first matching Author in the Authors collection. + + ``` + +1. Build your solution to ensure that there are no errors. + +### Refactor the JsonLoanRepository class using the Chat view in Agent mode + +The JsonLoanRepository class includes the **GetLoan** and **UpdateLoan** data access methods. You'll refactor these two methods, replacing foreach loops with LINQ to improve code readability and maintainability. + +Use the following steps to complete this section of the exercise: + +1. Open the **JsonLoanRepository.cs** file. + +1. Select the **GetLoan** method. + + The **GetLoan** method is designed to retrieve a loan by its ID. + + ```csharp + public async Task GetLoan(int id) + { + await _jsonData.EnsureDataLoaded(); + + foreach (Loan loan in _jsonData.Loans!) + { + if (loan.Id == id) + { + Loan populated = _jsonData.GetPopulatedLoan(loan); + return populated; + } + } + + return null; + } + ``` + +1. Enter a prompt that refactors the method using LINQ. + + For example, enter the following prompt: + + ```plaintext + refactor the foreach using LINQ. Use Where, Select, and GetPopulatedLoan return the first matching loan. + ``` + +1. Take a minute to review the suggested update. + + The suggested update should look similar to the following code: + + ```csharp + public async Task GetLoan(int id) + { + await _jsonData.EnsureDataLoaded(); + + return _jsonData.Loans! + .Where(l => l.Id == id) + .Select(l => _jsonData.GetPopulatedLoan(l)) + .FirstOrDefault(); + + } + ``` + + The updated code uses LINQ to filter the loans collection to include only the loan with the specified ID. Notice that **loan** should be declared as nullable (**Loan? loan**). It then transforms the loan using the **GetPopulatedLoan** method and returns the first result. If no matching loan is found, **FirstOrDefault** returns **null**. The method then returns this loan object, which may be null if no loan with the specified **id** exists. This approach ensures that the returned loan is fully populated with all necessary related data, providing a comprehensive view of the loan record. + + GitHub Copilot could also suggest the following code, which is functionally equivalent: + + ```csharp + public async Task GetLoan(int id) + { + await _jsonData.EnsureDataLoaded(); + + Loan? loan = _jsonData.Loans! + .Where(l => l.Id == id) + .Select(l => _jsonData.GetPopulatedLoan(l)) + .FirstOrDefault(); + + return loan; + } + ``` + +1. To accept the updated GetLoan method, select **Keep**. + +1. Select the **UpdateLoan** method. + + ```csharp + public async Task UpdateLoan(Loan loan) + { + Loan? existingLoan = null; + foreach (Loan l in _jsonData.Loans!) + { + if (l.Id == loan.Id) + { + existingLoan = l; + break; + } + } + + if (existingLoan != null) + { + existingLoan.BookItemId = loan.BookItemId; + existingLoan.PatronId = loan.PatronId; + existingLoan.LoanDate = loan.LoanDate; + existingLoan.DueDate = loan.DueDate; + existingLoan.ReturnDate = loan.ReturnDate; + + await _jsonData.SaveLoans(_jsonData.Loans!); + + await _jsonData.LoadData(); + } + } + ``` + +1. Enter a prompt that refactors the method using LINQ. + + For example, enter the following prompt: + + ```plaintext + + refactor selection using LINQ. find existing loan in `_jsonData.Loans!. replace existing loan. + + ``` + +1. Take a minute to review the suggested update. + + The suggested update should look similar to one of the following examples: + + ```csharp + + public async Task UpdateLoan(Loan loan) + { + var loans = _jsonData.Loans!; + var index = loans.FindIndex(l => l.Id == loan.Id); + + if (index >= 0) + { + loans[index] = loan; + await _jsonData.SaveLoans(loans); + await _jsonData.LoadData(); + } + } + + ``` + + or + + ```csharp + + public async Task UpdateLoan(Loan loan) + { + Loan? existingLoan = _jsonData.Loans!.FirstOrDefault(l => l.Id == loan.Id); + + if (existingLoan != null) + { + existingLoan.BookItemId = loan.BookItemId; + existingLoan.PatronId = loan.PatronId; + existingLoan.LoanDate = loan.LoanDate; + existingLoan.DueDate = loan.DueDate; + existingLoan.ReturnDate = loan.ReturnDate; + + await _jsonData.SaveLoans(_jsonData.Loans!); + + await _jsonData.LoadData(); + } + } + + ``` + + The updated code uses LINQ to find the existing loan in the loans collection. It then updates the existing loan with the new loan data. The method then saves the updated loans collection and reloads the data. This approach ensures that the loan data is updated correctly and that the changes are persisted to the data store. + + You can also add the code to ensure the data is loaded before the method is executed: + + ```csharp + + public async Task UpdateLoan(Loan loan) + { + await _jsonData.EnsureDataLoaded(); + + Loan? existingLoan = _jsonData.Loans!.FirstOrDefault(l => l.Id == loan.Id); + + if (existingLoan != null) + { + existingLoan.BookItemId = loan.BookItemId; + existingLoan.PatronId = loan.PatronId; + existingLoan.LoanDate = loan.LoanDate; + existingLoan.DueDate = loan.DueDate; + existingLoan.ReturnDate = loan.ReturnDate; + + await _jsonData.SaveLoans(_jsonData.Loans!); + + await _jsonData.LoadData(); + } + } + + ``` + +1. To accept the updated UpdateLoan method, select **Keep**. + +1. Build your solution to ensure that no errors were introduced. + + You'll see warnings. You can ignore them for now. + +### Refactor the JsonPatronRepository class using Agent mode + +The **JsonPatronRepository** class includes the following three methods: + +- SearchPatrons: The SearchPatrons method is used to search for patrons by name. This method returns a sorted list of patrons. +- GetPatron: The GetPatron method is used to retrieve a patron by ID. This method returns a populated patron object. +- UpdatePatron: The UpdatePatron method is used to update a patron's information. This method updates the existing patron with the new data and saves the updated patrons collection. + +Each of the three methods uses a foreach loop to iterate over the patrons and find matches based on the search input or ID. + +You'll use the Chat view in Agent mode to refactor the methods, replacing foreach loops with LINQ queries, in the same way that you did for the **JsonData** and **JsonLoanRepository** classes. + +Use the following steps to complete this section of the exercise: + +1. Open the **JsonPatronRepository.cs** file. + + The **JsonPatronRepository** class is designed to manage library patrons. + +1. Take a minute to review three methods included in the **JsonPatronRepository** class. + + The **SearchPatrons** method is designed to search for patrons by name. + + ```csharp + + public async Task> SearchPatrons(string searchInput) + { + await _jsonData.EnsureDataLoaded(); + + List searchResults = new List(); + foreach (Patron patron in _jsonData.Patrons) + { + if (patron.Name.Contains(searchInput)) + { + searchResults.Add(patron); + } + } + searchResults.Sort((p1, p2) => String.Compare(p1.Name, p2.Name)); + + searchResults = _jsonData.GetPopulatedPatrons(searchResults); + + return searchResults; + } + + ``` + + Notice that the **SearchPatrons** method uses a foreach loop to iterate over the patrons and find matches based on the **searchInput** string. The method then sorts the results by name and returns a list of populated patrons. + + The **GetPatron** method is designed to return the patron matching the specified **id**. + + ```csharp + + public async Task GetPatron(int id) + { + await _jsonData.EnsureDataLoaded(); + + foreach (Patron patron in _jsonData.Patrons!) + { + if (patron.Id == id) + { + Patron populated = _jsonData.GetPopulatedPatron(patron); + return populated; + } + } + return null; + } + + ``` + + Notice that the **GetPatron** method uses a foreach loop to iterate over the patrons and find a match based on the **id** parameter. The method then returns the populated patron object. + + The **UpdatePatron** method is designed to update the patron with the specified **id**. + + ```csharp + + public async Task UpdatePatron(Patron patron) + { + await _jsonData.EnsureDataLoaded(); + var patrons = _jsonData.Patrons!; + Patron existingPatron = null; + foreach (var p in patrons) + { + if (p.Id == patron.Id) + { + existingPatron = p; + break; + } + } + if (existingPatron != null) + { + existingPatron.Name = patron.Name; + existingPatron.ImageName = patron.ImageName; + existingPatron.MembershipStart = patron.MembershipStart; + existingPatron.MembershipEnd = patron.MembershipEnd; + existingPatron.Loans = patron.Loans; + await _jsonData.SavePatrons(patrons); + await _jsonData.LoadData(); + } + } + + ``` + + Notice that the **UpdatePatron** method uses a foreach loop to iterate over the patrons and find a match based on the **id** parameter. The method then updates the existing patron with the new data and saves the updated patrons collection. + +1. Take a minute to consider a prompt that will refactor the **JsonPatronRepository** class using LINQ. + + The goal is to replace the foreach loops with LINQ queries that produce the same result as the original foreach code. + + You can use your experience with the **JsonData** and **JsonLoanRepository** classes to help you write the task for the agent. The LINQ queries should use **Where**, **Select**, and **FirstOrDefault** to find matching patrons. The LINQ queries should also use **OrderBy** to preserve sorting in the original foreach code. + +1. To assign the agent task, enter the following prompt: + + ```plaintext + + Review the LINQ code used in the JsonData and JsonLoanRepository classes. Notice how Where, Select, and FirstOrDefault are used. Refactor the methods in the JsonPatronRepository class, replacing foreach loops with LINQ queries that produce the same result as the original foreach code. Use OrderBy to preserve sorting in original foreach code. Use ! to suppress nullability warnings when accessing collections. + + ``` + + This prompt tells the agent to refactor the **JsonPatronRepository** class. It specifies that the foreach loops should be replaced with LINQ queries that produce the same result as the original foreach code. It also specifies that **OrderBy** should be used to preserve sorting in the original foreach code, and that **!** should be used to suppress nullability warnings when accessing collections. + +1. Monitor the agent's progress as it refactors the code. + + Notice that the agent completes the task in several iterations. Each code edit pass is followed by a review pass that check for issues. If the agent encounters an issue, it will refactor the code to resolve the issue. If the agent is unable to resolve an issue, it will ask you to intervene. + +1. Once the agent has finished, take a minute to review the suggested updates. + + The suggested update should look similar to the following code: + + ```csharp + + public async Task> SearchPatrons(string searchInput) + { + await _jsonData.EnsureDataLoaded(); + + var searchResults = _jsonData.Patrons! + .Where(patron => patron.Name.Contains(searchInput)) + .OrderBy(patron => patron.Name) + .ToList(); + + return _jsonData.GetPopulatedPatrons(searchResults); + } + + public async Task GetPatron(int id) + { + await _jsonData.EnsureDataLoaded(); + + return _jsonData.Patrons! + .Where(patron => patron.Id == id) + .Select(patron => _jsonData.GetPopulatedPatron(patron)) + .FirstOrDefault(); + } + + public async Task UpdatePatron(Patron patron) + { + await _jsonData.EnsureDataLoaded(); + + var existingPatron = _jsonData.Patrons!.FirstOrDefault(p => p.Id == patron.Id); + if (existingPatron != null) + { + existingPatron.Name = patron.Name; + existingPatron.ImageName = patron.ImageName; + existingPatron.MembershipStart = patron.MembershipStart; + existingPatron.MembershipEnd = patron.MembershipEnd; + existingPatron.Loans = patron.Loans; + + if (_jsonData.Patrons != null) + { + await _jsonData.SavePatrons(_jsonData.Patrons); + await _jsonData.LoadData(); + } + } + } + + ``` + +1. To accept all updates, select **Keep**. + +### Build and run the application + +Now that you've refactored the code, it's time to build and run the application to ensure that everything is working correctly. You'll also test the application to ensure that the refactored code is functioning as expected. + +1. To clean the solution, right-click **AccelerateAppDevGitHubCopilot**, and then select **Clean**. + + This action removes any build artifacts from the previous build. Cleaning the solution will effectively reset the JSON data files to their original values (in the output directory). + +1. Ensure that the solution builds successfully. + + For example, in the SOLUTION EXPLORER view, right-click **AccelerateDevGHCopilot**, and then select **Build**. + + You'll see some Warnings, but there shouldn't be any Errors. + +1. To run the application, right-click **Library.Console**, select **Debug**, and then select **Start New Instance**. + + The following steps guide you through a simple use case. + +1. When prompted for a patron name, type **One** and then press Enter. + + You should see a list of patrons that match the search query. + + > **NOTE**: The application uses a case-sensitive search process. + +1. At the "Input Options" prompt, type **2** and then press Enter. + + Entering **2** selects the second patron in the list. + + You should see the patron's name and membership status followed by book loan details. + +1. At the "Input Options" prompt, type **1** and then press Enter. + + Entering **1** selects the first book in the list. + + You should see book details listed, including the due date and return status. + +1. At the "Input Options" prompt, type **r** and then press Enter. + + Entering **r** returns the book. + +1. Verify that the message "Book was successfully returned." is displayed. + + The message "Book was successfully returned." should be followed by the book details. Returned books are marked with **Returned: True**. + +1. To begin a new search, type **s** and then press Enter. + +1. When prompted for a patron name, type **One** and then press Enter. + +1. At the "Input Options" prompt, type **2** and then press Enter. + +1. Verify that first book loan is marked **Returned: True**. + +1. At the "Input Options" prompt, type **q** and then press Enter. + +1. Stop the debug session. + +## Summary + +In this exercise, you learned how to refactor code using GitHub Copilot. You used the Chat view in Ask mode to analyze the **EnumHelper** class and explore how to replace reflection with static dictionaries. You then used Agent mode to apply the refactoring. You also used the Chat view in Agent mode to refactor the **JsonData** and **JsonLoanRepository** classes, replacing foreach loops with LINQ queries. Finally, you used Agent mode to refactor the **JsonPatronRepository** class, replacing foreach loops with LINQ queries. + +## Clean up + +Now that you've finished the exercise, take a minute to ensure that you haven't made changes to your GitHub account or GitHub Copilot subscription that you don't want to keep. If you made any changes, revert them now. diff --git a/Instructions/Labs/LAB_AK_05_refactor_improve_existing_code_py.md b/Instructions/Labs/LAB_AK_05_refactor_improve_existing_code_py.md new file mode 100644 index 0000000..16f875a --- /dev/null +++ b/Instructions/Labs/LAB_AK_05_refactor_improve_existing_code_py.md @@ -0,0 +1,909 @@ +--- +lab: + title: Exercise - Refactor existing code using GitHub Copilot (Python) + description: Learn how to refactor and improve existing code sections using GitHub Copilot in Visual Studio Code. + duration: 30 minutes + level: 200 + islab: true + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Refactor existing code using GitHub Copilot + +GitHub Copilot can be used to evaluate your entire codebase and suggest updates that help you to refactor and improve your code. In this exercise, you use GitHub Copilot to refactor specified sections of a Python application while making improvements to code quality, reliability, performance, and security. + +This exercise should take approximately **30** minutes to complete. + +> **IMPORTANT**: To complete this exercise, you must provide your own GitHub account and GitHub Copilot subscription. If you don't have a GitHub account, you can sign up for a free individual account and use a GitHub Copilot Free plan to complete the exercise. If you have access to a GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscription from within your lab environment, you can use your existing GitHub Copilot subscription to complete this exercise. + +## Before you start + +Your lab environment must include the following: Git 2.48 or later, Python 3.10 or later, Visual Studio Code with the Python extension form Microsoft, and access to a GitHub account with GitHub Copilot enabled. + +If you're using a local PC as a lab environment for this exercise: + +- For help configuring your local PC as your lab environment, open the following link in a browser: Configure your lab environment resources. + +- For help enabling your GitHub Copilot subscription in Visual Studio Code, open the following link in a browser: Enable GitHub Copilot within Visual Studio Code. + +If you're using a hosted lab environment for this exercise: + +- For help enabling your GitHub Copilot subscription in Visual Studio Code, paste the following URL into a browser's site navigation bar: Enable GitHub Copilot within Visual Studio Code. + +- Open a command terminal and then run the following commands: + + To ensure that Visual Studio Code is configured to use the correct version of Python, verify your Python installation is version 3.10 or later: + + ```bash + python --version + ``` + + To ensure that Git is configured to use your name and email address, update the following commands with your information, and then run the commands: + + ```bash + + git config --global user.name "John Doe" + + ``` + + ```bash + + git config --global user.email johndoe@example.com + + ``` + +## Exercise scenario + +You're a developer working in the IT department of your local community. The backend systems that support the public library were lost in a fire. Your team needs to develop a temporary project to help the library staff manage their operations until the system can be replaced. Your team chose GitHub Copilot to accelerate the development process. + +You handed off an initial version of the library application for review. The review team identified opportunities to improve code quality, performance, readability, maintainability, and security. + +This exercise includes the following tasks: + +1. Set up the library application in Visual Studio Code. +1. Analyze and refactor code using the Chat view in Ask and Edit modes. +1. Refactor code using inline chat and the Chat view in Edit and Agent modes. + +## Set up the library application in Visual Studio Code + +You need to download the existing application, extract the code files, and then open the project in Visual Studio Code. + +Use the following steps to set up the library application: + +1. Open a browser window in your lab environment. + +1. To download a zip file containing the library application, paste the following URL into your browser's address bar: [GitHub Copilot lab - refactor existing code](https://github.com/MicrosoftLearning/mslearn-github-copilot-dev/raw/refs/heads/main/DownloadableCodeProjects/Downloads/AZ2007LabAppM5Python.zip) + + The zip file is named **AZ2007LabAppM5Python.zip**. + +1. Extract the files from the **AZ2007LabAppM5Python.zip** file. + + For example: + + 1. Navigate to the downloads folder in your lab environment. + + 1. Right-click **AZ2007LabAppM5Python.zip**, and then select **Extract all**. + + 1. Select **Show extracted files when complete**, and then select **Extract**. + +1. Open the extracted files folder, then copy the **AccelerateDevGHCopilot** folder to a location that's easy to access, such as your Windows Desktop folder. + +1. Open the **AccelerateDevGHCopilot** folder in Visual Studio Code. + + For example: + + 1. Open Visual Studio Code in your lab environment. + + 1. In Visual Studio Code, on the **File** menu, select **Open Folder**. + + 1. Navigate to the Windows Desktop folder, select **AccelerateDevGHCopilot** and then select **Select Folder**. + +1. In the Visual Studio Code EXPLORER view, verify the following project structure: + + - AccelerateDevGHCopilot/library + ├── application_core + ├── console + ├── infrastructure + └── tests + +1. Ensure that the initial code runs by tests successfully in the next section. Optionally, run the application if are familiar with the previous labs from the **\library** folder in the terminal using **`python console\main.py`**. + +### Enable Pytest + +Compared to Unittest, Pytest some advantages such as concise syntax, features like fixtures and parameterization, and better failure reporting. Pytest makes tests easier to write and maintain and Pytest runs Unittest test cases. + +1. Pytest is enabled from the install of the Visual Studio Microsoft Python extension, install if needed. + +1. Select the flask icon ![Screenshot showing the test flask icon.](./Media/m04-pytest-flask-py.png) on the toolbar once tests are discovered. If the icon isn't present review the previous instructions + +1. Choose "Configure Python Tests" or if previously configured: + +1. if the tests haven't been configured or are testing the correct project go to the next step. If you need to change the test project continue with: + - `Ctrl+Shift+P` to open the Command Palette. + - enter **"Python: Configure Tests"**. + - Select "pytest." + - Select the directory for your python code. + - Select the play icon to run tests. + +1. Select Pytest from the options. + +1. Choose the (`library\`) folder containing your test code. + +1. Select the play icon to run tests. + ![Screenshot showing the pytest results.](./Media/m04-pytest-configure-results-py.png) + +1. Optionally, run the ptytest command from the `library` path in the Terminal: + + ```plaintext + pytest -v + ``` + +## Analyze and refactor code using the Chat view in Ask and Edit mode + +Reflection is a powerful coding feature that allows you to inspect and manipulate objects at runtime. However, reflection can be slow and there are potential security risks associated with reflection that should be considered. + +You need to: + +1. Analyze your workspace and investigate how to address your assigned task. +1. Refactor the the use of Python Enum into an enum_helper class to use static dictionaries instead of reflection and pythons built in Enum. + +### Analyze Enum use with the Chat view in Ask mode + +GitHub Copilot's Chat view has three modes: **Ask**, **Edit**, and **Agent**. Each mode is designed for different types of interactions with GitHub Copilot. + +- **Ask**: Use this mode to ask GitHub Copilot questions about your codebase. You can ask GitHub Copilot to explain code, suggest changes, or provide information about the codebase. +- **Edit**: Use this mode to edit selected code files. You can use GitHub Copilot to refactor code, add comments, or make other changes to your code. +- **Agent**: Use this mode to run GitHub Copilot as an agent. You can use GitHub Copilot to run commands, execute code, or perform other tasks in your workspace. + +In this section of the exercise, you use the Chat view in **Ask** mode to analyze your coding assignment. + +Use the following steps to complete this section of the exercise: + +1. In the EXPLORER view, expand the **library** folder. + +1. Open the GitHub Copilot Chat view. + + The Chat view provides a managed conversational interface for interacting with GitHub Copilot. + + You can toggle the Chat view between open and closed using the **Toggle Chat** button, which is located at the top of the Visual Studio Code window, just to the right of the search textbox. + + ![Screenshot showing the Copilot Toggle Chat button.](./Media/m01-github-copilot-toggle-chat.png) + + You can also use the keyboard shortcut **Ctrl+Alt+I** to toggle the Chat view. + +1. Notice that the Chat view opens in **Ask** mode by default. + + The current Chat mode is displayed near the bottom-right corner of the Chat view. Chat responses are displayed in the Chat view when you're working in **Ask** mode. + +1. Set the chat mode to **Ask** and select the **Auto** model. + + The Set Mode and Pick Model menus are in the bottom-left corner of the Chat view. + + > **NOTE**: You can use a different model if your plan allows it, but responses may differ from those shown in this exercise. Free-plan users have a limited number of monthly chat requests, so each prompt counts against your quota. + +1. Review and then submit the following prompt: + + ```plaintext + @workspace Provide an explanation of how the current Python code can be improved with code refactoring to: + - avoid reflection + - avoid repeated `if/elif` + - make the code more explicit and efficient. + Then provide suggestion for refactoring this Python project to implement the improvement suggestions. + ``` + +1. Take a minute to review the response, such as this , with code suggestions removed: + + >**NOTE** It is normal to see a very different response, but there should be similar themes. + + ----- + + >### Code Improvement Explanation + > + > To improve the current code, consider the following refactoring strategies: + > + > 1. **Avoid Reflection**: + > Reflection can be slow and error-prone. Instead, use explicit method calls, interfaces, or dictionaries to map actions to functions. This makes the code more maintainable and performant. + > + > 2. **Avoid Repeated `if/elif` Chains**: + > Long `if/elif` chains are hard to read and maintain. Replace them with a dictionary (in Python) or a switch expression (in C# 8+), or use polymorphism (object-oriented design) to delegate behavior. + > + > 3. **Make Code More Explicit and Efficient**: + > Explicit code is easier to understand and debug. Use clear variable names, avoid magic strings/numbers, and structure logic so that intent is obvious. Efficiency can often be improved by reducing unnecessary checks or lookups. + > + >#### Refactoring Suggestions + > + > Suppose you have code like this (Python example): + > + > ```python + > # ...existing code... + > def handle_action(action, data): + > if action == "create": + > return create_item(data) + > elif action == "update": + > return update_item(data) + > elif action == "delete": + > return delete_item(data) + > else: + > raise ValueError("Unknown action") + > # ...existing code... + > ``` + > + > **Refactored Version (Avoids Reflection and if/elif, More Explicit)** + > + > ```python + > # ...existing code... + > def handle_action(action, data): + > action_map = { + > "create": create_item, + > "update": update_item, + > "delete": delete_item, + > } + > try: + > return action_map[action](data) + > except KeyError: + > raise ValueError(f"Unknown action: {action}") + > # ...existing code... + > ``` + > + > **Benefits:** + > + > - No reflection is used. + > - No repeated `if/elif`. + > - The mapping is explicit and easy to extend. + > - More efficient due to direct function lookup. + > + >### Summary + > + > - Use dictionaries or switch expressions instead of reflection or long `if/elif` chains. + > - Make mappings explicit for maintainability and efficiency. + > - Use clear error handling for unknown cases. + > + > These changes will make your codebase easier to maintain, more efficient, and less error-prone. + +1. Review and then submit the following prompt: + + ```plaintext + @workspace which Python files in this workspace use reflection or long `if/elif` chains? + ``` + + The response should tell you **console/console_app.py** (`getattr`) and that **console/console_app.py** also uses long `if/elif` chains for command and input handling. + +1. Add the **console/console_app.py** file to the Chat context: + + You can use a drag-and-drop operation to add the files from Visual Studio Code's explorer view to the Chat view. You can also use the **Add Context** button in the Chat view to add files and other resources. + + > **NOTE**: Adding files to the Chat context ensures that GitHub Copilot considers those files when generating a response. The relevance and accuracy of responses increase when GitHub Copilot understands the context associated with your prompts. + +1. Review and then submit the following prompt: + + ```plaintext + + @workspace I need to refactor the `library\console\console_app.py` Python file to: Use dictionaries or switch expressions instead of reflection or long `if/elif` chains; Make mappings explicit for maintainability and efficiency; Use clear error handling for unknown cases; Provide refactored code to apply. + + ``` + + When writing any prompt, clarity and context are important. Using chat participants, slash commands, and chat variables helps to define context in a way that GitHub Copilot can understand. + + For a prompt that asks GitHub Copilot how to solve a problem, start with the problem that you're trying to solve. Use concise sentences to describe details, specify constraints, and identify resources. Finally, be sure to tell GitHub Copilot what to include in the response. + + In this case, your prompt begins with a description of your problem/goal. You tell GitHub Copilot that you need to refactor the `library\console\console_app.py` file to: + + - Use dictionaries or switch expressions instead of reflection or long `if/elif` chains. + - Make mappings explicit for maintainability and efficiency. + - Use clear error handling for unknown cases. + + For clarity, to finish the prompt by you provide the instruction to "provide refactored code to apply." + +1. Take a minute to review the response provided by GitHub Copilot. You **don't apply the edits** in this step. + + > ### Code Improvement Explanation + > + > To improve the current code, consider the following refactoring strategies: + > + > 1. **Avoid Reflection**: + > Reflection can be slow and error-prone. Instead, use explicit method calls, interfaces, or dictionaries to map actions to functions. This makes the code more maintainable and performant. + > + > 2. **Avoid Repeated `if/elseif` Chains**: + > Long `if/elseif` chains are hard to read and maintain. Replace them with a dictionary (in Python) or a switch expression (in C# 8+), or use polymorphism (object-oriented design) to delegate behavior. + > + > 3. **Make Code More Explicit and Efficient**: + > Explicit code is easier to understand and debug. Use clear variable names, avoid magic strings/numbers, and structure logic so that intent is obvious. Efficiency can often be improved by reducing unnecessary checks or lookups. + > + > + > ### Refactoring Suggestions + > + > Suppose you have code like this (Python example): + > + > ```python + > # ...existing code... + > def _handle_patron_details_selection(self, selection, patron, valid_loans): + > if selection == 'q': + > return ConsoleState.QUIT + > elif selection == 's': + > return ConsoleState.PATRON_SEARCH + > elif selection == 'm': + > status = self._patron_service.renew_membership(patron.id) + > print(status) + > self.selected_patron_details = self._patron_repository.get_patron(patron.id) + > return ConsoleState.PATRON_DETAILS + > # ...existing code... + > ``` + > + > #### Refactored Version (Avoids Reflection and if/elif, More Explicit) + > + > ```python + > # ...existing code... + > def _handle_patron_details_selection(self, selection, patron, valid_loans): + > def renew_membership(): + > status = self._patron_service.renew_membership(patron.id) + > print(status) + > self.selected_patron_details = self._patron_repository.get_patron(patron.id) + > return ConsoleState.PATRON_DETAILS + > # ...existing code... + > ``` + > + > **Benefits:** + > + > - No reflection is used. + > - No repeated `if/elif`. + > - The mapping is explicit and easy to extend. + > - More efficient due to direct function lookup. + > + > + > ### Summary + > + > - Use dictionaries or switch expressions instead of reflection or long `if/elseif` chains. + > - Make mappings explicit for maintainability and efficiency. + > - Use clear error handling for unknown cases. + > + > These changes will make your codebase easier to maintain, more efficient, and less error-prone. + +1. In the Chat view, hover the mouse pointer over the code sample included in the response. + +1. Notice the three buttons that appear in the top-right corner of the code snippet. + +1. Hover the mouse pointer over each of the buttons to see a tooltip that describes the action. + + The first two buttons copies code into the editor. The third button copies code to the clipboard. You **don't apply the edits** in this step. + +> **NOTE**: You could use the Ask mode to update the code. However, the Edit mode refactors your code directly within the code editor and provides more options for accepting updates. + +### Refactor the class ConsoleApp (console/console_app.py) file using the Chat view in Edit mode + +The Chat view's Edit mode is designed for editing code in your workspace. You can use the Edit mode to refactor code, add comments, or make other changes to your code. + +1. In the Chat view, select **Set Mode**, and then select **Edit**. + + When prompted to start a new session in the Edit mode, select **Yes**. + + In **Edit** mode, GitHub Copilot displays responses as code update suggestions in code editor. The Edit mode is generally used when implementing a new feature, fixing a bug, or refactoring code. + +1. Add the console/console_app.py file to the Chat context: + +1. Review and then submit the following prompt: + + ```plaintext + + @codebase I need to refactor the ConsoleApp class. Use static dictionaries to supply enum description attributes. Use dictionaries or switch expressions instead of reflection or long `if/elif` chains. Make mappings explicit for maintainability and efficiency. Use clear error handling for unknown cases. + + ``` + + The Edit mode agent response should propose updates in the `ConsoleApp` class and proved a response similar to the following: + + ```plaintext + The ConsoleApp class has been refactored to use static dictionaries for enum descriptions, explicit dictionaries for state and input handling, and clear error handling for unknown cases, improving maintainability and efficiency. + ``` + +1. Take a minute to review the suggested code updates in **console_app.py**. Results should complete the following: + + - Introduced static dictionaries for enum descriptions (`CONSOLE_STATE_DESCRIPTIONS` and `COMMON_ACTIONS_DESCRIPTIONS`) to replace reflection and long `if/elif` chains. + - Refactored `write_input_options` to use the static dictionary for displaying input options. + - Replaced the main state loop in `run` with a dictionary-based handler lookup for maintainability and efficiency. + - Added explicit error handling for unknown states and next states. + - All action mappings in input handlers are now explicit dictionaries for clarity and maintainability. + +1. In the Chat view, to accept all updates, select **Keep**. + + You could also use the Chat Edits toolbar near the bottom of the code editor tab to accept or reject code updates. + +1. Take a minute to review the updated **run** method. + + GitHub Copilot should have updated the **run** method to replace the long if/elif chain with a state_handlers dictionary that maps each ConsoleState to its corresponding handler function. The updated method should look similar to one of the following: + + ```python + + def run(self) -> None: + state_handlers = { + ConsoleState.PATRON_SEARCH: self.patron_search, + ConsoleState.PATRON_SEARCH_RESULTS: self.patron_search_results, + ConsoleState.PATRON_DETAILS: self.patron_details, + ConsoleState.LOAN_DETAILS: self.loan_details, + ConsoleState.QUIT: lambda: ConsoleState.QUIT + } + while True: + handler = state_handlers.get(self._current_state) + if handler is None: + print(f"Unknown state: {self._current_state}") + break + next_state = handler() + if next_state == ConsoleState.QUIT: + print("Exiting application.") + break + if next_state not in state_handlers: + print(f"Unknown next state: {next_state}") + break + self._current_state = next_state + ``` + + This code uses the `state_handlers` dictionary that maps each `ConsoleState` to its corresponding handler function. It now retrieves the handler from the dictionary, raises an error for unknown states, and updates the current state based on the handler’s return value. + +1. Run Pytest and manually Test to ensure that there are no errors were introduced. + + You'll see the same warnings that you saw at the start of this exercise, but there shouldn't be any error messages. + +## Refactor code using inline chat, and the Chat view in Edit and Agent modes + +By adopting Python’s **list comprehensions**, **generator expressions**, and **built-in functions** like `any()`, `all()`, `sum()`, `map()`, `filter()`, and `sorted()`, the codebase can become more concise, expressive, and efficient, reducing boilerplate and potential errors associated with manual iteration and data processing. + +This section of the exercise includes the following tasks: + +- **Inline Chat: Generator expression refactoring** +- **Edit Mode Chat:List list comprehensions and Python built-in methods refactoring** +- **Agent Mode: Built-in Functions Refactoring** + +### Generator Expression refactoring using Inline Chat + +1. In the EXPLORER view, expand the **infrastructure/json_patron_repository.py** project, and then open the **json_loan_repository.py** file and examine the **`JsonLoanRepository` class**. + +1. To elect the **`JsonLoanRepository` class**, you highlight the entire class. + + ```python + + class JsonPatronRepository(IPatronRepository): + def __init__(self, json_data: JsonData): + self._json_data = json_data + + def get_patron(self, patron_id: int) -> Optional[Patron]: + for patron in self._json_data.patrons: + if patron.id == patron_id: + return patron + return None + + def search_patrons(self, search_input: str) -> List[Patron]: + results = [] + for p in self._json_data.patrons: + if search_input.lower() in p.name.lower(): + results.append(p) + n = len(results) + for i in range(n): + for j in range(0, n - i - 1): + if results[j].name > results[j + 1].name: + results[j], results[j + 1] = results[j + 1], results[j] + return results + + def update_patron(self, patron: Patron) -> None: + for idx in range(len(self._json_data.patrons)): + if self._json_data.patrons[idx].id == patron.id: + self._json_data.patrons[idx] = patron + self._json_data.save_patrons(self._json_data.patrons) + return + + def add_patron(self, patron: Patron) -> None: + self._json_data.patrons.append(patron) + self._json_data.save_patrons(self._json_data.patrons) + self._json_data.load_data() + + def get_all_patrons(self) -> List[Patron]: + return self._json_data.patrons + + def find_patrons_by_name(self, name: str) -> List[Patron]: + result = [] + for patron in self._json_data.patrons: + if patron.name.lower() == name.lower(): + result.append(patron) + return result + + def get_all_books(self): + return self._json_data.books + + def get_all_book_items(self): + return self._json_data.book_items + + ``` + +1. Open an inline Copilot Chat, and then enter a prompt that refactors the method. + + ```plaintext + #selection Refactor any manual aggregation or search over loans in this method to use generator expressions with built-in functions like any(), all(), sum(), or max() for improved readability and performance. + ``` + +1. Take a minute to review the suggested update. + + The suggested updates should look similar to the following code: + + ```python + # --code continues-- + def get_patron(self, patron_id: int) -> Optional[Patron]: + return next((patron for patron in self._json_data.patrons if patron.id == patron_id), None) + + def search_patrons(self, search_input: str) -> List[Patron]: + results = [p for p in self._json_data.patrons if search_input.lower() in p.name.lower()] + results.sort(key=lambda p: p.name) + return results + + def update_patron(self, patron: Patron) -> None: + for idx, existing_patron in enumerate(self._json_data.patrons): + if existing_patron.id == patron.id: + self._json_data.patrons[idx] = patron + self._json_data.save_patrons(self._json_data.patrons) + return + + # --code continues-- + def find_patrons_by_name(self, name: str) -> List[Patron]: + return [patron for patron in self._json_data.patrons if patron.name.lower() == name.lower()] + # --code continues-- + ``` + +1. To accept the suggested update, select **Accept**. + +#### Use **Inline Chat "Explain" smart action** to examine a method + +1. Use the **Explain** smart action to see an explanation of the `search_patrons` method. + +1. select the following lines of code (after the comment) from the `search_patrons` method.: + + ```python + # def search_patrons(self, search_input: str) -> List[Patron]: + results = [p for p in self._json_data.patrons if search_input.lower() in p.name.lower()] + results.sort(key=lambda p: p.name) + ``` + + To open the **Explain** smart action, select the `search_patrons` method code in the editor, right-click the selected code, select Copilot, and then select **Explain**. The **Explain** smart action provides a detailed explanation of the selected code. + + For example, the explanation might look like this: + + ```plaintext + + The `search_patrons` method is responsible for finding patrons whose names contain a given search string, regardless of case. It takes a single argument, `search_input`, which is the string to search for. The method iterates over all patrons stored in `self._json_data.patrons` and uses a list comprehension to filter those whose `name` attribute contains the `search_input` substring. Both the patron's name and the search input are converted to lowercase to ensure the search is case-insensitive. + + After collecting the matching patrons, the method sorts the results alphabetically by the patron's name using the `sort()` method with a key function that extracts the `name` attribute from each patron. Finally, the sorted list of matching patrons is returned. This ensures that users receive a case-insensitive, alphabetically ordered list of patrons matching their search criteria. + + A potential consideration is that the search will match any part of the name, not just the beginning, which may lead to more results than expected. Also, the sorting is done in-place on a new list, + + ``` + +1. Test & run your project to ensure that there are no errors were introduced. + +#### Continue Generator Expression refactoring using InlineChat + +1. Next, continue with the refactoring process for **loan_service.py**. + +1. In the EXPLORER view, expand the **application_core\services\loan_service.py** project, and then open the **loan_service.py** file and examine the **`LoanService` class**. + +1. To elect the **`LoanService` class**, you highlight the entire class, examine the`checkout_book` method. + + ```python + + class LoanService(ILoanService): + EXTEND_BY_DAYS = 14 + + def __init__(self, loan_repository: ILoanRepository): + self._loan_repository = loan_repository + + def return_loan(self, loan_id: int) -> LoanReturnStatus: + loan = self._loan_repository.get_loan(loan_id) + if loan is None: + return LoanReturnStatus.LOAN_NOT_FOUND + if loan.return_date is not None: + return LoanReturnStatus.ALREADY_RETURNED + loan.return_date = datetime.now() + try: + self._loan_repository.update_loan(loan) + return LoanReturnStatus.SUCCESS + except Exception: + return LoanReturnStatus.ERROR + + def extend_loan(self, loan_id: int) -> LoanExtensionStatus: + loan = self._loan_repository.get_loan(loan_id) + if loan is None: + return LoanExtensionStatus.LOAN_NOT_FOUND + if loan.patron and loan.patron.membership_end < datetime.now(): + return LoanExtensionStatus.MEMBERSHIP_EXPIRED + if loan.return_date is not None: + return LoanExtensionStatus.LOAN_RETURNED + if loan.due_date < datetime.now(): + return LoanExtensionStatus.LOAN_EXPIRED + try: + loan.due_date = loan.due_date + timedelta(days=self.EXTEND_BY_DAYS) + self._loan_repository.update_loan(loan) + return LoanExtensionStatus.SUCCESS + except Exception: + return LoanExtensionStatus.ERROR + + def checkout_book(self, patron, book_item, loan_id=None) -> None: + from ..entities.loan import Loan + from datetime import datetime, timedelta + # Generate a new loan ID if not provided + if loan_id is None: + all_loans = getattr(self._loan_repository, 'get_all_loans', lambda: [])() + max_id = 0 + for l in all_loans: + if l.id > max_id: + max_id = l.id + loan_id = max_id + 1 if all_loans else 1 + now = datetime.now() + due = now + timedelta(days=14) + new_loan = Loan( + id=loan_id, + book_item_id=book_item.id, + patron_id=patron.id, + patron=patron, + loan_date=now, + due_date=due, + return_date=None, + book_item=book_item + ) + self._loan_repository.add_loan(new_loan) + return new_loan + + ``` + +1. Open an inline chat, and then enter a prompt that refactors the method. + + ```plaintext + #selection Refactor any manual aggregation or search over loans in this method to use generator expressions with built-in functions like any(), all(), sum(), or max() for improved readability and performance. + ``` + +1. Take a minute to review the suggested update with the added code in the `checkout_book` method: + + ```python + + loan_id = max((l.id for l in all_loans), default=0) + 1 if all_loans else 1 + ``` + + The full `checkout_book` method should look similar to the following code: + + ```python + # --code continues-- + + def checkout_book(self, patron, book_item, loan_id=None) -> None: + from ..entities.loan import Loan + from datetime import datetime, timedelta + if loan_id is None: + all_loans = getattr(self._loan_repository, 'get_all_loans', lambda: [])() + loan_id = max((l.id for l in all_loans), default=0) + 1 if all_loans else 1 + now = datetime.now() + due = now + timedelta(days=14) + new_loan = Loan( + id=loan_id, + book_item_id=book_item.id, + patron_id=patron.id, + patron=patron, + loan_date=now, + due_date=due, + return_date=None, + book_item=book_item + ) + self._loan_repository.add_loan(new_loan) + return new_loan + ``` + + Refactoring to use generator expressions replaces manual iteration with concise, memory-efficient constructs that *work seamlessly with built-in functions* like `max()`, `any()`, and `sum()`. This not only reduces boilerplate code and potential errors but also improves performance, especially when processing large datasets, since generator expressions do not create intermediate lists in memory. Overall, these changes make the codebase more Pythonic, readable, and maintainable. + +### Refactor the JsonPatronRepository class using the Chat view in Edit mode + +The **JsonPatronRepository** class includes the following three methods: + +- SearchPatrons: The SearchPatrons method is used to search for patrons by name. This method returns a sorted list of patrons. +- GetPatron: The GetPatron method is used to retrieve a patron by ID. This method returns a populated patron object. +- UpdatePatron: The UpdatePatron method is used to update a patron's information. This method updates the existing patron with the new data and saves the updated patrons collection. + +Each of the three methods uses a foreach loop to iterate over the patrons and find matches based on the search input or ID. + +Use the following steps to complete this section of the exercise: + +1. Open the **json_loan_repository.py** file. + + The **JsonPatronRepository** class is designed to manage library patrons. + +1. Take a minute to review some of the methods included in the **JsonPatronRepository** class. + + The **SearchPatrons** method is designed to search for patrons by name. + + ```python + + # ----code continues---- + class JsonLoanRepository(ILoanRepository): + def **init**(self, json_data: JsonData): + self._json_data = json_data + + # ----code continues---- + def get_loans_by_patron_id(self, patron_id: int): + result = [] + for loan in self._json_data.loans: + if loan.patron_id == patron_id: + result.append(loan) + return result + + # ----code continues---- + def get_overdue_loans(self, current_date): + overdue = [] + for loan in self._json_data.loans: + if loan.return_date is None and loan.due_date < current_date: + overdue.append(loan) + return overdue + + def sort_loans_by_due_date(self): + # Manual bubble sort for demonstration + n = len(self._json_data.loans) + for i in range(n): + for j in range(0, n - i - 1): + if self._json_data.loans[j].due_date > self._json_data.loans[j + 1].due_date: + self._json_data.loans[j], self._json_data.loans[j + 1] = self._json_data.loans[j + 1], self._json_data.loans[j] + return self._json_data.loans + + # ----code continues---- + ``` + +1. In the Chat view, select **Set Mode**, and then select **Edit**. + + When prompted to start a new session in the Edit mode, select **Yes**. + + In **Edit** mode, GitHub Copilot displays responses as code update suggestions in code editor. The Edit mode is generally used when implementing a new feature, fixing a bug, or refactoring code. + +1. Enter the prompt + + ```plaintext + @codebase Refactor methods in the JsonLoanRepository class with for-loops to use list comprehensions + or Python built-in methods to improve code clarity, conciseness, and efficiency. + ``` + +1. Notice in the updated code that follows that `get_loans_by_patron_id` and `get_overdue_loans` methods are now using list comprehensions and the`sort_loans_by_due_date` method now uses a Python built-in `sorted` function. + + ```python + # ----code continues---- + + def get_loan(self, loan_id: int) -> Optional[Loan]: + return next((loan for loan in self._json_data.loans if loan.id == loan_id), None) + + def update_loan(self, loan: Loan) -> None: + idx = next((i for i, l in enumerate(self._json_data.loans) if l.id == loan.id), None) + if idx is not None: + self._json_data.loans[idx] = loan + self._json_data.save_loans(self._json_data.loans) + return + + # ----code continues---- + def get_loans_by_patron_id(self, patron_id: int): + return [loan for loan in self._json_data.loans if loan.patron_id == patron_id] + + # ----code continues---- + def get_overdue_loans(self, current_date): + return [loan for loan in self._json_data.loans if loan.return_date is None and loan.due_date < current_date] + + def sort_loans_by_due_date(self): + # Use built-in sorted for clarity and efficiency + self._json_data.loans = sorted(self._json_data.loans, key=lambda l: l.due_date) + return self._json_data.loans + ``` + + List comprehensions and built-in functions replaced manual for-loops for filtering and sorting, making the code shorter, clearer, and more efficient. + +### Refactor the JsonLoanRepository & JsonPatronRepository classes using the Chat view in Agent mode + +1. In the Chat view, change the mode to **Agent** + + Agent mode is designed for running GitHub Copilot as an agent. You can use natural language to specify a high-level task. The agent will evaluate the assigned task, plan the work needed, and apply the changes to your codebase. + + Agent mode uses a combination of code editing and tool invocation to accomplish the task you specified. As it processes your request, it monitors the outcome of edits and tools, and iterates to resolve any issues that arise. If the agent is unable to resolve an issue, it will ask you to intervene. For example, if the agent uses several iterations working to resolve the same issue, it will pause the process and ask you to provide additional context to clarify your request or cancel the process. + + > **IMPORTANT**: When you use the Chat view in agent mode, GitHub Copilot may make multiple premium requests to complete a single task. Premium requests can be used by user-initiated prompts and follow-up actions Copilot takes on your behalf. The total number of premium requests used is based on the complexity of the task, the number of steps involved, and the model selected. + +1. Take a minute to consider the task that you need to assign to the agent. + + The task is to refactor the **JsonLoanRepository** & **JsonPatronRepository** classes. The goal is to locate manual loops to refactor with **Built-in Python Functions** that produce the same result as the original foreach code. + + You can use your experience with the previous refactoring to help you write the task for the agent. + +1. To assign the agent task, enter the following prompt using **AGENT mode**: + + ```plaintext + + #codebase Review the manual loop code used in the methods of the JsonLoanRepository and JsonPatronRepository classes to make them more pythonic. Refactor with Built-in Python Functions that produce the same result as the original foreach code. + + ``` + + This prompt tells the agent to refactor the **JsonPatronRepository** class. It specifies that the foreach loops should be replaced with Built-in Python Functions. + +1. Monitor the agent's progress as it refactors the code. + + Notice that the agent completes the task in several iterations. Each code edit pass is followed by a review pass that check for issues. If the agent encounters an issue, it will refactor the code to resolve the issue. If the agent is unable to resolve an issue, it will ask you to intervene. + +1. Once the agent has finished, take a minute to review the suggested updates. + + The changes made in JsonLoanRepository should be: + - `update_patron` now uses `next` with `enumerate` for efficient index lookup. + - `search_patrons` uses a generator expression and `sorted` for concise, readable code. + - All manual bubble sort and redundant code have been removed. + + > **NOTE:** No changes were needed for JsonPatronRepository, as it already uses built-in functions and comprehensions for filtering and searching. + + The suggested updates should look similar to the following code: + + ```python + # ----code continues---- + def search_patrons(self, search_input: str) -> List[Patron]: + return sorted( + (p for p in self._json_data.patrons if search_input.lower() in p.name.lower()), + key=lambda patron: patron.name + ) + + def update_patron(self, patron: Patron) -> None: + idx = next((i for i, existing_patron in enumerate(self._json_data.patrons) if existing_patron.id == patron.id), None) + if idx is not None: + self._json_data.patrons[idx] = patron + self._json_data.save_patrons(self._json_data.patrons) + + # ----code continues---- + + ``` + +1. To accept all updates, select **Keep**. + +These updates make the code more concise, readable, and Pythonic while preserving the original behavior. + +### Test and run the application + +Now that you've refactored the code, it's time to test and run the application to ensure that everything is working correctly. You'll also test the application to ensure that the refactored code is functioning as expected. + +1. To run the application in Visual Studio Code using Python, open **console/main.py** in the editor, press **CTRL+Shift+D** to open the Run and Debug panel, choose **Python: Current File** or another debug configuration, and then **F5** to start debugging. + + The following steps guide you through a simple use case. + +1. When prompted for a patron name, type **One** and then press Enter. + + You should see a list of patrons that match the search query. + + > **NOTE**: The application uses a case-sensitive search process. + +1. At the "Input Options" prompt, type **2** and then press Enter. + + Entering **2** selects the second patron in the list. + + You should see the patron's name and membership status followed by book loan details. + +1. At the "Input Options" prompt, type **1** and then press Enter. + + Entering **1** selects the first book in the list. + + You should see book details listed, including the due date and return status. + +1. At the "Input Options" prompt, type **r** and then press Enter. + + Entering **r** returns the book. + +1. Verify that the message "Book was successfully returned." is displayed. + + The message "Book was successfully returned." should be followed by the book details. Returned books are marked with **Returned: True**. + +1. To begin a new search, type **s** and then press Enter. + +1. When prompted for a patron name, type **One** and then press Enter. + +1. At the "Input Options" prompt, type **2** and then press Enter. + +1. Verify that first book loan is marked **Returned: True**. + +1. At the "Input Options" prompt, type **q** and then press Enter. + +1. Stop the debug session. + +## Summary + +In this exercise, the focus was on refactoring and improving existing code with the help of GitHub Copilot. You systematically enhanced the codebase by replacing manual loops and repetitive patterns with Pythonic constructs such as list comprehensions, generator expressions, and built-in functions. You used GitHub Copilot's various modes—Ask, Inline, Edit, and Agent—to analyze code, generate refactoring suggestions, and apply improvements directly in Visual Studio Code. These approaches improved code quality, readability, maintainability, and performance. With these skills, you can now confidently refactor and modernize Python projects, making your codebase more robust and efficient as you continue to develop new features. + +## Clean up + +Now that you've finished the exercise, take a minute to ensure that you haven't made changes to your GitHub account or GitHub Copilot subscription that you don't want to keep. If you made any unwanted changes, revert them now. diff --git a/Instructions/Labs/LAB_AK_06_vibe_coding_prototype_ecommerce_app.md b/Instructions/Labs/LAB_AK_06_vibe_coding_prototype_ecommerce_app.md new file mode 100644 index 0000000..c3cc6cf --- /dev/null +++ b/Instructions/Labs/LAB_AK_06_vibe_coding_prototype_ecommerce_app.md @@ -0,0 +1,506 @@ +--- +lab: + title: Exercise - Get started with vibe coding using GitHub Copilot Agent + description: Learn how to create a prototype app using a vibe coding process and GitHub Copilot Agent. + duration: 30 minutes + level: 200 + islab: true + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Get started with vibe coding using GitHub Copilot Agent + +Vibe coding is an approach to programming that uses AI tools, such as GitHub Copilot Agent, to generate software. Instead of manually writing code, a user provides a natural language description of their intended app, and the AI generates the corresponding code. This shifts the programmer's role from traditional coding to guiding, testing, and refining the AI-generated output. + +In this exercise, you use a vibe coding process and GitHub Copilot Agent to create a prototype version of an online shopping app. Your prototype app includes the following pages: products, product details, shopping cart, and check out. The app includes basic navigation between pages and a limited dataset that helps to demonstrate app features. The prototype doesn't include any backend functionality, such as user authentication, payment processing, or database integration. + +This exercise should take approximately **30** minutes to complete. + +> **IMPORTANT**: To complete this exercise, you must provide your own GitHub account and GitHub Copilot subscription. If you don't have a GitHub account, you can sign up for a free individual account and use a GitHub Copilot Free plan to complete the exercise. If you have access to a GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscription from within your lab environment, you can use your existing GitHub Copilot subscription to complete this exercise. + +## Before you start + +Your lab environment must include the following: + +- Visual Studio Code. +- Access to a GitHub account with GitHub Copilot enabled. + +If you're using a local PC as your lab environment for this exercise: + +- You can download the Visual Studio Code installer file from the following URL: Download Visual Studio Code. + +- For help with enabling your GitHub Copilot subscription in Visual Studio Code, open the following link in a browser: Enable GitHub Copilot within Visual Studio Code. + +If you're using a hosted lab environment that supports this exercise: + +- For help with enabling your GitHub Copilot subscription in Visual Studio Code, open a browser and paste the following URL into the site navigation bar: Enable GitHub Copilot within Visual Studio Code. + +## Exercise scenario + +You're an entrepreneur who wants to use a vibe coding process to create a prototype shopping app. Your initial prototype needs to demonstrate the basic capabilities that a user expects from an online shopping app and specific features that you've envisioned. + +You identify the following base specifications to begin your development process: + +1. Use HTML, CSS, and JavaScript to create a client-side web app. +2. Include the following web pages: Products, ProductDetails, ShoppingCart, and Checkout. +3. Enable navigation between pages. + +This exercise includes the following tasks: + +1. **Define product requirements**: Use GitHub Copilot to help transition your base specifications into more detailed product requirements. + +1. **Create an initial prototype app**: Use GitHub Copilot Agent and your product requirements to create an initial prototype app. + +1. **Refine your prototype app**: Use GitHub Copilot Agent to complete a series of iterative updates that refine the user experience and ensure your app satisfies the intended requirements. + +> **NOTE**: A prototype app is an early, interactive model of an application that demonstrates its visual design and user experience. In this exercise, your prototype app should implement basic features and satisfy a small number of high-level use cases. + +## Define product requirements + +For an AI agent to develop your envisioned app, it needs to understand your product requirements and the intended user experience. You can communicate your intentions to the GitHub Copilot Agent using either of the following processes: + +- **Code first and iterate to define requirements**: This approach begins with minimal base specifications and jumps straight into coding. As development progresses, the app evolves organically through iterative cycles, gradually shaping the product’s features and user experience. This approach risks deviating from your original vision, for better or for worse, as you explore features implemented by the AI. An AI-led process can become unexpectedly time-consuming and may not yield the desired results, especially when the initial specifications are vague or open-ended. + +- **Explore requirements before coding**: This approach emphasizes clarity from the start. You collaborate with the AI to draft a Product Requirements Document (PRD) before writing any code. The PRD outlines the app’s purpose, target users, key features, and technical constraints. By establishing a clear vision upfront, you give the AI a solid foundation to generate code that aligns with your goals—reducing ambiguity and improving the chances of building the app you actually intended. + +In this task, you use GitHub Copilot to evaluate your base specifications and develop product requirements for your prototype app. + +Use the following steps to complete this section of the exercise: + +1. Open Visual Studio Code. + +1. On the File menu, select **Add folder to Workspace**. + +1. In the **Add Folder to Workspace** dialog, navigate to an easy-to-find folder location, create a new folder named **VibeCoding-PrototypeApp**, and then select **Add**. + + The folder location should be outside of any existing Git repository and should be easy to find. For example, if you're using a Windows PC, you can create a new folder named **VibeCoding-PrototypeApp** on your **Desktop** or in your **Documents** directory. + + After completing this lab exercise, you can either archive or delete the code project. + +1. Open GitHub Copilot's Chat view. + + The Chat view can be opened by selecting the GitHub Copilot icon located near the top-center of the Visual Studio Code window, just to the right of the search textbox. + +1. Ensure that the chat mode is set to **Ask** and the **Auto** model is selected. + + The *Set Mode* and *Pick Model* dropdown menus are located in the bottom-left corner of the Chat view. + + **GitHub Copilot modes**: Although their capabilities overlap, each of the chat modes (Ask, Edit, and Agent) is optimized for a specific purpose: + + - **Ask**: Use this mode to ask GitHub Copilot questions about your codebase. You can use Ask mode to explain code, suggest changes, or provide information about the codebase. + - **Edit**: Use this mode to edit specific code files in your workspace. You can use Edit mode to refactor code, add comments, implement tests, or add new features to your apps. + - **Agent**: Use this mode to run GitHub Copilot as an agent. You can use Agent mode to perform coding tasks autonomously. + + **Supported Models**: GitHub Copilot supports multiple models, each with different strengths. Some models prioritize speed and cost-efficiency, while others are optimized for accuracy, reasoning, or working with multimodal inputs (like images and code together). + +1. In the Chat view, enter the following prompt: + + ```plaintext + I want to create a product requirements document (PRD) for an app that I'll develop using a vibe coding process. I want the PRD to include information about the app's purpose, target audience, features, and technical requirements. I've defined the following high-level parameters for my app: 1 - Use HTML, CSS, and JavaScript to create a client-side web app. 2 - Include the following web pages: Products, ProductDetails, ShoppingCart, and Checkout. 3 - Enable navigation between pages. I want the prototype app to implement basic features and satisfy a small number of high-level use cases. The prototype should implement the following: basic use case functionality, simple navigation, a sample dataset, and basic styling. I'll be adding the PRD to the chat context, then asking GitHub Copilot Agent to create the prototype app. What sections should I include in the PRD to enable GitHub Copilot Agent to create my envisioned prototype? + ``` + +1. Review the response generated by GitHub Copilot. + + For example, your response may be similar to the following sample: + + ```md + To enable GitHub Copilot Agent to create your envisioned prototype app, your Product Requirements Document (PRD) should include the following sections: + + 1. App Overview / Purpose + - Briefly describe what the app does and its main goal. + + 2. Target Audience + - Define who will use the app (e.g., general consumers, online shoppers, etc.). + + 3. Core Features + - List and describe the main features (e.g., view products, view product details, add to cart, checkout). + + 4. Page Descriptions + - Detail each required page (Products, ProductDetails, ShoppingCart, Checkout) and what should be displayed or possible on each. + + 5. Navigation + - Explain how users move between pages (e.g., navigation bar, links, buttons). + + 6. Sample Data + - Describe the sample dataset (e.g., a few example products with name, price, description, image). + + 7. Technical Requirements + - Specify technologies to use (HTML, CSS, JavaScript, client-side only, no backend). + - Mention any libraries or frameworks (if any) or state "vanilla JS". + + 8. Styling + - Indicate the level of styling (e.g., basic, clean, minimal, responsive). + + 9. Use Cases / User Flows + - List a few high-level use cases (e.g., browse products, view details, add to cart, proceed to checkout). + + 10. Out of Scope + - Clarify what is not required (e.g., user authentication, payment processing, backend integration). + + Including these sections will give clear, actionable guidance for building your prototype app. + ``` + + > **NOTE**: Don't worry if your response includes different section titles or if the sections appear in a different order. The responses that AI tools generate are often slightly different from one chat session to another. The selected AI model, your chat history, and the context of your chat session can also affect responses. + +1. Take a few minutes to consider the information that's required to complete each section of the PRD. + + A well-defined PRD helps ensure that GitHub Copilot Agent has a clear understanding of your vision for the app. The PRD should provide enough detail to enable the Agent to create a prototype app that meets your requirements and intended user experience. Your PRD should build upon the base specifications listed earlier in the exercise. + + If you're not sure about what information to include in a specific section, you can ask GitHub Copilot Agent to help you generate the content for that section. For example, you can ask GitHub Copilot for ideas about what to include in the 'Core Features' or 'Use Cases' sections. + + > **Tip**: You can provide natural language text that describes your app's requirements and have GitHub Copilot format that information as a PRD. You can also use GitHub Copilot to help you review and update the PRD, and to ensure that it provides the level of detail required for GitHub Copilot Agent to create the prototype. + +1. In the Chat view, enter the following prompt: + + ```plaintext + The PRD sections that you suggested look good. Here's some information that should help you construct the PRD: + + My prototype app targets online shoppers interested in ordering my products. The prototype should include the following: + + - A dynamic user interface that scales automatically to appear correctly on large or small screens (desktop and phone devices). + - A simple dataset that defines 10 fruit products. The dataset should include: product name, description, price per unit (where unit could be the number of items, ounces, pounds, etc.). If possible, I want to include a simple image (an emoji) that represents the product. + - A navigation menu on the left side of the screen that allows users to navigate between the Products, ProductDetails, ShoppingCart, and Checkout pages. + - Basic styling that makes the user interface visually appealing, but it doesn't need to be fully responsive or polished. + + The prototype app won't include any backend functionality, such as user authentication, payment processing, or database integration. It will be a static prototype that demonstrates the basic concepts. + + Here's a description of the user interface: + + - The Products page should display a list of products with basic information such as product name, price per unit, and an image (an emoji). The Products page should also provide a way to select a desired quantity of a product and an option to add selected items to the shopping cart. + - The ProductDetails page should display detailed information about a product when the product is selected from the Products page. The ProductDetails page should display the product name, a description of the product, the price per unit, and an image (an emoji) representing the product. The ProductDetails page should also provide a way to navigate back to the Products page. + - The ShoppingCart page should display the list of products added to the cart. The list should include the product name, quantity, and total price for that product. The ShoppingCart page should also provide a way to update the quantity of each product that's in the cart, and an option to remove products from the cart. + - The Checkout page should display a summary of the products being purchased, including product name, quantity, and price. The total price should be clearly displayed along with the option to "Process Order". + - The left-side navigation menu should provide basic navigation between pages. The navigation bar should collapse down to display a one or two letter abbreviation when the display width drops below 300 pixels. The navigation bar should allow users to navigate between the app pages. + ``` + + GitHub Copilot should generate a response that includes a suggested PRD based on the information you provided. The response should include the sections that you reviewed earlier, and it should include content for each section based on the information you provided. + +1. In the Chat view, select the **Agent** mode. + + The Set Mode dropdown menu is located in the bottom-left corner of the Chat view. + +1. In the Chat view, enter the following prompt: + + ```md + Create a markdown file named VibeCodingPRD.md using your suggested sections and the inputs that I've provided. + ``` + +1. In the Chat view, to save the suggested VibeCodingPRD.md file, select **Keep**. + +1. Take a couple minutes to review the PRD file. + + The suggested PRD file should be sufficient for this exercise. However, if you see any gaps or discrepancies, or if you want to add more information, you can edit the file directly in Visual Studio Code. You can also ask GitHub Copilot to help you refine the PRD by generating additional information or implementing clarifications. + +1. Open Visual Studio Code's EXPLORER view. + +1. In the Chat view, enter the following prompt: + + ```md + Can you create low-fidelity wireframe diagrams (or text-based layouts) that represent the web pages and navigation bar (expanded and collapsed) for my prototype app? Use the PRD that I've provided as a reference. + ``` + + GitHub Copilot Agent should generate low-fidelity text-based layouts that represent the user interface for your prototype app. These "wireframe diagrams" should help define the user interface and how users will interact with the app. + + For example, the response may look similar to the following: + + ```md + Certainly! Here are text-based, low-fidelity wireframe layouts for each main page of your prototype app, based on your PRD: + + --- + + ### 1. Products Page + + + +------------------------------------------------------+ + | [Nav] Products | ProductDetails | Cart | Checkout | + +----------------+-------------------------------------+ + | 🍎 Apple $1.00/each [Qty: - 1 +] [Add] | + | 🍌 Banana $0.50/each [Qty: - 1 +] [Add] | + | 🍊 Orange $0.80/each [Qty: - 1 +] [Add] | + | ... (other fruits) | + +------------------------------------------------------+ + + --- + + ### 2. ProductDetails Page + + +------------------------------------------------------+ + | [Nav] Products | ProductDetails | Cart | Checkout | + +----------------+-------------------------------------+ + | 🍎 Apple | + | Fresh, crisp apples | + | Price: $1.00/each | + | [Qty: - 1 +] [Add to Cart] | + | [Back to Products] | + +------------------------------------------------------+ + + --- + + ### 3. ShoppingCart Page + + +------------------------------------------------------+ + | [Nav] Products | ProductDetails | Cart | Checkout | + +----------------+-------------------------------------+ + | Cart Items: | + | 🍎 Apple Qty: [ - 2 + ] $2.00 [Remove] | + | 🍌 Banana Qty: [ - 1 + ] $0.50 [Remove] | + | -------------------------------------------------- | + | [Update Cart] | + | [Proceed to Checkout] | + +------------------------------------------------------+ + + --- + + ### 4. Checkout Page + + +------------------------------------------------------+ + | [Nav] Products | ProductDetails | Cart | Checkout | + +----------------+-------------------------------------+ + | Order Summary: | + | 🍎 Apple x2 $2.00 | + | 🍌 Banana x1 $0.50 | + | -------------------------------------------------- | + | Total: $2.50 | + | [Process Order] | + +------------------------------------------------------+ + + --- + + ### 5. Navigation Bar (Left Side, Collapsible) + + +-------------------+ +-----+ + | Products | | P | + | ProductDetails | | D | + | Cart | | C | + | Checkout | | CO | + +-------------------+ +-----+ + (Full width) (Collapsed <300px) + + --- + + These wireframes provide a clear, simple structure for each page and the navigation menu, matching your requirements. If you want these added to your PRD or as a separate markdown file, let me know! + ``` + + > **NOTE**: There are many other ways to create wireframe diagrams. For an AI-based approach that aligns with your vibe coding process, you can use Microsoft's M365 Copilot. Just provide M365 Copilot with a description of your app (the contents of your PRD) and ask the AI to create images of low-fidelity wireframe diagrams. For high-fidelity wireframe diagrams that you create manually, you can use a UI/UX design tool such as Figma. + +1. In the Chat view, enter the following prompt: + + ```md + Save the low-fidelity wireframe diagrams as text files, one file for each web page and one for navigation. + ``` + +1. Monitor the Chat view to ensure that all of the files are saved, and then select **Keep**. + +1. Take a couple minutes to review the wireframe diagrams. + + If you see any obvious issues that you want to correct, you can edit the wireframe diagrams directly in Visual Studio Code. You can also ask GitHub Copilot to help you refine the wireframe diagrams. + + For this exercise, your wireframe diagrams (text layouts) don't need to be exact, and the suggested wireframes should be sufficient without modification. However, if you experience issues later in the exercise that you attribute to the wireframe diagrams, you can ask GitHub Copilot Agent to help you refine them. + + > **Tip**: If you're unsure how to interpret a wireframe diagram, or if you think one of the diagrams may be incorrect, ask GitHub Copilot to explain the diagram(s). For example, you can ask GitHub Copilot Agent to "review the wireframe diagrams and use them to explain the layout of the user interface and how the user interacts with the app." If GitHub Copilot's explanation doesn't match your expectations, you can ask GitHub Copilot Agent for help updating the wireframe diagrams to better match your intended user experience. + +## Create an initial prototype app + +GitHub Copilot Agent can use product requirements and wireframe diagrams to develop a prototype application. Providing sufficiently detailed product requirements and wireframe diagrams helps the agent understand the user experience, app features, and design goals that you intend for your app. + +- The PRD provides detailed information about the app's purpose, target audience, features, and technical requirements. +- The wireframe diagrams show the intended user interface and help to describe the user interactions. + +In this task, you use GitHub Copilot Agent to create an initial prototype app based on the PRD and wireframe diagrams that you created. + +Use the following steps to complete this section of the exercise: + +1. In Visual Studio Code, create a new folder named **ShoppingApp** in the VibeCoding-PrototypeApp folder. + + GitHub Copilot Agent needs an empty folder to use as a workspace for the new app files. + + The EXPLORER view in Visual Studio Code should look similar to the following: + + ```plaintext + UNTITLED (WORKSPACE) + └── VibeCoding-PrototypeApp + ├── ShoppingApp + ├── VibeCodingPRD.md + ├── wireframe-checkout.txt + ├── wireframe-navigation.txt + ├── wireframe-product-details.txt + ├── wireframe-products.txt + └── wireframe-shopping-cart.txt + ``` + +1. Add the PRD and wireframe diagrams to the chat context. + + Adding these files to the chat context tells GitHub Copilot Agent to reference the files when generating a response. + + You can add files to the chat context by dragging and dropping them from the EXPLORER view onto the Chat view, or by using the **Add Context** button located in the bottom-left area of the Chat view. + +1. In the EXPLORER view, select the **ShoppingApp** folder. + +1. In the Chat view, enter the following prompt: + + ```md + I want you to create a prototype shopping app using the information in my PRD and wireframe diagrams. Create the prototype app in the selected 'ShoppingApp' folder. The prototype should implement the following: basic use case functionality, simple navigation, a sample dataset, and basic styling. After creating the prototype app, add a '.github/copilot-instructions.md' file to the workspace. Add the contents of the PRD and wireframe files to the 'copilot-instructions.md' file. + ``` + + GitHub Copilot Agent uses this prompt to generate an initial prototype app based on the requirements that you've defined. + + - The agent checks the **ShoppingApp** folder to ensure that it's empty and ready to use as a workspace. + - The agent uses the PRD and wireframe diagrams to create the prototype app files. The following files are created in the **ShoppingApp** folder: + + - **app.js**: Contains the JavaScript code that implements the app's functionality, such as managing the product catalog, shopping cart, and navigation. + - **index.html**: Serves as the entry point for the web application, setting up the basic structure and linking the styles and scripts. + - **styles.css**: Provides the visual layout and responsive design for the prototype web app. + + - The agent adds a **.github/copilot-instructions.md** file to the workspace, and then adds the contents of the PRD and wireframe files to the **copilot-instructions.md** file. + + > **TIP**: You can store custom instructions in your workspace or repository in a .github/copilot-instructions.md file. Custom instructions enable you to describe common guidelines or rules to get responses that match your specific coding practices and tech stack. Instead of manually including this context in every chat query, custom instructions automatically incorporate this information with every chat request. These instructions only apply to the workspace where the file is located. + +1. Monitor the Chat view to track the agent's progress as it works on your prototype app. + + > **NOTE**: Although GitHub Copilot Agent performs tasks as an autonomous agent, it may ask for assistance when performing certain tasks. To assist the agent, respond to any prompts that appear in the Chat view. For example, if the agent asks for permission to run a command in the terminal, select **Run** to allow the agent to run the command. If the agent asks for clarification about your requirements, provide a response that helps the agent understand your requirements. + +1. In the Chat view, to save the prototype app files, select **Keep**. + +1. Expand the **ShoppingApp** folder. + + The folder should contain the following files: + + ```plaintext + ShoppingApp + ├── .github + │ └── copilot-instructions.md + ├── app.js + ├── index.html + ├── styles.css + ``` + +1. Take a couple minutes to review each of the code files. + + - The **index.html** file serves as the entry point for a web application. It sets up the basic structure of the app and links the styles and scripts files. + - The **styles.css** file provides the visual layout and responsive design for your prototype web app. + - The **app.js** file contains the JavaScript code that manages the product catalog, shopping cart, navigation, and UI rendering. + + If time permits, consider asking GitHub Copilot to generate a detailed explanation of each file. + +1. Open the **index.html** file in the Visual Studio Code editor. + +1. On the **Run** menu, select **Run Without Debugging**. + + If prompted, select your choice of browser to run the app. + +1. With your prototype app open in the browser, test the use cases you listed in your PRD and verify that your prototype app delivers the expected functionality. + + The use cases describe basic functionality that your prototype app should implement. For example: + + - As a user, I can browse a list of fruit products. + - As a user, I can view detailed information about a selected product. + - As a user, I can add products to my shopping cart and adjust quantities. + - As a user, I can review and update my cart before checkout. + - As a user, I can view a summary of my order and "process" it (no real transaction). + - As a user, I can navigate between the Products, ProductDetails, ShoppingCart, and Checkout pages. + +1. After verifying the use cases, test the dynamic behavior of the app by resizing the browser window. + + The prototype app should have a dynamic user interface that automatically scales to accommodate viewing on desktop and phone devices. + +1. Try to test the collapsed navigation bar. + + You specified that the navigation bar should collapse when the page width drops below 300 pixels. When collapsed, the navigation bar should display one or two letters to represent each of the web pages in the app. + + > **NOTE**: Most desktop browsers (including Microsoft Edge) enforce a minimum window width that's greater than 300px (often around 320–400px). This means you may not be able to manually resize the browser window small enough to trigger the collapse of the navigation bar. + +1. (Optional) Perform additional testing to ensure that the prototype app meets your expectations. + + If you want, take notes during your testing. You can use your notes during the next task to help refine your prototype app. + +1. Close the browser window or stop the app in Visual Studio Code. + +## Refine your prototype app + +Your initial prototype app should already provide a basic implementation of the product requirements. However, it can probably be refined and improved, and it may not fully achieve the intended user experience. + +In this task, you use GitHub Copilot Agent to refine the features and behavior of your prototype app. + +Use the following steps to complete this section of the exercise: + +1. In the Chat view, to adjust the breakpoint for the collapsed navigation bar, enter the following prompt: + + ```md + #codebase Refactor the prototype app to use a higher breakpoint for the collapsed navigation bar. Change from 300 to 600px. Update the copilot-instructions.md file to explain the updated 600px requirement. + ``` + + If the agent implemented a navigation bar that changes orientation when the screen narrows (switches from vertical to horizontal), use the following command to update the navigation bar's behavior: + + ```md + #codebase Refactor the code to ensure that the navigation bar stays on the left-side of the app for all devices types and sizes. The navigation bar should be responsive and maintain its position, in either an expanded or collapsed mode. + ``` + +1. Take a minute to review the code updates that GitHub Copilot Agent generates in response to your prompt. + +1. In the Chat view, select **Keep** to save the updated prototype app files. + +1. Run the application again, and ensure that the navigation bar collapses when the width is below 600 pixels. + +1. Close the browser window or stop the app in Visual Studio Code. + +1. In the Chat view, enter the following prompt, and then monitor the agent's progress: + + ```md + #codebase Update the prototype app to display an emoji in the nav bar for each of the web pages. Ensure that the emoji is centered horizontally in the nav bar when the nav bar is collapsed. Update the copilot-instructions.md file to include this product requirement. + ``` + +1. Take a minute to review the code updates. + +1. In the Chat view, to save the updated prototype app files, select **Keep**. + +1. Run the application again and verify that emojis are displayed correctly in the navigation bar. + + The navigation bar should display an emoji that represents each web page. The emoji should be centered horizontally in the navigation bar when it's collapsed. + + If you see any additional issues with the navigation bar, you can ask GitHub Copilot Agent to help you refine the navigation bar's behavior. For example, you can ask the agent to "#codebase Refactor the code to ensure that the navigation bar is always visible and has only two stages, expanded or collapsed." + +1. Close the browser window or stop the app in Visual Studio Code. + +1. In the Chat view, to identify additional opportunities for improvements, enter the following prompt: + + ```md + #codebase Review the product requirements and wireframe diagrams in the copilot-instructions.md file. Are there any features or requirements that are missing from the implementation? Are there obvious opportunities to improve the user experience? + ``` + +1. Review the response from GitHub Copilot Agent. + + Identify three or more suggested improvements that you'd like to implement. + +1. Create a prompt that describes the improvements that you want to implement. + + Use GitHub Copilot's suggestions, and any testing notes that you created, to implement improvements. For example, you can ask GitHub Copilot Agent to help you implement the following changes: + + ```md + #codebase Implement the following improvements to the prototype app: + + - Replace alert() popups with in-app notification banners or toasts. + - Add a confirmation/thank you message after processing an order. + - Add a visual indicator (badge) for the number of items in the cart on the nav bar. + + Ensure that the copilot-instructions.md file is updated to reflect any changes to the product features, technical requirements, user experience, or other measurable characteristics. + ``` + + > **TIP**: You can copy information from GitHub Copilot's response to help construct your new prompt. You can also refer to sections of the previous response in your prompt. + +1. If time permits, continue refining your app using GitHub Copilot's suggestions and your own ideas. + +1. On the File menu, select **Save Workspace As...**. + +1. To save the workspace configuration file (VibeCoding-PrototypeApp.code-workspace) in the **VibeCoding-PrototypeApp** folder, select **Save**. + + This file allows you to save and reopen your workspace with the same folder structure and settings. + +## Summary + +In this exercise, you learned how to use GitHub Copilot Agent to create a prototype app using a vibe coding process. You defined product requirements, created an initial prototype app, and refined the prototype app to better meet the intended user experience and functionality. + +## Clean up + +Now that you've finished the exercise, take a minute to ensure that you haven't made changes to your GitHub account or GitHub Copilot subscription that you don't want to keep. If you made any changes, revert them as needed. If you're using a local PC as your lab environment, you can archive or delete the prototype app folder that you created for this exercise. diff --git a/Instructions/Labs/LAB_AK_07_consolidate_duplicate_code.md b/Instructions/Labs/LAB_AK_07_consolidate_duplicate_code.md new file mode 100644 index 0000000..76752b2 --- /dev/null +++ b/Instructions/Labs/LAB_AK_07_consolidate_duplicate_code.md @@ -0,0 +1,448 @@ +--- +lab: + title: Exercise - Consolidate duplicate code using GitHub Copilot + description: Learn how to analyze a complex codebase and consolidate duplicated code logic using GitHub Copilot tools. + duration: 30 minutes + level: 200 + islab: true + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Consolidate duplicate code using GitHub Copilot + +Duplicate code logic is often introduced when developing/extending a codebase that includes similar or related features. It might not be intentional, and it can be as simple as reusing code to prototype a new feature. If duplicated logic evolves to match the surrounding code over time, the issue can become more complicated. Changes to variable names, function names, and code structures in one location (but not the other) can mask duplicated logic. A rushed schedule, poor documentation, and a lack of proper code reviews can exacerbate the issue. In the end, duplicated logic makes the code difficult to read, maintain, debug, and test. + +In this exercise, you review an existing project that contains duplicated code logic, analyze your options for consolidation, consolidate the duplicated code logic, and test the refactored code to ensure it works as intended. You use GitHub Copilot in Ask mode to gain an understanding of an existing code project and explore options for consolidating the logic. You use GitHub Copilot in Agent mode to refactor the code by combining duplicate logic into shared helper methods. You test the original and refactored code to ensure the consolidated logic works as intended. + +This exercise should take approximately **30** minutes to complete. + +> **IMPORTANT**: To complete this exercise, you must provide your own GitHub account and GitHub Copilot subscription. If you don't have a GitHub account, you can sign up for a free individual account and use a GitHub Copilot Free plan to complete the exercise. If you have access to a GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscription from within your lab environment, you can use your existing GitHub Copilot subscription to complete this exercise. + +## Before you start + +Your lab environment must include the following resources: Git 2.48 or later, .NET SDK 9.0 or later, Visual Studio Code with the C# Dev Kit extension, and access to a GitHub account with GitHub Copilot enabled. + +### Configure your lab environment + +If you're using a local PC as a lab environment for this exercise: + +- For help with configuring your local PC as your lab environment, open the following link in a browser: Configure your lab environment resources. + +- For help with enabling your GitHub Copilot subscription in Visual Studio Code, open the following link in a browser: Enable GitHub Copilot within Visual Studio Code. + +If you're using a hosted lab environment for this exercise: + +- For help with enabling your GitHub Copilot subscription in Visual Studio Code, paste the following URL into a browser's site navigation bar: Enable GitHub Copilot within Visual Studio Code. + +- To ensure that the .NET SDK is configured to use the official NuGet.org repository as a source for downloading and restoring packages: + + Open a command terminal and then run the following command: + + ```bash + + dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org + + ``` + +### Download sample code project + +Use the following steps to download the sample project and open it in Visual Studio Code: + +1. Open a browser window in your lab environment. + +1. To download a zip file containing the sample app project, open the following URL in your browser: [GitHub Copilot lab - consolidate duplicate code](https://github.com/MicrosoftLearning/mslearn-github-copilot-dev/raw/refs/heads/main/DownloadableCodeProjects/Downloads/GHCopilotEx7LabApps.zip) + + The zip file is named **GHCopilotEx7LabApps.zip**. + +1. Extract the files from the **GHCopilotEx7LabApps.zip** file. + + For example: + + 1. Navigate to the downloads folder in your lab environment. + + 1. Right-click **GHCopilotEx7LabApps.zip**, and then select **Extract all**. + + 1. Select **Show extracted files when complete**, and then select **Extract**. + +1. Copy the **GHCopilotEx7LabApps** folder to a location that's easy to access, such as your Windows Desktop folder. + +1. Open the **GHCopilotEx7LabApps** folder in Visual Studio Code. + + For example: + + 1. Open Visual Studio Code in your lab environment. + + 1. In Visual Studio Code, on the **File** menu, select **Open Folder**. + + 1. Navigate to the Windows Desktop folder, select **GHCopilotEx7LabApps** and then select **Select Folder**. + +1. In the Visual Studio Code SOLUTION EXPLORER view, verify the following project structure: + + - GHCopilotEx7LabApps\ + - ECommerceOrderAndReturn\ + - Dependencies\ + - Configuration\ + - AppConfig.cs + - Models\ + - Order.cs + - Return.cs + - Security\ + - SecurityValidator.cs + - Services\ + - AuditService.cs + - EmailService.cs + - InventoryService.cs + - EXPECTED_OUTPUT.md + - OrderProcessor.cs + - Program.cs + - README.md + - ReturnProcessor.cs + +## Exercise scenario + +You're a software developer working for a consulting firm. Your clients need help with consolidating duplicate code logic. Your goal is to improve code maintainability while preserving the existing functionality. You're assigned to the following app: + +- E-CommerceOrdersAndReturns: This app is an E-commerce app that processes customer orders and handles product returns. It includes core business logic for validating orders and returns, calculating shipping costs, sending email notifications, logging audit activities, and managing inventory levels. + +This exercise includes the following tasks: + +1. Review the E-commerce orders and returns codebase manually. +1. Identify duplicate code using GitHub Copilot Chat (Ask mode). +1. Consolidate duplicate logic using GitHub Copilot Chat (Agent mode). +1. Test the refactored E-commerce orders and returns code. + +### Review the E-commerce orders and returns codebase manually + +The first step in any refactoring effort is to ensure that you understand the existing codebase. It's important to understand the code structure, the business logic, and the results generated when the code runs. + +In this task, you review the main components of the E-commerce order and return processing project, scan the code for duplicate code patterns, and test the code. + +Use the following steps to complete this task: + +1. Take a minute to review the ECommerceOrderAndReturn project structure. + + The codebase follows a layered architecture typical of enterprise applications. The main architectural layers include: Models, Configuration, Security, Services, and Processing. + +1. Examine the main processing classes. + + Open **OrderProcessor.cs** and **ReturnProcessor.cs** side by side. These classes represent the core business logic for processing customer orders and product returns respectively. + + Notice that the two classes have similar method signatures and processing patterns. This similarity is the most obvious type of duplication, but there are other, more subtle duplications throughout the codebase. + +1. Review the Services layer. + + Navigate to the **Services** folder and examine **EmailService.cs**, **AuditService.cs**, and **InventoryService.cs**. + + You might notice that these services implement similar patterns for handling email notifications, audit logging, and inventory management. Each service has methods that follow similar structures but are duplicated for different business processes (orders vs returns). + +1. Run the application and review the console output. + + > **NOTE**: A copy of the console output is stored in the EXPECTED_OUTPUT.md file that's included in the project directory. You'll use this file to verify that the application behavior hasn't changed after consolidating the duplicate code. + + You can run the application from the SOLUTION EXPLORER view by right-clicking **ECommerceOrdersAndReturns**, selecting **Debug**, and then selecting **Start New Instance**. + + The console output includes: + + - Initial inventory levels + - Order processing with validation, shipping calculation, payment processing, inventory reservation, email notifications, and audit logging + - Return processing with similar steps but adapted for returns + - Updated inventory levels + - Security validation tests with various invalid inputs + + The application runs five test scenarios to demonstrate both successful processing and security validation failures. + +1. Take a minute to categorize any duplicate code patterns that you observed. + + For example: + + **Duplicated Methods**: OrderProcessor and ReturnProcessor have identical `Validate()` and similar `CalculateShipping()` methods. + + **Similar Patterns in the Service Layer**: Each service has methods that follow similar patterns but are duplicated for different business processes (orders vs returns). + +It's important to understand the existing functionality before making changes. By running the code and reviewing the output, you establish a baseline that you can use to verify that your refactoring doesn't break existing functionality. + +### Identify duplicate code using GitHub Copilot Chat (Ask mode) + +GitHub Copilot Chat's Ask mode is a great tool for analyzing complex codebases and identifying subtle duplication patterns that might not be immediately obvious. In Ask mode, Copilot acts as an intelligent code reviewer that can analyze multiple files simultaneously and identify both obvious and hidden (code logic) duplications. + +In this task, you use GitHub Copilot to systematically identify the various types of duplicate code patterns in the e-commerce application. + +Use the following steps to complete this task: + +1. Set the chat mode to **Ask** and select the **Auto** model. + + The Set Mode and Pick Model menus are in the bottom-left corner of the Chat view. + + If the Chat view isn't already open, select the **Chat** icon at the top of the Visual Studio Code window. + +1. Close any files that are open in the editor. + + GitHub Copilot uses files that are open in the editor to establish context. Closing unwanted file tabs helps to reduce noise in the analysis. + +1. Add the OrderProcessor and ReturnProcessor files to the Chat context. + + Use a drag-and-drop operation to add the **OrderProcessor.cs** and **ReturnProcessor.cs** files from the SOLUTION EXPLORER to the Chat context. + + Adding a file to the chat context tells GitHub Copilot to include that file in context when analyzing your prompt. + + If you're using the default folder view rather than SOLUTION EXPLORER, you can right-click a file and then select **Add File to Chat**. You can also open a file in the code editor to help establish context. + +1. Ask GitHub Copilot to identify duplicate code patterns in the selected files. + + For example, submit the following prompt to analyze the core duplication: + + ```text + What duplicate code exists between OrderProcessor.cs and ReturnProcessor.cs? Identify specific methods and logic that are duplicated between these classes. Describe opportunities to consolidate duplicate code. + ``` + +1. Take a minute to review GitHub Copilot's response. + + GitHub Copilot should identify the `Validate()` method duplication and the similar patterns in `CalculateShipping()` methods. It might also note similar patterns in relation to audit logging, error handling, and payment/refund processing. + +1. Update the chat context to specify the **EmailService.cs** file. + + Open **EmailService.cs** in the editor. You can also add the **EmailService.cs** file to the Chat context using a drag-and-drop operation. Remove all other files from the context. + +1. Ask GitHub Copilot to identify duplications in the EmailService class. + + For example: + + ```text + Analyze the EmailService class. What duplicate logic exists within this service for handling order confirmations versus return confirmations? Describe opportunities to consolidate duplicate code. + ``` + +1. Take a minute to review GitHub Copilot's response. + + GitHub Copilot should identify the duplicate logic for handling order confirmations and return confirmations. The response includes a templated approach to preparing and sending emails. GitHub Copilot might also identify duplicate patterns related to helper methods and console output. + +1. Update the chat context to specify the **AuditService.cs** and **InventoryService.cs** files. + + Open **AuditService.cs** and **InventoryService.cs** in the editor. You can also add these files to the Chat context using a drag-and-drop operation. Remove all other files from the context. + +1. Ask GitHub Copilot to identify duplications in the audit and inventory service files. + + For example: + + ```text + Analyze the AuditService and InventoryService classes. Identify the methods that contain duplicate logic patterns that could be consolidated. Describe opportunities to consolidate duplicate code. + ``` + +1. Take a minute to review GitHub Copilot's response. + + GitHub Copilot should identify patterns like audit entry creation/validation/storage in AuditService, and inventory validation/updating/logging in InventoryService. GitHub Copilot might also identify duplicate patterns related to helper methods. + +1. Ask GitHub Copilot to perform a comprehensive duplication analysis. + + For example: + + ```text + @workspace Analyze the entire ECommerceOrderAndReturn codebase and identify all forms of code duplication, including method-level, service-level, and architectural pattern duplications. Prioritize the duplications by impact and refactoring difficulty. Describe an approach for consolidating this code. + ``` + + After analyzing specific files, you can get a broader view by asking GitHub Copilot to include the entire codebase in its analysis. The increased scope can reveal other duplication patterns, such as similar error handling, logging, and validation logic across multiple files or layers. Having a single response that informs and prioritizes your refactoring strategy is often helpful. + + > **NOTE**: The `@workspace` and `#codebase` keywords tell GitHub Copilot to include the entire codebase in the context of its analysis. The `@workspace` keyword is only available when using GitHub Copilot in the Ask mode. The `#codebase` keyword can be used in any mode (Ask, Edit, or Agent). + +1. Take a minute to review GitHub Copilot's response. + + The response should provide a comprehensive analysis of all duplication patterns and suggest an approach for consolidating duplicate code. In a production scenario, you should analyze each section of the response, and consider using follow-up prompts to dig deeper into specific areas. + + For this training exercise, it's important to understand what types of information GitHub Copilot can provide, but you can focus on the summary section when considering your refactoring strategy. + + For example: + + ```md + + **Summary** + + The codebase contains significant method-level and service-level duplication, especially in validation, shipping calculation, audit logging, email notification, and inventory management. The highest priority and easiest wins are consolidating validation and shipping logic. Service-level helper methods should be refactored next. The processor workflow pattern can be abstracted for further maintainability, though this is more complex. + + Start with shared services for validation and shipping, then refactor service helpers, and finally consider architectural abstraction for processors. + + ``` + +1. Verify GitHub Copilot's analysis with manual code review. + + Cross-reference GitHub Copilot's findings with your own observations. Ensure that you understand not just what is duplicated, but why these patterns exist and how they should be consolidated while maintaining the business logic integrity. + +GitHub Copilot's Ask mode is good at identifying subtle duplications that go beyond simple copy-paste scenarios. It can recognize similar logical patterns, equivalent business rules implemented differently, and architectural duplications that span multiple files. + +> **NOTE**: The analysis generated during this task is used to inform the refactoring strategy that you implement in the next section. + +### Consolidate duplicate logic using GitHub Copilot Chat (Agent mode) + +GitHub Copilot's Agent mode enables you to assign moderately complex, multi-step refactoring tasks that span multiple files and architectural layers. The agent can autonomously create new files, modify existing code, and implement comprehensive refactoring strategies while keeping you informed of its progress. + +In this task, you use GitHub Copilot Agent to systematically eliminate the duplicate code patterns identified in the previous task, starting with the most straightforward duplications and progressing to more complex service-layer consolidations. + +Use the following steps to complete this task: + +1. Switch the GitHub Copilot Chat view to Agent mode. + + When you use Agent mode, GitHub Copilot can make autonomous changes to your codebase. Agent mode is good at moderately complex, multi-step refactoring tasks. + + To change modes, locate the mode selector (typically in the bottom-left corner of the Chat view) and select **Agent**. + +1. Take a minute to plan your refactoring strategy. + + Before assigning tasks to GitHub Copilot Agent, consider the logical order for refactoring. Use the analysis from the previous task to inform your decisions. + + For example: + + If GitHub Copilot suggested: + + "Start with shared services for validation and shipping, then refactor service helpers, and finally consider architectural abstraction for processors." + + You can use the following phased approach: + + - **Phase 1**: Core business logic duplication (validation and shipping calculation) + - **Phase 2**: Service-layer duplications (email, audit, inventory services) + - **Phase 3**: Cross-cutting concerns and architectural improvements + + This phased approach ensures that changes are manageable and can be tested incrementally. + +1. Ask GitHub Copilot Agent to consolidate the validation logic in the OrderProcessor and ReturnProcessor classes. + + For example, submit the following task to GitHub Copilot Agent: + + ```text + Create a shared ValidationService class that consolidates the duplicate Validate() method logic from OrderProcessor and ReturnProcessor. The service should handle ID validation for both orders and returns while maintaining all existing security checks, business rules, and logging. Update both processor classes to use the new shared service and remove the duplicate private methods. Build and test the code to ensure the functionality remains intact. Continue working until the validation service is fully integrated. + ``` + +1. Monitor the agent's progress in the Chat view. + + The agent's progress should be visible in the chat as it completes the assigned task. + + To assist the agent as the task is being processed, provide permission to continue or supply more context as needed. For example, if the agent asks for permission to Build or Run the application, select **Continue** + + The agent completes the following steps during this task: + + - Create a new **ValidationService.cs** file in the Services folder + - Extract the validation logic into a reusable method + - Update both processor classes (to use the new service) + - Remove the duplicate private validation methods + - Verify functionality with a successful build and test run + +1. Review and accept the validation service changes. + + Use the code editor tabs to examine the changes proposed by the agent. You can scroll through the chat edits to see the specific code modifications. Select **Keep** on the editor tab to accept the current edit. You can select **Undo** on the editor tab to reject an edit. + + Select **Keep** or **Undo** in the Chat view to accept or reject all changes + + The new validation service should maintain all the existing validation logic while providing a single, reusable implementation. If the changes look correct, accept them. + + > **NOTE**: When you're working on production code, it's important to thoroughly test your code after significant refactoring operations. This involves building and testing the application to verify that features are working as intended, unit tests are passing, and the output remains consistent with the original behavior. To save time during this training exercise, we're relying on the agent to perform incremental testing. An additional (manual verification) test will be done after all refactoring tasks are complete. + +1. Ask GitHub Copilot Agent to consolidate shipping calculation logic. + + For example, use the following task to consolidate shipping calculations: + + ```text + Create a shared ShippingCalculationService that consolidates the similar CalculateShipping() logic from OrderProcessor and ReturnProcessor. The service should handle both order shipping (with free shipping thresholds) and return shipping (with processing fees) while maintaining the different business rules for each type. Update both processor classes to use the new shared service. Build and test the code to ensure the functionality remains intact. Continue working until the shipping calculation service is fully integrated. + ``` + + The agent should create a shipping service that handles both scenarios while preserving the different business rules for orders versus returns. + + > **NOTE**: If the agent encounters any issues during the code refactoring process, it should refer to the original processor classes for guidance, and it should continue to update and test the code until the shipping calculation service is fully integrated. If the agent fails to accomplish the assigned task, or if the agent enters a code revision loop that it's unable to resolve, stop the agent and undo the edits. The chat session should include sufficient context to troubleshoot the issue and update the task (prompt). You can also ask GitHub Copilot for help updating the assigned task in a way that resolves the issue. + +1. Review and accept the changes. + +1. Ask GitHub Copilot Agent to refactor the EmailService duplications. + + For example, use the following task to consolidate the email service duplications: + + ```text + Refactor the EmailService class to eliminate duplicate logic in the helper methods. Create a unified approach for template building, subject formatting, email sending, and activity logging that can handle both order and return confirmations. The public methods SendOrderConfirmation and SendReturnConfirmation should remain, but they should use shared private helper methods. Build and test the code to ensure the functionality remains intact. Continue working until the unified approach is fully integrated. + ``` + +1. Review and accept the changes. + +1. Ask GitHub Copilot Agent to consolidate AuditService duplications. + + For example, use the following task to consolidate the audit service duplications: + + ```text + Refactor the AuditService class to consolidate the duplicate logic in LogOrderActivity and LogReturnActivity. Create shared helper methods for audit entry creation, validation, storage, and compliance checking. The public methods should remain but use common underlying logic. Build and test the code to ensure the functionality remains intact. Continue working until the shared helper methods are fully integrated. + ``` + +1. Review and accept the changes. + +1. Ask GitHub Copilot Agent to address InventoryService duplications. + + For example, use the following task to handle the inventory service duplications: + + ```text + Refactor the InventoryService class to eliminate duplicate logic between ReserveOrderInventory and RestoreReturnInventory. Create shared helper methods for inventory validation, level updates, and transaction logging while maintaining the different business logic for reservations versus restorations. Build and test the code to ensure the functionality remains intact. Continue working until the shared helper methods are fully integrated. + ``` + +1. Ask GitHub Copilot Agent to consolidate any remaining duplications in the codebase. + + For example, use the following task to address any remaining duplications: + + ```text + Analyze the entire ECommerceOrderAndReturn codebase and identify any remaining duplicate code patterns that should be consolidated. Focus on cross-cutting concerns like payment processing, status updates, and error handling. Create shared services or helper methods as needed to eliminate these duplications while maintaining existing functionality. Build and test the code to ensure the functionality remains intact. Continue working until all identified duplications are fully integrated. + ``` + + Assigning tasks like this "catch any remaining issues" to GitHub Copilot Agent should only be done at the end of the refactoring process, and should only be used when necessary. Attempting this type of broad stroke analysis too early can produce unexpected results or task failures that need to be rolled back. It's best to create a planned approach and address revisions on a priority basis using a staged process. + + > **Note:** Even after GitHub Copilot "consolidates the remaining duplicated code patterns", there may be opportunities for further consolidation. However, the planned approach that you implemented should consolidate all major duplications in the codebase. If you have concerns, you can repeat the analysis and refactoring process. Remember that GitHub Copilot should not be used as a substitute for a formal code review process. + +GitHub Copilot Agent excels at complex, multi-file refactoring tasks that require understanding of business logic and architectural patterns. By breaking the refactoring into logical phases and testing incrementally, you ensure that the consolidation maintains system integrity while significantly improving code quality, maintainability, extensibility, and reusability. + +### Test the refactored E-commerce orders and returns code + +Manual testing and verification are crucial to ensure that your refactored code maintains the intended business logic and functionality. A successful refactoring process should achieve the intended goal (such as consolidating duplicate code logic) while producing identical behavior to the original implementation. + +In this task, you verify that the refactored code maintains all original functionality and that the consolidation was successful. + +Use the following steps to complete this task: + +1. Build the refactored project to check for compilation errors. + + If there are any compilation errors, review the refactored code and resolve issues. You can use GitHub Copilot to help diagnose and fix any problems that arise from the refactoring process. + +1. Run the refactored application and capture the output. + + The application should run all five test scenarios exactly as before: + + - Initial inventory display + - Valid order processing with complete workflow + - Valid return processing with complete workflow + - Updated inventory levels + - Security validation tests with invalid inputs + +1. Ask GitHub Copilot to compare the output generated by the refactored code with the original output. + + For example: + + ```text + Run the app and compare the generated output with the contents of the EXPECTED_OUTPUT.md file. Does the current output match the stored output? Explain any differences. + ``` + + The original output, **EXPECTED_OUTPUT.md**, is included in the ECommerceOrderAndReturn folder. + + You can create a second output file and ask GitHub Copilot to identify any differences between the two files. + + The output should be identical, confirming that the business logic remains unchanged while consolidating the duplicate code logic. + +1. Perform a final code review. + + Review the refactored codebase to ensure: + + - **Code Quality**: Methods are well-named and follow consistent patterns + - **Maintainability**: Changes to business rules now require updates in only one location + - **Readability**: The code structure is clear and logical + - **Reusability**: Shared services can be easily extended for future requirements + - **Extensibility**: New features can be added with minimal effect on existing code + +Manual testing verifies that your consolidation efforts achieved the intended goal: eliminating duplicate code while maintaining system functionality. The architecture now provides a more maintainable foundation for future development, where business rule changes can be implemented in a single location rather than requiring updates across multiple duplicate implementations. + +## Summary + +In this exercise, you learned how to use GitHub Copilot to consolidate duplicate code in an application. You explored the E-commerce Order and Return Processing System, identified duplicate code patterns, and used GitHub Copilot to refactor the codebase for improved maintainability and readability. + +## Clean up + +Now that you finished the exercise, take a minute to ensure that you haven't made changes to your GitHub account or GitHub Copilot subscription that you don't want to keep. If you made any changes, revert them as needed. If you're using a local PC as your lab environment, you can archive or delete the sample projects folder that you created for this exercise. diff --git a/Instructions/Labs/LAB_AK_08_refactor_large_functions.md b/Instructions/Labs/LAB_AK_08_refactor_large_functions.md new file mode 100644 index 0000000..d446993 --- /dev/null +++ b/Instructions/Labs/LAB_AK_08_refactor_large_functions.md @@ -0,0 +1,568 @@ +--- +lab: + title: Exercise - Refactor large functions using GitHub Copilot + description: Learn how to analyze complex code and refactor large functions into smaller, more focused methods using GitHub Copilot tools. + duration: 30 minutes + level: 200 + islab: true + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Refactor large functions using GitHub Copilot + +Large functions can be difficult to read, maintain, and test. They often contain multiple responsibilities and can be challenging to understand at a glance. Code readability and maintainability improves when large functions are refactored into smaller, more focused functions. + +In this exercise, you review an existing project that contains a large function, analyze your options for smaller single-responsibility functions, refactor the large function into smaller functions, and test the refactored code to ensure it works as intended. You use GitHub Copilot in Ask mode to gain an understanding of an existing code project and explore options for refactoring the logic. You use GitHub Copilot in Agent mode to refactor the code by extracting code sections from the large function to create smaller functions. You test the original and refactored code to ensure the refactored code works as intended. + +This exercise should take approximately **30** minutes to complete. + +> **IMPORTANT**: To complete this exercise, you must provide your own GitHub account and GitHub Copilot subscription. If you don't have a GitHub account, you can sign up for a free individual account and use a GitHub Copilot Free plan to complete the exercise. If you have access to a GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscription from within your lab environment, you can use your existing GitHub Copilot subscription to complete this exercise. + +## Before you start + +Your lab environment must include the following resources: Git 2.48 or later, .NET SDK 9.0 or later, Visual Studio Code with the C# Dev Kit extension, and access to a GitHub account with GitHub Copilot enabled. + +### Configure your lab environment + +If you're using a local PC as a lab environment for this exercise: + +- For help with configuring your local PC as your lab environment, open the following link in a browser: Configure your lab environment resources. + +- For help with enabling your GitHub Copilot subscription in Visual Studio Code, open the following link in a browser: Enable GitHub Copilot within Visual Studio Code. + +If you're using a hosted lab environment for this exercise: + +- For help with enabling your GitHub Copilot subscription in Visual Studio Code, paste the following URL into a browser's site navigation bar: Enable GitHub Copilot within Visual Studio Code. + +- To ensure that the .NET SDK is configured to use the official NuGet.org repository as a source for downloading and restoring packages: + + Open a command terminal and then run the following command: + + ```bash + + dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org + + ``` + +### Download sample code project + +Use the following steps to download the sample project and open it in Visual Studio Code: + +1. Open a browser window in your lab environment. + +1. To download a zip file containing the sample app project, open the following URL in your browser: [GitHub Copilot lab - refactor large functions](https://github.com/MicrosoftLearning/mslearn-github-copilot-dev/raw/refs/heads/main/DownloadableCodeProjects/Downloads/GHCopilotEx8LabApps.zip) + + The zip file is named **GHCopilotEx8LabApps.zip**. + +1. Extract the files from the **GHCopilotEx8LabApps.zip** file. + + For example: + + 1. Navigate to the downloads folder in your lab environment. + + 1. Right-click **GHCopilotEx8LabApps.zip**, and then select **Extract all**. + + 1. Select **Show extracted files when complete**, and then select **Extract**. + +1. Copy the **GHCopilotEx8LabApps** folder to a location that's easy to access, such as your Windows Desktop folder. + +1. Open the **GHCopilotEx8LabApps** folder in Visual Studio Code. + + For example: + + 1. Open Visual Studio Code in your lab environment. + + 1. In Visual Studio Code, on the **File** menu, select **Open Folder**. + + 1. Navigate to the Windows Desktop folder, select **GHCopilotEx8LabApps** and then select **Select Folder**. + +1. In the Visual Studio Code SOLUTION EXPLORER view, verify the following project structure: + + - GHCopilotEx8LabApps\ + - ECommerceOrderProcessing\ + - src\ + - ECommerce.ApplicationCore\ + - Entities\ + - Exceptions\ + - Interfaces\ + - Services\ + - OrderProcessor.cs + - ECommerce.Console\ + - order_audit_log.txt + - Program.cs + - ECommerce.Infrastructure\ + - Services\ + - ServerLogAnalysisUtility\ + +## Exercise scenario + +You're a software developer working for a consulting firm. Your clients need help with refactoring large functions in legacy applications. Your goal is to improve code readability and maintainability while preserving the existing functionality. You're assigned to the following app: + +- E-CommerceOrderProcessing: This e-commerce app is used to process customer orders. The process includes order validation, inventory management, payment processing, shipping coordination, and customer notifications. The application uses Clean Architecture principles with a layered structure, but contains a large method in the **OrderProcessor** class that handles multiple responsibilities and needs to be refactored into smaller, more focused methods. + +This exercise includes the following tasks: + +1. Review the e-commerce order processing codebase manually. +1. Identify refactoring opportunities using GitHub Copilot Chat (Ask mode). +1. Refactor a large function into smaller, more manageable functions using GitHub Copilot Chat (Agent mode). +1. Test the refactored e-commerce order processing code. + +### Review the e-commerce order processing codebase manually + +The first step in any refactoring effort is to ensure that you understand the existing codebase. It's important to understand the code structure, the business logic, and the results generated when the code runs. + +In this task, you review the main components of the E-commerce order processing project and run the app to observe its functionality. + +Use the following steps to complete this task: + +1. Ensure that you have the GHCopilotEx8LabApps folder open in Visual Studio Code. + + Refer to the **Before you start** section if you haven't downloaded the sample code project. + +1. Take a minute to review the ECommerceOrderProcessing project structure. + + The codebase follows a layered architecture pattern with three main projects: + + - **ECommerce.ApplicationCore**: Contains domain entities, business logic interfaces, and the main OrderProcessor service. + - **ECommerce.Console**: Contains the console application entry point and dependency injection setup. + - **ECommerce.Infrastructure**: Contains service implementations for external integrations (payment, shipping, inventory, etc.). + + This structure represents a real-world .NET application using Clean Architecture principles, where business logic is separated from infrastructure concerns. + +1. Open the GitHub Copilot Chat view. + + If the Chat view isn't already open, you can open it by selecting the **Chat** icon at the top of the Visual Studio Code window. + +1. Set the chat mode to **Ask** and select the **Auto** model. + + The Set Mode and Pick Model menus are in the bottom-left corner of the Chat view. + + You'll be using GitHub Copilot's **Agent** mode later in this exercise, but for now you should use **Ask** mode for code analysis and explanations. + + > **NOTE**: GitHub Copilot's responses can vary based on the selected model. We suggest that you use the specified model when performing this lab exercise. You can repeat the exercise with a different model if you want to see the differences. + +1. Use the SOLUTION EXPLORER view to locate the **OrderProcessor.cs** file. + + The **OrderProcessor.cs** file is located in the **src/ECommerce.ApplicationCore/Services** folder. + +1. Open the **OrderProcessor.cs** file in the code editor. + + The OrderProcessor class is responsible for processing customer orders. It contains the main business logic for order processing, including validation, payment processing, and notification. + +1. Take a minute to review the **OrderProcessor** class. + + Notice the ProcessOrder method. This method represents the core business logic for processing customer orders. Notice that it handles multiple distinct operations. The ProcessOrder method is intentionally large and complex to demonstrate real-world scenarios where business logic grew in complexity over time, making it difficult to read, test, and maintain. + +1. Right-click the **ProcessOrder** method, and then select **Copilot** > **Explain**. + + If prompted to **Select an enclosing range to explain**, select **ProcessOrder**. + + GitHub Copilot analyzes the ProcessOrder method and provide a detailed explanation of what the code does, helping you understand the business logic before you investigate refactoring options. + +1. Take a couple minutes to review GitHub Copilot's explanation. + + The explanation should highlight the main processing steps and business rules, such as the comprehensive validation procedures, security risk assessments, multi-service coordination, and error handling with rollback capabilities. + +1. Run the application to gain an understanding of its current behavior. + + You have several options for running the application. For example: + + In the SOLUTION EXPLORER view, right-click the **ECommerce.Console** project, select **Debug**, and then select **Start New Instance**. Or, if you have the **Program.cs** file open in Visual Studio Code, you can select the run button above the editor. + + You can also navigate to the **src/ECommerce.Console** folder in the terminal and enter the following .NET CLI command: + + ```bash + dotnet run + ``` + +1. Review the console output generated by the application. + + The application generates output for four test cases. Each test case demonstrates a different scenario: + + - **Test 1**: Valid order processing with multiple items. + - **Test 2**: Invalid email address validation. + - **Test 3**: Declined payment handling. + - **Test 4**: Suspicious order security checks. + + The output for each test case shows the step-by-step processing including validation messages, inventory checks, payment processing, shipping scheduling, and notifications. The output also shows how different failure scenarios are handled with appropriate error messages and cleanup procedures. + +1. Take a minute to consider your refactoring opportunities for the ProcessOrder method. + + The ProcessOrder method has several distinct responsibilities. Each of the corresponding code sections could be extracted into a separate method. + +Understanding the existing functionality and identifying refactoring opportunities helps you create a refactoring strategy that maintains business logic while improving code structure. The layered architecture already provides good separation of concerns at the project level, but the large ProcessOrder method needs attention. + +### Identify refactoring opportunities using GitHub Copilot Chat (Ask mode) + +GitHub Copilot Chat's Ask mode is a great tool for analyzing complex code and identifying opportunities for refactoring large methods. In Ask mode, Copilot can analyze your code structure and suggest ways to break down monolithic methods into smaller, more focused methods. + +In this task, you use GitHub Copilot to evaluate the ProcessOrder method and identify refactoring opportunities that maintain business logic while improving code structure. + +Use the following steps to complete this task: + +1. Ensure that you have the GitHub Copilot Chat view open with **Ask** mode and the **Auto** model selected. + +1. If you opened any files other than OrderProcessor.cs, close them now. + + GitHub Copilot uses files that are open in the editor to establish context. Having only the target file open helps focus the analysis on the code you want to refactor and ensures GitHub Copilot provides the most relevant suggestions. + +1. Add the OrderProcessor.cs file to the Chat context. + + Use a drag-and-drop operation to add the **src/ECommerce.ApplicationCore/Services/OrderProcessor.cs** file from the SOLUTION EXPLORER to the Chat context. Adding a file to the chat context tells GitHub Copilot to include that file when analyzing your prompt, which improves the accuracy of its analysis. + +1. Ask GitHub Copilot to analyze the ProcessOrder method for refactoring opportunities. + + Submit a prompt that asks GitHub Copilot to analyze the ProcessOrder method and identify specific areas for improvement. Consider including details about what you want to achieve with the refactoring. + + For example: + + ```text + Analyze the ProcessOrder method in the OrderProcessor class. This method handles multiple responsibilities. Identify opportunities to break this large method into smaller, more focused methods. What specific functions could be extracted, and what would be the benefits of doing so? + ``` + +1. Take a couple minutes to review GitHub Copilot's response. + + GitHub Copilot should identify the various responsibilities within the ProcessOrder method and suggest how to extract them into separate methods. The analysis should identify distinct logical sections that can become individual methods, such as validation logic, security assessments, inventory operations, payment processing, shipping coordination, notification handling, and order finalization. + + For example, GitHub Copilot might identify: + + - Input validation and security checks as candidates for extraction. + - Inventory management operations that could be grouped together. + - Payment processing logic with its own error handling. + - Shipping and notification logic as separate concerns. + - Order finalization steps that could be isolated. + +1. Ask GitHub Copilot to provide a detailed refactoring plan. + + For example: + + ```text + Create a detailed refactoring plan for the ProcessOrder method. Show me what the ProcessOrder method would look like after refactoring and provide a list of the methods that should be extracted. I'd like to keep the input validation and security checks together in a single method. Include suggestions for method signatures and return types that would maintain the current error handling behavior. + ``` + + Follow-up prompts like this one can be used to gain additional insights, but you should avoid prompts that deviate from your goal. Side-tracking the chat conversation can influence GitHub Copilot's responses. A clean chat history is important. + +1. Take a few minutes to review GitHub Copilot's refactoring plan. + + GitHub Copilot should provide a clear outline showing how the ProcessOrder method could be transformed from a large monolithic method into a series of smaller, focused method calls. This plan should maintain the existing business logic while improving code structure and readability. + + The response should include: + - A high-level flow showing the main steps as separate method calls. + - Suggested method names and signatures for the extracted methods. + - Guidance on how to handle errors consistently across methods. + - Explanations of how the refactored structure improves maintainability. + +1. Ask for more guidance on error handling patterns. + + Understanding the error handling process is crucial for maintaining existing behavior when you refactor the ProcessOrder method. You can have GitHub Copilot analyze the current error handling strategy and suggest a way to maintain or improve the existing behavior. + + For example: + + ```text + In the current ProcessOrder method, there are multiple error scenarios with specific cleanup procedures (like releasing inventory on payment failure). In the refactored version, how should I handle errors consistently across the extracted methods? Should each method return an OrderResult object, throw exceptions, or use another pattern to maintain the existing error handling behavior? + ``` + +1. Take a couple minutes to review the error handling recommendations. + + GitHub Copilot should provide guidance on maintaining consistent error handling patterns across the refactored methods. This guidance is critical because the current method has complex error handling with rollback procedures that must be preserved. + + The recommendations should address: + - How to maintain the current rollback behavior (like releasing inventory on payment failures). + - Whether to use return values, exceptions, or result objects for error signaling. + - How to preserve audit logging throughout the refactored methods. + - Ways to ensure cleanup procedures are still executed in error scenarios. + +GitHub Copilot's Ask mode excels at analyzing complex code structures and providing strategic guidance for refactoring. The insights from this analysis will inform the specific refactoring approach you implement in the next section, ensuring that you maintain business logic integrity while achieving better code organization. + +### Refactor large functions using GitHub Copilot Chat (Agent mode) + +Agent mode enables you to assign complex code refactoring tasks to GitHub Copilot. The assigned tasks can include creating and/or updating multiple files. GitHub Copilot Agent processes tasks autonomously, testing and debugging updates as it works, and keeps you informed by reporting its progress in the Chat view. + +In this task, you use GitHub Copilot Agent to systematically refactor the ProcessOrder method by extracting smaller, focused methods while preserving the existing business logic and error handling behavior. + +Use the following steps to complete this task: + +1. Ensure that the GitHub Copilot Chat view is open in Visual Studio Code. + +1. In the Chat view, select the **Agent** mode. + + The **Set Mode** dropdown is located in the bottom-left corner of the Chat view. In **Agent** mode, GitHub Copilot processes its assigned tasks (your prompts) autonomously. + +1. Take a minute to consider your refactoring strategy. + + Use the analysis from the previous task to formulate a strategy for refactoring the ProcessOrder method. Your approach should support incremental testing. + + For example, consider this phased refactoring strategy: + + - **Phase 1**: Create stub methods inside the OrderProcessor class. + - **Phase 2**: Extract input validation and security assessment logic. + - **Phase 3**: Extract inventory management operations (checking and reservation). + - **Phase 4**: Extract payment processing with fraud detection and error handling. + - **Phase 5**: Extract shipping coordination and tracking management. + - **Phase 6**: Extract notification and communication logic. + - **Phase 7**: Extract order finalization and completion procedures. + + This phased approach ensures that changes are manageable and that updates can be tested incrementally. The refactored code should maintain the same business logic and error handling as the original method. + +1. In the code editor, scroll to the bottom of the OrderProcessor class, and then create the following code comment after the closing brace of the ProcessOrder method: + + ```csharp + + } + + // Add stub methods here + + } + + ``` + + The code comment should be located after the final closing brace of the ProcessOrder method and before the closing brace of the OrderProcessor class. + + You can instruct GitHub Copilot to use this location when it creates the new single-purpose methods inside the OrderProcessor class. + +1. Ask GitHub Copilot Agent to create stub methods that can be used to hold the extracted code. + + For example: + + ```text + Review the current conversation. I want to start by creating stub code for the new single-purpose methods that will be used when refactoring the ProcessOrder method. Use the method declarations that you proposed in this conversation. Create the new stub methods below the "Add stub methods here" comment in the OrderProcessor class. Ensure that you're using appropriate method parameters and return types. Do not extract any code from the ProcessOrder method, just create the stub methods. After the stub methods are created, open the terminal, navigate to the "ECommerceOrderProcessing/src/ECommerce.Console" directory, then run a "dotnet build" command. Ensure that there are no build errors. + ``` + + Building the stub methods first helps ensure that the new methods are correctly defined and integrated into the existing code structure. This approach allows for incremental testing and validation of each method's functionality before fully extracting the code from ProcessOrder. + +1. Monitor the agent's progress and provide assistance when required. + + GitHub Copilot Agent will create the new methods in the specified location, then ask for permission to run a build command in the terminal. Select the **Continue** button when prompted (to allow the agent to proceed). + + If the agent encounters an issue, it should notify you, and then attempt to fix the issue automatically. Continue to provide assistance when required. + + After performing the build task, the agent should inform you that the new methods are correctly defined and that there are no syntax errors. + +1. To accept the edits, select **Keep**. + + You can accept (or reject) edits individually in the code editor, or all at once in the GitHub Copilot chat interface. + +1. Ask GitHub Copilot Agent to refactor the ProcessOrder method. + + Refactoring large methods works best when you're able to break the task down into manageable stages. In this case, the stages align to the single-process methods that you already identified. The same approach should be applied when using an agent to refactor the code for you. Write a task that instructs the agent to refactor one section at a time, and then test the updates before moving on to the next section. + + However, working with GitHub Copilot Agent can be like having a developer available who can work independently on a series of assignments. In this case, the series of assignments is to refactor each code section and test the updates before moving on to the next section. In other words, you can write a single task (prompt) that asks GitHub Copilot Agent to refactor each of the code sections, testing each of the single-purpose methods before it moves on to the refactoring the next section. + + For example: + + ```text + Review the current conversation. Examine the ProcessOrder method and identify the code sections that should be extracted into the single-purpose stub methods that you already created. Move the identified code sections into the associated single-purpose stub methods, constructing and testing the methods in the suggested order. Replace the extracted code sections with a call to the associated single-purpose method. Use local variables of the associated return value type to ensure that the ProcessOrder method maintains the same error handling behavior that's provided by the original code. As each method is updated, use a "dotnet run" command to ensure that the code features and error handling processes work correctly (including rollback features, like releasing inventory on payment failure). Also verify that the four test case scenarios generate the expected console output when the app is run (all test cases should pass). Continue extracting code into the new single-purpose methods (and testing the app) until all methods are complete and the application generates the expected console output for the four test case scenarios. Don't stop working on this task until all methods are constructed and tested. Display the step-by-step approach that you'll use to complete this task and then begin. + ``` + + > **NOTE**: When you're working on production code, it's important to thoroughly test your code after significant refactoring operations. This involves building and testing the application to verify that features are working as intended, unit tests are passing, and the output remains consistent with the original behavior. To save time during this training exercise, we're relying on the agent to perform incremental testing. You'll complete an additional (manual verification) test after the code refactoring tasks are completed. + +1. Monitor the agent's progress and provide assistance when required. + + GitHub Copilot Agent should start by describing its plan for refactoring each section of the ProcessOrder method. The plan should include a step-by-step approach for each code section that includes: moving code from the ProcessOrder method into the corresponding single-process method, replacing the extracted code in ProcessOrder with a method call, and testing the app to ensure that the refactored code works as intended. + + The agent also provides updates in the Chat view that describe its progress, including any issues it encounters. You can interact with the agent to clarify instructions or provide additional context as needed. + + GitHub Copilot Agent usually asks for permission to Build or Run the application during the refactoring process. When this occurs, select the **Continue** button in the Chat view to allow the agent to proceed. + + > **IMPORTANT**: If GitHub Copilot Agent stops processing the assigned task before all of the sections in the ProcessOrder method are refactored, enter a prompt telling the agent to proceed with the refactoring task. + +1. Once the refactoring process is complete, accept the changes. + + Select the **Keep** button in the Chat view to accept all changes made by the agent. + +1. Take a minute to review the updated OrderProcessor class. + + After the agent completes its refactoring tasks, the OrderProcessor class should contain several smaller, focused methods that handle specific aspects of order processing. The ProcessOrder method should be significantly shorter and more readable, with each step of the order processing workflow clearly defined in its own method. + + For example, the updated ProcessOrder method should look similar to the following: + + ```csharp + public OrderResult ProcessOrder(Order order) + { + // Log the start of order processing for audit trail + _auditLogger.LogOrderProcessingStarted(order.Id, order.CustomerEmail); + + try + { + // Validate order and perform security checks + if (!ValidateOrderAndSecurity(order, out string? validationFailure)) + { + return OrderResult.Failure(validationFailure ?? "Validation failed for unknown reasons"); + } + + Console.WriteLine($"Processing Order {order.Id} for {_securityValidator.MaskEmail(order.CustomerEmail)}..."); + Console.WriteLine($"Order contains {order.Items.Count} items, Total: ${order.TotalAmount:F2}"); + + // Check inventory and reserve stock + if (!CheckAndReserveInventory(order, out string? inventoryFailure)) + { + return OrderResult.Failure(inventoryFailure ?? "Inventory check failed for unknown reasons"); + } + Console.WriteLine("Inventory reserved successfully."); + _auditLogger.LogInventoryReserved(order.Id, order.Items.Count); + + // Payment Processing with Enhanced Security + Console.WriteLine("Processing payment..."); + // Process payment + if (!ProcessPayment(order, out string? paymentFailure)) + { + return OrderResult.Failure(paymentFailure ?? "Payment processing failed for unknown reasons"); + } + + // Shipping and Logistics Management + Console.WriteLine("Scheduling shipping..."); + // Schedule shipping + if (!ScheduleShipping(order, out string? shippingFailure)) + { + return OrderResult.Failure(shippingFailure ?? "Shipping scheduling failed for unknown reasons"); + } + + // Customer Communication and Notifications + Console.WriteLine("Sending notifications..."); + // Send notifications + SendNotifications(order); + + // Order Finalization and Data Recording + Console.WriteLine("Finalizing order..."); + order.Status = OrderStatus.Completed; + order.CompletionDate = DateTime.UtcNow; + order.ProcessingDuration = DateTime.UtcNow - order.OrderDate; + + // In a real app, this would update the order record in a database + // _orderRepository.UpdateOrder(order); + + Console.WriteLine($"Order {order.Id} completed successfully in {order.ProcessingDuration.TotalSeconds:F1} seconds."); + _auditLogger.LogOrderCompleted(order.Id, order.TotalAmount); + + // Finalize the order + FinalizeOrder(order); + + return OrderResult.Success(order.Id, order.TrackingNumber ?? ""); + } + catch (Exception ex) + { + HandleUnexpectedError(order, ex); + return OrderResult.Failure("An unexpected error occurred during order processing"); + } + } + + ``` + +GitHub Copilot Agent excels at systematic refactoring tasks that require understanding of code flow, business logic, and error handling patterns. By breaking the refactoring into logical phases, you ensure that each change is manageable, testable, and maintains the original system behavior while significantly improving code organization and maintainability. + +### Test the refactored e-commerce order processing code + +Manual testing and verification ensure that your refactored code maintains the intended business logic and functionality. A successful refactoring process should improve code structure while producing identical behavior to the original implementation. + +In this task, you test the refactored code to verify that all business logic is preserved and that code readability and maintainability are improved. + +Use the following steps to complete this task: + +1. Run the refactored application and verify expected behavior. + + Compare the output with the behavior you observed before refactoring. The console output should be identical, including: + + - **Test 1 (Valid Order)**: Should complete successfully with payment processing, shipping scheduling, and notifications + - **Test 2 (Invalid Email)**: Should fail validation with the same error message + - **Test 3 (Declined Payment)**: Should fail payment processing and trigger inventory rollback + - **Test 4 (Suspicious Order)**: Should be flagged by security assessment and rejected + + The refactored code should produce exactly the same results, demonstrating that the business logic has been preserved throughout the refactoring process. + +1. Create and test more edge case scenarios to ensure robustness. + + Create extra test scenarios to verify that error handling still works correctly in various edge cases. You can modify the test cases in **Program.cs** temporarily to test other scenarios. + + For example, you can add the following code snippet before the code that displays the test summary: + + ```csharp + // Test with empty items list (should fail validation) + System.Console.WriteLine("\n--- Test Case 5: Empty Order ---"); + var emptyOrder = CreateSampleOrder("ORD-EMPTY", "test@example.com", "123 Test St", + new List(), + new PaymentInfo { CardNumber = "4111111111111111", CardCVV = "123", CardHolderName = "Test User", ExpiryMonth = "12", ExpiryYear = "2025", BillingAddress = "123 Test St" }); + + var result5 = processor.ProcessOrder(emptyOrder); + testResults.Add($"Test 5: {(result5.IsSuccess ? "FAILED" : "PASSED")} - Should reject empty order"); + + // Test with invalid shipping address (should fail validation) + System.Console.WriteLine("\n--- Test Case 6: Invalid Shipping Address ---"); + var invalidAddressOrder = CreateSampleOrder("ORD-ADDR", "user@example.com", "", + new List { new() { ProductId = "BOOK-001", Quantity = 1, Price = 15.99m } }, + new PaymentInfo { CardNumber = "4111111111111111", CardCVV = "123", CardHolderName = "Test User", ExpiryMonth = "12", ExpiryYear = "2025", BillingAddress = "123 Test St" }); + + var result6 = processor.ProcessOrder(invalidAddressOrder); + testResults.Add($"Test 6: {(result6.IsSuccess ? "FAILED" : "PASSED")} - Should reject invalid shipping address"); + ``` + + These extra tests help verify that the refactored validation logic handles edge cases correctly and that error messages remain consistent with the original implementation. + +1. Run the application and verify test results. + + When you run the application, you should see the test results displayed in the console, indicating whether each test case passed or failed. Pay attention to any error messages or logs that are generated during the test runs. + + For example, if you added the test 5 and test 6 scenarios listed above, the new test output and updated summary should look similar to the following sample: + + ```text + + --- Test Case 5: Empty Order --- + [AUDIT] 2025-08-20 18:28:11.099 UTC | ORDER_PROCESSING_STARTED | Order: ORD-EMPTY | Started processing order for t***@example.com + [AUDIT] 2025-08-20 18:28:11.100 UTC | VALIDATION_FAILURE | Order: ORD-EMPTY | Empty order items + + --- Test Case 6: Invalid Shipping Address --- + [AUDIT] 2025-08-20 18:28:11.101 UTC | ORDER_PROCESSING_STARTED | Order: ORD-ADDR | Started processing order for u***@example.com + [AUDIT] 2025-08-20 18:28:11.101 UTC | VALIDATION_FAILURE | Order: ORD-ADDR | Invalid shipping address + + === TEST SUMMARY === + Test 1: PASSED - ORD-001 + Test 2: PASSED - Should reject invalid email + Test 3: PASSED - Should reject declined payment + Test 4: PASSED - Should flag suspicious order + Test 5: PASSED - Should reject empty order + Test 6: PASSED - Should reject invalid shipping address + + ``` + +1. Run the application again to verify that audit logging continues to work correctly. + + Check the **order_audit_log.txt** file to ensure that audit logging is still functioning properly throughout the refactored methods. The most recent events are located at the bottom of the file. + + The audit trail should be complete and demonstrate that logging is preserved across all the extracted methods. + + > **TIP**: The order_audit_log.txt file is created/updated in the current working directory of the application. Depending on how you choose to run the ECommerce.Console project, the working directory could be the "src/ECommerce.Console/bin/Debug/net9.0" directory rather than the "src/ECommerce.Console" directory. To generate the audit file in the "src/ECommerce.Console" directory, run the application from the Terminal using a .NET CLI command. + + The following event types can be stored in the order audit log file: + + - Order Processing Events: + - LogOrderProcessingStarted(string orderId, string email) + - LogOrderCompleted(string orderId, decimal amount) + - Security Events: + - LogSecurityEvent(string eventType, string details) + - Validation Events: + - LogValidationFailure(string orderId, string reason) + - Inventory Events: + - LogInventoryIssue(string orderId, string productId, string issue) + - LogInventoryReserved(string orderId, int itemCount) + - Payment Events: + - LogPaymentProcessed(string orderId, decimal amount, string reference) + - LogPaymentFailure(string orderId, string reason) + - Shipping Events: + - LogShippingScheduled(string orderId, string trackingNumber) + - LogShippingFailure(string orderId, string reason) + - Notification Events: + - LogNotificationSent(string orderId, string type) + - LogNotificationFailure(string orderId, string reason) + - Unexpected Errors: + - LogUnexpectedError(string orderId, string error) + +Manual testing verifies that your refactoring efforts achieved the goal of improving code structure while maintaining system functionality. The refactored code now provides a much more maintainable foundation where each method has a clear, focused responsibility, making future enhancements and bug fixes easier to implement. + +## Summary + +In this exercise, you learned how to use GitHub Copilot to refactor large functions in an application. You explored the E-commerce Order Processing System, identified large functions that needed refactoring, and used GitHub Copilot to break down monolithic methods into smaller, more focused functions for improved maintainability and readability. + +## Clean up + +Now that you finished the exercise, take a minute to ensure that you haven't made changes to your GitHub account or GitHub Copilot subscription that you don't want to keep. If you made any changes, revert them as needed. If you're using a local PC as your lab environment, you can archive or delete the sample projects folder that you created for this exercise. diff --git a/Instructions/Labs/LAB_AK_09_simplify_complex_conditionals.md b/Instructions/Labs/LAB_AK_09_simplify_complex_conditionals.md new file mode 100644 index 0000000..f612291 --- /dev/null +++ b/Instructions/Labs/LAB_AK_09_simplify_complex_conditionals.md @@ -0,0 +1,581 @@ +--- +lab: + title: Exercise - Simplify complex conditionals using GitHub Copilot + description: Learn how to refactor complex conditional logic in C# codebases using GitHub Copilot tools. + duration: 30 minutes + level: 200 + islab: true + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Simplify complex conditionals using GitHub Copilot + +The conditional logic in business applications often grows more complex and deeply nested over time, making the code difficult to read, maintain, and test. Unfortunately, the same complexity that makes the code difficult to maintain can also make it difficult to refactor. The difficulty increases when the code is tightly coupled with business logic. + +In this exercise, you use GitHub Copilot to analyze code that contains deeply nested conditional logic, refactor the code logic, and then test the refactored code to ensure it works as intended. You use GitHub Copilot in Ask mode to gain an understanding of the code and explore options simplifying the logic. You use GitHub Copilot in Agent mode to refactor the code by extracting complex conditional logic into smaller, focused helper methods, and to reduce nesting. Simplifying complex conditionals makes it easier to read, maintain, and test your code. + +This exercise should take approximately **30** minutes to complete. + +> **IMPORTANT**: To complete this exercise, you must provide your own GitHub account and GitHub Copilot subscription. If you don't have a GitHub account, you can sign up for a free individual account and use a GitHub Copilot Free plan to complete the exercise. If you have access to a GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscription from within your lab environment, you can use your existing GitHub Copilot subscription to complete this exercise. + +## Before you start + +Your lab environment must include the following: Git 2.48 or later, .NET SDK 9.0 or later, Visual Studio Code with the C# Dev Kit extension, and access to a GitHub account with GitHub Copilot enabled. + +### Configure your lab environment + +If you're using a local PC as a lab environment for this exercise: + +- For help with configuring your local PC as your lab environment, open the following link in a browser: Configure your lab environment resources. + +- For help with enabling your GitHub Copilot subscription in Visual Studio Code, open the following link in a browser: Enable GitHub Copilot within Visual Studio Code. + +If you're using a hosted lab environment for this exercise: + +- For help with enabling your GitHub Copilot subscription in Visual Studio Code, paste the following URL into a browser's site navigation bar: Enable GitHub Copilot within Visual Studio Code. + +- To ensure that the .NET SDK is configured to use the official NuGet.org repository as a source for downloading and restoring packages: + + Open a command terminal and then run the following command: + + ```bash + + dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org + + ``` + +### Download sample code projects + +Use the following steps to download the sample projects and open them in Visual Studio Code: + +1. Open a browser window in your lab environment. + +1. To download a zip file containing the sample app projects, open the following URL in your browser: [GitHub Copilot lab - develop code features](https://github.com/MicrosoftLearning/mslearn-github-copilot-dev/raw/refs/heads/main/DownloadableCodeProjects/Downloads/GHCopilotEx9LabApps.zip) + + The zip file is named **GHCopilotEx9LabApps.zip**. + +1. Extract the files from the **GHCopilotEx9LabApps.zip** file. + + For example: + + 1. Navigate to the downloads folder in your lab environment. + + 1. Right-click **GHCopilotEx9LabApps.zip**, and then select **Extract all**. + + 1. Select **Show extracted files when complete**, and then select **Extract**. + +1. Copy the **GHCopilotEx9LabApps** folder to a location that's easy to access, such as your Windows Desktop folder. + +1. Open the **GHCopilotEx9LabApps** folder in Visual Studio Code. + + For example: + + 1. Open Visual Studio Code in your lab environment. + + 1. In Visual Studio Code, on the **File** menu, select **Open Folder**. + + 1. Navigate to the Windows Desktop folder, select **GHCopilotEx9LabApps** and then select **Select Folder**. + +1. In the Visual Studio Code SOLUTION EXPLORER view, verify the following project structure: + + - GHCopilotEx9LabApps\ + - ECommercePricingEngine\ + - Dependencies\ + - ECommercePricingDemo.cs + - Output-ECommercePricingEngine.txt + - SecurityTest.cs + - LoanApprovalWorkflow\ + - Dependencies\ + - LoanApprovalDemo.cs + - Output-LoanApprovalWorkflow.txt + - SecurityTest.cs + +## Exercise scenario + +You're a software developer working for a consulting firm. Your clients need help with refactoring complex conditional logic to improve code readability and maintainability. You're assigned to the following apps: + +- E-commerce pricing engine: The first app is an E-commerce Pricing Engine that calculates dynamic pricing based on various business rules. Conditionals include membership levels, order values, coupon codes, product categories, and shipping rules. +- Loan approval workflow: The second app is a Loan Approval Workflow that evaluates loan applications based on various factors. Conditionals include income, employment status, debt ratios, collateral, and credit history. + +This exercise includes the following tasks: + +1. Review the E-commerce pricing engine codebase. +1. Identify refactoring opportunities in the E-commerce pricing code using GitHub Copilot. +1. Refactor the E-commerce pricing code using GitHub Copilot Agent. +1. Test the refactored E-commerce pricing code. +1. (OPTIONAL) Simplify complex conditionals in the LoanApprovalWorkflow demo app. + +### Review the E-commerce pricing engine codebase + +The first step in any refactoring effort is to ensure that you understand the existing codebase. + +In this task, you open the E-commerce pricing engine project and use GitHub Copilot to help analyze the complex conditional logic. + +Use the following steps to complete this task: + +1. Ensure that you have the GHCopilotEx9LabApps folder open in Visual Studio Code. + + Refer to the **Before you start** section if you didn't download the sample code projects. + +1. Verify that the **ECommercePricingEngine** code project builds successfully. + + For example, in the SOLUTION EXPLORER view, right-click **ECommercePricingEngine**, and then select **Build**. + + You'll see warnings "Cannot convert null literal to non-nullable reference type." when you build the project, but there shouldn't be any errors. You can ignore the warnings for the purposes of this exercise. + +1. Open the GitHub Copilot Chat view. + + If the Chat view isn't already open, you can open it by selecting the **Chat** icon at the top of the Visual Studio Code window, just to the right of the Search textbox. + +1. Set the chat mode to **Ask** and select the **Auto** model. + + The Set Mode and Pick Model menus are in the bottom-left corner of the Chat view. GitHub Copilot's **Ask** mode is used to ask general coding questions and to generate code related explanations. + + You'll use GitHub Copilot's **Agent** mode later in this exercise, but for now you'll use **Ask** mode for code analysis and explanations. + + > [!NOTE] + > Some models are better suited for specific tasks than others. The model that you select can affect the responses generated by GitHub Copilot. After completing this lab exercise using the recommended settings, you might want to repeat the exercise using different models and compare the results. + +1. In Visual Studio Code, open the **ECommercePricingDemo.cs** file. + + This file includes the following classes: + + - User: Represents a customer, with properties for membership level, purchase history, and special statuses (student, employee, corporate, etc.). Used to determine eligibility for various discounts and benefits. + - Coupon: Represents a discount or shipping coupon, with properties for code, validity, type (percent or shipping), and value. Used in pricing calculations to apply extra discounts or free shipping. + - Item: Represents a product in an order, with name, category, and price. Used to build up orders and calculate subtotals and category-specific discounts. + - Order: Represents a customer’s order, containing a list of items, shipping region, coupon, event, payment method, and other order-specific flags. Provides methods to calculate subtotals, check for category presence, and determine order characteristics (for example, high value, mixed categories). + - PricingEngine: Contains the main logic for calculating the final price of an order. Applies discounts based on user status, order details, coupons, and category-specific rules. Handles security checks and ensures discounts and prices stay within safe bounds. + - Program: The entry point. Creates test users, coupons, and orders, then runs a series of complex pricing scenarios using the above classes. Demonstrates how the pricing engine applies its logic in different situations. + + Each class models a real-world entity or process in an e-commerce pricing system, and they interact in the Program class to simulate and test pricing calculations. For example, User, Order, and Coupon instances are passed to PricingEngine.CalculateFinalPrice to compute and display the final price with all applicable discounts. + +1. Locate the **PricingEngine** class, and then select the entire **CalculateFinalPrice** method. + + The CalculateFinalPrice method contains complex and deeply nested conditional logic that evaluates membership levels, seasonal events, corporate accounts, subscription services, and various discount scenarios. + +1. Take a minute to scroll through the conditional logic in the **CalculateFinalPrice** method. + + The method is complex and difficult to read, with multiple nested conditionals that handle different discount scenarios. The complexity arises from the various business rules that need to be applied based on user status, order details, and coupon codes. + +1. Right-click the selected code, and then select **Copilot** > **Explain**. + + GitHub Copilot analyzes the CalculateFinalPrice method and provides a detailed explanation of what the code does. + +1. Take a few minutes to review GitHub Copilot's explanation. + + The explanation should highlight the main discount categories such as the hierarchical membership, coupon applications, bulk purchase incentives, and category-specific rules. + + For example: + + ```md + The CalculateFinalPrice method is a comprehensive pricing engine for an e-commerce scenario. It starts by validating its inputs, ensuring that both the user and order objects are not null and that the order data is valid. This prevents null reference exceptions and guards against invalid or malicious input. The method then calculates the base total of the order and checks that it falls within a reasonable range, aborting the calculation if the total is suspiciously low or high. + + Next, the method initializes variables for the discount percentage, shipping cost, and a list to track all applied discounts. The core of the function is a series of nested conditional blocks that determine which discounts the user is eligible for. These are based on membership level (Premium, Gold, Silver), user attributes (such as being a student, employee, or first-time buyer), order characteristics (like high value, bulk purchase, or mixed categories), and special events (such as Black Friday or Back to School). Each eligible discount is added using the SafeAddDiscount helper, which ensures the total discount does not exceed a maximum cap and records a description of the discount. + + The method also handles coupon codes, applying either a percentage discount or free shipping, with additional enhancements for certain membership levels or order types. For example, Premium members may receive a boosted coupon value, and further stacking is possible during special events or for corporate accounts. + + After all discounts are considered, the method applies additional bulk purchase incentives if the order contains a large number of items. It then enforces a final cap on the total discount to prevent excessive reductions. The final price is calculated using ApplyCategorySpecificDiscounts, which applies category-based discount caps (e.g., electronics discounts are limited to 15%). The shipping cost is added, and the final price is ensured to be above a minimum threshold. + + Finally, the method outputs a summary of the calculation, including the base total, all applied discounts, the total discount percentage (with a note about category caps), the shipping cost, and the final price. This approach ensures transparency and traceability in how the final price is determined, while also enforcing business rules and security best practices. + ``` + + Taking the time to understand the existing code helps you recognize and select refactoring options later in this exercise. + +1. In the Chat view, to get a deeper analysis of the calculation process, enter the following prompt: + + ```plaintext + @workspace Explain the business logic flow in the CalculateFinalPrice method. What are the different discount paths and how do they interact with each other? What are the key business rules that govern pricing calculations? + ``` + + This analysis should help you understand how the different discount categories interact and what business rules are applied at each step of the pricing calculation. + +1. Take a few minutes to review the explanation generated by GitHub Copilot. + + The response should identify the primary discount paths and how they interact with each other. For example: + + ```md + The `CalculateFinalPrice` method in `ECommercePricing.PricingEngine` implements a layered, rule-driven approach to pricing calculation for an e-commerce order. Here’s how the business logic flows and how the discount paths interact: + + ### 1. **Input Validation** + - The method first validates that both `user` and `order` are not null and that the order data is valid (e.g., no negative prices, all required fields present). + - It checks that the order subtotal is within allowed bounds (greater than zero and less than $1,000,000). + + ### 2. **Base Discount Initialization** + - The method initializes the discount percentage (`discountPercent`) to 0 and calculates the base shipping cost. + + ### 3. **Membership-Based Discount Paths** + - The first major decision point is the user's membership level: + - **Premium:** Starts with a 15% discount, then applies additional discounts for high-value orders, seasonal events, corporate accounts, subscriptions, loyalty (years as member), lifetime spending, and express shipping. Each condition is nested, so deeper benefits require all previous conditions to be met. + - **Gold:** Starts with a 12% discount, then adds bonuses for seasonal events, bulk purchases, category diversity, employee status, pre-orders, and alternative payment methods. These are also nested, but less deeply than Premium. + - **Silver:** Starts with 8%, with further bonuses for students, back-to-school events, bulk purchases, electronics focus, gift wrap, and express shipping. These are nested within the student path. + - **First-Time Buyer:** If the user is a first-time buyer (and not a member), a 10% discount is applied, with further bonuses for seasonal events, order size, payment method, high-value orders, premium shipping regions, and express shipping. + + ### 4. **Coupon Application** + - If a coupon is present: + - **Valid Percent Coupon:** The coupon value is capped at 50%. Premium and Gold members get a multiplier (Premium: 1.3x up to 50%, Gold: 1.2x up to 40%). Additional stacking occurs for Premium members on Black Friday, corporate accounts, and bulk orders, with each step further capping the discount. + - **Valid Shipping Coupon:** Shipping is set to zero for domestic orders or Premium members. + - **Expired Coupon:** No discount is applied, and a message is logged. + + ### 5. **Bulk Purchase Incentives** + - Additional discounts are applied for orders with 10+ items (5%) or 20+ items (8%). + + ### 6. **Discount Capping** + - The total discount percentage is capped at 95% to prevent excessive reductions. + + ### 7. **Category-Specific Discount Rules** + - The final discount is applied with category-specific caps: + - **Electronics:** Discount capped at 15%. + - **Clothing:** Discount capped at 20% (25% during Back-to-School). + - **Accessories/Other:** Full discount applies. + + ### 8. **Final Price Calculation** + - The final price is the sum of all discounted category subtotals plus shipping, with a minimum enforced price of $0.01. + + ### 9. **Result Output** + - The method prints the base total, all applied discounts, the total discount percentage (with a note about the electronics cap), shipping cost, and the final price. + + --- + + ## **Discount Path Interactions** + - **Nesting:** Many discounts are nested, meaning deeper discounts require all previous conditions to be true (e.g., Premium members with high-value orders, during Black Friday, who are also corporate accounts, etc.). + - **Stacking:** Discounts from different sources (membership, coupons, bulk, etc.) are added together, but always subject to the overall cap. + - **Category Caps:** Even if the total discount is high, certain categories (like electronics) have stricter caps, so the effective discount for those items may be lower than the overall discount. + + --- + + ## **Key Business Rules** + - **Validation:** Orders and users must be valid; otherwise, pricing is aborted. + - **Membership Drives Discounts:** Higher membership levels unlock deeper and more complex discount paths. + - **Coupons Are Enhanced by Membership:** Premium and Gold members get more value from percent-off coupons. + - **Bulk and Seasonal Bonuses:** Large orders and seasonal events can trigger additional discounts. + - **Category Protection:** Electronics and clothing have maximum discount caps to protect margins. + - **Shipping Can Be Discounted:** Shipping can be free with certain coupons or for Premium members. + - **Discount Caps:** No combination of discounts can exceed 95% off the order. + - **Transparency:** All applied discounts are tracked and displayed for auditability. + + For more details, see the full implementation in ECommercePricingDemo.cs, especially the `ECommercePricing.PricingEngine.CalculateFinalPrice` method. + ``` + +1. Compare GitHub Copilot's explanation with your own observations of the complex conditional logic in the `CalculateFinalPrice` method. + + Your own observations probably identified the primary discount sources: membership levels, coupon codes, and bulk purchase incentives. GitHub Copilot's explanation should support your observations and provide more insights into how these discount paths interact and the key business rules that govern the pricing calculations. + + - Notice how the different membership levels (Premium, Gold, Silver) apply discounts and how first-time buyers are treated. + - Notice how coupons validation is evaluated and applied, and how coupon discounts interact with membership discounts. + - Notice how volume-based discounts are applied based on item counts. + + Business rules are enforced through the nested conditionals, ensuring that discounts are applied correctly based on user status, order details, and coupon codes. These rules must move forward with the refactoring process. + +1. Run the ECommercePricingEngine project and review the output. + + The output should show the base and discounted prices for various combinations of user, order, and coupon. At the end of this exercise, your refactored code must produce the same results as the original code. + + You might notice that the output includes basic security testing for invalid inputs and malicious attempts to manipulate pricing. Although these tests don't represent all possible attack vectors or the level of testing required in a production app, they serve as a reminder that ensuring code quality and security is a requirement. The **SecurityTest.cs** file is part of the ECommercePricingEngine project. + + > [!NOTE] + > A copy of the output can be found in the **Output-ECommercePricingEngine.txt** file that's included in the ECommercePricingEngine folder. You can create your own output file if you want to compare results or if you modify sample data. When you reach the end of this exercise, you'll use the output file to ensure that your refactored code produces the same results as the original code. + + To run the project: If you have the ECommercePricingDemo.cs file open in the Visual Studio Code editor, you can run the project by selecting the run button (Run project associated with this file) that's located above the top-right corner of the editor pane. To run the project from the SOLUTION EXPLORER view, right-click **ECommercePricingEngine**, select **Debug**, and then select **Start New Instance**. + +### Identify refactoring opportunities in the E-commerce pricing code using GitHub Copilot + +GitHub Copilot is a great tool for analyzing complex code and identifying code refactoring opportunities. + +In this task, you'll use GitHub Copilot to identify specific refactoring opportunities and suggest helper methods that simplify the complex conditions. You'll use observations from the previous task to help construct the prompts that you supply to GitHub Copilot. + +Use the following steps to complete this task: + +1. Ensure you have the GitHub Copilot Chat view open with **Ask** mode and the **Auto** model selected. + +1. Add the **ECommercePricingDemo.cs** file to the Chat context using drag-and-drop operation. + + Although ECommercePricingDemo.cs is already open in the Visual Studio Code editor, adding it to the Chat context encourages GitHub Copilot to analyze the entire code file, which can result in more accurate suggestions. Adding relevant files to the Chat context is a best practice when using GitHub Copilot, even when you include the **@workspace** or **#codebase** tags in your prompt. + +1. Submit a prompt that asks GitHub Copilot to identify refactoring opportunities that improve code modularity related to the main discount paths. + + Consider the following items when constructing your prompt: + + - Tell GitHub Copilot to focus on the **CalculateFinalPrice** method and its nested conditionals. + - Ask GitHub Copilot to suggest options that move complex logic into more manageable, single-responsibility helper methods. + - List the main discount paths that should be considered for extraction into helper methods. + - Emphasize the importance of maintaining the business logic while simplifying the code structure. + + It's often beneficial to have GitHub Copilot consider code interactions when analyzing your code. You can use the **@workspace** or **#codebase** tag to tell GitHub Copilot to include your entire codebase in its analysis. + + For example: + + ```plaintext + @workspace Analyze the CalculateFinalPrice method and suggest refactoring opportunities. Include options that move the nested conditional logic into more manageable, single-responsibility helper methods. Focus on the main discount paths: membership discounts, coupon discounts, and bulk discounts. Maintain the business logic while simplifying the code structure. + ``` + + GitHub Copilot analyzes the method and suggests specific refactoring opportunities, identifying patterns in the nested conditionals that can be extracted into helper methods. + +1. Take a couple minutes to review GitHub Copilot's suggestions. + + The response should identify opportunities that improve code manageability and modularity, such as extracting the major discount paths into helper methods and one or more approaches to reducing nesting. + + For example: + + ```md + Collecting workspace informationThe `ECommercePricing.PricingEngine.CalculateFinalPrice` method is functionally rich but contains deeply nested conditional logic, especially for membership, coupon, and bulk discounts. This makes the code harder to read, maintain, and test. Here are refactoring opportunities and suggestions: + + ## **Refactoring Opportunities** + + ### 1. **Extract Membership Discount Logic** + Move the entire membership-based discount logic into a helper method, e.g., `ApplyMembershipDiscounts`. This method would encapsulate all the nested membership, event, and user-attribute checks, returning the updated discount and applied discounts. + + ### 2. **Extract Coupon Discount Logic** + Move coupon handling into a method like `ApplyCouponDiscounts`. This would handle coupon validation, membership-based coupon boosts, and shipping coupon logic. + + ### 3. **Extract Bulk Discount Logic** + Move the bulk purchase logic into a method such as `ApplyBulkDiscounts`. + + ### 4. **Reduce Nesting** + Within each helper, use early returns or guard clauses where possible to reduce nesting. + + ### 5. **Encapsulate Discount State** + Consider using a small struct or class (e.g., `DiscountContext`) to pass and update the discount percent and applied discounts list, reducing parameter clutter. + ``` + +1. Submit a follow-up prompt that asks GitHub Copilot how to simplify complex conditional logic and reduce nesting levels in the CalculateFinalPrice method. + + Follow-up prompts are a great way to refine your analysis and get more specific suggestions from GitHub Copilot. In this case, you want additional information on reducing nesting levels, still within the context of complex conditionals in the CalculateFinalPrice method. You can also use follow-up prompts to ask for best practices or to understand the impact that suggested changes might have on the code. + + For example: + + ```plaintext + How can I simplify the complex conditional logic and reduce nesting levels in the CalculateFinalPrice method? For example, the method has multiple nested conditionals that apply different membership discounts based on user status and order details. What are some best practices for reducing nesting levels and improving readability? Explain the benefit of each approach when applied to the CalculateFinalPrice method. + ``` + +1. Take a minute to review GitHub Copilot's suggestions for simplifying the conditional logic and reducing nesting levels. + + GitHub Copilot should provide a list of suggestions for simplifying the conditional logic and reducing nesting levels. Some of the suggestions might be repeats of previous suggestions, that's to be expected. However, you should see new suggestions related to reducing nesting levels and improving readability, such as: + + - Use early returns or guard clauses to flatten logic, reduce indentation, and clarify flow. + - Use local helper functions to remove duplication and clarify intent. + - Use switch expressions or pattern matching to make membership logic explicit and maintainable. + - Use smaller methods per membership to modularize logic and make it easier to test and update. + - Use Boolean variables for conditions to improve readability of complex checks. + +> [!NOTE] +> You'll use the results of your analysis to help construct the task assigned to GitHub Copilot Agent in the next section of the exercise. + +### Refactor the E-commerce pricing code using GitHub Copilot Agent + +GitHub Copilot has three modes, **Ask**, **Edit**, and **Agent**. When running in Agent mode, GitHub Copilot works as an autonomous AI agent. + +In Agent mode: + +- Your prompt specifies the task assigned to GitHub Copilot Agent. +- GitHub Copilot uses the task information and your codebase to identify the relevant code files and establish the context for the task. +- GitHub Copilot formulates a process that it can use to accomplish the task. The agent uses an iterative approach and code reviews to help ensure the task is completed successfully. +- GitHub Copilot uses the Chat view to keep you informed as it works on the assigned task. It may also provide explanations or justifications for the changes being made. +- GitHub Copilot can invoke tools to help it accomplish the task or to verify that code changes are working correctly. +- GitHub Copilot may pause during the task and ask you for assistance or clarification. It's important to monitor the chat and respond when prompted to assist the autonomous agent. +- GitHub Copilot updates your code file in the Visual Studio Code editor. Once the task is complete, you should review the changes made by GitHub Copilot before applying them to your codebase (individually or collectively). + +In this section of the exercise, you'll use GitHub Copilot Agent to refactor the PricingEngine class and simplify the complex conditional logic in the CalculateFinalPrice method. The task that you assign to GitHub Copilot Agent will be based on the suggestions provided by GitHub Copilot during your code analysis (the previous task). + +Use the following steps to complete this task: + +1. Ensure that the GitHub Copilot Chat view is open in Visual Studio Code. + +1. In the chat view, select the **Agent** mode. + + The **Set Mode** dropdown is located in the bottom-left corner of the Chat view. When you select **Agent**, GitHub Copilot will switch to Agent mode, which allows it to autonomously work on tasks that you assign. + +1. Take a minute to identify the requirements for the task that you'll assign to GitHub Copilot Agent. + + You need to write a task that explains your goal and describes the steps that GitHub Copilot Agent can use to achieve that goal. + + In this case, your goal includes the following items: + + - Simplify the complex conditional logic and reduce nesting levels. + - Improve code maintainability and readability. + - Preserve existing functionality and ensure that the refactored code produces the same output as the original code. + + In the previous task, you asked GitHub Copilot to analyze the code and identify refactoring opportunities. You generated two sets of suggestions, one for extracting complex logic into helper methods to improve modularity and another for reducing nesting levels and improving code readability. + + The refactoring opportunities identified in the previous task may include: + + - Extract membership-level discount logic into a helper method. + - Extract coupon validation and application logic into a helper method. + - Extract volume-based discount logic into a helper method. + - Use early returns or guard clauses to flatten logic, reduce indentation, and clarify flow. + - Use local helper functions to remove duplication and clarify intent. + - Use switch expressions or pattern matching to make membership logic explicit and maintainable. + - Use smaller methods per membership to modularize logic and make it easier to test and update. + - Use Boolean variables for conditions to improve readability of complex checks. + + The task that you create for GitHub Copilot Agent should combine your goal statement and the refactoring opportunities that you identified. + + The task should implement the following organizational structure: + + 1. Goal statement: Explain the overall goal of the task. + 1. Code structure/modularity updates: Identify any large code sections that can be extracted to improve modularity. + 1. Small or detailed code improvements: Describe the specific approaches to reduce nesting levels and improve readability. + 1. Outcome: Describe the expected outcome of the refactoring process. + +1. Construct a task that meets your requirements. + + Your task should include your goal and the suggested refactoring opportunities arranged using the suggested organizational structure. + + For example: + + ```plaintext + I need to simplify the complex conditional logic and reduce nesting levels in the CalculateFinalPrice method. The main areas of complexity are associated with membership discounts, coupon processing, and bulk discounts. Each of these areas could be moved into separate helper methods that are called from CalculateFinalPrice. There are several options to reduce nesting levels in the helper methods. Use early returns or guard clauses to flatten logic, reduce indentation, and clarify flow. Use local helper functions to remove duplication and clarify intent. Use switch expressions or pattern matching to make membership logic explicit and maintainable. Use smaller methods per membership to modularize logic and make it easier to test and update. Use Boolean variables for conditions to improve readability of complex checks. The goal is to improve code maintainability and readability while preserving the existing functionality and generated output. + ``` + + This task uses natural language text to describe the goal, major restructuring opportunities, and specific approaches to reduce nesting levels and improve readability. The task also describes the expected outcome of the refactoring process. + +1. Use the Chat view to assign your task to GitHub Copilot Agent. + + GitHub Copilot Agent evaluates the task and the codebase to develop an approach for refactoring CalculateFinalPrice and extracting the membership discount logic into a new helper method. The agent tests the code updates at various stages to ensure that the refactoring is successful and that the business logic remains intact. + + After submitting the task, GitHub Copilot Agent will start working on the task. You can monitor its progress in the Chat view. + + > [!NOTE] + > If you need to make changes to the task, you can edit the text in the Chat view and resubmit it. GitHub Copilot Agent reevaluates the task and continues working on it. + +1. Monitor the Chat view as GitHub Copilot Agent works on the task. + + The agent provides updates on its progress, including any challenges it encounters and how it plans to address them. It may also ask for clarification or additional information if needed. If necessary, provide assistance by responding to the agent's prompts in the Chat view. + +1. Once the refactoring task is complete, review the suggested updates in the Visual Studio Code editor. + + Always review the changes suggested by GitHub Copilot before accepting them. Verify that the updates align with your intended business logic, app functionality, and coding standards. + + The refactored code should include a greatly simplified **CalculateFinalPrice** method that primarily consists of method calls to helper methods that manage membership discounts, coupon discounts, and bulk discounts. The complexity of the original method should be significantly reduced, making it easier to read and maintain. + + The membership discounts helper method should also be refactored to handle the complex logic around membership levels, with an additional helper method for each level (Premium, Gold, Silver, and first-time buyers). + +1. In the Chat view, to accept all edits, select **Keep**. + + You can reject the suggested changes if they don't meet your expectations, or you can accept a subset of the changes and reject others. Working with GitHub Copilot to refactor your code is often an iterative process that includes refining your prompts to achieve the intended results. + + If you accept GitHub Copilot's suggested updates, and then realize that the suggestions introduced issues that may be difficult to resolve moving forward, you can revert the changes by selecting **Undo Last Request** in the Chat view, or by using Visual Studio Code's undo functionality. + +1. Ask GitHub Copilot to explain the code refactoring updates that it implemented. + + For example: + + ```plaintext + Explain the refactoring updates that you implemented to simplify the complex conditional logic, reduce nesting levels, and improve code maintainability and readability. + ``` + + GitHub Copilot will provide an explanation of the changes it made, including how the complex conditional logic was simplified and how the code structure was improved. + + For example: + + ```md + Here’s an explanation of the refactoring updates made to simplify the complex conditional logic, reduce nesting, and improve maintainability and readability in the `CalculateFinalPrice` method: + + ### 1. **Separation of Concerns with Helper Methods** + - The main discount logic was split into three focused helper methods: `ApplyMembershipDiscounts`, `ApplyCouponDiscounts`, and `ApplyBulkDiscounts`. + - Each helper method is responsible for a single aspect of the discount calculation, making the main method concise and easy to follow. + + ### 2. **Flattened Membership Discount Logic** + - The deeply nested membership discount logic was moved into `ApplyMembershipDiscounts`, which uses a `switch` statement to dispatch to smaller, membership-specific methods (`ApplyPremiumDiscounts`, `ApplyGoldDiscounts`, `ApplySilverDiscounts`, `ApplyFirstTimeBuyerDiscounts`). + - Each of these methods uses guard clauses (early returns) and Boolean variables to flatten the logic, reducing indentation and clarifying the flow. + + ### 3. **Guard Clauses and Early Returns** + - Instead of multiple nested `if` statements, each helper method uses guard clauses to exit early when a condition is not met. This keeps the main logic path at the top level and reduces cognitive load. + + ### 4. **Switch Expressions and Pattern Matching** + - The use of `switch` in `ApplyMembershipDiscounts` makes the membership logic explicit and easy to extend or modify. + - This approach also makes it clear which logic applies to which membership type. + + ### 5. **Boolean Variables for Readability** + - Boolean variables (e.g., `altPayment`, `premiumPayment`) are used to clarify complex conditions, making the code more self-documenting and easier to read. + + ### 6. **Local Helper Methods for Duplication and Intent** + - Smaller, focused methods encapsulate repeated or complex logic, such as safe discount addition and category percentage calculation. + - This reduces code duplication and clarifies the intent of each operation. + + ### 7. **Preserved Business Logic and Output** + - All business rules, discount caps, and output formatting are preserved, ensuring that the refactoring does not change the program’s behavior. + + ## **Benefits** + - **Readability:** The main method is now a clear sequence of high-level steps. + - **Maintainability:** Each discount rule is isolated, making it easier to update or test. + - **Extensibility:** Adding or modifying discount rules is straightforward. + - **Testability:** Each helper method can be unit tested independently. + + This refactoring transforms a complex, deeply nested method into a modular, maintainable, and readable pricing engine, while preserving all business requirements and output. + ``` + +1. Take a minute to review the refactored code in the PricingEngine class. + +1. Submit a follow-up prompt that asks GitHub Copilot to explain specific parts of the refactored code or to compare the original and refactored versions. + + For example: + + ```plaintext + Compare the original PricingEngine class with the new refactored version of the PricingEngine class. Explain the relationship between methods in the two versions. Explain the implementation of business rules in the two versions. + ``` + +### Test the refactored E-commerce pricing code + +Testing is crucial to ensure that your refactoring doesn't change the business logic behavior. You'll run the code with various scenarios to verify that the calculations remain consistent. + +Use the following steps to complete this task: + +1. Build the project to ensure there are no compilation errors. + + For example, in the SOLUTION EXPLORER view, right-click **ECommercePricingEngine**, and then select **Build**. + + If there are any compilation errors, review the refactored code and fix any issues. GitHub Copilot can help resolve compilation errors if needed. + +1. Run the application to test the refactored pricing logic. + + If you have the ECommercePricingDemo.cs file open in the Visual Studio Code editor, you can run the project by selecting the run button (Run project associated with this file) that's located above the top-right corner of the editor pane. To run the project from the SOLUTION EXPLORER view, right-click **ECommercePricingEngine**, select **Debug**, and then select **Start New Instance**. + + The application should execute without errors and display pricing calculations for various test scenarios. + + > [!IMPORTANT] + > If the refactored code encounters a runtime error, review the changes made by GitHub Copilot Agent and resolve the issues using GitHub Copilot. If necessary, you can use the **Undo Last Request** option in the Chat view to revert the last set of changes and then update the task assigned to GitHub Copilot Agent. Iterating your prompts/tasks to refine the results is standard practice when working with AI-enabled tools. + +1. Ask GitHub Copilot to compare the output generated by the refactored code with the original output. + + The original output, **Output-ECommercePricingEngine.txt**, is included in the ECommercePricingEngine folder. + + You can create a second output file and ask GitHub Copilot to identify any differences between the two files. + + The output should be identical, confirming that the business logic remains unchanged while improving code maintainability and readability. + +### (OPTIONAL) Simplify complex conditionals in the LoanApprovalWorkflow demo app + +If time permits, simplify complex conditionals in the LoanApprovalWorkflow demo app using the same process that you used to simplify pricing logic in the Loan Approval Workflow sample app. + +In this optional task, you'll apply the same techniques you used in the E-commerce pricing engine to refactor the complex conditional logic in the Loan Approval Workflow demo app. You'll analyze the loan approval code, identify refactoring opportunities, and then use GitHub Copilot Agent to extract complex conditionals into smaller, focused helper methods. + +> [!IMPORTANT] +> The instructions for this task include the high-level process steps, but they don't include suggested prompts or detailed explanations. You can refer to the previous tasks in this exercise for examples of how to construct prompts and use GitHub Copilot effectively. + +Use the following steps to complete this task: + +1. Expand the **LoanApprovalWorkflow** project folder, open the **LoanApprovalDemo.cs** file, and then review the code. + +1. Use GitHub Copilot to explain the **LoanApprovalDemo.cs** code, including the complex conditional logic in the Evaluate method of the LoanEvaluator class. + +1. Run the LoanApprovalWorkflow project to test the initial loan approval logic and create a record of the output. + +1. Use GitHub Copilot to identify code refactoring opportunities that simplify complex conditionals, reduce nesting levels, and improve readability and maintainability in the LoanEvaluator class. + +1. Assign a task to GitHub Copilot Agent that simplifies complex conditional logic, reduces nesting levels, and improves readability and maintainability in the LoanEvaluator class (refactor the Evaluate method). + + For example, you could ask GitHub Copilot Agent to create helper methods for credit score evaluation, income and employment verification, financial ratio calculations, government program eligibility, and loan terms determination. + +1. Use GitHub Copilot Agent to simplify complex conditional logic and reduce nesting levels in the helper methods. + +1. Test your refactored code and ensure that the loan approval demo application produces the same results as the original implementation. + +## Summary + +In this exercise, you learned how to use GitHub Copilot to simplify complex conditional logic in a codebase. You explored the E-commerce pricing engine and Loan Approval Workflow demo apps, identified refactoring opportunities, and used GitHub Copilot Agent to extract complex conditionals into smaller, focused helper methods. You also learned how to reduce nesting levels in the code to improve readability while maintaining the same business logic. + +## Clean up + +Now that you've finished the exercise, take a minute to ensure that you haven't made changes to your GitHub account or GitHub Copilot subscription that you don't want to keep. If you made any changes, revert them as needed. If you're using a local PC as your lab environment, you can archive or delete the sample projects folder that you created for this exercise. diff --git a/Instructions/Labs/LAB_AK_10_implement_performance_profiling.md b/Instructions/Labs/LAB_AK_10_implement_performance_profiling.md new file mode 100644 index 0000000..2106123 --- /dev/null +++ b/Instructions/Labs/LAB_AK_10_implement_performance_profiling.md @@ -0,0 +1,592 @@ +--- +lab: + title: Exercise - Implement performance profiling using GitHub Copilot + description: Learn how to identify and address performance bottlenecks and code inefficiencies using GitHub Copilot tools. + duration: 30 minutes + level: 200 + islab: true + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Implement performance profiling using GitHub Copilot + +Performance profiling is an important aspect of software development that helps identify and address performance bottlenecks and code inefficiencies. + +In this exercise, you review an existing project that contains poor performing and inefficient code, analyze your options for improving code performance, refactor the code to address the identified issues, and test the refactored code to ensure code performance has improved while retaining functionality and readability. You use GitHub Copilot in Ask mode to gain an understanding of an existing code project and to explore options for refactoring the identified issues. You use GitHub Copilot in Agent mode to refactor the code and improve performance. You test the original and refactored code to measure the impact of your changes. + +This exercise should take approximately **30** minutes to complete. + +> **IMPORTANT**: To complete this exercise, you must provide your own GitHub account and GitHub Copilot subscription. If you don't have a GitHub account, you can sign up for a free individual account and use a GitHub Copilot Free plan to complete the exercise. If you have access to a GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscription from within your lab environment, you can use your existing GitHub Copilot subscription to complete this exercise. + +## Before you start + +Your lab environment must include the following resources: Git 2.48 or later, .NET SDK 9.0 or later, Visual Studio Code with the C# Dev Kit extension, and access to a GitHub account with GitHub Copilot enabled. + +### Configure your lab environment + +If you're using a local PC as a lab environment for this exercise: + +- For help with configuring your local PC as your lab environment, open the following link in a browser: Configure your lab environment resources. + +- For help with enabling your GitHub Copilot subscription in Visual Studio Code, open the following link in a browser: Enable GitHub Copilot within Visual Studio Code. + +If you're using a hosted lab environment for this exercise: + +- For help with enabling your GitHub Copilot subscription in Visual Studio Code, paste the following URL into a browser's site navigation bar: Enable GitHub Copilot within Visual Studio Code. + +- To ensure that the .NET SDK is configured to use the official NuGet.org repository as a source for downloading and restoring packages: + + Open a command terminal and then run the following command: + + ```bash + + dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org + + ``` + +### Download sample code project + +Use the following steps to download the sample project and open it in Visual Studio Code: + +1. Open a browser window in your lab environment. + +1. To download a zip file containing the sample app projects, open the following URL in your browser: [GitHub Copilot lab - implement performance profiling](https://github.com/MicrosoftLearning/mslearn-github-copilot-dev/raw/refs/heads/main/DownloadableCodeProjects/Downloads/GHCopilotEx10LabApps.zip) + + The zip file is named **GHCopilotEx10LabApps.zip**. + +1. Extract the files from the **GHCopilotEx10LabApps.zip** file. + + For example: + + 1. Navigate to the downloads folder in your lab environment. + + 1. Right-click **GHCopilotEx10LabApps.zip**, and then select **Extract all**. + + 1. Select **Show extracted files when complete**, and then select **Extract**. + +1. Copy the **GHCopilotEx10LabApps** folder to a location that's easy to access, such as your Windows Desktop folder. + +1. Open the **GHCopilotEx10LabApps** folder in Visual Studio Code. + + For example: + + 1. Open Visual Studio Code in your lab environment. + + 1. In Visual Studio Code, on the **File** menu, select **Open Folder**. + + 1. Navigate to the Windows Desktop folder, select **GHCopilotEx10LabApps** and then select **Select Folder**. + +1. In the Visual Studio Code SOLUTION EXPLORER view, verify the following project structure: + + - GHCopilotEx10LabApps\ + - ContosoOnlineStore\ + - Benchmarks\ + - Configuration\ + - Exceptions\ + - Services\ + - appsettings.json + - InventoryManager.cs + - Orders.cs + - OrderItem.cs + - OrderProcessor.cs + - PERFORMANCE_GUIDE.md + - Product.cs + - ProductCatalog.cs + - Program.cs + - README.md + - ContosoOnlineStore.Tests\ + - ContosoOnlineStoreTests.cs + - Usings.cs + - DataAnalyzerReporter\ + - data.txt + - DataAnalyzer.cs + - FileLoader.cs + - output.txt + - Program.cs + - README.md + - ReportGenerator.cs + +## Exercise scenario + +You're a software developer working for a consulting firm. Your clients need help with implementing performance profiling in legacy applications. Your goal is to improve code performance while preserving readability and the existing functionality. You're assigned to the following app: + +- ContosoOnlineStore: ContosoOnlineStore is an e-commerce application that processes customer orders. The application includes product catalog management with search capabilities, inventory tracking with stock reservations, order processing with validation and receipts, email notification services, and security validation. The application uses modern .NET architecture patterns including dependency injection, structured logging, and configuration management, but contains performance bottlenecks that mirror real-world scenarios. + +> **NOTE**: Code bottlenecks include intentional inefficiencies and performance issues, as well as simulated delays that approximate real-world timing for external dependencies. Simulated delays should be retained when the code is refactored to allow for "before and after" performance comparisons. + +This exercise includes the following tasks: + +1. Review the ContosoOnlineStore codebase manually. +1. Identify performance bottlenecks using GitHub Copilot Chat (Ask mode). +1. Refactor performance-critical code using GitHub Copilot Chat (Agent mode). +1. Test and verify the refactored ContosoOnlineStore code. + +### Review the ContosoOnlineStore codebase manually + +The first step in any code refactoring effort is to understand the existing codebase, including the project structure and business logic. When you're working on performance improvements, it's also important to establish baseline performance metrics. + +In this task, you examine the main components of the ContosoOnlineStore project, run the application to establish baseline performance metrics, and identify potential areas for optimization. + +Use the following steps to complete this task: + +1. Take a minute to examine the ContosoOnlineStore project structure. + + The codebase follows modern .NET architecture patterns with clear separation of concerns. The main architectural components include: + + - **Configuration**: Strongly typed configuration with validation + - **Services**: Business services with interfaces for testability + - **Exceptions**: Custom domain-specific exceptions + - **Benchmarks**: Professional performance testing with BenchmarkDotNet + - **Tests**: Unit tests with mocking framework + +1. Take a few minutes to review the **ProductCatalog.cs**, **OrderProcessor.cs**, and **InventoryManager.cs** classes. + + These classes contain the core business logic and are likely candidates for performance optimization. + + > **NOTE**: The codebase includes comments that help identify performance issues. Look for comments marked "Performance bottleneck" or "Performance issue" that highlight intentional inefficiencies. Simulated delays are also included to approximate real-world timing for external dependencies (slow queries, network calls, etc.). These delays will be retained when refactoring the codebase to allow for "before and after" performance comparisons. + + - **ProductCatalog.cs**: The ProductCatalog class provides methods for retrieving, searching, and categorizing products, as well as caching search results. + + - **OrderProcessor.cs**: The OrderProcessor class handles order validation, total calculation, and finalization, including inventory updates and email notifications. + + - **InventoryManager.cs**: The InventoryManager class manages stock levels, reservations, and low-stock alerts. + +1. Expand the **Services** and **Configuration** folders. + + These folders contain additional business logic and configuration settings that support the main application functionality. + +1. Take a few minutes to review the **Program.cs** and **AppSettings.cs** files. + + Examine the relationship between the Program.cs and AppSettings.cs files. Notice that the Program.cs file initializes and injects the AppSettings configuration into the application's services, enabling centralized and flexible control over application behavior. The application configuration is strongly typed and validated at startup, ensuring that all required settings are present and correctly formatted. + +1. Take a few minutes to review the **EmailService.cs** and **SecurityValidationService.cs** files. + + Examine the implementation of these services. Notice that they supply business logic with configurable timeouts, security validation rules, and email notification workflows. The services use dependency injection and logging, following enterprise development patterns. + +1. Run the application and observe the baseline performance. + + You can run the application from the Visual Studio Code integrated terminal by navigating to the project folder and running the following .NET CLI command: + + ```bash + dotnet run + ``` + + The application executes a comprehensive performance test that includes: + + - Order processing with timing measurements. + - Product catalog operations (search, lookup, category filtering). + - Inventory management operations. + - Concurrent operation testing. + - Email notification simulation. + +1. Store the baseline performance metrics in a file named **baseline_metrics.txt**. + + Use the EXPLORER view to create a text file named baseline_metrics.txt in the Benchmarks folder, and then copy the console output into the baseline_metrics.txt file. + +1. Review the baseline_metrics.txt file. + + Notice the timing information displayed under the *Running Performance Analysis* section. Key performance metrics include: + + - Product lookup performance. + - Search performance. + - Order processing performance. + - Inventory operations performance. + - Concurrent operation performance. + + The application also runs a comprehensive performance analysis suite that tests various operations and reports timing details. + +1. Take a minute to examine the performance benchmark capabilities provided by the OrderProcessingBenchmarks.cs file. + + The application includes professional benchmarking using BenchmarkDotNet. You can run detailed performance benchmarks by executing: + + ```bash + dotnet run -c Release -- benchmark + ``` + + Executing this command generates detailed performance reports including memory allocation patterns and statistical analysis. If you want, you can save the detailed performance benchmark reports for later comparison. + +Understanding the existing architecture and baseline performance metrics prepares the way for identifying optimization opportunities. + +### Identify performance bottlenecks using GitHub Copilot Chat (Ask mode) + +GitHub Copilot Chat's Ask mode is an excellent tool for analyzing complex codebases and identifying performance bottlenecks. In Ask mode, Copilot can analyze your code patterns, identify inefficient algorithms, and suggest optimization strategies based on best practices. + +In this task, you use GitHub Copilot to systematically analyze the ContosoOnlineStore application and identify specific performance improvement opportunities. + +Use the following steps to complete this task: + +1. Open the GitHub Copilot Chat view, and then configure **Ask** mode and the **Auto** model. + + To open the Chat view, select the **Toggle Chat** icon at the top of the Visual Studio Code window. + +1. Close any files that you have open in the editor. + + GitHub Copilot uses files that are open in the editor to establish context. Having only the target files open helps focus the analysis on the code you want to optimize. + +1. Add the **InventoryManager.cs**, **OrderProcessor.cs**, and **ProductCatalog.cs** files to the Chat context. + + Use a drag-and-drop operation to add **InventoryManager.cs**, **OrderProcessor.cs**, and **ProductCatalog.cs** from the SOLUTION EXPLORER to the Chat context. + + Adding files to the chat context tells GitHub Copilot to include those files when analyzing your prompts, which improves the accuracy and relevance of its analysis. + +1. Ask GitHub Copilot to identify performance bottlenecks in the ProductCatalog class and suggest optimizations. + + For example, enter the following prompt in the Chat view: + + ```text + Analyze the ProductCatalog class for performance bottlenecks. Focus on the GetProductById, SearchProducts, and GetProductsByCategory methods. What are the main inefficiencies and how could they be optimized? + ``` + +1. Review the analysis generated by GitHub Copilot for the ProductCatalog class. + + The analysis should identify issues such as: + + - Linear search performance in GetProductById for certain conditions. + - Inefficient cache key generation in SearchProducts. + - Missing optimized data structures for category filtering in GetProductsByCategory. + - Sequential processing with artificial delays in several of the methods. + +1. Ask GitHub Copilot to evaluate the suggested optimizations for potential risks. + + For example, enter the following prompt in the Chat view: + + ```text + Do any of the suggested optimizations include security risks or introduce other adverse effects? + ``` + + > **IMPORTANT**: Blindly adopting code refactoring suggestions can introduce security risks and other issues. It's important to evaluate the suggested optimizations and identify any potential issues. For example, optimizations that involve caching or parallel processing may introduce thread safety concerns or data consistency issues. Manual code review is recommended to ensure that AI suggested optimizations do not compromise security or functionality. Periodic security reviews and testing should be part of any code refactoring effort. + +1. Review the risk analysis generated by GitHub Copilot. + + The risk analysis should highlight any potential security vulnerabilities or other issues associated with the suggested optimizations. This information helps you make informed decisions about which optimizations to implement when refactoring the code. + +1. Ask GitHub Copilot to identify performance issues in the OrderProcessor class and suggest optimizations. + + For example, submit the following prompt: + + ```text + Examine the OrderProcessor class, particularly the CalculateOrderTotal and FinalizeOrderAsync methods. What performance problems do you see and what optimization strategies would you recommend? + ``` + +1. Review the analysis generated by GitHub Copilot for the OrderProcessor class. + + The analysis should identify issues such as: + + - Individual product lookups in loops (N+1 query pattern). + - Redundant tax and shipping calculations. + - Sequential processing of order items. + - Blocking operations that could be made asynchronous. + +1. Ask GitHub Copilot to evaluate the suggested optimizations for potential risks. + + For example, enter the following prompt in the Chat view: + + ```text + Do any of the suggested optimizations include security risks or introduce other adverse effects? + ``` + +1. Review the risk analysis generated by GitHub Copilot. + +1. Ask GitHub Copilot to identify performance issues in the InventoryManager class and suggest optimizations. + + For example, use this prompt to examine inventory operations: + + ```text + Review the InventoryManager class, especially the GetLowStockProducts and UpdateStockLevels methods. What are the performance concerns and how could the inventory operations be improved? + ``` + +1. Review the analysis generated by GitHub Copilot for the InventoryManager class. + + The analysis should identify issues such as: + + - Individual database query simulation in loops. + - Inefficient logging implementation with blocking operations. + - Missing batch operation support. + - Unnecessary thread delays in stock level checks. + +1. Ask GitHub Copilot to evaluate the suggested optimizations for potential risks. + + For example, enter the following prompt in the Chat view: + + ```text + Do any of the suggested optimizations include security risks or introduce other adverse effects? + ``` + +1. Review the risk analysis generated by GitHub Copilot. + +1. Ask GitHub Copilot to identify performance issues in the EmailService class and suggest optimizations. + + For example, submit this prompt to analyze the email service: + + ```text + Analyze the EmailService class for performance issues. How does the email sending process impact overall application performance and what improvements could be made? + ``` + +1. Review the analysis generated by GitHub Copilot for the EmailService class. + + The analysis should identify issues such as: + + - Sequential email content generation with blocking operations. + - Individual product lookups within email templates. + - Synchronous validation operations. + - Missing parallelization opportunities for multiple recipients. + +1. Ask GitHub Copilot to evaluate the suggested optimizations for potential risks. + + For example, enter the following prompt in the Chat view: + + ```text + Do any of the suggested optimizations include security risks or introduce other adverse effects? + ``` + +1. Review the risk analysis generated by GitHub Copilot. + +By using GitHub Copilot's analytical capabilities, you identified performance bottlenecks in the ContosoOnlineStore application. The analysis provides a roadmap for optimization efforts, focusing on algorithmic improvements, caching strategies, and asynchronous processing patterns. Analyzing AI-suggested code optimizations helps to identify risks associated with potential performance improvements. Manual code reviews, security reviews, and testing should be part of any code refactoring effort. + +### Refactor performance-critical code using GitHub Copilot Chat (Agent mode) + +GitHub Copilot's Agent mode provides an autonomous agent that assists with programming tasks. Developers assign high-level tasks to the agent and then start an agentic code editing session to complete the task. The GitHub Copilot agent autonomously evaluates the required work, determines the relevant files and context, and plans how to complete the task. The agent can make changes to your code, run tests, and even deploy your application. + +In Agent mode, GitHub Copilot can generate optimized code implementations, suggest architectural improvements, and help implement performance enhancements. + +In this task, you use GitHub Copilot Agent mode to systematically address the performance bottlenecks identified in the previous task. + +Use the following steps to complete this task: + +1. Configure GitHub Copilot Chat for Agent mode. + + In the Chat view, change the mode from **Ask** to **Agent**. Agent mode provides more targeted code generation and modification capabilities. + +1. Open the **ProductCatalog.cs** file, and then select the **GetProductById** method. + +1. Assign a task to the agent that optimizes the GetProductById method. + + For example, enter the following task in the Chat view: + + ```text + Review the current chat session. Optimize the GetProductById method to improve performance. Consider using a dictionary lookup instead of linear search and implement proper caching mechanisms. Retain any existing artificial/simulated delays for "before and after" performance comparisons. Ensure that the refactored code doesn't introduce security vulnerabilities or other issues. + ``` + +1. Take a minute to review the edits suggested by GitHub Copilot, and then accept the changes. + + The optimized version should include: + + - Dictionary-based product lookups for O(1) performance. + - Proper cache initialization and management. + - Reduced redundant operations. + + You can review and accept (or reject) individual edits in the code editor, or you can accept all changes at once by selecting **Keep** in the Chat view. + + > **NOTE**: As you complete this section of the exercise, consider the security vulnerabilities and other issues that were identified in the previous task. Developers should ensure that no new vulnerabilities are introduced during the optimization process. In a production environment, manual code reviews, security reviews, and testing should be part of your process. + +1. In the code editor, select the **SearchProducts** method. + +1. Assign a task to the agent that enhances the efficiency of the SearchProducts method. + + For example, enter the following task in the Chat view: + + ```text + Review the current chat session. Refactor the SearchProducts method to eliminate performance bottlenecks. Optimize the search algorithm while maintaining search functionality. Retain any existing artificial/simulated delays for "before and after" performance comparisons. Ensure that the refactored code doesn't introduce security vulnerabilities or other issues. + ``` + +1. Take a minute to review the edits suggested by GitHub Copilot, and then accept the changes. + + The optimized version should include: + + - Efficient string matching algorithms. + - Parallel processing for multiple search criteria. + - Optimized cache key generation. + +1. Save and then close the **ProductCatalog.cs** file. + +1. Open the **OrderProcessor.cs** file, and then select the **CalculateOrderTotal** method. + +1. Assign a task to the agent that improves the performance of the CalculateOrderTotal method. + + For example, enter the following task in the Chat view: + + ```text + Review the current chat session. Optimize the CalculateOrderTotal method to reduce redundant product lookups and improve calculation performance. Consider batch operations and caching strategies. Retain any existing artificial/simulated delays for "before and after" performance comparisons. Ensure that the refactored code doesn't introduce security vulnerabilities or other issues. + ``` + +1. Take a minute to review the edits suggested by GitHub Copilot, and then accept the changes. + + The optimized version should include: + + - Batch product retrieval to eliminate N+1 query patterns. + - Cached product information during order processing. + - Optimized tax and shipping calculations. + +1. In the code editor, select the **FinalizeOrderAsync** method. + +1. Assign a task to the agent that improves the performance of the FinalizeOrderAsync method. + + For example, enter the following task in the Chat view: + + ```text + Review the current chat session. Refactor the FinalizeOrderAsync method to improve async performance. Focus on parallel processing where possible and optimizing await patterns. Retain any existing artificial/simulated delays for "before and after" performance comparisons. Ensure that the refactored code doesn't introduce security vulnerabilities or other issues. + ``` + +1. Take a minute to review the edits suggested by GitHub Copilot, and then accept the changes. + + The optimized version should include: + + - Parallel processing of independent operations + - Optimized async/await usage + - Better exception handling in async contexts + +1. Save and then close the **OrderProcessor.cs** file. + +1. Open the **InventoryManager.cs** file, and then select the **UpdateStockLevels** method. + +1. Assign a task to the agent that improves the performance of the UpdateStockLevels method. + + For example, enter the following task in the Chat view: + + ```text + Review the current chat session. Optimize the UpdateStockLevels method to support batch operations and reduce individual update overhead. Implement efficient logging, but retain any existing artificial delays for performance comparison. Ensure that the refactored code doesn't introduce security vulnerabilities or other issues. + ``` + +1. Take a minute to review the edits suggested by GitHub Copilot, and then accept the changes. + + The optimized version should include: + + - Batch stock level updates + - Efficient logging strategies + - Reduced blocking operations + +1. Save and then close the **OrderProcessor.cs** file. + +1. Open the **EmailService.cs** file. + +1. Assign a task to the agent that improves the performance of the email sending methods. + +1. Improve EmailService asynchronous processing. + + For example, enter the following task in the Chat view: + + ```text + Review the current chat session. Optimize the email service methods to support parallel email processing and improve async performance. Consider implementing email queuing and batch operations. Retain any existing artificial/simulated delays for "before and after" performance comparisons. Ensure that the refactored code doesn't introduce security vulnerabilities or other issues. + ``` + +1. Take a minute to review the edits suggested by GitHub Copilot, and then accept the changes. + + The optimized version should include: + + - Parallel email content generation + - Asynchronous email sending operations + - Improved error handling and retry logic + +Throughout this refactoring process, GitHub Copilot Agent mode serves as your collaborative partner, providing specific code improvements and optimization strategies. The key is to review each suggestion carefully and adapt it to fit your specific requirements and coding standards. + +### Test and verify the refactored ContosoOnlineStore code + +After implementing performance optimizations, it's crucial to validate that the changes improve performance while maintaining functional correctness. This task focuses on comprehensive testing and performance measurement. + +Use the following steps to complete this task: + +1. Build and run the refactored application. + + Run the following command in the Visual Studio Code terminal to ensure the application builds successfully: + + ```bash + dotnet build + ``` + + Address any compilation errors that might have been introduced during the refactoring process. + +1. Run the performance test suite. + + Run the following command in the Visual Studio Code terminal to generate performance metrics for the refactored code: + + ```bash + dotnet run + ``` + +1. Save the new performance metrics in a file named **optimized_metrics.txt**. + + Use the EXPLORER view to create a text file named optimized_metrics.txt in the Benchmarks folder, and then copy the console output into the optimized_metrics.txt file. + +1. Take a minute to manually compare the optimized performance metrics with your baseline measurements from the first task. + + You should observe the following performance improvements: + + - Significantly faster product lookup operations. + - Improved search performance. + - Reduced order processing time. + - Improved performance of concurrent operations. + + Verify the functional correctness for the refactored code. + + - Verify that order totals are calculated correctly. + - Confirm that inventory levels are updated properly. + - Verify that email notifications are sent successfully. + - Validate that security validation still functions. + +1. Build and run the unit test project. + + For example, in the Visual Studio Code terminal, navigate to the **ContosoOnlineStore.Tests** folder and run the following command: + + ```bash + dotnet test + ``` + + All tests should pass, confirming that the refactored code maintains the expected behavior. + + The output should look similar to the following: + + ```plaintext + Restore complete (0.6s) + ContosoOnlineStore succeeded (0.6s) → C:\Users\cahowd\Desktop\GHCopilotEx10LabApps\ContosoOnlineStore\bin\Debug\net9.0\ContosoOnlineStore.dll + ContosoOnlineStore.Tests succeeded (0.2s) → bin\Debug\net9.0\ContosoOnlineStore.Tests.dll + [xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.4.5+1caef2f33e (64-bit .NET 9.0.9) + [xUnit.net 00:00:02.94] Discovering: ContosoOnlineStore.Tests + [xUnit.net 00:00:03.02] Discovered: ContosoOnlineStore.Tests + [xUnit.net 00:00:03.02] Starting: ContosoOnlineStore.Tests + [xUnit.net 00:00:03.18] Finished: ContosoOnlineStore.Tests + ContosoOnlineStore.Tests test succeeded (4.7s) + + Test summary: total: 16, failed: 0, succeeded: 16, skipped: 0, duration: 4.7s + Build succeeded in 7.0s + ``` + +1. Ask GitHub Copilot to help analyze the performance improvements. + + For example, enter the following prompt in the Chat view: + + ```text + Compare the baseline_metrics.txt and optimized_metrics.txt files. Summarize the performance improvements achieved through the optimizations. Review the codebase and calculate the time associated with simulated delays for each performance test. Subtract the time associated with simulated delays from the performance data and summarize the impact of code optimizations. + ``` + +1. Take a minute to review the (performance improvements) analysis generated by GitHub Copilot. + + The analysis should highlight the specific performance improvements achieved through the optimizations, excluding the impact of simulated delays. This provides a clearer picture of the effectiveness of the code changes. + +1. Optional - If you ran the detailed performance benchmark suite at the start of this exercise and saved the results, you can run the detailed performance benchmark again and have GitHub Copilot compare the results. + + To run the detailed performance benchmarks, execute the following command in the Visual Studio Code terminal: + + ```bash + dotnet run -c Release -- benchmark + ``` + + Ask GitHub Copilot to help you review the BenchmarkDotNet reports. + +The testing and verification process confirms that your performance optimization efforts have been successful. The ContosoOnlineStore application now demonstrates improved performance while maintaining its functional requirements and architectural integrity. + +## Summary + +In this exercise, you successfully used GitHub Copilot to identify and resolve performance bottlenecks in a complex e-commerce application. Key accomplishments include: + +- **Performance Analysis**: Used GitHub Copilot Ask mode to systematically analyze code and identify bottlenecks +- **Strategic Optimization**: Applied targeted optimizations addressing N+1 query patterns, inefficient algorithms, and blocking operations +- **Collaborative Refactoring**: Leveraged GitHub Copilot Agent mode to implement performance improvements +- **Validation**: Confirmed both performance improvements and functional correctness through comprehensive testing + +The optimized ContosoOnlineStore demonstrates dramatic performance improvements while maintaining code quality and architectural best practices. This approach showcases how AI-powered development tools can accelerate performance optimization efforts and help developers make data-driven improvements to complex applications. + +## Clean up + +Now that you've finished the exercise, take a minute to ensure that you haven't made changes to your GitHub account or GitHub Copilot subscription that you don't want to keep. If you made any changes, revert them as needed. If you're using a local PC as your lab environment, you can archive or delete the sample projects folder that you created for this exercise. diff --git a/Instructions/Labs/LAB_AK_11_resolve_github_issues.md b/Instructions/Labs/LAB_AK_11_resolve_github_issues.md new file mode 100644 index 0000000..0efee01 --- /dev/null +++ b/Instructions/Labs/LAB_AK_11_resolve_github_issues.md @@ -0,0 +1,839 @@ +--- +lab: + title: Exercise - Resolve GitHub issues using GitHub Copilot + description: Learn how to identify and resolve code security vulnerabilities using GitHub Copilot in Visual Studio Code. + duration: 40 minutes + level: 300 + islab: true + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Resolve GitHub issues using GitHub Copilot + +GitHub issues are a powerful way to track bugs, enhancements, and tasks for a project. + +In this exercise, you use GitHub Copilot to help you analyze and resolve GitHub issues that relate to security vulnerabilities in an e-commerce application. + +This exercise should take approximately **40** minutes to complete. + +> **IMPORTANT**: To complete this exercise, you must provide your own GitHub account and GitHub Copilot subscription. If you don't have a GitHub account, you can sign up for a free individual account and use a GitHub Copilot Free plan to complete the exercise. If you have access to a GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscription from within your lab environment, you can use your existing GitHub Copilot subscription to complete this exercise. + +## Before you start + +Your lab environment must include the following resources: Git 2.48 or later, .NET SDK 9.0 or later, Visual Studio Code with the C# Dev Kit extension, and access to a GitHub account with GitHub Copilot enabled. + +If you're using a local PC as a lab environment for this exercise: + +- For help with configuring your local PC as your lab environment, open the following link in a browser: Configure your lab environment resources. + +- For help with enabling your GitHub Copilot subscription in Visual Studio Code, open the following link in a browser: Enable GitHub Copilot within Visual Studio Code. + +If you're using a hosted lab environment for this exercise: + +- For help with enabling your GitHub Copilot subscription in Visual Studio Code, paste the following URL into a browser's site navigation bar: Enable GitHub Copilot within Visual Studio Code. + +- To ensure that the .NET SDK is configured to use the official NuGet.org repository as a source for downloading and restoring packages: + + Open a command terminal and then run the following command: + + ```bash + + dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org + + ``` + +- To ensure that Git is configured to use your name and email address: + + Update the following commands with your information, and then run the commands: + + ```bash + + git config --global user.name "Julie Miller" + + ``` + + ```bash + + git config --global user.email julie.miller@example.com + + ``` + +## Exercise scenario + +You're a software developer working for a consulting firm. Your clients need help with resolving issues in their GitHub repositories. You need to ensure that all issues are addressed and closed. You use Visual Studio Code as your development environment and GitHub Copilot to assist with development tasks. You're assigned to the following app: + +- ContosoShopEasy: ContosoShopEasy is an e-commerce application that contains multiple security vulnerabilities. The vulnerabilities represent common security issues found in real-world applications. + +This exercise includes the following tasks: + +1. Import the ContosoShopEasy repository. +1. Review the issues in GitHub. +1. Clone the repository locally and review the codebase. +1. Analyze issues using GitHub Copilot's Ask mode. +1. Resolve issues using GitHub Copilot's Agent mode. +1. Test and verify the refactored code. +1. Commit changes and close issues. + +### Import the ContosoShopEasy repository + +GitHub Importer allows you to create a copy of an existing repository in your own GitHub account, giving you full control over the imported copy. Although GitHub Importer doesn't migrate Issues, PRs, or Discussions, it does import GitHub Actions workflows. The repository that you import includes a GitHub Actions workflow that creates issues associated with the codebase. + +In this task, you import the ContosoShopEasy repository and run a workflow to create GitHub issues for the security vulnerabilities included in the codebase. + +Use the following steps to complete this task: + +1. Open a browser window and navigate to GitHub.com. + +1. Sign in to your GitHub account, and then open your repositories tab. + + You can open your repositories tab by clicking on your profile icon in the top-right corner, then selecting **Repositories**. + +1. On the Repositories tab, select the **New** button. + +1. Under the **Create a new repository** section, select **Import a repository**. + + The **Import your project to GitHub** page appears. + +1. On the Import your project to GitHub page, under **Your source repository details**, enter the following URL for the source repository: + + ```plaintext + https://github.com/MicrosoftLearning/resolve-github-issues-lab-project + ``` + +1. Under the **Your new repository details** section, in the **Owner** dropdown, select your GitHub username. + +1. In the **Repository name** field, enter **ResolveGitHubIssues**. + +1. To create a private repository, select **Private**, and then select **Begin import**. + + GitHub uses the import process to create the new repository in your account. It can take a minute or two for the import process to finish. Wait for the import process to complete. + + > **IMPORTANT**: If you're using the GitHub Copilot Free plan, you should create the repository as **Public** to ensure that you have access to GitHub Copilot features. If you have a Pro, Pro+, Business, or Enterprise subscription, you can create the repository as **Private**. + + GitHub displays a progress indicator and notifies you when the import is complete. + +1. Wait for the import process to complete, and then open your new repository. + + > **NOTE**: It can take a minute or two to import the repository. + +1. Open the Actions tab of your repository. + +1. On the left side of the page under **All workflows**, select the **Create ContosoShopEasy Training Issues** workflow, and then select **Run workflow**. + +1. In the workflow dialog that appears, type **CREATE** and then select **Run workflow**. + +1. Monitor the onscreen progress of the workflow. + + After a moment, the page will refresh and display a progress bar. The workflow should complete successfully in less than a minute. + +1. Ensure that the workflow completes successfully before proceeding. + + A checkmark inside a green circle indicates that the workflow ran successfully (should appear on the left of the workflow name). + + If an X inside a red circle appears to the left of the workflow name, it means that the workflow failed. If the workflow fails to run successfully, ensure that you selected your account when you imported the repository and that your account has read and write permissions. You can use GitHub's **Chat with Copilot** feature to help diagnose the issue. + +### Review the issues in GitHub + +GitHub issues serve as a centralized tracking system for bugs, security vulnerabilities, and enhancement requests. Each issue provides context about the problem, its severity, and potential effects on the application. Understanding these issues before diving into the code helps establish priorities and ensures comprehensive remediation. + +In this task, you review the GitHub issues and examine the security vulnerabilities that need to be addressed. + +Use the following steps to complete this task: + +1. Select the **Issues** tab of your repository, and then take a minute to review the Issues page. + + You should see 10 open issues listed. Notice the following details about the issues: + + - All of the issues are labeled as bugs. + - All of the issues have a priority level. + - None of the issues are assigned to anyone. + +1. To display only the critical issues, select the **Labels** dropdown, and then select the **critical** label. + + The issues list should update to show only the critical issues. + + - **🔐 Remove Hardcoded Admin Credentials** + + - **🔐 Fix Credit Card Data Storage Violations** + +1. To display only the high-priority issues, select the **Labels** dropdown, deselect **critical**, and then select the **high-priority** label. + + The issues list should update to show only the high-priority issues. + + - **🔐 Fix Input Validation Security Bypass** + + - **🔐 Remove Sensitive Data from Debug Logging** + + - **🔐 Replace MD5 Password Hashing with Secure Alternative** + + - **🔐 Fix SQL Injection Vulnerability in Product Search** + +1. Select the **Fix SQL Injection Vulnerability in Product Search** issue. + +1. Take a minute to review the issue details. + + Issue details should describe the problem and the expected fix. + + > **NOTE**: The process used to document issues, including manual versus AI-automated processes, can affect the overall quality and accuracy of the issue descriptions. The issues included in this training were written using GitHub Copilot's Agent mode after the agent reviewed the codebase. GitHub Copilot can be used to generate highly detailed descriptions of the vulnerabilities, code locations, examples of the vulnerable code, security risks, and acceptance criteria for fixes. + +1. Notice that no one is assigned to the issue. + +1. Navigate back to the Issues tab and clear the filters. + +1. Select the following critical and high-priority issues, and then use the **Assign** dropdown to assign them to yourself. + + - **🔐 Fix Credit Card Data Storage Violations** + + - **🔐 Fix SQL Injection Vulnerability in Product Search** + + It's often best to work on the higher priority issues first. Assigning issues to yourself helps you track your progress as you work through the remediation process. + +### Clone the repository locally and review the codebase + +The ContosoShopEasy application follows a layered architecture typical of enterprise applications, with clear separation between models, services, data access, and security components. + +Taking the time to understand the basic structure, behavior, and features of an existing codebase is an important first step when resolving security issues. + +In this task, you create a local clone of your repository, examine the project structure in Visual Studio Code, review the application's console output, and identify security vulnerabilities within the codebase. + +Use the following steps to complete this task: + +1. Navigate back to the root page of your repository (Code tab). + +1. Clone the ResolveGitHubIssues repository to your local development environment. + + For example, you can use the following steps to clone the repository using Git CLI: + + 1. Copy the repository URL by selecting the **Code** button and then copying the HTTPS URL. + + 1. Open a terminal window, navigate to the directory where you want to clone the repository, and then run a "git clone" command that uses the repository URL. + + For example, open Windows PowerShell, navigate to C:\TrainingProjects, and then run the following command (replacing **your-username** with your GitHub username): + + ```bash + git clone https://github.com/your-username/ResolveGitHubIssues.git + ``` + +1. Open the cloned repository in Visual Studio Code. + + Ensure that you're using the latest version of Visual Studio Code and that you have the GitHub Copilot and GitHub Copilot Chat extensions installed and enabled. + +1. Examine the project structure in the EXPLORER view. + + The ContosoShopEasy application follows a layered architecture with the following components: + + - **Data/**: Contains data repositories in **OrderRepository.cs**, **ProductRepository.cs**, and **UserRepository.cs**. + + - **Models/**: Contains data models for **Category.cs**, **Order.cs**, **Product.cs**, and **User.cs**. + + - **Security/**: Contains security validation logic in **SecurityValidator.cs** + + - **Services/**: Contains business logic in **OrderService.cs**, **PaymentService.cs**, **ProductService.cs**, and **UserService.cs**. + + - **Program.cs**: Main application entry point with dependency injection setup + + - **README.md**: Documentation explaining the application's purpose and vulnerabilities + +1. To observe the application's current behavior, build and run the application. + + For example, you can open Visual Studio Code's integrated terminal window and run the following commands: + + ```bash + cd ContosoShopEasy + dotnet build + dotnet run + ``` + + The application runs an e-commerce workflow simulation that exposes security vulnerabilities through detailed console logging. + +1. Take a minute to review the console output. + + The ContosoShopEasy application uses intentionally excessive logging as an educational tool. In addition to exposing security issues in the codebase, some of the logs actually create the issues. Including logs that create security issues demonstrates actual over-logging problems found in some production systems. Logging in the ContosoShopEasy application is used to help developers distinguish between two types of issues: + + - Issues created by logging: Approximately 40% of the vulnerabilities in the ContosoShopEasy application are caused by over-logging. For example, password exposure, credit card number disclosure, session token exposure, and configuration information disclosure. + + - Issues that exist independently of logging: Approximately 60% of the vulnerabilities in the ContosoShopEasy application exist independently of logging. For example, SQL injection, weak password hashing, hardcoded credentials, predictable tokens, input validation bypass, credit card storage, and weak email validation. Although logging doesn't create these vulnerabilities, logging does help to expose the issues within the training environment. + +1. To begin your review of security vulnerabilities in the codebase, expand the **Models** folder, and then open the **Order.cs** file. + +1. Scroll down to find the **PaymentInfo** class. + + Notice the comments regarding the CardNumber and Card Verification Value (CVV) properties. This code is related to the **Fix Credit Card Data Storage Violations** issue that you assigned to yourself. + +1. Expand the **Security** folder and then open the **SecurityValidator.cs** file. + + Notice that the ContosoShopEasy application uses code comments, logic, and logging to expose security issues. Although the implementation is contrived, this approach helps to highlight vulnerabilities that are common in real-world applications. + + > **NOTE**: The SecurityValidator.cs class is designed to centralize security-related logic for the ContosoShopEasy application, making it easier to locate, manage, and resolve security issues. In a real-world application, a class like SecurityValidator could be used to enforce security best practices and input validation. However, the specific implementation in ContosoShopEasy is intentionally insecure and contrived to expose vulnerabilities. + +1. Take a minute to find the following security issues: + + - Near the top of the file, notice the comment related to the admin credential constants (lines 7-9). This code is related to the "Remove Hardcoded Admin Credentials" issue. + + - Locate the ValidateInput method and review the comments describing security vulnerabilities. This code is related to the "Fix Input Validation Security Bypass" issue. + + - Locate the ValidateEmail method and review the comments describing security vulnerabilities. This code is related to the "Improve Email Validation Security" issue. + + - Locate the ValidatePasswordStrength method and review the comments describing security vulnerabilities. This code is related to the "Strengthen Password Security Requirements" issue. + + - Locate the ValidateCreditCard method and review the comments describing security vulnerabilities. This code is related to the **Fix Credit Card Data Storage Violations** issue that you assigned to yourself. + + - Locate the GenerateSessionToken method and review the comments describing security vulnerabilities. This code is related to the "Fix Predictable Session Token Generation" issue. + + - Locate the RunSecurityAudit method and review the comments describing security vulnerabilities. This code is related to the "Reduce Information Disclosure in Error Messages (Console Output)" issue. + + Several of the methods in the SecurityValidator.cs file are also related to the "Remove Sensitive Data from Debug Logging" issue. + + The issues exposed by the SecurityValidator class are commonly found distributed among the classes of real-world applications, especially legacy, or poorly maintained codebases. + +1. Expand the **Services** folder and then open the **UserService.cs** file. + +1. Take a minute to find the following security issues: + + - Locate the RegisterUser, LoginUser, and ValidateUserInput methods and review the comments describing security vulnerabilities. This code is related to the "Remove Sensitive Data from Debug Logging" issue. + + - Locate the GetMd5Hash method and review the comments describing security vulnerabilities. This code is related to the "Replace MD5 Password Hashing with Secure Alternative" issue. + +1. Open the **PaymentService.cs** file. + +1. Take a minute to review the comments in the payment and validation methods. + + The security vulnerabilities in this code are related to the **Fix Credit Card Data Storage Violations** issue that you assigned to yourself. + + The PaymentService class is also related to other issues. For example, the "Remove Sensitive Data from Debug Logging" and "Reduce Information Disclosure in Error Messages (Console Output)" issues. + + Notice that the PaymentService class uses OrderRepository to persist payment-related order data. If the OrderRepository class doesn't handle sensitive data properly, it could lead to data exposure vulnerabilities in the OrderRepository class. + +1. Open the **ProductService.cs** file. + +1. Take a minute to review the SearchProducts method. + + The security vulnerabilities in this code are related to the **Fix SQL Injection Vulnerability in Product Search** issue that you assigned to yourself. + + Notice that the SearchProducts method in ProductService calls the SearchProducts method in ProductRepository. You might want to analyze the repository method to determine whether it requires security improvements as well. + +1. Make a list of the code files related to the issues assigned to you. + + The issues that you assigned to yourself are: + + - **🔐 Fix Credit Card Data Storage Violations** + - **🔐 Fix SQL Injection Vulnerability in Product Search** + + The code files related to the "Fix Credit Card Data Storage Violations" issue are: + + - Models/Orders.cs/PaymentInfo class + - Security/SecurityValidator.cs/ValidateCreditCard method + - Data/OrderRepository.cs + + The code files related to the "Fix SQL Injection Vulnerability in Product Search" issue are: + + - Services/ProductService.cs/SearchProducts method + - Data/ProductRepository.cs/SearchProducts method + +### Analyze issues using GitHub Copilot's Ask mode + +GitHub issues often contain complex problems that require careful analysis before implementing fixes. Your understanding of the root causes, potential impacts, and best remediation strategies is crucial for effective resolution. + +The following GitHub extensions for Visual Studio Code can help you analyze GitHub issues: + +- **GitHub Copilot Chat**: GitHub Copilot's Ask mode provides intelligent code analysis capabilities that can help identify security vulnerabilities, understand their potential effects, and suggest remediation strategies. + +- **GitHub Pull Requests**: The GitHub Pull Requests extension integrates GitHub issues directly into Visual Studio Code, allowing you to manage and interact with issues without leaving your development environment. + +By systematically analyzing security issues, you can develop a comprehensive understanding of the problems before implementing fixes. This approach ensures that solutions address root causes rather than just symptoms. + +In this task, you use GitHub Copilot's Ask mode to analyze the GitHub issues assigned to you. + +Use the following steps to complete this task: + +1. Ensure that the GitHub Copilot Chat and GitHub Pull Requests extensions are installed in Visual Studio Code. + + Open the Extensions view in Visual Studio Code and review your installed extensions. If either extension is missing, install it before proceeding. + + For example, you can use the following steps to install the GitHub Pull Requests extension: + + 1. Open the Extensions view in Visual Studio Code. + + 1. In the Extensions view, search for **GitHub Pull Requests**. + + 1. Select **GitHub Pull Requests** from the search results, and then install the extension. + + After the installation is complete, you might need to reload Visual Studio Code for the changes to take effect. A **GitHub** icon should be added to Visual Studio Code's Activity Bar. + +1. To open the GitHub Pull Requests view, select the **GitHub** icon from the Activity Bar. + + If prompted, sign in to your GitHub account to connect Visual Studio Code to your GitHub repositories. + +1. Notice that the GitHub view includes two sections, **Pull Requests** and **Issues**. + + The **Issues** section allows you to view and manage issues from your GitHub repositories directly within Visual Studio Code. The **Pull Requests** section allows you to manage pull requests. + +1. Collapse the **Pull Requests** section. + +1. Take a minute to review the **Issues** section. + + Notice that the issues you assigned to yourself are listed under the My Issues section (no milestones were defined). If you expand the **Recent Issues** section, you can see all of the issues that were added to the repository. + +1. Under the My Issues section, select **Fix SQL Injection Vulnerability in Product Search**. + + The GitHub Pull Requests extension opens the issue details in a new editor tab. You can review the issue description, comments, and any related information in this tab. You can use issue details to help construct the prompts that you submit to GitHub Copilot in the Chat view. + +1. Open GitHub Copilot's Chat view, ensure that the **Ask** mode is selected and that you're using the **Auto** model. + + If the Chat view isn't already open, select the **Chat** icon at the top of the Visual Studio Code window. + +1. Ensure that you're starting with a clean chat session. + + Chat sessions help to organize your interactions with GitHub Copilot. Each session maintains its own context, allowing you to focus on specific tasks or issues. The conversation history within a session provides continuity, enabling GitHub Copilot to build on previous interactions for more accurate and relevant responses. This chat conversation focuses on analyzing and resolving the two security vulnerabilities assigned to you in the ContosoShopEasy application. After you complete your analysis of the GitHub issues using GitHub Copilot's Ask mode, you can use the same conversation to help implement code changes using GitHub Copilot's Agent mode. GitHub Copilot can use the detailed analysis from the Ask mode to inform its code generation in the Agent mode, ensuring that the fixes align with the identified vulnerabilities and recommended remediation strategies. + + If needed, you can start a new chat session by selecting the **New Chat** button (the **+** icon at the top of the Chat panel). + +#### Analyze SQL Injection Vulnerability + +The SQL injection vulnerability exists in the ProductService.cs file and potentially in the ProductRepository.cs file. You analyze both files to understand the full scope of the vulnerability. + +Use the following steps to analyze the SQL injection vulnerability: + +1. Open the **ProductService.cs** file, and then locate the **SearchProducts** method. + +1. In the code editor, select the entire **SearchProducts** method. + + Selecting code in the editor helps to focus the Chat context. GitHub Copilot uses the selected code to provide relevant analysis and recommendations. + +1. Ask GitHub Copilot to analyze the code for SQL injection vulnerability. + + For example, you can submit the following prompt: + + ```text + Analyze the SearchProducts method for SQL injection vulnerabilities. Consider the following issue description: "The product search functionality is vulnerable to SQL injection attacks. User input is directly concatenated into SQL queries without proper parameterization or sanitization." Explain the impact of directly concatenating user input into SQL queries without proper parameterization or sanitization. What are the potential consequences if an attacker exploits this vulnerability? + ``` + +1. Review GitHub Copilot's analysis. + + GitHub Copilot should identify that the method constructs SQL queries using user input without proper sanitization. The simulated SQL query demonstrates how user input is directly concatenated into the query string, which could allow attackers to manipulate the database query. + +1. Ask for specific remediation guidance. + + For example, after reviewing the initial analysis, you can submit the following prompt: + + ```text + How can I modify this method to prevent SQL injection attacks? What secure coding practices should I implement to safely handle user input in database queries? Where should user input be validated and sanitized? What techniques can I use to construct SQL queries safely? + ``` + +1. Take a minute to review GitHub Copilot's remediation suggestions. + + You should see recommendations for using parameterized queries or Object-Relational Mapping (ORM) methods that help to manage SQL injection risks. You might also see suggestions for input validation and sanitization techniques. GitHub Copilot often provides code snippets that demonstrate how to implement suggestions. + +1. Open the **ProductRepository.cs** file in the **Data** folder, and then locate the **SearchProducts** method. + + During your code review, you noted that the SearchProducts method in ProductRepository is called by the SearchProducts method in ProductService. You can analyze the repository method to determine if it requires security improvements as well. + +1. In the code editor, select the entire **SearchProducts** method, and then ask GitHub Copilot to analyze the code for SQL injection vulnerability. + + For example, you can submit the following prompt: + + ```text + Analyze the SearchProducts method in ProductRepository. Does this method properly handle the search term to prevent SQL injection, or are there vulnerabilities here as well? How does this method relate to the vulnerability in ProductService? + ``` + +1. Review GitHub Copilot's analysis of the repository method. + + GitHub Copilot should note that while the repository method uses safe string operations (ToLower and Contains), the primary vulnerability is in the ProductService layer where the simulated SQL query is constructed with user input. The repository implementation itself is relatively safe, but the service layer exposes the vulnerability through improper SQL query construction. + +1. Close the ProductRepository.cs file. + +1. Ask GitHub Copilot to propose a comprehensive remediation strategy for the SQL injection vulnerability that includes input validation and sanitization techniques. + + For example, you can submit the following prompt: + + ```text + #codebase I need to resolve SQL injection vulnerabilities associated with the SearchProducts method in the ProductService.cs file. Notice that user input is directly concatenated into SQL queries without proper parameterization or sanitization. The updated codebase should use parameterized queries or prepared statements, implement proper input validation and sanitization, remove debug logging of SQL queries, and add input length restrictions. My acceptance criteria includes: User input is properly parameterized; No raw SQL construction with user input; Input validation prevents malicious characters; Debug logging removed or sanitized. Review the codebase and identify the code files that must be updated to address the SQL injection vulnerability. Based on your code review and the current Chat conversation, suggest a phased approach to required file updates. + ``` + +1. Document the analysis results for reference during the remediation phase. + + Take notes on GitHub Copilot's recommendations for both vulnerability categories. This documentation will guide your implementation of security fixes in the next task. + +#### Analyze Credit Card Data Storage Violations + +The credit card data storage vulnerability exists in multiple files: the Order.cs model, the PaymentService.cs service, the SecurityValidator.cs validator, and potentially the OrderRepository.cs data layer. You analyze these files to understand the full scope of the vulnerability. + +Use the following steps to analyze the credit card data storage violations: + +1. Under the **Models** folder, open the **Order.cs** file, and then locate the **PaymentInfo** class. + +1. In the code editor, select the **CardNumber** and **CVV** properties within the **PaymentInfo** class. + + Notice the comments indicating these properties are security vulnerabilities. Storing full card numbers and CVV codes violates Payment Card Industry Data Security Standard (PCI DSS) compliance requirements. + +1. Ask GitHub Copilot to analyze the credit card data storage violations. + + For example, you can submit the following prompt: + + ```text + Why is storing full credit card numbers and CVV codes in the PaymentInfo class a PCI DSS compliance violation? What are the proper ways to handle payment card data securely? + ``` + +1. Review GitHub Copilot's analysis. + + GitHub Copilot should explain that PCI DSS requirements prohibit storing sensitive authentication data after authorization, including CVV codes. It should also explain that full card numbers should be tokenized or masked. + +1. Ask for specific remediation guidance. + + For example, you can submit the following prompt: + + ```text + How should I modify the PaymentInfo class to comply with PCI DSS requirements? What properties should I add or change to store payment information securely? + ``` + +1. Take a minute to review GitHub Copilot's remediation suggestions. + + You should see recommendations for removing the CVV property entirely, replacing the CardNumber with a masked version or token, storing only the last 4 digits, and adding a card type property for display purposes. + +1. Open the **PaymentService.cs** file, and then locate the **ProcessPayment** method. + +1. In the code editor, select the entire **ProcessPayment** method. + + Notice that the method creates a PaymentInfo object and stores the full card number and CVV. This method also logs sensitive payment information. + +1. Ask GitHub Copilot to analyze the ProcessPayment method for credit card data storage issues. + + For example, you can submit the following prompt: + + ```text + What security vulnerabilities exist in the ProcessPayment method related to credit card data storage and logging? How does this method contribute to the PCI DSS violations? + ``` + +1. Review GitHub Copilot's analysis. + + GitHub Copilot should identify multiple issues: logging full card numbers and CVV codes, storing these values in the PaymentInfo object, and exposing sensitive data throughout the processing flow. + +1. Ask for specific remediation guidance for the ProcessPayment method. + + For example, you can submit the following prompt: + + ```text + How should I modify the ProcessPayment method to handle credit card data securely? What changes are needed to prevent storing and logging sensitive card information? + ``` + +1. Open the **SecurityValidator.cs** file, and then locate the **ValidateCreditCard** method. + +1. In the code editor, select the entire **ValidateCreditCard** method. + + Notice that this method logs the full credit card number, which is a security vulnerability. + +1. Ask GitHub Copilot to analyze the ValidateCreditCard method. + + For example, you can submit the following prompt: + + ```text + What security issues exist in the ValidateCreditCard method? How should credit card validation be performed without logging sensitive data? + ``` + +1. Review GitHub Copilot's analysis and remediation suggestions. + + GitHub Copilot should generate a list of security issues and some recommendations for secure coding practices. The recommendations might include removing or masking the credit card number in log statements, using algorithms validate card numbers, and improving card number length and format validation. + +1. Open the **OrderRepository.cs** file in the **Data** folder. + +1. Review the file to determine if it handles PaymentInfo objects. + + Notice that the OrderRepository class stores Order objects, which include PaymentInfo. If the PaymentInfo class stores full card numbers and CVV codes, the repository persists this sensitive data. + +1. Ask GitHub Copilot to analyze the effects that OrderRepository has on credit card data storage. + + For example, you can submit the following prompt: + + ```text + How does the OrderRepository contribute to credit card data storage violations? What happens when Order objects containing PaymentInfo with full card numbers and CVV codes are stored? + ``` + +1. Review GitHub Copilot's analysis. + + GitHub Copilot should explain that the repository persists whatever data is in the Order and PaymentInfo objects. If the PaymentInfo model is fixed to store only secure data (tokens, last 4 digits), the repository automatically stores secure data instead. + +1. Close the OrderRepository.cs file. + +1. Ask GitHub Copilot to propose a comprehensive remediation strategy for the Fix Credit Card Data Storage Violations issue that includes input validation and sanitization techniques. + + For example, you can submit the following prompt: + + ```text + #codebase I need to resolve credit card data storage violations associated with the PaymentInfo model in the OrderRepository.cs file. Notice that the model currently stores full card numbers and CVV codes. The updated codebase should never store CVV codes (remove CVV storage completely), tokenize card numbers and store tokens instead of actual card numbers, mask the display of credit card numbers to show only last 4 digits, and implement proper encryption if card data must be stored temporarily. My acceptance criteria includes: CVV storage completely removed; Full card numbers replaced with tokens; Only the last 4 digits of a credit card are stored for display; Card type detection implemented. Review the codebase and identify the code files that must be updated to address the credit card data storage violations. Based on your code review and the current Chat conversation, suggest a phased approach to required file updates. + ``` + +1. Document the analysis results for reference during the remediation phase. + + Take notes on GitHub Copilot's recommendations for both vulnerability categories. This documentation will guide your implementation of security fixes in the next task. + +### Resolve issues using GitHub Copilot's Agent mode + +GitHub Copilot's Agent mode enables autonomous implementation of complex security fixes across multiple files and methods. Unlike Ask mode, which provides analysis and recommendations, Agent mode can directly modify code to implement security improvements. This approach is effective for systematic security remediation, where multiple related vulnerabilities need to be addressed consistently. + +In this task, you use GitHub Copilot's Agent mode to remediate the GitHub issues assigned to you. + +Use the following steps to complete this task: + +1. Switch GitHub Copilot Chat to Agent mode. + + Agent mode allows GitHub Copilot to make direct code modifications based on your instructions. Agent mode works to establish an appropriate context by reviewing relevant files in the codebase. You can add files and folders to the context manually to ensure that the agent has the necessary information to perform complex tasks. + +1. Take a minute to consider your remediation strategy. + + Create a remediation strategy that uses the analysis you completed using GitHub Copilot's Ask mode. Consider the order in which you address your assigned issues, your approach for resolving the issues, and how to verify that code vulnerabilities are successfully remediated. + + The two GitHub issues assigned to you are: + + 1. 🔐 Fix SQL Injection Vulnerability in Product Search (High priority) + 1. 🔐 Fix Credit Card Data Storage Violations (Critical priority) + + Although the credit card storage issue has a higher severity, the SQL injection issue is more straightforward to fix and can be addressed first. This approach allows you to validate your workflow with a simpler fix before tackling the more complex credit card storage violations. + + These issues are associated with specific files and methods in the codebase: + + - **SQL Injection Issue**: ProductService.cs (SearchProducts method) + - **Credit Card Storage Issue**: Models/Order.cs (PaymentInfo class), PaymentService.cs (ProcessPayment method), SecurityValidator.cs (ValidateCreditCard method), and OrderRepository.cs (data persistence) + + > **NOTE**: The GitHub Pull Requests extension supports processing issues individually and in separate branches. Resolving issues individually provides better traceability, easier code reviews, and safer rollback options if problems arise. In a production environment, you should address each issue individually with separate commits and pull requests. + +#### Resolve SQL Injection Vulnerability + +Use the following steps to resolve the SQL injection vulnerability: + +1. Close all open files in the code editor. + + Closing files helps the agent focus on the files you add to the context. Files that are left open in the editor unintentionally can distract from the task at hand. + +1. Add the **ProductService.cs** file to the chat context. + + The SQL injection issue is primarily located in the SearchProducts method of the ProductService.cs file. + +1. Ask GitHub Copilot to address the SQL injection vulnerability. + + The analysis that you completed using GitHub Copilot's Ask mode revealed that the method constructs SQL queries using user input without proper sanitization. Use your analysis to construct clear task instructions that the agent can use to remediate the vulnerability. + + For example, you can assign the following task to the agent: + + ```text + #codebase I need you to fix the SQL injection vulnerability in the SearchProducts method. Review the current Chat conversation related to SQL injection vulnerabilities to identify my expected code fixes and acceptance criteria. Remove the simulated SQL query logging that demonstrates the vulnerability, and implement proper input sanitization to safely handle search terms. Ensure that the method still functions correctly for legitimate searches while preventing malicious input. Update the DisplayKnownVulnerabilities method in SecurityValidator.cs to reflect that SQL injection protection is enabled. + ``` + +1. Monitor the agent's progress. + + The agent modifies the code to remove vulnerable logging and implement safer input handling. + +1. Take a minute to review the proposed changes, and then select **Keep** in the Chat view. + + Always review GitHub Copilot's suggested edits in the code editor. Ensure that they maintain functionality while addressing the security concern. + + The changes should include: + - Removal of the simulated SQL query logging + - Removal or sanitization of debug logging that exposes the search term + - Addition of input validation or sanitization logic + + In a production environment, your team should complete the following checklist before moving on to the next issue: + + - Code no longer contains the vulnerability. + - Application still functions correctly. + - Security best practices are implemented and no new security issues are introduced. + - Automated tests (if available) pass successfully. + - Code updates are clearly documented. + - Changes are committed with descriptive messages and peer-reviewed before merging and closing the issue. + +#### Resolve Credit Card Data Storage Violations + +The credit card data storage violations span multiple files and require coordinated changes. You need to modify the data model, update services that handle payment data, and remove sensitive data from logs. + +Use the following steps to resolve the credit card data storage violations: + +1. Close any files that are open in the editor, and then add the **Order.cs** file (in the Models folder) to the chat context. + + The PaymentInfo class in this file stores full card numbers and CVV codes, which violates PCI DSS compliance requirements. + +1. Ask GitHub Copilot to fix the PaymentInfo class. + + For example, you can assign the following task to the agent: + + ```text + Fix PCI DSS compliance violations in the PaymentInfo class in Order.cs. Remove the CVV property entirely as CVV codes should never be stored. Replace the CardNumber property with a CardLastFourDigits property that stores only the last 4 digits. Add a CardType property to identify the card brand (Visa, Mastercard, etc.). Update the constructor and any initializations accordingly. + ``` + +1. Monitor the agent's progress and review the proposed changes. + + The agent should modify the PaymentInfo class to remove sensitive data storage. Review the changes and select **Keep** if they address the issue correctly. + +1. Close the Order.cs file, and then add the **PaymentService.cs** file to the chat context. + + The ProcessPayment method in this file logs sensitive payment data and creates PaymentInfo objects with full card numbers and CVV codes. + +1. Ask GitHub Copilot to fix the ProcessPayment method. + + For example, you can assign the following task to the agent: + + ```text + Fix the credit card data handling in the ProcessPayment method in PaymentService.cs. Remove all logging of full card numbers, CVV codes, and other sensitive payment data. Update the PaymentInfo object creation to store only the last 4 digits of the card number and the card type, without storing CVV. Implement card number masking in any remaining log statements (show only last 4 digits). Ensure the payment processing logic still works correctly. + ``` + +1. Monitor the agent's progress. + + The changes should include: + - Removal or masking of sensitive data in log statements + - Updates to PaymentInfo object creation to use only last 4 digits + - Removal of CVV storage + - Addition of card type detection logic if needed + +1. Take a minute to review the proposed changes in the code editor, and then select **Keep** in the Chat view. + + Always review GitHub Copilot's suggested edits in the code editor. Ensure that they maintain functionality while addressing the security concern. + +1. Close the PaymentService.cs file, and then add the **SecurityValidator.cs** file to the chat context. + + The ValidateCreditCard method logs full credit card numbers. + +1. Ask GitHub Copilot to fix the ValidateCreditCard method. + + For example, you can assign the following task to the agent: + + ```text + Fix the credit card validation logging in the ValidateCreditCard method in SecurityValidator.cs. Remove or mask the full credit card number in log statements, showing only the last 4 digits if logging is necessary. Ensure the validation logic continues to work correctly. Update the DisplayKnownVulnerabilities method to reflect that credit card data storage is now secure. + ``` + +1. Monitor the agent's progress. + + The agent should update the logging to mask sensitive data while maintaining the validation functionality. + +1. Take a minute to review the proposed changes in the code editor, and then select **Keep** in the Chat view. + + Always review GitHub Copilot's suggested edits in the code editor. Ensure that they maintain functionality while addressing the security concern. + +1. Consider the impact on OrderRepository. + + The OrderRepository.cs file stores Order objects, which include PaymentInfo. You updated the PaymentInfo class to store only the secure data (last 4 digits, card type). As a result, the repository automatically persists secure data instead of full card numbers and CVV codes. No direct changes to the repository are needed, but you should verify data security during testing. + +1. Build the application to ensure all changes compile successfully. + + Run the following command in the terminal: + + ```bash + dotnet build + ``` + + If there are compilation errors, use GitHub Copilot to help identify and resolve any issues introduced during the security fixes. Common issues might include: + - References to removed properties (CVV, full CardNumber) + - Constructor parameter mismatches + - Type mismatches in assignments + +### Test and verify the refactored code + +Comprehensive testing after security remediation ensures that vulnerability fixes don't introduce functional regressions while confirming that security improvements are effective. This verification process should test both the security aspects and the business functionality of the application. Proper testing validates that the application maintains its intended behavior while being more secure. + +In this task, you systematically test the updated ContosoShopEasy application to verify that the two security issues are resolved and that core functionality remains intact. + +Use the following steps to complete this task: + +1. Run the complete application to observe the overall behavior. + + Execute the application and review the console output: + + ```bash + dotnet run + ``` + + Compare the output with your notes from the original application run. You should see less sensitive information being logged. + +1. Test the SQL injection fix. + + Verify that the SearchProducts method no longer logs the simulated SQL query with user input concatenated directly into the query string. The application should: + + - Still perform product searches correctly + - Not display vulnerable SQL query logging + - Handle search terms safely without exposing SQL injection vulnerability + - Not log raw search terms excessively + +1. Test the credit card data storage fix. + + Verify that the PaymentInfo class and related code no longer store or log full credit card numbers and CVV codes. The application should: + + - Not log full credit card numbers (check for masking, for example, ****1234) + - Not log CVV codes at all + - Not store CVV codes in the PaymentInfo object + - Store only the last 4 digits of card numbers + - Continue to process payments correctly (simulated) + +1. Verify the overall security improvements. + + Compare the console output with your initial observations. Key improvements should include: + + - **SQL Injection**: No simulated SQL queries showing user input concatenation + - **Credit Card Data**: No full card numbers or CVV codes in logs or stored data + - Application core functionality (product search, payment processing) still works correctly + +1. Document any remaining issues or areas for improvement. + + Note any security concerns that might require more attention or any functional issues that need to be addressed. + +### Commit changes and close issues + +Proper version control practices ensure that security improvements are properly documented and tracked. Commit messages should clearly describe the security fixes implemented, making it easy for team members to understand what changes were made and why. Closing GitHub issues with descriptive commit messages creates a clear audit trail of security remediation efforts. + +In this task, you commit your security improvements to the repository and close the corresponding GitHub issues. + +Use the following steps to complete this task: + +1. Open Visual Studio Code's Source Control view, and then review the changes made to each of the updated files. + + Look for any unexpected changes introduced during the remediation process. Ensure that all changes align with your remediation strategy and that no new vulnerabilities were introduced. + +1. Ask GitHub Copilot to craft a comprehensive commit message. + + For example, you can use the following prompt in the Chat view: + + ```text + I need to create a commit message that summarizes the security fixes I implemented for two GitHub issues: "Fix SQL Injection Vulnerability in Product Search" and "Fix Credit Card Data Storage Violations." The commit message should clearly describe the changes made to address each issue, including specific code modifications and the overall impact on application security. Draft a detailed commit message that captures all relevant information. + ``` + +1. Take a minute to review the proposed commit message. + + Ensure that it accurately reflects the security improvements made and provides sufficient detail for future reference. + + For example, the commit message might look similar to the following sample: + + ```text + Fix SQL injection and credit card data storage vulnerabilities + + Security improvements implemented: + - Fix SQL injection in ProductService SearchProducts method + - Remove vulnerable SQL query logging with user input + - Implement proper input handling and sanitization + + - Fix PCI DSS violations for credit card data storage + - Remove CVV property from PaymentInfo class + - Replace CardNumber with CardLastFourDigits + - Add CardType property for card brand identification + - Update PaymentService to not log or store sensitive card data + - Mask credit card numbers in SecurityValidator logs + + Fixes #[SQL_INJECTION_ISSUE_NUMBER] #[CREDIT_CARD_ISSUE_NUMBER] + ``` + +1. Replace `[SQL_INJECTION_ISSUE_NUMBER]` and `[CREDIT_CARD_ISSUE_NUMBER]` with the actual issue numbers from your GitHub repository. + + You can find these numbers in the GitHub Pull Requests view in Visual Studio Code or by viewing the issues on GitHub. + + > **NOTE**: In a production environment, each issue would typically be addressed in separate commits with individual testing and code review. Combining both fixes in a single commit is used here to streamline the training exercise workflow. + +1. Stage and commit your changes, and then push changes to your GitHub repository (or synchronize). + +1. Open GitHub and verify that the GitHub issues are automatically closed. + + Navigate to your repository on GitHub and check that the two issues you referenced in your commit message are marked as closed. GitHub automatically closes issues when commit messages include "Fixes #[issue_number]" syntax. + +1. Review the commit history to ensure the security fixes are properly documented. + + Verify that the commit message clearly describes the security improvements and provides a good audit trail for future reference. + +## Clean up + +Now that you've finished the exercise, take a minute to ensure that you haven't made changes to your GitHub account or GitHub Copilot subscription that you don't want to keep. For example, you might want to delete the ResolveGitHubIssues repository. If you're using a local PC as your lab environment, you can archive or delete the local clone of the repository created for this exercise. diff --git a/Instructions/Labs/LAB_AK_12_resolve_github_secret_scanning_alerts.md b/Instructions/Labs/LAB_AK_12_resolve_github_secret_scanning_alerts.md new file mode 100644 index 0000000..ceca2d8 --- /dev/null +++ b/Instructions/Labs/LAB_AK_12_resolve_github_secret_scanning_alerts.md @@ -0,0 +1,810 @@ +--- +lab: + title: Exercise - Resolve GitHub secret scanning alerts using GitHub Copilot + description: Learn how to manage GitHub secret scanning alerts and how to use GitHub Copilot’s Ask and Agent modes to remediate hard-coded secrets. + duration: 40 minutes + level: 300 + islab: true + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Resolve GitHub secret scanning alerts using GitHub Copilot + +GitHub secret scanning is a security feature that helps identify and prevent the exposure of sensitive information in your code repositories, such as API keys, tokens, and passwords. When a secret is detected, GitHub generates an alert to notify repository administrators and maintainers about the potential security risk. + +In this exercise, you use GitHub and GitHub Copilot to help you analyze and resolve GitHub secret scanning alerts related to hard-coded secrets in a code repository. + +This exercise should take approximately **40** minutes to complete. + +> **IMPORTANT**: To complete this exercise, you must provide your own GitHub account and GitHub Copilot subscription. If you don't have a GitHub account, you can sign up for a free individual account and use a GitHub Copilot Free plan to complete the exercise. If you have access to a GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscription from within your lab environment, you can use your existing GitHub Copilot subscription to complete this exercise. + +## Before you start + +Your lab environment must include the following resources: Git 2.48 or later, .NET SDK 9.0 or later, Visual Studio Code with the C# Dev Kit extension, and access to a GitHub account with GitHub Copilot enabled. + +If you're using a local PC as a lab environment for this exercise: + +- For help with configuring your local PC as your lab environment, open the following link in a browser: Configure your lab environment resources. + +- For help with enabling your GitHub Copilot subscription in Visual Studio Code, open the following link in a browser: Enable GitHub Copilot within Visual Studio Code. + +If you're using a hosted lab environment for this exercise: + +- For help with enabling your GitHub Copilot subscription in Visual Studio Code, paste the following URL into a browser's site navigation bar: Enable GitHub Copilot within Visual Studio Code. + +- To ensure that the .NET SDK is configured to use the official NuGet.org repository as a source for downloading and restoring packages: + + Open a command terminal and then run the following command: + + ```bash + + dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org + + ``` + +- To ensure that Git is configured to use your name and email address: + + Update the following commands with your information, and then run the commands: + + ```bash + + git config --global user.name "Julie Miller" + + ``` + + ```bash + + git config --global user.email julie.miller@example.com + + ``` + +## Exercise scenario + +You're a software developer working for a consulting firm. Your clients need help with removing hard-coded secrets from legacy applications. You plan to use GitHub Secret Scanning, GitHub Push Protection, and GitHub Copilot to detect and remediate hard-coded secrets. You use Visual Studio Code as your development environment and GitHub Copilot to assist with development tasks. You're assigned to the following legacy app: + +- ContosoOrderProcessor: An e-commerce app that provides an order processing workflow. The workflow includes customer validation, payment processing, email notifications, and database operations. The code contains hard-coded secrets that need to be managed in a secure manner. + +This exercise includes the following tasks: + +1. Import the ContosoOrderProcessor repository to your GitHub account. +1. Review security alerts on GitHub. +1. Review the code project in Visual Studio Code. +1. Configure environment variables and run the application. +1. Use GitHub Copilot's Ask mode to analyze secret scanning alerts. +1. Use GitHub Copilot's Agent mode to remediate secret scanning alerts. +1. Push changes to GitHub and close secret scanning alerts. +1. Test the GitHub Push protection feature. + +### Import the ContosoOrderProcessor Repository to your GitHub account + +GitHub Importer allows you to create a copy of an existing repository in your own GitHub account, giving you full control over the imported copy. + +In this task, you use your GitHub account to import the ContosoOrderProcessor repository. + +Use the following steps to complete this task: + +1. Open a browser window and navigate to GitHub.com. + +1. Sign in to your GitHub account, and then open your repositories tab. + + You can open your repositories tab by clicking on your profile icon in the top-right corner, then selecting **Repositories**. + +1. On the Repositories tab, select the **New** button. + +1. Under the **Create a new repository** section, select **Import a repository**. + +1. On the **Import your project to GitHub** page, under **Your source repository details**, enter the following URL for the source repository: + + ```plaintext + https://github.com/MicrosoftLearning/resolve-github-security-alerts-lab-project + ``` + +1. Under the **Your new repository details** section, in the **Owner** dropdown, select your GitHub username. + +1. Enter **ResolveGitHubSecurityAlerts** in the **Repository name** field. + + GitHub automatically checks the availability of the repository name. If this name is already taken, append a unique suffix (for example, your initials or a random number) to the repository name to make it unique. + +1. Ensure that the repository is set to **Public**. + + Secret Scanning is enabled by default for public repositories. + +1. Select the **Begin import** button. + + GitHub uses the import process to create the new repository in your account. + + > **NOTE**: It can take a minute or two for the import process to finish. + +1. Wait for the import process to complete, and then open the **ResolveGitHubSecurityAlerts** repository. + + The ResolveGitHubSecurityAlerts repository contains the ContosoOrderProcessor application. Hard-coded secrets are included for training purposes. + +### Review security alerts on GitHub + +GitHub's secret scanning feature detects API keys, tokens, passwords, and other secrets that are accidentally committed to a repository. Security alerts are generated when secrets are detected, providing information about the type of secret, its location in the code, and recommendations for remediation. + +In this task, you examine the code repository and review the security alerts generated by GitHub secret scanning. + +Use the following steps to complete this task: + +1. Take a minute to review the repository's README.md file. + + The README.md file provides an overview of the ContosoOrderProcessor application, including a description of the intentionally exposed secrets and instructions for running the application. + + > **IMPORTANT**: Notice that you need to set up environment variables before running the application. + +1. Select the **Settings** tab. + +1. In the left sidebar, select **Advanced Security**. + + The Advanced Security page displays various security features that you can enable for your repository. + +1. Scroll to the bottom of the page, and then verify that **Secret Protection** and **Push protection** are enabled. + + The **Disable** buttons, which can be used to turn off the features, show that the two features are currently enabled. + + > **NOTE**: For public repositories, secret scanning (Secret Protection and Push protection) are enabled by default. For private repositories, you must enable secret scanning manually. + +1. At the top of the page, select the **Security** tab. + + The Security tab displays a security overview for your repository, including any security advisories, Dependabot alerts, code scanning alerts, and secret scanning alerts. + + You can find the Security tab at the top of the repository page (alongside Code, Issues, Pull Requests, etc.). + +1. In the left sidebar, under the **Vulnerability alerts** section, select **Secret scanning**. + + The Secret scanning alerts page displays the security alerts for secrets detected in your repository. Each alert includes information about the type of secret, the file and line number where the secret was found, and the status of the alert. + +1. Take a minute to review the Secret scanning alerts page. + + You should see a list that includes the following alerts: + + - ✓ Square Access Token - PaymentService.cs. + - ✓ Stripe API Key - PaymentService.cs. + - ✓ Slack Incoming Webhook URL - AppConfig.cs. + - ✓ Slack API Token - AppConfig.cs. + - ✓ Mailgun API Key - EmailService.cs. + - etc. + + > **NOTE**: GitHub's secret scanning feature uses pattern matching to detect secrets in your codebase. The alerts you see are based on the secrets that were intentionally included in the ContosoOrderProcessor application for training purposes. + +1. To view one of the alerts, select the **Mailgun API Key** alert. + + The alert details page provides information about the secret, including: + + - The exposed secret. + - Remediation steps. + - The file path where the secret was found. + - A code snippet showing the secret in context. + - The commit that introduced the secret. + +1. Take a minute to review the code snippet shown in the alert. + + Notice that the alert points to the EmailService.cs file (line 19). The code exposes a Mailgun API key. + +1. Navigate back to the Secret scanning alerts page. + +1. Select the **Stripe API Key** alert and review the details. + + Notice that this alert points to the PaymentService.cs file (line 24). The code exposes a Stripe live API key. + +1. Navigate back to the Secret scanning alerts page and quickly review the other alerts. + + As you review the alerts, notice the following details: + + - All of the alerts are currently in the "Open" status, indicating they need to be addressed. + - Each alert identifies the file and line number where the secret was found. + - The identified secrets are located in the following three files: + - AppConfig.cs + - EmailService.cs + - PaymentService.cs + - The alerts can be assigned to team members for resolution. + +### Review the code project in Visual Studio Code + +The ContosoOrderProcessor application is a C# console application that simulates an e-commerce order processing workflow. Hard-coded secrets are included in the code for training purposes, along with comments that indicate where the secrets are located. Other secrets are loaded securely from environment variables. + +In this task, you clone the repository to your local development environment and review the project in Visual Studio Code. + +Use the following steps to complete this task: + +1. Navigate back to the root page of your repository. + + The root page displays the list of files and folders in the repository. + +1. Clone the ResolveGitHubSecurityAlerts repository to your local development environment. + + For example: + + 1. Open the **Code** button dropdown. + + 1. Select the **Copy URL to clipboard** icon to copy the repository URL. + + 1. Open a terminal or command prompt. + + 1. Navigate to the directory where you want to clone the repository. + + 1. Run the following command to clone the repository: + + ```bash + git clone https://github.com//ResolveGitHubSecurityAlerts.git + ``` + + Replace **``** with your GitHub username. + +1. Open the cloned repository in Visual Studio Code. + + For example: + + 1. Launch Visual Studio Code. + + 1. On the **File** menu, select **Open Folder**. + + 1. Navigate to the directory where you cloned the repository. + + 1. Select the **ResolveGitHubSecurityAlerts** folder and then select **Select Folder**. + +1. Ensure that you're using the latest version of Visual Studio Code and that you have GitHub Copilot installed and enabled. + + You can verify that Visual Studio Code is up to date by selecting the Manage icon (gear icon) in the lower-left corner of the Visual Studio Code window, then selecting **Check for Updates**. + + You can verify that GitHub Copilot is enabled by selecting the Copilot icon in lower-right corner of the Visual Studio Code window, or by opening the Chat view and ensuring that the Chat features are active. + +1. Use Visual Studio Code's EXPLORER view to expand the ContosoOrderProcessor folder, and then take a minute to review the project structure. + + The ContosoOrderProcessor application follows a simple layered architecture. It includes the following files and folders: + + - **Configuration**: Contains the AppConfig.cs file with application-wide configuration constants (including exposed secrets). + - **Models**: Contains the Customer.cs and Order.cs model classes. + - **Security**: Contains the SecurityValidator.cs file with security validation logic. + - **Services**: Contains service classes for database access, email sending, and payment processing (DatabaseService.cs, EmailService.cs, PaymentService.cs). + - **Program.cs**: The main entry point that demonstrates the order processing workflow. + +1. Open the **Program.cs** file and take a minute to review the code. + + Notice that the Main method performs the following tasks: + + 1. First, the Main method builds and validates a configuration object. + + Notice the ValidateRequiredConfiguration method at the bottom of the Program.cs file. This method validates that all required configuration values are present before the application starts. It checks for configuration data in appsettings.json and secrets in environment variables. If any are missing, it displays a formatted error message listing the missing items, instructs the user to run a setup script, and exits the application with an error code. + + > **NOTE**: The application terminates early if the configuration isn't validated successfully. For example, if the environment variables haven't been set. This prevents the application from running with incomplete or incorrect settings. + + 1. Second, after the configuration is validated, the Main method simulates an order processing workflow that includes the following tasks: + + - Customer retrieval. + - Customer validation. + - Order creation. + - Order validation. + - Fraud detection. + - Payment processing. + - Database persistence. + - Email notification. + - Shipping notification. + +1. Use Visual Studio Code's EXPLORER view to expand the **Configuration** folder. + +1. Open the **AppConfig.cs** file and take a minute to review the code. + + AppConfig.cs is a static configuration class that manages all application settings, credentials, and feature flags for ContosoOrderProcessor. It implements a dual-source configuration pattern where some secrets are loaded securely from IConfiguration (environment variables) with fallback chains, while others remain intentionally hard-coded as public constants for training purposes. The class provides utility methods for initialization, configuration validation, and credential retrieval. + +1. Expand the **Services** folder, and then take a couple minutes to review the following files: + + - **PaymentService.cs**: This class handles payment processing using payment gateways. It contains hard-coded secrets for Stripe and Square payment providers. + - **EmailService.cs**: This class manages email notifications using the Mailgun email service. It contains a hard-coded Mailgun API key and SMTP credentials. + +> **NOTE**: Hard-coded secrets are included in the application for training purposes only. In a real-world application, secrets should never be hard-coded. + +### Configure environment variables and run the application + +The ContosoOrderProcessor application verifies that all required configuration values, including environment variables, are present before running an order processing workflow. If the environment variables aren't set, the application displays an error message and exits. + +Running the application and observing the console output helps you understand the intended behavior of the application before you remediate the secret scanning alerts. + +> **NOTE**: Although environment variables can be used to manage secrets, they have limitations in terms of security and scalability. Azure Key Vault and other secret management solutions are recommended for production deployments. + +In this task, you create a PowerShell script that sets up environment variables, run the application, and record the console output. + +Use the following steps to complete this task: + +1. Use Visual Studio Code's EXPLORER view to create a file named **setup-secrets.ps1** at the root of your project. + + The script file is used to set environment variables for the current PowerShell session. + +1. Add the following PowerShell code to the **setup-secrets.ps1** file: + + ```powershell + # This script sets environment variables for the current PowerShell session only. + # + # These variables are required to run the application. + # + # IMPORTANT NOTES: + # • These variables are set for THIS PowerShell session only + # • Variables will be lost when you close this window + # • All secret values are FICTIONAL for training purposes only + # + Write-Host "════════════════════════════════════════════════════════" -ForegroundColor Cyan + Write-Host " Contoso Order Processor - Environment Setup Script " -ForegroundColor Cyan + Write-Host "════════════════════════════════════════════════════════" -ForegroundColor Cyan + Write-Host "" + + Write-Host "Setting environment variables for current session..." -ForegroundColor Yellow + Write-Host "" + + # AWS Credentials + $env:Aws__AccessKeyId = "AKIA1234567890EXAMPLE" + $env:Aws__SecretAccessKey = "1234567890abcdefghijklmnopqrstuvwxyzABCD" + Write-Host "✓ AWS credentials configured" -ForegroundColor Green + + # SendGrid API Key + $env:SendGrid__ApiKey = "SG.1234567890abcdefghij.1234567890abcdefghijklmnopqrstuvwxyzABCDEF" + Write-Host "✓ SendGrid API key configured" -ForegroundColor Green + + # PayPal Credentials + $env:PayPal__ClientId = "AY1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmno" + $env:PayPal__ClientSecret = "EJ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklm" + Write-Host "✓ PayPal credentials configured" -ForegroundColor Green + + # Azure Storage Connection String + $env:Azure__StorageConnectionString = "DefaultEndpointsProtocol=https;AccountName=contosostorageacct;AccountKey=TRAINING-PURPOSE-ONLY-NOT-A-REAL-KEY-1234567890abcdefghijklmnopqrstuvwxyzABCDEFGH==;EndpointSuffix=core.windows.net" + Write-Host "✓ Azure Storage connection string configured" -ForegroundColor Green + + # SQL Server Connection String + $env:Database__ConnectionString = "Server=tcp:contoso-orders.database.windows.net,1433;Initial Catalog=ContosoOrdersDB;User ID=orderadmin;Password=MyPassword123!;Encrypt=True;TrustServerCertificate=False;MultipleActiveResultSets=False;Connection Timeout=30;" + Write-Host "✓ SQL Server connection string configured" -ForegroundColor Green + + # GitHub Personal Access Token + $env:GitHub__PersonalAccessToken = "ghp_1234567890abcdefghijklmnopqrstuvwxyzAB" + Write-Host "✓ GitHub Personal Access Token configured" -ForegroundColor Green + + # npm Token + $env:Npm__Token = "npm_1234567890abcdefghijklmnopqrstuvwxy" + Write-Host "✓ npm Token configured" -ForegroundColor Green + + Write-Host "" + Write-Host "════════════════════════════════════════════════════════" -ForegroundColor Cyan + Write-Host "✓ All environment variables configured successfully!" -ForegroundColor Green + Write-Host "════════════════════════════════════════════════════════" -ForegroundColor Cyan + Write-Host "" + ``` + + This script sets environment variables for AWS credentials, SendGrid API key, PayPal credentials, Azure Storage connection string, SQL Server connection string, GitHub Personal Access Token, and npm token. All secret values are fictional and intended for training purposes only. + +1. Open Visual Studio Code's integrated terminal. + + On the top menu bar, select **Terminal**, and then select **New Terminal**. + +1. To configure environment variables, enter the following command in the terminal: + + ```powershell + . .\setup-secrets.ps1 + ``` + + The script should generate the following output: + + ```plaintext + ════════════════════════════════════════════════════════ + Contoso Order Processor - Environment Setup Script + ════════════════════════════════════════════════════════ + Setting environment variables for current session... + ✓ AWS credentials configured + ✓ SendGrid API key configured + ✓ PayPal credentials configured + ✓ Azure Storage connection string configured + ✓ SQL Server connection string configured + ✓ GitHub Personal Access Token configured + ✓ npm Token configured + ════════════════════════════════════════════════════════ + ✓ All environment variables configured successfully! + ════════════════════════════════════════════════════════ + ``` + + > **NOTE**: The environment variables are temporary and only exist in the current PowerShell window. They're lost when the terminal closes. Environment variables are used to simplify the training environment. A more secure approach would involve using Azure Key Vault or other secure secret management solutions. + +1. To run the application, enter the following commands in the terminal: + + ```powershell + cd ContosoOrderProcessor + dotnet run + ``` + + The application validates the configuration and then runs a simulated workflow. + + The steps of the order processing workflow include retrieving and validating customer information, creating an order, and performing security checks such as order validation and fraud risk assessment. If the order passes these checks, the workflow processes the payment, saves the order to the database, logs the transaction, and sends confirmation and shipping notification emails to the customer. Throughout, the workflow provides detailed console output for each step and handles errors gracefully, ensuring that missing configuration or failed operations are clearly reported. + +1. Take a minute to review the console output generated by the ContosoOrderProcessor application. + + The application logs the entire workflow process, including configuration details and processing steps. Notice that some messages expose secrets. For example, you can see connection strings, API keys, and other sensitive information being logged to the console. + +1. Add a file named **OriginalConsoleOutput.txt** to the root of your project. + + You can create the file by right-clicking in the EXPLORER view, selecting **New File**, naming it **OriginalConsoleOutput.txt**. + +1. Copy the console output to the **OriginalConsoleOutput.txt** file. + + You compare this original console output with the final console output after remediating the secret scanning alerts. + +### Use GitHub Copilot's Ask mode to analyze secret scanning alerts + +GitHub Copilot's Ask mode provides intelligent code analysis that can help you understand security vulnerabilities, their potential impacts, and suggested remediation strategies. By analyzing the code that contains exposed secrets, you can develop a comprehensive understanding of the problems before implementing fixes. + +In this task, you use GitHub Copilot's Ask mode to analyze the hard-coded secrets in the ContosoOrderProcessor application. You review the security risks associated with the exposed secrets and gather remediation suggestions. + +Use the following steps to complete this task: + +1. Ensure that GitHub Copilot's Chat view is open in Visual Studio Code. + + To open the Chat view, select the Toggle Chat button at the top of the Visual Studio Code window, or press `Ctrl+Alt+I` (Windows/Linux) or `Cmd+Option+I` (Mac). + +1. Ensure that you're starting with a new/clean Chat session. + + If you analyzed or updated the codebase in the current Chat session, you might want to start fresh. Select the **New Chat** button (the **+** icon at the top of the Chat panel) to begin a new conversation. A new Chat session ensures that GitHub Copilot has no prior context that could influence its analysis. + +1. Use the Set Agent button to select **Ask** mode, and then use the Pick Model button to select the **Auto** model. + +1. Open the **Services/PaymentService.cs** file in the code editor. + +1. Ask GitHub Copilot to identify the secrets exposed in the PaymentService class, analyze the secrets, and explain the security risks. + + For example, enter the following prompt in the Chat: + + ```plaintext + Analyze the PaymentService class. Identify secrets exposed in the code. Analyze the secret types and providers. Explain the security risks associated with the identified secrets. + ``` + +1. Take a minute to review GitHub Copilot's analysis. + + GitHub Copilot should provide a detailed analysis that identifies hard-coded secrets in the PaymentService class. The secrets should include a Stripe API Key and a Square Access Token. The explanation should describe the types of secrets, their providers, the locations in the code, and the associated security risks. + + The analysis should be similar to the following example: + + ```md + The `PaymentService` class exposes two hard-coded secrets: + + 1. **Stripe API Key** + - **Type:** API Key + - **Provider:** Stripe + - **Location:** `private const string StripeApiKey = "sk_live_51MqxYzABC123def456GHI789jklMNO012pqrSTU345vwxYZ678abcDEF901ghiJKL234mnoPQR567stuVWX890yzABC";` + - **Risk:** Anyone with access to this key can make authenticated requests to Stripe, potentially initiating fraudulent transactions, accessing payment data, or causing financial loss. + + 2. **Square Access Token** + - **Type:** Access Token + - **Provider:** Square + - **Location:** `private const string SquareAccessToken = "EAAAEOuLQavTvyym5PByGZrGdRLWiL_RB0n8YF0gELTxFqLhNp6bKHRhA6P7Uv5F";` + - **Risk:** Exposure allows attackers to access Square APIs, potentially viewing sensitive transaction data, issuing refunds, or manipulating payment flows. + + **Security Risks:** + - **Credential Leakage:** Hard-coded secrets are easily discoverable in source code, especially if the repository is public or shared. + - **Unauthorized Access:** Attackers can use these credentials to impersonate your service, access customer data, or perform malicious transactions. + - **Compliance Violations:** Storing secrets in code might violate PCI DSS, GDPR, or other regulatory requirements. + - **Lack of Rotation:** Hard-coded secrets are difficult to rotate, increasing the risk if they are ever compromised. + + **Best Practices:** + - Remove hard-coded secrets from code. + - Store secrets in secure configuration (environment variables, secret managers). + - Audit and rotate exposed credentials immediately. + + You can find these exposed secrets in `PaymentService.StripeApiKey` and `PaymentService.SquareAccessToken`. + ``` + +1. Ask GitHub Copilot for specific remediation guidance for the Stripe API key. + + For example, enter the following prompt in the Chat: + + ```plaintext + What's the best practice for storing API keys like Stripe in a C# application? How should I refactor this code to remove the hard-coded Stripe API key? + ``` + +1. Take a minute to review GitHub Copilot's remediation suggestions. + + GitHub Copilot should recommend best practices for securely storing API keys, such as using environment variables or secret management services. It should also provide code snippets demonstrating how to refactor the PaymentService class to read the Stripe API Key from a secure location instead of hard-coding the secret. + + GitHub Copilot will use the Ask mode conversation to inform the remediation steps you take in the next task using Agent mode. You can also take notes on GitHub Copilot's recommendations. Your insights can guide remediation. + +1. Ask GitHub Copilot to suggest remediation strategies for the Square access token. + + For example, enter the following prompt in the Chat: + + ```plaintext + Suggest remediation strategies for the Square access token exposed in the PaymentService class. How can I securely manage this secret? Should I use the same approach to manage both the Square access token and the Stripe API key? + ``` + +1. Take a minute to review GitHub Copilot's remediation suggestions. + + You should see that GitHub Copilot recommends using a consistent strategy for managing both the Square access token and the Stripe API key. GitHub Copilot suggests using environment variables or secret management services. + +1. Open the **Services/EmailService.cs** file in the code editor. + +1. Ask GitHub Copilot to analyze the EmailService class, identify secrets, and suggest remediation strategies. + + For example, enter the following prompt in the Chat: + + ```plaintext + Analyze the EmailService class. Identify secrets exposed in the code. Suggest remediation strategies for securely managing secrets. + ``` + +1. Take a minute to review GitHub Copilot's remediation suggestions. + + You should see that GitHub Copilot identifies the hard-coded Mailgun API key and some SMTP credentials that weren't recognized by GitHub Secret Scanning. Secret scanning tools might not detect all types of secrets, especially if they don't match known patterns. + + GitHub Copilot recommends using environment variables or secret management services to securely manage the secret. + +1. Ask GitHub Copilot why GitHub Secret Scanning didn't detect the SMTP credentials. + + For example, enter the following prompt in the Chat: + + ```plaintext + Why didn't GitHub Secret Scanning detect the SMTP credentials in the EmailService class? Explain the limitations of secret scanning tools. + ``` + +1. Review GitHub Copilot's response. + + GitHub Copilot should explain that secret scanning tools rely on pattern matching and might not detect all types of secrets, especially if they don't match known patterns or formats. It should also highlight the importance of using multiple layers of security, including code reviews and static analysis tools, to identify potential vulnerabilities. + +1. Open the **Configuration/AppConfig.cs** file in the code editor. + +1. Ask GitHub Copilot to review the AppConfig.cs file and then suggest the remediation strategy that should be used to manage all hard-coded secrets in the ContosoOrderProcessor application. + + For example, enter the following prompt in the Chat: + + ```plaintext + Review the AppConfig class and identify exposed secrets. Consider the secrets previously identified in the PaymentService and EmailService classes. Should the application's existing approach for securely managing secrets be extended for all secrets in the ContosoOrderProcessor application? Explain your reasoning and suggest a remediation strategy. + ``` + +1. Take a minute to review GitHub Copilot's remediation suggestions. + + You should see that GitHub Copilot recommends extending the existing approach of using environment variables for securely managing all secrets in the ContosoOrderProcessor application (or using a secrets manager like Azure Key Vault). It should explain that this approach centralizes secret management, reduces the risk of exposure, and aligns with best practices for secure application development. + +### Use GitHub Copilot's Agent mode to remediate secret scanning alerts + +GitHub Copilot's Agent mode can help you implement security fixes by directly replacing hard-coded secrets with secure alternatives in your code files. The Agent mode goes beyond analysis to actively edit code files using security best practices. + +In this task, you use GitHub Copilot's Agent mode to remediate some of the secret scanning alerts that you analyzed in the previous task. You apply the remediation strategy identified during your Ask mode analysis. You intentionally leave some secrets unfixed to test GitHub's Push Protection feature. + +Use the following steps to complete this task: + +1. In Visual Studio Code, close any files that are open in the editor. + + Agent mode reviews the codebase and the assigned task to establish context and identify areas that require remediation. + +1. In the Chat view, switch to GitHub Copilot's **Agent** mode. + + In the lower-left corner of the Chat view, use the Set Agent dropdown to select **Agent** mode. + +1. Assign a task to GitHub Copilot that remediates the security alerts associated with the PaymentService class. + + For example, enter the following prompt in the Chat: + + ```plaintext + Review the current conversation and then review the PaymentService.cs file. Notice how some secrets are managed using environment variables and others are hard-coded. I need you to remove hard-coded secrets from the PaymentService class and implement environment variables that securely manage the secrets. Update the setup-secrets.ps1 script for the new environment variables, and ensure that the formatting matches the existing environment variable declarations. Ensure that the suggested updates don't introduce errors and run a build task to verify the app builds correctly after the updates. + ``` + +1. Run the task and monitor the agent's progress. + + The agent should begin analyzing the PaymentService.cs file and proposing code changes to remove the hard-coded secrets. Progress is reported in the Chat view. GitHub Copilot Agent suggests code updates directly within the code files. + + > **NOTE**: The agent might ask for permission to access certain files or perform specific actions. Grant permission as needed to allow the agent to complete the task. + +1. Take a minute to review the changes proposed by GitHub Copilot Agent. + + Review the PaymentService.cs and setup-secrets.ps1 files in the editor. + + You can review each edit individually in the code editor. You can scroll through the edits manually, or use the Chat Edits navigation bar to move up and down through the proposed changes. The edits should align with the remediation strategy identified during your Ask mode analysis. + +1. Apply the changes and save the updated files. + + Always review the edits suggested by GitHub Copilot. + + If the proposed changes look correct and match the remediation strategy from your analysis, select **Keep** to apply the edits to your PaymentService.cs and setup-secrets.ps1 files. You can use the **Keep** button in the Chat view accept all proposed changes (all files). + + After accepting the edits, you should see that the hard-coded secrets are removed from the source code and the secrets are securely managed using environment variables. + + > **NOTE**: If you notice any issues with the proposed changes, you can select **Undo** to reject the edits. You can also manually modify the code as needed. If you accept the changes and later find issues, you can use the Chat view's **Undo Last Request** feature to revert the most recent changes. You can also use Visual Studio Code's undo features to revert code changes. + +1. Assign a task to GitHub Copilot that remediates the security alerts associated with the EmailService class. + + For example, enter the following prompt in the Chat: + + ```plaintext + Now review the EmailService.cs file. Remove hard-coded Mailgun and SMTP secrets from the EmailService class and implement environment variables that securely manage the secrets. Update the setup-secrets.ps1 script for the new environment variables, and ensure that the formatting matches the existing environment variable declarations. Ensure that the suggested updates don't introduce errors and run a build task to verify the app builds correctly after the updates. + ``` + +1. Run the task and monitor the agent's progress. + + The agent should begin analyzing the EmailService.cs file and proposing code changes to remove the hard-coded secrets. If the agent asks for assistance or permission to access certain files, grant permission as needed to allow the agent to complete the task. + +1. Take a minute to review the changes proposed and then accept the updates. + + The edits should align with the remediation strategy identified during your Ask mode analysis. If the proposed changes look correct and match the remediation strategy from your analysis, select **Keep** to apply the edits to your EmailService.cs and setup-secrets.ps1 files. The hard-coded secrets should now be removed from the source code and securely managed using environment variables. + +1. Assign a task to GitHub Copilot that updates the code used to validate the configuration settings in Program.cs and AppConfig.cs. + + For example, enter the following prompt in the Chat: + + ```plaintext + I need you to ensure that the app's configuration is validated correctly and that the app runs as expected. Review the Program.cs and AppConfig.cs files. Update the ValidateRequiredConfiguration method to include validation for all environment variables before starting the workflow. Update the ValidateRequiredConfiguration method to include error messages for any missing environment variable. Update console logging in Program.cs to use safe placeholder values for secrets. Also, update the AppConfig class to include static properties for the new secrets, loaded from configuration/environment variables. The validation in AppConfig.cs should also be updated to check all environment variables. Ensure that the suggested updates don't introduce errors and run a build task to verify the app builds correctly after the updates. + ``` + +1. Run the task and monitor the agent's progress. + + The agent should begin analyzing the Program.cs and AppConfig.cs files and proposing code changes to update the configuration validation logic. Progress is reported in the Chat view. Code edits appear in the code editor as they're proposed by GitHub Copilot. + + > **NOTE**: The agent might not be able to run the setup script (setup-secrets.ps1) and the application in the same PowerShell session. It should be able to run the application (without the setup script) to demonstrate the validation logic. + +1. Take a minute to review and apply the proposed changes, and then save the updated files. + + The Program.cs file should be updated to validate the new environment variables that were added to manage the secrets. The error message displayed when configuration validation fails should also be updated to include any missing secrets. + + The AppConfig.cs file should be updated to include static properties for the new secrets, loaded from configuration/environment variables. Adding the properties provides centralized, maintainable access to the secrets. Updates to AppConfig.cs enables proper validation, easier configuration management, and improved security practices. + + If the proposed changes look correct and match the remediation strategy from your analysis, select **Keep** to apply the edits. Save the files. + +1. To verify that the application runs successfully after the changes, enter the following command in the terminal: + + Open the integrated terminal, and then run the following commands: + + ```powershell + . .\setup-secrets.ps1 + cd ContosoOrderProcessor + dotnet run + ``` + + The application should validate the configuration and run the simulated order processing workflow without errors. + +1. Save the console output to a file named **RemediatedConsoleOutput.txt** at the root of your project. + + You can create the file by right-clicking in the EXPLORER view, selecting **New File**, naming it **RemediatedConsoleOutput.txt**, and then copying the console output into the file. + +1. Compare the **OriginalConsoleOutput.txt** and **RemediatedConsoleOutput.txt** files to verify that the application behavior remains consistent after remediating the secret scanning alerts. + + The console output should show that the application runs successfully both before and after the remediation, with no errors related to missing configuration or secrets. + + The RemediatedConsoleOutput.txt file shouldn't display the remediated secrets. + +1. Ask the Agent to compare the two console output files and verify that the application behavior remains consistent after remediating the secret scanning alerts. + + For example, enter the following prompt in the Chat: + + ```plaintext + Compare the OriginalConsoleOutput.txt and RemediatedConsoleOutput.txt files. Verify that the application behavior remains consistent after remediating the secret scanning alerts. Highlight any differences in behavior or output. + ``` + +1. Review GitHub Copilot's comparison of the two console output files. + + GitHub Copilot should confirm that the application behavior remains consistent after remediating the secret scanning alerts. It should highlight that the order processing workflow executed successfully in both cases, with no errors related to missing configuration or secrets. Any differences in output should be related to the removal of sensitive information from the console logs. + +1. Delete the two output files and the setup-secrets.ps1 script file. + + These files were only needed to run and verify the application before and after remediating the secret scanning alerts. They should be removed to keep the repository clean. + +### Push changes to GitHub and close secret scanning alerts + +After removing hard-coded secrets, you need to commit and push your changes to GitHub. These actions allow you to observe how GitHub updates the security alerts based on the remediated code. + +In this task, you commit and push your code updates, then review the process for closing GitHub secret scanning security alerts. + +Use the following steps to complete this task: + +1. Open the Source Control view in Visual Studio Code. + + Select the Source Control icon from the Activity Bar on the left side, or press `Ctrl+Shift+G` (Windows/Linux) or `Cmd+Shift+G` (Mac). + +1. Take a minute to review the file changes. + + You should see the modified files (Program.cs, AppConfig.cs, EmailService.cs, and PaymentService.cs) listed under **Changes**. AppConfig.cs still includes some hard-coded secrets that you intentionally left unfixed to test Push Protection. + +1. Create a commit message and the stage and commit the changes. + + Try using the AI-automated **Generate Commit Message** feature or enter a custom commit message. + + You can also enter your own commit message. For example: + + ```plaintext + Replace hard-coded secrets with environment variables in PaymentService and EmailService classes. Update configuration validation in Program.cs and AppConfig.cs. + ``` + + Once the commit message is created, select the **Commit** button (checkmark icon). If prompted to stage the changes, select **Yes**. + +1. Push (or Sync) the changes to GitHub. + + Select the **Sync Changes** button or the **Push** option in the Source Control view. If you're using the command line, you can run: + + ```bash + git push origin main + ``` + + > **NOTE**: If Push Protection is already enabled for your repository, you won't be blocked because you've removed secrets from the code rather than adding them. + +1. Wait for the push/sync to complete. + +1. Open your repository on GitHub. + +1. Open the Security tab. + + Select the **Security** tab at the top of your repository page. + +1. Select **Secret scanning** from the left sidebar. + +1. Notice that Secret Scanning still flags the same secrets. + + Secret Scanning continues to flag secrets in past commits, even after removing them in the most recent commit. This behavior is by design to ensure that exposed secrets are properly remediated. + + Here are best practices for managing the Security alerts: + + 1. Review the Alert Details: Open the security alert to confirm which secrets were detected and verify they're no longer present in the most recent commits. + + 1. Invalidate the Secret (If Not Already Done): If the exposed secrets were for sensitive systems (API keys, credentials, etc.), make sure you rotate or invalidate them. Removing the secret from code doesn't protect you if it is compromised. + + 1. Document Your Actions: Include a comment in the alert that describes the remediation (including the commit SHA and date). This helps with auditing and future reference. + + 1. Suppress or Dismiss the Alert: If the secret is fully remediated (removed and rotated), you can dismiss the security alert in GitHub. Select an appropriate reason (such as "Revoked" or "Used in test") to help others understand the context. + + 1. Protect Your History (Optional): For public repositories or in highly sensitive cases, you might consider rewriting your git history to delete the secret entirely. **Important**: rewriting your git history will force contributors to reclone and can disrupt forks. + + 1. Monitor for Future Leaks: Keep Secret Scanning enabled on the repo. Consider enabling push protection to prevent future accidental commits of secrets. + + Summary: Suppress/dismiss the alert only after removing and rotating the secret. Document your response and, if possible, communicate to your team. Be aware: even dismissed secrets remain visible in past commits unless you rewrite history. + +1. Close the secret scanning alerts for the remediated secrets. + + Use the following steps to close each of the remediated alerts: + + 1. Open an alert and review the details. + + 1. Open the associated code file and verify that the secrets have been removed from the code in the current commit. + + 1. Go back to the alert, select **Close as**, and then select appropriate reason (such as **Revoked** or **Used in test**). The selected reason should help others understand the context. + + 1. Enter a comment summarizing the remediation steps taken. Include the commit number and date. + + 1. Select **Close alert**. + + The Slack and Twilio secrets weren't remediated. Leave those alerts open to test GitHub's Push Protection feature in the next task. + +### Test the GitHub Push protection feature + +GitHub's Push Protection feature prevents secrets from being accidentally pushed to your repository. When enabled, it scans commits for known secret patterns and blocks the push if secrets are detected, giving you a chance to remove them before they enter the repository history. + +In this task, you enable Push Protection for your repository and test it by attempting to push a commit containing a dummy secret. + +Use the following steps to complete this task: + +1. Return to the ContosoOrderProcessor app in Visual Studio Code. + +1. Open the **Configuration/AppConfig.cs** file in the code editor. + +1. Scroll down to find the declaration statement for the **SlackBotToken** constant. + +1. Change one digit of the assigned SlackBotToken to a different value. + + This action simulates a developer accidentally adding a new hard-coded secret to the code. + +1. Save the AppConfig.cs file. + +1. Open the SOURCE CONTROL view, and then stage and commit the change. + + For example, enter a commit message, select **Commit**, and then select **Yes** to stage the changes if prompted. + +1. Attempt to sync/push the commit. + + For example, select **Sync Changes** and then select **Ok** to push the commit. + +1. Notice that the GitHub Push protection feature blocks the push. + + A dialog box appears with the message "Can't push refs to remote". + +1. On the dialog box, select **Open Git Log**. + + The log should tell you that the push was rejected because secrets were detected. The error message lists the detected secret pattern and offer instructions on how to proceed. + +1. Use the SOURCE CONTROL view to amend the commit and revert the SlackBotToken change in AppConfig.cs. + + For example, use the SOURCE CONTROL view as follows: select **Undo Last Commit**, select **Unstage All Changes**, and then select **Discard Changes**. + +Push Protection prevents secrets from being pushed to your repository. In a real-world scenario, this feature would catch accidental commits of API keys, tokens, passwords, and other sensitive information before they become part of your repository history. + +## Clean up + +Now that you've finished the exercise, take a minute to ensure that you haven't made changes to your GitHub account or GitHub Copilot subscription that you don't want to keep. For example, you might want to delete the ResolveGitHubSecurityAlerts repository. If you're using a local PC as your lab environment, you can archive or delete the local clone of the repository created for this exercise. diff --git a/Instructions/Labs/LAB_AK_13_get-started-spec-driven-development.md b/Instructions/Labs/LAB_AK_13_get-started-spec-driven-development.md new file mode 100644 index 0000000..86795f6 --- /dev/null +++ b/Instructions/Labs/LAB_AK_13_get-started-spec-driven-development.md @@ -0,0 +1,672 @@ +--- +lab: + title: Exercise - Develop a greenfield application using GitHub Spec Kit + description: Learn how to install GitHub Spec Kit and how to use GitHub Spec Kit workflows to implement the spec-driven development methodology for a greenfield application. + duration: 60 minutes + level: 300 + islab: true + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Develop a greenfield application using GitHub Spec Kit + +GitHub Spec Kit is an open-source toolkit that enables Spec-Driven Development (SSD) by integrating specifications with AI coding assistants like GitHub Copilot. + +In this exercise, you learn how to use the GitHub Spec Kit to develop a new greenfield application. You begin by initializing the GitHub Spec Kit for a new .NET project. You then use GitHub Spec Kit workflows to create the constitution, specification, plan, and tasks documents for the new application. Finally, you use GitHub Spec Kit's implementation workflow to implement an initial MVP version of the application. + +This exercise takes approximately **60** minutes to complete. + +> **IMPORTANT**: To complete this exercise, you must provide your own GitHub account and GitHub Copilot subscription. If you don't have a GitHub account, you can sign up for a free individual account and use a GitHub Copilot Free plan to complete the exercise. If you have access to a GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscription from within your lab environment, you can use your existing GitHub Copilot subscription to complete this exercise. + +## Before you start + +Your lab environment MUST include the following resources: Git 2.48 or later, .NET SDK 8.0 or later, Visual Studio Code with the C# Dev Kit and GitHub Copilot Chat extensions, Python 3.11 or later, the uv package manager, Specify CLI, and access to a GitHub account with GitHub Copilot enabled. + +For help with configuring your lab environment, open the following link in a browser: Configure your GitHub Spec Kit lab environment. + +## Exercise scenario + +You're a software developer working for a consulting firm. Your firm is moving to a spec-driven development methodology using GitHub Spec Kit and GitHub Copilot in Visual Studio Code. You're asked to start using SDD and GitHub Spec Kit as soon as possible. + +One of your clients, Contoso Corporation, needs you to develop an initial MVP version for an RSS feed reader app. Contoso stakeholders documented the project goals, initial features, and technical requirements for the app. You'll use the stakeholder documents to generate the constitution, spec, plan, and tasks documents, then implement the initial MVP version of the application. Contoso has indicated that additional features will be requested after initial sign-off. + +Implementing the SDD methodology with GitHub Spec Kit ensures that the MVP app is delivered quickly, that it meets stakeholder requirements, and that new features can be rolled out seamlessly when needed. + +This exercise includes the following tasks: + +1. Create a project folder and initialize GitHub Spec Kit. +1. Generate the constitution using stakeholder documentation. +1. Generate the spec.md file using stakeholder documentation. +1. Generate the plan.md file using stakeholder documentation and spec.md. +1. Generate the tasks.md file using the spec.md, plan.md, and constitution.md. +1. Implement the tasks required for an MVP application. + +## Create a project folder and initialize GitHub Spec Kit + +The Specify CLI is used to initialize GitHub Spec Kit in a project folder. GitHub Spec Kit uses the project folder to store configuration files, templates, scripts, and agents that support the spec-driven development workflows. + +In this task, you create a new project folder and initialize GitHub Spec Kit in your project directory. + +Use the following steps to complete this task: + +1. Open a terminal window, and then navigate to the root of your C: drive. + + At the command prompt, to navigate to the root of your C: drive, enter the following command: + + ```powershell + cd C:\ + ``` + +1. To create a new folder named for your RSSFeedReader project, enter the following command: + + ```powershell + mkdir TrainingProjects\RSSFeedReader + ``` + +1. To navigate to the new project folder, enter the following command: + + ```powershell + cd TrainingProjects\RSSFeedReader + ``` + +1. To initialize GitHub Spec Kit in the current directory, enter the following command: + + ```powershell + specify init --here --integration copilot --script ps + ``` + + > **NOTE:** If you're using macOS or Linux with bash/zsh, replace `--script ps` with `--script sh`. + + This command specifies the following parameters: + + - `--here` - Initializes GitHub Spec Kit in the current directory (your existing RSSFeedReader project). + - `--integration copilot` - Configures the project to use GitHub Copilot as the AI assistant. + - `--script ps` - Specifies that PowerShell scripts will be used. + + The `specify init` command completes the following actions: + + - Creates agent prompt files in the `.github/agents/` and `.github/prompts/` directories. + - Creates template files in the `.specify/memory/` and `.specify/templates/` directories. + - Creates script files in the `.specify/scripts/powershell/` directory. + - Creates a settings.json file in the `.vscode/` directory. + - Displays a success message ("Project ready"). + - Suggests some optional next steps. + + When you use the `specify init` command for a brownfield project, it recognizes that the current directory isn't empty and asks for confirmation before proceeding. The command preserves any existing application files. + + **Troubleshooting**: If you encounter issues: + + - **"specify command not found"**: To ensure that you installed the Specify CLI, run `specify version`. + - **Permission denied errors**: On Windows, ensure you're running PowerShell with appropriate permissions. On macOS/Linux, check file permissions. + - **Git clone errors**: Verify that you're signed in to GitHub, and that you have access to your imported repository. + - **GitHub Spec Kit commands not appearing**: Ensure `.github/prompts/` exists in your workspace root. Try reloading Visual Studio Code. + +1. To open the RSSFeedReader project in Visual Studio Code, enter the following command: + + ```powershell + code . + ``` + + The `code .` command opens the current directory (RSSFeedReader) in Visual Studio Code. + +1. Use Visual Studio Code's EXPLORER view to expand the .github and .specify folders. + + You should see a folder structure that's similar to the following example: + + ```plaintext + RSSFEEDREADER (root) + ├── .github/ + │ ├── agents/ (GitHub Spec Kit executable workflows that can be triggered via commands) + │ └── prompts/ (GitHub Spec Kit prompt files that provide detailed instructions for each of the agent workflows) + ├── .specify/ (GitHub Spec Kit configuration) + │ ├── extensions/ (GitHub Spec Kit stores installed extension packages and their resources - commands, templates, hooks, and config - that add optional capabilities beyond the core Specify workflow.) + │ ├── integrations/ (GitHub Spec Kit stores the project’s active AI-agent integration state and manifests so Specify can install, switch, upgrade, or uninstall agent-specific command wiring safely.) + │ ├── memory/ (GitHub Spec Kit stores the project constitution defining core principles and governance rules that all features must follow) + │ ├── scripts/powershell/ (GitHub Spec Kit uses automation utilities (scripts) for creating features, setting up plans, and managing the specification workflow) + │ └── templates/ (GitHub Spec Kit provides standardized markdown formats for specs, plans, tasks, and checklists to ensure consistent documentation across all features) + └── .vscode/ (Visual Studio Code configuration) + ``` + +1. Ensure that GitHub Copilot's Chat view is open. + + GitHub Spec Kit works with GitHub Copilot through Visual Studio Code's chat interface. When you run "specify init --integration copilot" in your project directory, the toolkit configures your workspace to recognize "/speckit.*" commands. + +1. In the Chat view, to verify that GitHub Spec Kit commands are available, type **/speckit** + + You should see autocomplete suggestions that show the available commands. For example: + + - `/speckit.analyze` - Audit implementation plans. + - `/speckit.checklist` - Validate specification completeness. + - `/speckit.clarify` - Refine specifications through question and answer process. + - `/speckit.constitution` - Define project governing principles. + - `/speckit.implement` - Execute the implementation. + - `/speckit.plan` - Generate technical implementation plans. + - `/speckit.specify` - Create feature specifications. + - `/speckit.tasks` - Break down work into actionable tasks. + - `/speckit.taskstoissues` - Convert the tasks in tasks.md into GitHub issues. + + If the '/speckit.' commands don't appear, try closing and then reopening the project in Visual Studio Code. + + > **IMPORTANT**: This lab exercise was tested successfully using the GPT-5.2 and Claude Sonnet 4.5 models. Although both models were able to generate working applications, we did notice some differences. The Claude Sonnet 4.5 model tended to generate more detailed output. For example, the tasks.md file tended to have a larger number of tasks and phases. The Claude model's responses were consistent and performance was reliable. The GPT-5.2 model tended to generate less detailed output. For example, a shorter list of more broadly scoped tasks, organized under fewer phases. The GPT model was able to implement tasks successfully, but might have used extra iterations to resolve bugs. The GPT model's performance was generally good, but less consistent during our testing. For example, there were a couple times when the AI became unresponsive while processing a /speckit command. Restarting the command in the Chat view got things back on track quickly. Testing with older models, such as GPT-4.x and GPT-5 mini, often generated unexpected results. If possible, we suggest using newer language models that are optimized for complex reasoning when running GitHub Spec Kit commands. + +1. Publish your project to a new GitHub repository. + + Publishing the project to a GitHub repository isn't required at this point in the process, but GitHub Spec Kit will eventually require a Git repository and it doesn't hurt to set one up now. Having a repository enables you to track changes, collaborate with others, and use GitHub's features for issue tracking and project management. + + You can use the following steps to publish the project to a new GitHub repository: + + 1. Open Visual Studio Code's SOURCE CONTROL view. + + 1. Select **Publish Branch**. + + The repository name, RSSFeedReader, is suggested automatically based on the local folder name. + + 1. Select **Publish to GitHub private repository**. + + If prompted to sign in to GitHub, follow the sign-in process to authenticate your GitHub account. + + > **IMPORTANT**: If you're using the GitHub Copilot Free plan, you should publish to a **Public** repository to ensure that you have access to GitHub Copilot features. If you have a Pro, Pro+, Business, or Enterprise subscription, you can publish to a **Private** repository. + +1. Verify that the repository was created successfully. + + For example: + + Option 1: Open a browser and navigate to your GitHub profile page. You should see the new RSSFeedReader repository listed among your repositories. Refresh the page if necessary. + + Option 2: Open Visual Studio Code's notifications (bottom right). You should see a notification confirming that the repository was published successfully, along with a link to view it on GitHub. + +With the project folder created, GitHub Spec Kit initialized, and source control configured, you're ready to begin using GitHub Spec Kit to create the constitution, specification, plan, and tasks for the RSS Feed Reader application. + +## Generate the constitution using stakeholder documentation + +The constitution.md file defines policies, requirements, and technical standards that must be followed throughout the development process. + +GitHub Spec Kit includes several resources that help you create and maintain the constitution.md file: + +- The .specify/memory/constitution.md file contains a template for the constitution document. +- The .github/agents/speckit.constitution.agent.md file contains detailed instructions that are used to generate (or update) the constitution.md file. +- The .github/prompts/speckit.constitution.prompt.md file contains a "routing stub" that tells Copilot Chat to run the agent named speckit.constitution when the /speckit.constitution command is invoked. +- The /speckit.constitution command is used to generate a constitution.md file for the project. + +The initial version of the **constitution.md** file specifies that the constitution should include the following: + +- Project Name +- Section 1: Core Principles. The Core Principles section needs to include five core principles. Examples for the principles and their descriptions are provided in the template. +- Section 2: unnamed. Examples for the section name and content are provided in the template. +- Section 3: unnamed. Examples for the section name and content are provided in the template. +- Section 4: Governance. Examples for how the governance section is applied and governance rules are provided in the template. + +The **speckit.constitution.agent.md** file provides instructions for updating the constitution.md file based on the text or file input provided with the `/speckit.constitution` command. When no guidance is provided, the agent uses what it can find in the codebase to fill in the constitution template. + +The /speckit.constitution workflow uses text input, file input, and the codebase to collect the policies, standards, requirements, and guidelines that go into the constitution.md file. Providing detailed inputs helps to generate a more accurate and comprehensive constitution. + +In this task, you download the stakeholder documents for the RSSFeedReader project, evaluate their relationship to the GitHub Spec Kit commands, and then use the stakeholder documents to generate the constitution.md file. + +Use the following steps to complete this task: + +1. To download the stakeholder documents, open the following link in a browser: [RSSFeedReader - stakeholder documents](https://github.com/MicrosoftLearning/mslearn-github-copilot-dev/raw/refs/heads/main/DownloadableCodeProjects/Downloads/GHSpecKitEx13StakeholderDocuments.zip). + +1. Open the folder containing the downloaded ZIP file. + +1. Extract the contents of the downloaded ZIP file to a temporary folder, copy the files, and then paste them into the root folder of the RSSFeedReader project. + + The updated RSSFeedReader project should resemble the following example: + + ```plaintext + RSSFEEDREADER (root) + ├── .github/ + │ ├── agents/ (GitHub Spec Kit executable workflows that can be triggered via commands) + │ └── prompts/ (GitHub Spec Kit prompt files that provide detailed instructions for each of the agent workflows) + ├── .specify/ (GitHub Spec Kit configuration) + │ ├── memory/ (GitHub Spec Kit stores the project constitution defining core principles and governance rules that all features must follow) + │ ├── scripts/powershell/ (GitHub Spec Kit uses automation utilities (scripts) for creating features, setting up plans, and managing the specification workflow) + │ └── templates/ (GitHub Spec Kit provides standardized markdown formats for specs, plans, tasks, and checklists to ensure consistent documentation across all features) + ├── .vscode/ (Visual Studio Code configuration) + ├── StakeholderDocuments (folder containing stakeholder supplied documents) + └── README.md (a readme file describing the project documentation) + ``` + +1. In Visual Studio Code's EXPLORER view, expand the **StakeholderDocuments** folder. + + The StakeholderDocuments folder should include the following files: + + - **ProjectGoals.md** - High-level project goals, purpose, scope, delivery approach, rollout plan, quality goals, and standards/guidelines. + - **AppFeatures.md** - Detailed user-facing feature requirements. + - **TechStack.md** - Technology choices and architectural rationale. + + These documents include natural language descriptions of the project goals and constraints, app features, and technical requirements. Understanding this context is essential for creating an effective specification, plan, and tasks. The level of detail is typical of what you might find in preliminary documentation for many real-world projects. + + Project documentation and the details provided by the documents can vary greatly depending on company policies and project complexity. The GitHub Spec Kit commands are designed to work with any level of detail that's available, and use that information to create the constitution, spec, plan, and tasks documents required for a successful spec-driven development process. However, detailed inputs lead to more predictable results. + +1. In the Chat view, to start a constitution workflow using a combination of inline text and stakeholder documents, enter the following command: + + ```plaintext + /speckit.constitution --text "Code projects emphasize security, maintainability, and code quality. Ensure that all principles are specific, actionable, and relevant to the project context." --files StakeholderDocuments/ProjectGoals.md StakeholderDocuments/AppFeatures.md StakeholderDocuments/TechStack.md + ``` + +1. Monitor GitHub Copilot's response. + + It can take several minutes for GitHub Copilot to analyze the project requirements and then update the constitution.md file. + +1. Once GitHub Copilot is finished generating the constitution, take a minute to review the suggested edits. + + Notice that the constitution workflow extracts underlying principles from your inputs (both text and files) and uses that information to add details to the constitution. + + You should review the constitution to ensure it captures requirements accurately. This step is important when you're working in a production environment where the constitution represents your business requirements and technical governance. For a training exercise, this review is mainly to help you become familiar with the constitution content. + + Each principle should be clearly stated and actionable. For example: + + - ❌ Vague: "Apply security best practices." is too general. + - ✅ Clear: "All API endpoints MUST validate inputs before processing (URL format validation, length limits, null checks)." is specific and actionable. + + If critical requirements are missing or unclear, you can edit the constitution.md file directly to add or modify principles. + + In a production scenario, it's important to review the constitution against the following criteria: + + - Completeness: All major areas are covered. + - Clarity: Each principle is specific and unambiguous. + - Consistency: Principles don't contradict each other. + - Relevance: All principles relate to the RSSFeedReader project. + +1. If the /speckit.constitution workflow updated files in the **templates** folder, take a minute to review those updates as well. + +1. To accept the changes to all updated files, select the **Keep** button in the Chat view. + +1. Save and then close the updated files. + +1. Commit and push the updated files to your Git repository. + + For example: + + 1. Open Visual Studio Code's SOURCE CONTROL view. + 1. Enter a commit message like "Updated constitution using stakeholder requirements." + 1. Stage and commit the changes. + 1. Push the changes to your Git repository. + + You can verify the commit by checking your GitHub repository in the browser. The updated constitution.md file should now appear with your commit message. + +The constitution serves as a "contract" between business requirements and technical implementation, ensuring consistency throughout the spec-driven development process. When you use the GitHub Spec Kit to generate the spec, plan, and tasks, it references these principles to ensure the implementation aligns with specified requirements. + +## Generate the spec.md file using stakeholder documentation + +The specification (spec.md) defines what you're building from the user's perspective. It describes the features, user stories, acceptance criteria, and business requirements without prescribing how to implement them. A well-written spec serves as the foundation for creating the implementation plan and tasks. + +In this task, you use GitHub Copilot's `/speckit.specify` command to generate a detailed specification for the RSS Feed Reader based on the requirements provided by Contoso's business stakeholders. + +Use the following steps to complete this task: + +1. Use Visual Studio Code's EXPLORER view to examine the **spec-template.md** and **speckit.specify.agent.md** files. + + Notice the following things about these files: + + - The spec-template.md file defines the structure and sections of a specification document. It includes examples and/or placeholders for feature descriptions, user stories, acceptance criteria, and other relevant information. + - The speckit.specify.agent.md file provides detailed instructions for the /speckit.specify command. It guides GitHub Copilot on how to create a specification based on the provided requirements. + - The speckit.specify.agent.md file generates a repository branch at the beginning of the workflow. Creating a branch generally requires user permissions, so GitHub Copilot prompts for permission when the workflow is run. + +1. Use Visual Studio Code's EXPLORER view to examine the **ProjectGoals.md** and **AppFeatures.md** stakeholder documents. + + The AppFeatures.md file is your primary resource for user-facing feature requirements and provides the context needed to create a comprehensive specification. The ProjectGoals.md file provides information about the MVP and rollout plan that can also help to inform the specification. + +1. Create a summary description of the RSS Feed Reader app based on the stakeholder documents. + + The summary description should be concise (a sentence or two) and capture the core functionality of the RSS Feed Reader app. For example: + + "MVP RSS reader: a simple RSS/Atom feed reader that demonstrates the most basic capability (add subscriptions) without the complexity of a production-ready application." + +1. Close any files that you have open in the editor. + +1. Ensure that the Chat view is open. + + Notice that GitHub Copilot retains the context of previous interactions in the current chat session. If you generated the constitution.md file in the current session, GitHub Copilot provides a **Build Specification** button near the bottom of the Chat view that could be used to start generating the specification. In this case, you want to provide the requirements document explicitly, so you don't use the Build Specification button. + +1. In the Chat view, to start a specification workflow that generates a spec.md file using information from your stakeholders document, enter the following command: + + ```plaintext + /speckit.specify --text "MVP RSS reader: a simple RSS/Atom feed reader that demonstrates the most basic capability (add subscriptions) without the complexity of a production-ready application." --files StakeholderDocuments/ProjectGoals.md StakeholderDocuments/AppFeatures.md + ``` + + If you don't specify the `--text` option, you might be asked to provide a description of the app features before you can continue. + +1. Monitor GitHub Copilot's response and provide assistance as needed. + + > **IMPORTANT**: GitHub Copilot asks for assistance when generating the spec.md file. For example, GitHub Copilot requests permission to create a new branch for the repository. Grant permission when required by responding in the Chat view. + + It can take 4-6 minutes to create the spec.md file and the requirements checklist used to validate your specification. If the workflow process is inactive for more than 6 minutes without reporting successful completion, you can use GitHub Copilot's **retry** command to restart the workflow. + +1. Once the specify workflow is complete, use Visual Studio Code's EXPLORER view to expand the **specs** and **checklists** folders. + +1. In the EXPLORER view, select **spec.md**, and then take a couple minutes to review the spec.md file. + + The spec.md file is based on the template located in the **.specify/templates/spec-template.md** file. The updated spec.md file should include a detailed specification for the RSS Feed Reader app based on the stakeholder requirements that you provided. + + The specification should be clear, comprehensive, and well-structured. It should also provide a solid foundation for creating the technical plan and tasks. + + Ensure that the spec.md file includes the mandatory sections defined in the spec template. For example: + + - **User Scenarios & Testing**: User-focused descriptions of feature capabilities and how to test them. + - **Requirements**: Detailed requirements that must be met, organized by category. + - **Success Criteria**: Measurable outcomes, assumptions, and out-of-scope items. + +1. Verify that the User Stories (and Acceptance Scenarios) in the **spec.md** file are specific and testable: + + The acceptance scenarios should follow the **Given-When-Then** format. The scenarios should provide clear conditions for success or failure. For example: + + - ✅ Good: **Given** the application is running, **When** the user enters a valid RSS feed URL and submits it, **Then** the feed is added to their subscription list. + + - ✅ Good: **Given** the user enters an invalid URL (not a proper URI), **When** they try to submit it, **Then** they see an error message indicating the URL is malformed. + + - ❌ Avoid: Vague criteria like "Upload should work well" or "System should be fast". + +1. Verify that the Requirements section of the **spec.md** file includes key requirements from your stakeholder requirements document. + + For example, you should see requirements that are similar to the following example: + + - System MUST allow users to add a feed subscription by providing a feed URL. + +1. In the EXPLORER view, select **requirements.md**, and then take a minute to review the requirements.md file. + + Verify that no issues are reported in the **requirements.md** file. You should see that all checklist items passed successfully. + + > **NOTE**: The `/speckit.clarify` command can be used to identify ambiguities, gaps, and underspecified areas in your specification. In a production environment, it's recommended to run the clarification process after generating the initial specification to ensure all requirements are clear and complete before moving to the technical planning phase. For this lab exercise, you'll skip the clarification step. + +1. Accept the suggested file updates, and then save the **spec.md** and **requirements.md** files. + +1. Commit the specification files and publish the new branch to your Git repository. + + For example: + + Open Visual Studio Code's SOURCE CONTROL view, stage the changes, enter a commit message like "Add specification for the RSS Feed Reader app", and then publish the new branch to your Git repository. + +The specification defines the "what" without the "how." It doesn't specify programming languages, frameworks, database schemas, or code organization - those implementation details are determined in the Plan and Tasks phases based on the constitution's technical constraints. The spec focuses on user needs and business requirements, making it easier to review with nontechnical stakeholders. + +## Generate the plan.md file using stakeholder documentation and spec.md + +The technical plan bridges the gap between the "what" (specification) and the "how" (implementation). It defines the architecture, technology choices, data models, API designs, and implementation approach while adhering to the constraints defined in the constitution. + +In this task, you use GitHub Copilot's `/speckit.plan` command to generate a comprehensive technical implementation plan. + +Use the following steps to complete this task: + +1. Use Visual Studio Code's EXPLORER view to open the **plan-template.md** and **speckit.plan.agent.md** files. + +1. Take a minute to review the **plan-template.md** and **speckit.plan.agent.md** files. + + Notice the following: + + - The plan-template.md file defines the structure and sections of a technical plan document. + - The speckit.plan.agent.md file provides detailed instructions for the /speckit.plan command. It guides GitHub Copilot on how to create a technical plan based on the specification and constitution. + +1. Close any files that you have open in the editor. + +1. In the Chat view, to start a workflow that generates the plan.md file, enter the following command: + + ```dotnetcli + /speckit.plan --files StakeholderDocuments/ProjectGoals.md StakeholderDocuments/TechStack.md + ``` + +1. Monitor GitHub Copilot's response and provide assistance in the Chat view. + + GitHub Copilot analyzes the constitution.md, spec.md, and your stakeholder files to generate the plan. Provide permission and assistance when required. + + It can take 6-8 minutes for GitHub Copilot to generate the technical plan and associated markdown files. + +1. Once the plan workflow is complete, verify that the following files were added to the root of the **specs** folder: + + - **plan.md** + - **research.md** + - **quickstart.md** + - **data-model.md** + + You might also see one or more files listed under a **contracts** folder. + +1. Take a few minutes to review the **research.md**, **plan.md**, **quickstart.md**, and **data-model.md** files. + + - The research.md file captures research findings and technology decisions for the RSS Feed Reader app. + - The plan.md file outlines the technical implementation plan for the RSS Feed Reader app. + - The quickstart.md file provides setup instructions and a high-level overview of how to get started with the implementation. + - The data-model.md file defines the data entities, properties, and relationships needed for the RSS Feed Reader app. + + For a production scenario, you need to ensure that the plan provides a comprehensive description of the technical context and a clearly defined implementation strategy for the new app/features. The research, quickstart, and data model files should complement the plan by providing additional context and details. For this exercise, focus on becoming familiar with the content associated with each of the files. + +1. After reviewing the files, accept all of the suggested edits. + + If the plan omits important details or makes assumptions you disagree with, you can: + + - Edit the plan.md file directly, or + - Ask follow-up questions in GitHub Copilot Chat. + +1. Save the files, and then commit and sync your changes. + +The technical plan now serves as a blueprint for implementation. It translates business requirements into concrete technical decisions while respecting organizational constraints. + +## Generate the tasks.md file using the spec.md, plan.md, and constitution.md + +The tasks.md file breaks down the technical plan into specific, actionable implementation steps. Each task should be small enough to complete in a reasonable timeframe (typically a few hours to a day when implemented without AI assistance) and have clear acceptance criteria. + +In this task, you use the GitHub Spec Kit's `/speckit.tasks` command to generate a comprehensive tasks list and phased implementation plan. + +Use the following steps to complete this task: + +1. Use Visual Studio Code's EXPLORER view to open the **tasks-template.md** and **speckit.tasks.agent.md** files. + +1. Take a minute to review the **tasks-template.md** and **speckit.tasks.agent.md** files. + + Notice that the tasks-template.md file organizes tasks into logical phases, while the speckit.tasks.agent.md file describes the steps that the /speckit.tasks workflow should follow: + + - What inputs to read (spec.md, plan.md, etc.) + - What to produce (tasks.md) + - How to sequence the tasks (by phase, user story, etc.) + - How to define each task (specific, actionable, testable) + - What checks/gates to apply (coverage, ordering, scope) + +1. Close any files that you have open in the editor. + +1. In the Chat view, to start generating the tasks.md file, enter the following command: + + ```dotnetcli + /speckit.tasks + ``` + +1. Monitor GitHub Copilot's response and provide assistance in the Chat view. + + GitHub Copilot analyzes the spec.md and plan.md files and generates tasks in the tasks.md file. + + It can take 4-6 minutes for GitHub Copilot to generate the tasks.md file. Provide permission and assistance when required. + +1. Once the tasks workflow is complete, take a few minutes to review the **tasks.md** file. + + Verify that tasks are ordered logically by phase and user story. For example: + + - Setup and Foundation tasks come first. + - Backend API tasks build on the foundation. + - Frontend tasks reference backend endpoints. + - Testing tasks come after implementation. + - Deployment tasks come last. + + In a production scenario, you should also take the time to verify that every requirement (from spec.md) and every key design commitment (from plan.md) maps to at least one concrete task (usually several). For example: + + - Design commitments from the plan.md file should have corresponding implementation tasks. + - User story acceptance criteria should have corresponding implementation and verification tasks. + - Functional requirements should have corresponding implementation tasks. + - Security requirements should have corresponding implementation tasks. + - Performance requirements should have testing tasks. + +1. Accept the suggested file updates, and then save the **tasks.md** file. + +1. Commit the changes and then sync the updates. + +The tasks.md file now provides a clear roadmap for implementation. + +## Implement the tasks required for an MVP application + +With a clear specification, technical plan, and tasks document in place, you're ready to implement the RSS Feed Reader app. + +The tasks.md file provides a phased implementation strategy that breaks down the work into manageable chunks. The implementation can be approached in different ways, depending on project priorities and constraints. For example, you could consider one of the following strategies: + +- Implementing the app features incrementally, one phase at a time. +- Implementing the entire app in a single pass. +- Implementing the MVP app features first, then building out additional features. + +GitHub Spec Kit's implement workflow demonstrates how to use the tasks.md file to guide the implementation process. + +In this task, you review the implementation strategy and then use `speckit/implement` to implement the MVP version of the application (the ability to add and view subscriptions). + +Use the following steps to complete this task: + +1. Open the **tasks.md** file, locate the **Implementation Strategy** section, and then review the suggested MVP strategy. + + The MVP (or MVP First) strategy is intended to deliver a working app as quickly as possible. It should focus on completing the Setup (initialization) and Foundational (blocking) phases first before building out the first User Story (US1). + + For example, the MVP implementation strategy (the ability to add and view subscriptions) might be similar to the following example: + + **Phases**: Setup → Foundation → US1 only + **Tasks**: T001 - T050 (50 tasks) + **Deliverable**: Users can add a known-good feed URL; refresh; see items; restart and confirm persistence. + + The MVP First strategy isn't always limited to the first user story. Depending on the feature complexity, it might include several user stories. They should be listed sequentially after the foundational phase and clearly marked as part of the MVP implementation strategy. + +1. In the Chat view, enter a command that starts the implement workflow for the MVP First strategy: + + Create a command that specifies the range of tasks required to implement the MVP version of the feature. Use the task range specified in the Implementation Strategy section of the tasks.md file, or calculate it based on the tasks listed under each phase. + + > **IMPORTANT**: The command that you enter must reference the specific task range defined in your tasks.md file. + + For example (referencing the MVP implementation example from the previous step), you might enter the following command: + + ```dotnetcli + /speckit.implement Implement the MVP First strategy (Tasks: T001 - T050) + ``` + + This command instructs GitHub Copilot to begin implementing the tasks required for the MVP First strategy of the RSS Feed Reader app. + + In this exercise, you implement all of the tasks for the MVP First strategy using a single /speckit.implement command. In a production environment, you would probably follow a phased approach, such as implementing the tasks for the Setup and Foundational phases first, and then implementing the tasks for each User Story phase one at a time. + +1. Monitor GitHub Copilot's response and provide assistance in the Chat view. + + The agent builds the app incrementally, task by task, following the order defined in the tasks.md file. + + > **NOTE**: GitHub Copilot displays frequent requests for assistance during the implementation phase. The time required to complete the implementation can be affected by how quickly you respond to requests for assistance/permission. + +1. Continue monitoring the implementation workflow until all tasks required for the MVP application are complete. + + GitHub Copilot should notify in the Chat view when the backend and frontend applications are complete. + +1. Accept all changes made to the project files. + + For this lab exercise, it's okay to accept all changes made by GitHub Copilot without a review. However, in a production environment, it's important to review all code changes carefully to ensure they meet quality standards and align with project requirements. + +1. Save all updated files. + +1. Start the backend application, and then start the frontend application. + + You can use a split terminal to run both applications side by side. Ensure that both applications start without errors. You can ask GitHub Copilot for the commands required to start both applications if you're unsure. + + If either application fails to start, report the issue to GitHub Copilot in the Chat view. Provide a detailed description of the problem, including any error messages or logs that can help diagnose the issue. GitHub Copilot will use this information to begin debugging and resolving the problem. + +1. Verify that the frontend application opens successfully in the browser. + + GitHub Copilot should specify a frontend URL in the Chat view that's similar to the following: `http://localhost:5213`. + + If the frontend application reports an error in the UI when it opens, report a detailed description of the issue to GitHub Copilot. + + For example: + + 1. You start the backend application. The backend application is running locally on `http://localhost:5151`. + 1. You start the frontend application. The frontend application is running locally on `http://localhost:5213`. + 1. You open a browser and navigate to the URL specified for the frontend application. + 1. When the page loads, it reports an error: "An unhandled error has occurred. Reload". + 1. You select the "Reload" option, but the error persists. + 1. You open the browser's developer tools console to investigate further. + 1. In the developer tools console, you see the following error message: + + ```plaintext + "Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100] + Unhandled exception rendering component: The following routes are ambiguous: + '' in 'RSSFeedReader.UI.Pages.Home' + '' in 'RSSFeedReader.UI.Pages.Subscriptions' + + System.InvalidOperationException: The following routes are ambiguous: + '' in 'RSSFeedReader.UI.Pages.Home' + '' in 'RSSFeedReader.UI.Pages.Subscriptions' + + at Microsoft.AspNetCore.Components.RouteTableFactory.DetectAmbiguousRoutes(:5213/TreeRouteBuilder builder) + at Microsoft.AspNetCore.Components.RouteTableFactory.Create(:5213/Dictionary2 templatesByHandler, IServiceProvider serviceProvider) at Microsoft.AspNetCore.Components.RouteTableFactory.Create(:5213/List1 componentTypes, IServiceProvider serviceProvider) + at Microsoft.AspNetCore.Components.RouteTableFactory.Create(:5213/RouteKey routeKey, IServiceProvider serviceProvider) + at Microsoft.AspNetCore.Components.Routing.Router.RefreshRouteTable((index)) + at Microsoft.AspNetCore.Components.Routing.Router.Refresh(:5213/Boolean isNavigationIntercepted) + at Microsoft.AspNetCore.Components.Routing.Router.RunOnNavigateAsync(:5213/String path, Boolean isNavigationIntercepted) + at Microsoft.AspNetCore.Components.Routing.Router.<>c__DisplayClass82_0.b__1(:5213/RenderTreeBuilder builder) + at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(:5213/RenderBatchBuilder batchBuilder, RenderFragment renderFragment, Exception& renderFragmentException)" + ``` + + 1. You report the issue to GitHub Copilot in the Chat view. + + For example: + + ```plaintext + I was able to start the backend and frontend apps successfully. I opened the frontend app in the browser at http://localhost:5213. When the page opens, I see an error message: "An unhandled error has occurred. Reload". Selecting reload doesn't resolve the issue. When I check the browser's developer tools console, I see the following error message: + + "Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100] + Unhandled exception rendering component: The following routes are ambiguous: + '' in 'RSSFeedReader.UI.Pages.Home' + '' in 'RSSFeedReader.UI.Pages.Subscriptions' + + System.InvalidOperationException: The following routes are ambiguous: + '' in 'RSSFeedReader.UI.Pages.Home' + '' in 'RSSFeedReader.UI.Pages.Subscriptions' + + at Microsoft.AspNetCore.Components.RouteTableFactory.DetectAmbiguousRoutes(:5213/TreeRouteBuilder builder) + at Microsoft.AspNetCore.Components.RouteTableFactory.Create(:5213/Dictionary2 templatesByHandler, IServiceProvider serviceProvider) at Microsoft.AspNetCore.Components.RouteTableFactory.Create(:5213/List1 componentTypes, IServiceProvider serviceProvider) + at Microsoft.AspNetCore.Components.RouteTableFactory.Create(:5213/RouteKey routeKey, IServiceProvider serviceProvider) + at Microsoft.AspNetCore.Components.Routing.Router.RefreshRouteTable((index)) + at Microsoft.AspNetCore.Components.Routing.Router.Refresh(:5213/Boolean isNavigationIntercepted) + at Microsoft.AspNetCore.Components.Routing.Router.RunOnNavigateAsync(:5213/String path, Boolean isNavigationIntercepted) + at Microsoft.AspNetCore.Components.Routing.Router.<>c__DisplayClass82_0.b__1(:5213/RenderTreeBuilder builder) + at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(:5213/RenderBatchBuilder batchBuilder, RenderFragment renderFragment, Exception& renderFragmentException)". + ``` + + 1. GitHub Copilot analyzes the information you provided and begins debugging the issue. + + When you report an issue, GitHub Copilot uses the information you provided to begin debugging. A detailed description, including what is working, helps GitHub Copilot understand the problem better. GitHub Copilot might need extra details, such as specific error messages to resolve some issues. Provide any additional information requested by GitHub Copilot to help diagnose (and resolve) the problem. + + Continue to provide assistance until the issue is resolved. Once the issue is resolved, GitHub Copilot should ask you to resume manual testing. + +1. Take a couple minutes to verify that the frontend application is working as expected. + + Use the following feed URLs to test the application: + + - https://devblogs.microsoft.com/dotnet/feed/ + - https://devblogs.microsoft.com/visualstudio/feed/ + + You can find the acceptance scenarios in the spec.md file, under the **User Scenarios & Testing** section. + + For example, the acceptance scenarios for the MVP application might be similar to the following example: + + 1. **Given** no subscriptions have been added, **When** the user loads the page, **Then** an empty state is shown (for example, "No subscriptions yet" message) + 2. **Given** the subscription management interface is loaded, **When** the user enters a valid feed URL in the input field and clicks "Add Subscription", **Then** the system accepts the URL and confirms the subscription was added + 3. **Given** the user has entered a feed URL, **When** the user submits the form, **Then** the input field is cleared and ready for another URL + 4. **Given** the user enters an empty string or whitespace-only input, **When** they attempt to add the subscription, **Then** the system prevents submission (basic client-side validation) + 5. **Given** the user has added one subscription, **When** the page displays, **Then** the subscription URL is visible in the list + 6. **Given** the user has added multiple subscriptions, **When** the page displays, **Then** all subscription URLs are visible in the list in the order they were added (newest last) + + You can also ask GitHub Copilot for the steps required to perform manual testing of your MVP implementation. For example, you could enter the following prompt in the Chat view: + + ```plaintext + Can you provide the steps required to manually test the MVP implementation? + ``` + + Use Visual Studio Code to run the application, and then manually test the RSS Feed Reader functionality to ensure that it works as expected. + +1. Continue testing the frontend (and reporting any issues to GitHub Copilot) until all acceptance scenarios for the MVP application pass successfully. + +Key observations: + +- The implementation process can be iterative and might require multiple rounds of testing and debugging. +- Clear communication with GitHub Copilot is essential for effective troubleshooting. +- Thorough testing ensures that the MVP application meets the specified requirements and functions as intended. + +## Clean up + +Now that you've finished the exercise, take a minute to ensure that you haven't made changes to your GitHub account or GitHub Copilot subscription that you don't want to keep. For example, you might want to delete the ContosoDashboard repository. If you're using a local PC as your lab environment, ensure that you want to keep any tools that might have installed during the exercise. You can archive or delete the local clone of the repository that you created for this exercise. diff --git a/Instructions/Labs/LAB_AK_14_implement-spec-driven-development.md b/Instructions/Labs/LAB_AK_14_implement-spec-driven-development.md new file mode 100644 index 0000000..f07d664 --- /dev/null +++ b/Instructions/Labs/LAB_AK_14_implement-spec-driven-development.md @@ -0,0 +1,846 @@ +--- +lab: + title: Exercise - Implement a product feature using GitHub Spec Kit + description: Learn how to add new features to existing applications using GitHub Spec Kit workflows, Visual Studio Code, and GitHub Copilot. + duration: 75 minutes + level: 400 + islab: true + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Implement a product feature using GitHub Spec Kit + +GitHub Spec Kit is an open-source toolkit that enables Spec-Driven Development (SSD) by integrating specifications with AI coding assistants like GitHub Copilot. + +In this exercise, you learn how to use the GitHub Spec Kit to implement a new feature for an existing application. You begin by initializing the GitHub Spec Kit for an existing .NET project. You then use GitHub Spec Kit workflows to create the constitution, specification, plan, and tasks documents for a new app feature. Finally, you use GitHub Spec Kit's implementation workflow to implement an initial MVP version of the app that includes the new feature. + +This exercise should take approximately **75** minutes to complete. + +> **IMPORTANT**: To complete this exercise, you must provide your own GitHub account and GitHub Copilot subscription. If you don't have a GitHub account, you can sign up for a free individual account and use a GitHub Copilot Free plan to complete the exercise. If you have access to a GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscription from within your lab environment, you can use your existing GitHub Copilot subscription to complete this exercise. + +## Before you start + +Your lab environment MUST include the following resources: Git 2.48 or later, .NET SDK 8.0 or later, Visual Studio Code with the C# Dev Kit and GitHub Copilot Chat extensions, SQL Server LocalDB, Python 3.11 or later, the uv package manager, Specify CLI, and access to a GitHub account with GitHub Copilot enabled. + +For help with configuring your lab environment, open the following link in a browser: Configure your GitHub Spec Kit lab environment. + +## Exercise scenario + +You're a software developer working for a consulting firm. The firm is adopting a spec-driven development (SDD) approach using GitHub Spec Kit and GitHub Copilot in Visual Studio Code. Your client, Contoso Corporation, needs you to add a new "document upload and management" feature to their internal employee dashboard application (ContosoDashboard). + +Contoso's business stakeholders documented the requirements for the new feature. Essentially, employees need the ability to upload work-related documents, organize them by category and project, and share them with team members. The new feature must integrate seamlessly with the existing dashboard interface while maintaining security and compliance standards. + +You need to follow a spec-driven development methodology with GitHub Spec Kit to implement the new feature. The GitHub Spec Kit enables you to create the constitution.md, spec.md, plan.md, and tasks.md files that guide the development process. The SDD approach with GitHub Spec Kit ensures that the implementation aligns with business requirements and organizational constraints. + +This exercise includes the following tasks: + +1. Import the ContosoDashboard repository and initialize GitHub Spec Kit. +1. Review the ContosoDashboard project and GitHub Spec Kit files. +1. Generate a constitution based on repository files. +1. Create the feature specification using stakeholder requirements and the constitution. +1. Update the specification with clarified requirements. +1. Generate the technical plan using the specification and constitution. +1. Generate the tasks file using the specification, plan, and constitution. +1. Implement the tasks required for an MVP application. + +## Import the ContosoDashboard repository and initialize GitHub Spec Kit + +GitHub Importer can be used to create a copy of an existing repository in your own GitHub account, giving you full control over the imported copy. + +In this task, you import the existing ContosoDashboard application repository to your GitHub account and initialize GitHub Spec Kit in your project directory. + +Use the following steps to complete this task: + +1. Open a browser window and navigate to GitHub.com. + + You can log in to your GitHub account using the following URL: GitHub login. + +1. Sign in to your GitHub account, and then open your repositories tab. + + You can open your repositories tab by clicking on your profile icon in the top-right corner, then selecting **Repositories**. + +1. On the Repositories tab, select the **New** button. + +1. Under the **Create a new repository** section, select **Import a repository**. + +1. On the **Import your project to GitHub** page, under **Your source repository details**, enter the following URL for the source repository: + + ```plaintext + https://github.com/MicrosoftLearning/ContosoDashboard-SSD.git + ``` + +1. Under the **Your new repository details** section, in the **Owner** dropdown, select your GitHub username. + +1. Enter **ContosoDashboard** in the **Repository name** field. + + GitHub automatically checks the availability of the repository name. If this name is already taken, append a unique suffix (for example, your initials or a random number) to the repository name to make it unique. + +1. To create a private repository, select **Private**, and then select **Begin import**. + + GitHub uses the import process to create the new repository in your account. It can take a minute or two for the import process to finish. Wait for the import process to complete. + + > **IMPORTANT**: If you're using the GitHub Copilot Free plan, you should create the repository as **Public** to ensure that you have access to GitHub Copilot features. If you have a Pro, Pro+, Business, or Enterprise subscription, you can create the repository as **Private**. + + GitHub displays a progress indicator and notifies you when the import is complete. + +1. Once the import is complete, open your new repository. + + A link to your repository should be displayed. Your repository should be located at: `https://github.com/YOUR-USERNAME/ContosoDashboard`. + + You can create a local clone of your ContosoDashboard repository and then initialize GitHub Spec Kit within the project directory. + +1. On your ContosoDashboard repository page, select the **Code** button, and then copy the HTTPS URL. + + The URL should be similar to: `https://github.com/YOUR-USERNAME/ContosoDashboard.git` + +1. Open a terminal window in your development environment, and then navigate to the location where you want to create the local clone of the repository. + + For example: + + Open a terminal window (Command Prompt, PowerShell, or Terminal), and then run: + + ```powershell + cd C:\TrainingProjects + ``` + + Replace `C:\TrainingProjects` with your preferred location. You can use any directory where you have write permissions, and you can create a new folder location if needed. + +1. To clone your ContosoDashboard repository, enter the following command: + + Be sure to replace `YOUR-USERNAME` with your actual GitHub username before running the command. + + ```powershell + git clone https://github.com/YOUR-USERNAME/ContosoDashboard.git + ``` + + You might be prompted to authenticate using your GitHub credentials during the clone operation. You can authenticate using your browser. + +1. To navigate into your ContosoDashboard directory, enter the following command: + + ```powershell + cd ContosoDashboard + ``` + + > **IMPORTANT**: GitHub Spec Kit must be initialized in the root directory of your cloned repository. For example, if you cloned the repository to `C:\TrainingProjects\ContosoDashboard`, ensure that you run the `specify init` command from within the `C:\TrainingProjects\ContosoDashboard` directory. + +1. To initialize GitHub Spec Kit within your existing project, enter the following command: + + ```powershell + specify init --here --integration copilot --script ps + ``` + + The command uses the following components: + + - `--here` - Initializes GitHub Spec Kit in the current directory (your existing ContosoDashboard project). + - `--integration copilot` - Configures the project for GitHub Copilot. + - `--script ps` - Uses PowerShell scripts (use `--script sh` for bash/zsh on macOS/Linux). + + If you're using macOS or Linux, replace `--script ps` with `--script sh`. + + The CLI detects an existing Git repository ("Current directory isn't empty") and ask for confirmation to proceed. + +1. Enter **y** to continue with the initialization process. + + The CLI will: + + - Create agent prompt files in the `.github/agents/` and `.github/prompts/` directories. + - Create template files in the `.specify/memory/` and `.specify/templates/` directories. + - Create script files in the `.specify/scripts/powershell/` directory. + - Update or create a settings.json file in the `.vscode/` directory. + - Preserve all existing application files. + - Display a success message ("Project ready"). + - Suggest some optional next steps. + +## Review the ContosoDashboard project and GitHub Spec Kit files + +GitHub Spec Kit works with GitHub Copilot through Visual Studio Code's chat interface. When you run "specify init --integration copilot" in your project directory, the toolkit configures your workspace to recognize "/speckit.*" commands. + +In this task, you explore the ContosoDashboard project files in Visual Studio Code, verify that GitHub Spec Kit is properly initialized, and then *push* the updated files to your GitHub repository. + +Use the following steps to complete this task: + +1. Open the ContosoDashboard project in Visual Studio Code. + + For example, if the terminal window is still open, you can use the following command to open the project: + + ```powershell + code . + ``` + + The `code .` command opens the current directory (ContosoDashboard) in Visual Studio Code. + + Wait for Visual Studio Code to fully load the project. + +1. Take a minute to familiarize yourself with the project structure. + + Use Visual Studio Code's EXPLORER view to expand the application folders. You should see a folder structure that's similar to the following example: + + ```plaintext + CONTOSODASHBOARD (root) + ├── .github/ + │ ├── agents/ (GitHub Spec Kit executable workflows that can be triggered via commands) + │ └── prompts/ (GitHub Spec Kit prompt files that provide detailed instructions for each of the agent workflows) + ├── .specify/ (GitHub Spec Kit configuration) + │ ├── extensions/ (GitHub Spec Kit stores installed extension packages and their resources - commands, templates, hooks, and config - that add optional capabilities beyond the core Specify workflow.) + │ ├── integrations/ (GitHub Spec Kit stores the project’s active AI-agent integration state and manifests so Specify can install, switch, upgrade, or uninstall agent-specific command wiring safely.) + │ ├── memory/ (GitHub Spec Kit stores the project constitution defining core principles and governance rules that all features must follow) + │ ├── scripts/powershell/ (GitHub Spec Kit uses automation utilities (scripts) for creating features, setting up plans, and managing the specification workflow) + │ └── templates/ (GitHub Spec Kit provides standardized markdown formats for specs, plans, tasks, and checklists to ensure consistent documentation across all features) + ├── ContosoDashboard/ (Main application folder) + │ ├── Data/ (ApplicationDbContext.cs) + │ ├── Models/ (Announcement, Notification, Project, ProjectMember, TaskComment, TaskItem, User) + │ ├── Pages/ (_Host, _Imports, Index, Login, Logout, Notifications, Profile, ProjectDetails, Projects, Tasks, Team) + │ ├── Properties/ (launchSettings.json) + │ ├── Services/ (CustomAuthenticationStateProvider, DashboardService, NotificationService, ProjectService, TaskService, UserService) + │ ├── Shared/ (_Imports, MainLayout, NavMenu, RedirectToLogin) + │ ├── wwwroot/ (Static files, CSS) + │ └── Program.cs (App configuration) + ├── StakeholderDocs/ (Business requirements) + └── README.md (Application documentation) + ``` + +1. Ensure that GitHub Copilot's Chat view is open. + + Using one of the newer language models might improve the quality of responses. This lab exercise was originally tested using the GPT-5 and Claude Sonnet 4.5 models. Results were comparable between the two models. + +1. In the Chat view, to verify that GitHub Spec Kit commands are available, type **/speckit** + + You should see autocomplete suggestions that show the available commands. For example: + + - `/speckit.analyze` - Audit implementation plans. + - `/speckit.checklist` - Validate specification completeness. + - `/speckit.clarify` - Refine specifications through question and answer process. + - `/speckit.constitution` - Define project governing principles. + - `/speckit.implement` - Execute the implementation. + - `/speckit.plan` - Generate technical implementation plans. + - `/speckit.specify` - Create feature specifications. + - `/speckit.tasks` - Break down work into actionable tasks. + - `/speckit.taskstoissues` - Convert the tasks in tasks.md into GitHub issues. + + > **Note**: If the '/speckit.' commands don't appear, try closing and then reopening the project in Visual Studio Code. + + **Troubleshooting**: If you encounter issues: + + - **"specify command not found"**: Ensure you completed Task 1 and installed the Specify CLI. Run `specify version` to verify installation. + - **Permission denied errors**: On Windows, ensure you're running PowerShell with appropriate permissions. On macOS/Linux, check file permissions. + - **Git clone errors**: Verify that you're signed in to GitHub, and that you have access to your imported repository. + - **GitHub Spec Kit commands not appearing**: Ensure `.github/prompts/` exists in your workspace root. Try reloading Visual Studio Code. + +1. Ask GitHub Copilot to explain the current project and GitHub Spec Kit files. + + For example, enter the following prompt in the Chat view: + + ```plaintext + Review the current codebase. Explain the ContosoDashboard application features and the purpose of the GitHub Spec Kit files located under the .github\ and .specify\ directories. + ``` + +1. Take a couple minutes to review GitHub Copilot's response. + + GitHub Copilot's response should summarize the application features and explain the purpose of the GitHub Spec Kit files. + + You can also review the project's README.md file for a description of the current application features, mock authentication system, and security implementation. + +1. Open the **ContosoDashboard/ContosoDashboard.csproj** file in the editor. + + Notice the following: + + - The project file specifies .NET 8 as the target framework. If your development environment has a different .NET SDK version installed (.NET 9 or .NET 10), you need to update the project file to target the installed version. + - The project file includes a reference to SQL Server LocalDB for local development. If you're using a PC with an ARM processor, you need to switch from SQL Server LocalDB to SQLite for local development. + +1. Ensure that the ContosoDashboard.csproj file specifies the .NET version installed in your development environment. + + You can use GitHub Copilot to update the project file. For example, to update the ContosoDashboard.csproj file for .NET 10, enter the following prompt in the Chat view: + + ```plaintext + I have the .NET 10 SDK installed. My project was written using .NET 8. Update the .csproj file for .NET 10 and ensure that the project builds correctly? + ``` + + > **NOTE**: The ContosoDashboard application was developed using .NET 8. If the .NET 8 SDK isn't installed in your development environment, but you have the .NET 9 or .NET 10 SDK installed, the ContosoDashboard.csproj file must be updated to target the installed .NET version before you build and run the application. + +1. Ensure that you're using the correct database provider for your development environment. + + If you're using a PC with an ARM processor for your development environment, you need to switch from SQL Server LocalDB to SQLite for local development. The following files are affected by this change: + + - ContosoDashboard.csproj: Update the database provider package reference. + - Program.cs: Update the database context configuration. + - appsettings.json: Update the connection string. + + You can use GitHub Copilot to update the your project files (ContosoDashboard.csproj, Program.cs, and appsettings.json). For example, to update the codebase for SQLite, enter the following prompt in the Chat view: + + ```plaintext + My PC uses an ARM64 processor. I need you to update the codebase to use SQLite rather than SQL Server LocalDB. + ``` + + > **NOTE**: If you're using a PC with an x64 processor or a Mac, you can skip this step since SQL Server LocalDB works correctly in those environments. + +1. In the EXPLORER view, right-click **ContosoDashboard** and then select **Open in Integrated Terminal**. + + The terminal prompt should open in the ContosoDashboard project directory. For example: + + ```plaintext + PS C:\TrainingProjects\ContosoDashboard\ContosoDashboard> + ``` + +1. To build and run the application, enter the following commands: + + ```dotnetcli + dotnet restore + dotnet build + dotnet run + ``` + + Some **warning** messages are displayed when you build and run the application, but there shouldn't be any errors. + +1. Wait for the application to start, then open a browser window and navigate to the localhost URL listed in the terminal. + + You should see a URL similar to `https://localhost:5000`. + + When you open the ContosoDashboard application in the browser, you should see a login page. + +1. On the ContosoDashboard login page, select **Ni Kang (Employee)** from the dropdown list, and then select **Login**. + +1. Take a minute to explore the ContosoDashboard application. + + The application includes basic dashboard features such as project tracking, task management, notifications, and user profile management. + + It's important to verify that the application is working, and to explore the existing features and behaviors, before you design and develop a new feature. However, you don't need to spend too much time exploring the user interface, just take a minute to observe the basic functionality. + +1. Logout and then close the browser tab. + + You can minimize the browser window, but keep it open for now. + +1. In Visual Studio Code's terminal panel, to stop the running application, press **Ctrl+C** and then close the terminal. + +1. Use Visual Studio Code's Source Control view to commit and then push/sync the updated project files. + + For example: + + - Select the Source Control icon in the left-hand activity bar. + - Enter a commit message such as: "Add GitHub Spec Kit files to the ContosoDashboard project" + - Select the checkmark icon to commit the changes (and select Yes to stage the changes if prompted). + - Select **Sync Changes** to push the commit to GitHub (and select OK if prompted). + + Pushing the GitHub Spec Kit files to your repository enables you to track the spec-driven development process. + +1. Open your GitHub repository in a browser window and verify that the push succeeded. + + You should see the GitHub Spec Kit files alongside the existing application code. You might need to refresh the page to see the latest changes. + +You now have a working ContosoDashboard application with GitHub Spec Kit initialized. + +## Generate a constitution based on repository files + +The GitHub Spec Kit uses a constitution.md file to establish the governing principles and constraints that guide all development decisions for the ContosoDashboard project. It captures organizational policies, technical standards, security requirements, and development practices that must be followed throughout implementation. + +In this task, you use GitHub Copilot's `/speckit.constitution` command to generate a comprehensive constitution based on Contoso stakeholder requirements and the existing project files. + +Use the following steps to complete this task: + +1. Use Visual Studio Code's EXPLORER view to expand the **.github/agents** and **.specify/memory** folders. + + These folders contain the GitHub Spec Kit resources used to create a constitution.md file. It might be helpful to familiarize yourself with these resource files before working on your constitution file. + +1. In the **.github/agents** folder, open the **speckit.constitution.agent.md** file. + +1. Take a minute to review the **speckit.constitution.agent.md** file. + + Notice the detailed instructions provided in this markdown file. These instructions are used by GitHub Copilot to generate the constitution.md file. The agent follows a systematic approach to generate a constitution that captures key principles and constraints. + +1. In the **.specify/memory** folder, open the **constitution.md** file. + + The initial version of the constitution.md file contains the default template for a constitution. + +1. Take a minute to review the **constitution.md** template. + + Notice that the template is initialized with example content that illustrates principles and constraints. The template includes examples for security, performance, quality, technical standards, etc. + + You can keep the constitution file open. + +1. Ensure that the Chat view is open, then start a new chat session. + + You can start a new session by selecting the **New Chat** button (the **+** icon at the top of the Chat panel). Starting a new Chat session ensures a clean context. + +1. In the Chat view, to start a constitution workflow, enter the following command: + + ```plaintext + /speckit.constitution + ``` + + The GitHub Spec Kit supports "greenfield" and "brownfield" project types. Preliminary requirements are more significant for greenfield projects since there's no existing codebase. In this exercise, ContosoDashboard is a brownfield project with an existing codebase, so the agent analyzes the current project files to generate the constitution. + +1. Monitor GitHub Copilot's response. + + GitHub Copilot uses the Chat view to communicate progress as it updates the constitution.md file. + + It can take a minute or two for GitHub Copilot to analyze the project requirements and then construct the constitution document. If the workflow updates the templates for other GitHub Spec Kit files (spec.md, plan.md, tasks.md), you can accept the updates without reviewing the changes. You generate those files in later tasks. + + > **NOTE**: If GitHub Copilot reports that it isn't able to access or edit files, open Visual Studio Code **Settings**, expand **Features**, select **Chat**, and then ensure that **Chat > Agent** is enabled. + +1. Review the updated constitution.md file in the editor. + + Best practice: Always review the suggestions created by an agent. + + After GitHub Copilot updates the constitution, review the document to ensure it captures requirements accurately. This step is important when you're working in a production environment where the constitution represents your business requirements and technical governance. For a training exercise, this review is mainly to help you become familiar with the constitution content. + + Notice that GitHub Copilot recognizes the underlying principles of the ContosoDashboard project and incorporates them into the constitution. The constitution enforces a spec-driven development approach and recognizes the distinction between a training app and production code. + + Each principle should be clearly stated and actionable. For example: + + - ❌ Vague: "Use good security practices" is too general. + - ✅ Clear: "All API endpoints must validate authentication tokens and enforce role-based permissions" is specific and actionable. + + If any critical requirements are missing or unclear, you can edit the constitution.md file directly to add or modify principles. + +1. Ensure that the constitution document is complete, and then accept the changes. + + For a real-world project, it's important to review the constitution against the following criteria before saving: + + - Completeness: All major areas (security, performance, quality, technical standards) are covered. + - Clarity: Each principle is specific and unambiguous. + - Consistency: Principles don't contradict each other. + - Relevance: All principles relate to the ContosoDashboard project. + +1. Save and then close the **constitution.md** file. + +1. Commit and push the updated files to your Git repository. + + For example, if the constitution.md file is the only file that was updated, you can use the following commands in the terminal: + + ```powershell + git add constitution.md + git commit -m "Add project constitution with development principles and constraints" + git push + ``` + + You can verify the commit by checking your GitHub repository in the browser. The constitution.md file should now appear with your commit message. + +The constitution serves as a "contract" between business requirements and technical implementation, ensuring consistency throughout the spec-driven development process. When you use the GitHub Spec Kit to generate the spec, plan, and tasks, it references these principles to ensure the implementation aligns with specified requirements. + +## Create the feature specification using stakeholder requirements and the constitution + +The specification (spec.md) defines what you're building from the user's perspective. It describes features, user stories, acceptance criteria, and business requirements without prescribing how to implement them. A well-written spec serves as the foundation for creating the implementation plan and tasks. + +In this task, you use GitHub Copilot's `/speckit.specify` command to generate a detailed specification for the "document upload and management feature" based on the requirements provided by Contoso's business stakeholders. + +Use the following steps to complete this task: + +1. In Visual Studio Code's EXPLORER view, under the **.github/agents** folder, open the **speckit.specify.agent.md** file. + +1. Take a minute to review the **speckit.specify.agent.md** file. + + Notice the detailed instructions provided to GitHub Copilot. The agent follows a systematic approach to generate a spec file that clearly defines the requirements. + +1. In Visual Studio Code's EXPLORER view, expand the **StakeholderDocs** folder, and then open the **document-upload-and-management-feature.md** file. + +1. Take a couple minutes to read through the **document-upload-and-management-feature.md** file. + + The document-upload-and-management-feature.md file includes detailed stakeholder requirements for the feature that you're adding to the ContosoDashboard application. Clear and detailed requirements are essential for creating an effective specification. + + The document explains that Contoso Corporation employees need a feature that allows them to upload, organize, and share work-related documents within the ContosoDashboard application. The feature must address current challenges with scattered document storage across multiple locations by providing a centralized, secure, role-based document management solution. The document indicates that the feature must work offline for training purposes while maintaining clean abstractions for future Azure cloud migration. The specification defines six core requirement areas (upload capabilities, organization and browsing, access management, integration with existing features, performance requirements, and reporting) along with detailed technical constraints ensuring the implementation follows the offline-first architecture pattern with interface-based abstractions for production deployment. Performance targets and success metrics are provided to ensure the feature meets user needs and business goals. + + It's best to prepare a comprehensive description of the feature ahead of time. However, if you didn't have a detailed requirements document like the one in the StakeholderDocs folder, you can try using a shorter description that highlights the key features, constraints, and success criteria. For example, the following (simplified) description could be used for the document upload and management feature: + + ```plaintext + Feature: Document Upload and Management for ContosoDashboard + + Enable employees to upload work-related documents (PDF, Office, images, text), organize by category/project, share with team members, and search efficiently. Must integrate with existing dashboard features while maintaining security. + + Target Users: All 5,000 Contoso employees with role-based access (Employee, Team Lead, Project Manager, Administrator). + + Core Capabilities: + 1. Upload: Multiple files, max 25 MB each, supported types (PDF, Office docs, images, text), metadata (title, category, description, project, tags), progress indicator, virus scanning. + 2. Organization: My Documents view, Project Documents view, search by title/description/tags/uploader/project (results under 2 seconds). + 3. Management: Download, in-browser preview (PDF/images), edit metadata, replace files, delete documents, sharing with notifications. + 4. Integration: Attach to tasks, dashboard Recent Documents widget, notifications for sharing/new project docs. + 5. Performance: Upload in 30s (25 MB files), list load in 2s (500 docs), search in 2s, preview in 3s. + 6. Audit: Log all uploads/downloads/deletions/sharing, admin reports. + + Security: Azure Blob Storage encryption at rest, TLS 1.3 in transit, RBAC enforcement, virus scanning. + + Success Criteria: 70% adoption in 3 months, find docs under 30s, 90% properly categorized, zero security incidents. + + Constraints: Azure Blob Storage, ASP.NET Core integration, 8-10 week timeline, Entra ID authentication. + + Out of Scope: Version history, storage quotas, soft delete/trash, collaborative editing, external integrations, mobile apps. + ``` + +1. Ensure that the Chat view is open. + + Notice that GitHub Copilot retains the context of previous interactions in the current chat session. If you generated the constitution.md file in the current session, GitHub Copilot provides a **Build Specification** button near the bottom of the Chat view that could be used to start generating the specification. In this case, you want to provide the requirements document explicitly, so you don't use the Build Specification button. + +1. In the Chat view, to start a specify workflow that generates a specification from your stakeholders document, enter the following command: + + ```plaintext + /speckit.specify --file StakeholderDocs/document-upload-and-management-feature.md + ``` + + If you don't specify a requirements document using the `--file` option, you're prompted to describe the feature that you want to build. + +1. Monitor GitHub Copilot's response and provide assistance as needed. + + > **IMPORTANT**: GitHub Copilot asks for assistance when generating the spec.md file. For example, GitHub Copilot requests permission to create a repository branch for the new feature. Grant permission when required by responding in the Chat view. + + It can take 4-6 minutes to create and validate the spec.md file. + +1. Once the specify workflow is complete, use Visual Studio Code's EXPLORER view to expand the **specs** and **checklists** folders. + +1. In the EXPLORER view, select **spec.md**, and then take a couple minutes to review the spec.md file. + + The spec.md file should include a detailed specification for the document upload and management feature based on the stakeholder requirements. + + The specification should be clear, comprehensive, and well-structured. It should provide a solid foundation for creating the technical plan and tasks. + + The spec.md file is based on the template located in the **.specify/templates/spec-template.md** file. The updated spec.md file should include a detailed specification for the 'document upload and management feature' based on the stakeholder requirements that you provided. + + Ensure that the spec.md file includes the mandatory sections defined in the spec template. For example: + + - **User Scenarios & Testing**: User-focused descriptions of feature capabilities and how to test them. + - **Requirements**: Detailed requirements that must be met, organized by category. + - **Success Criteria**: Measurable outcomes, assumptions, and out-of-scope items. + +1. Review the **spec.md** file and verify that key requirements (from your stakeholder requirements document) are captured under the Requirements section. + + For example, you should see requirements related to: + + - File size limits (25 MB per file) + - Supported file types (PDF, Office documents, images, text files) + - Performance targets (2-second page loads, 30-second uploads) + +1. Review the **spec.md** file and verify that acceptance scenarios (associated with user scenarios) are specific and testable: + + The acceptance scenarios should follow the **Given-When-Then** format. The scenarios should provide clear conditions for success or failure. For example: + + - ✅ Good: **Given** I'm logged in as an employee, **When** I navigate to the documents page and select a PDF file under 25 MB with valid metadata (title and category), **Then** the document uploads successfully and appears in my "My Documents" list with all metadata displayed correctly + + - ✅ Good: **Given** an employee attempts to upload a 30MB file, **When** validation occurs, **Then** they see an error message stating the 25MB limit + + - ❌ Avoid: Vague criteria like "Upload should work well" or "System should be fast" + +1. In the EXPLORER view, select **requirements.md**, and then take a minute to review the requirements.md file. + + Verify that no issues are reported in the **requirements.md** file. You should see that all checklist items passed successfully. + +1. Accept the suggested file updates, and then save the **spec.md** and **requirements.md** files. + +1. Commit the specification files and publish the new branch to your Git repository. + + For example: + + Open Visual Studio Code's SOURCE CONTROL view, stage the changes, enter a commit message like "Add specification for document upload and management feature," and then publish the new branch to your Git repository. + +The specification defines the "what" without the "how." It doesn't specify programming languages, frameworks, database schemas, or code organization - those implementation details are determined in the Plan and Tasks phases based on the constitution's technical constraints. The spec focuses on user needs and business requirements, making it easier to review with nontechnical stakeholders. + +## Update the specification with clarified requirements + +The `/speckit.clarify` command helps identify ambiguities, gaps, and underspecified areas in your specification. GitHub Copilot analyzes the spec and asks targeted questions to ensure all requirements are clear and complete before moving to the technical planning phase. + +In this task, you use the clarification process to refine the document upload and management specification. + +Use the following steps to complete this task: + +1. Ensure the GitHub Copilot Chat view is open. + +1. In the Chat view, to start the clarification process, enter the following command: + + ```plaintext + /speckit.clarify + ``` + +1. Monitor GitHub Copilot's response and provide assistance as needed. + + GitHub Copilot analyzes the spec.md file and evaluates whether clarification questions are necessary. + + For example, you might receive questions that are similar to the following sample questions: + + - "When a user is removed from a project after uploading documents to that project, what should happen to those documents?" + - "When a project is deleted, what should happen to all documents associated with that project?" + - "When a shared document is deleted by the owner, what happens to recipients who had access to it?" + - "When a user uploads a document with a filename that contains special characters (for example, Q4 Report (2025) - Finance & Ops.pdf), how should the system handle it?" + - "When disk storage becomes full during a document upload, how should the system respond?" + + The questions are presented one at a time. + +1. If clarifications are needed, consider each question appropriately before answering. + + In a production environment, your answers should reflect careful analysis of business needs, user experience considerations, and technical constraints. However, for this training, you can select the recommended option for each question. + + When you provide an answer, GitHub Copilot updates the spec.md file with clarifications. + + > **NOTE**: If GitHub Copilot presents additional rounds of questions, continue answering until it indicates there are no further clarifications needed. The clarification process typically involves 1-2 rounds of questions as GitHub Copilot refines the specification. + + Once the clarification process is complete, review the updated **spec.md** file, and then accept the changes. + + - Check that your answers are accurately reflected in the specification + - Verify that previously ambiguous areas now have clear requirements + - Look for any newly added acceptance criteria based on your clarifications + + You can make any manual edits if needed. For example, if GitHub Copilot interpreted an answer differently than you intended, edit the spec directly to correct it. + +1. If the clarification process resulted in changes, save the updated **spec.md** file, and then commit and sync the changes. + +Ensuring that specification provides clear and comprehensive guidance is important. By addressing ambiguities upfront, you reduce the risk of building the wrong solution or having to make significant changes later in the development process. + +## Generate the technical plan using the specification and constitution + +The technical plan bridges the gap between the "what" (specification) and the "how" (implementation). It defines the architecture, technology choices, data models, API designs, and implementation approach while adhering to the constraints defined in the constitution. + +In this task, you use GitHub Copilot's `/speckit.plan` command to generate a comprehensive technical implementation plan. + +Use the following steps to complete this task: + +1. In Visual Studio Code's EXPLORER view, under the **.github/agents** folder, open the **speckit.plan.agent.md** file. + +1. Take a minute to review the **speckit.plan.agent.md** file. + + Notice the detailed instructions provided to GitHub Copilot. The agent follows a systematic approach to generate a plan file that outlines the technical implementation strategy. + + If you're interested, you can also review the **.specify/templates/plan-template.md** file to see the structure that's used for the plan.md file. + +1. Ensure the GitHub Copilot Chat view is open. + +1. In the Chat view, to start the technical planning process, enter the following command: + + ```dotnetcli + /speckit.plan + ``` + +1. Monitor GitHub Copilot's response and provide assistance in the Chat view. + + GitHub Copilot analyzes the constitution.md and spec.md files to generate the plan. Provide permission and assistance when required. + + It can take 6-8 minutes for GitHub Copilot to generate the technical plan and associated markdown files. + +1. Once the plan workflow is complete, ensure that the following files are listed under the **specs** folder: + + - **plan.md** + - **research.md** + - **quickstart.md** + - **data-model.md** + + You might also see one or more files listed under a **contracts** folder. + +1. Take a few minutes to review the **research.md**, **plan.md**, **quickstart.md**, and **data-model.md** files. + + - The research.md file captures research findings and technology decisions for the document upload and management feature. + - The plan.md file outlines the technical implementation plan for the document upload and management feature. + - The quickstart.md file provides setup instructions and a high-level overview of how to get started with the implementation. + - The data-model.md file defines the data entities, properties, and relationships needed for the document upload and management feature. + + > **NOTE**: For a production scenario, you need to ensure that the plan provides a comprehensive description of the technical context and a clearly defined implementation strategy for the new feature. The research, quickstart, and data model files should complement the plan by providing additional context and details. For this exercise, focus on becoming familiar with the content associated with each of the files. + +1. After reviewing the files, accept the updates. + + If the plan omits important details or makes assumptions you disagree with, you can: + + - Edit the plan.md file directly, or + - Ask follow-up questions in GitHub Copilot Chat. For example: + + ```plaintext + The plan should include a background job for processing virus scans. Add details about using Azure Functions with Queue Storage triggers to handle async file scanning after upload. + ``` + +1. Save the files, and then commit and sync your changes. + +The technical plan now serves as a blueprint for implementation. It translates business requirements into concrete technical decisions while respecting organizational constraints. + +## Generate the tasks file using the specification, plan, and constitution + +The tasks.md file breaks down the technical plan into specific, actionable implementation steps. Each task should be small enough to complete in a reasonable timeframe (typically a few hours to a day when implemented without AI assistance) and have clear acceptance criteria. + +In this task, you use the GitHub Spec Kit's `/speckit.tasks` command to generate a comprehensive tasks list and phased implementation plan. + +Use the following steps to complete this task: + +1. In Visual Studio Code's EXPLORER view, under the **.github/agents** folder, open the **speckit.tasks.agent.md** file. + +1. Take a minute to review the **speckit.tasks.agent.md** file. + + Notice the detailed instructions provided to GitHub Copilot. The agent follows a systematic approach to generate a tasks.md file that breaks down the implementation plan into manageable tasks. + +1. Ensure the GitHub Copilot Chat view is open. + +1. In the Chat view, to start generating the tasks.md file, enter the following command: + + ```dotnetcli + /speckit.tasks + ``` + +1. Monitor GitHub Copilot's response and provide assistance in the Chat view. + + GitHub Copilot analyzes the spec.md and plan.md files and generate tasks in the tasks.md file. + + It can take 3-4 minutes for GitHub Copilot to generate the tasks.md file. Provide permission and assistance when required. + +1. Once the tasks workflow is complete, take a few minutes to review the **tasks.md** file. + + The tasks.md file should provide a list of tasks organized by phase and user story. + + Verify that the tasks cover the requirements from the specification and plan. For example: + + - Each functional requirement should map to one or more tasks. + - Security requirements should have corresponding implementation tasks. + - Performance requirements should have testing tasks. + - Integration points should have dedicated tasks. + + Verify that tasks are ordered logically. For example: + + - Foundation tasks (database, models) come first. + - Backend API tasks build on the foundation. + - Frontend tasks reference backend endpoints. + - Testing tasks come after implementation. + - Deployment tasks come last. + +1. Ensure that each task is specific and actionable: + + - ✅ Good: "Create Document entity with properties: DocumentId, Title, Description, FileName, FileSize, BlobStorageUrl" + - ❌ Vague: "Set up database stuff" + + Verify that tasks have reasonable scope: + + - Developers should be able to complete individual tasks in a few hours to a day. + - If a task seems too large it might need to be broken down during implementation. + + You can add task dependencies or notes if needed. For example: + + ```markdown + - [ ] Task 12: Implement DocumentController POST /api/documents endpoint + - Depends on: Task 11 (DocumentService) + - Note: Include comprehensive error handling for file size limits and unsupported types + ``` + +1. Accept the suggested file updates, and then save the **tasks.md** file. + +1. Commit the changes and then sync the updates. + +The tasks.md file now provides a clear roadmap for implementation. + +## Implement the tasks required for an MVP application + +With a clear specification, technical plan, and tasks document in place, you're ready to implement the document upload and management feature. The implement workflow demonstrates how spec-driven development guides implementation and how GitHub Copilot assists with code generation based on the context you established. + +In this task, you review the implementation strategy and then use `speckit/implement` to implement the MVP version of the application. + +Use the following steps to complete this task: + +1. Open the **tasks.md** file, locate the **Implementation Strategy** section, and then review the suggested "MVP first" strategy. + + The MVP first strategy is intended to deliver working feature as quickly as possible. It should focus on completing the critical blocking phases first to establish a functional foundation before building out the first user story (US1). + + For example, the MVP implementation strategy might be similar to the following example: + + ```plaintext + **Phases**: Setup → Foundation → US1 only + **Tasks**: T001 - T045 (45 tasks) + **Estimated Time**: 6-8 hours for developer familiar with ASP.NET Core/Blazor + **Deliverable**: Users can upload and view their documents + ``` + +1. In the Chat view, enter a command that starts the implement workflow using the MVP first strategy: + + Create a command that specifies the range of tasks required to implement the MVP version of the feature. Use the task range specified in the Implementation Strategy section of the tasks.md file. + + > **IMPORTANT**:The command that you enter must reference the specific task range defined in your tasks.md file. + + For example (referencing the MVP implementation example from the previous step), you might enter the following command: + + ```dotnetcli + /speckit.implement Implement the MVP first strategy (Tasks: T001 - T045) + ``` + + This command instructs GitHub Copilot to begin implementing the tasks required for the MVP version of the document upload and management feature. + +1. Monitor GitHub Copilot's response and provide assistance in the Chat view. + + The agent builds the feature incrementally, task by task, following the order defined in the tasks.md file. + + > **NOTE**: GitHub Copilot is diligent about checking its work during the implementation, which is great. GitHub Copilot also keeps you involved during the implementation process. Requests for assistance occur frequently. The time required to complete the implementation can be affected by how quickly you respond to its requests for assistance. + +1. If manual testing is required to verify a task, perform the steps described in the Chat view, and then report the results back to GitHub Copilot. + + You might encounter issues during manual testing. For example: + + 1. GitHub Copilot tells you that manual testing is required to verify that file uploads are working correctly. + 1. The application is already running locally. The Chat view provides the URL to open in the browser (for example, `http://localhost:5000`). + 1. You open the application in the browser, login as Ni Kang, and then navigate to the My Documents page. + 1. The app appears to be unresponsive with a message "Loading documents..." displayed in the user interface. + 1. You select the Upload Document button, but nothing happens. + 1. You try logging out, but the application remains unresponsive. None of the buttons work. + + At this point you need to report the issue to GitHub Copilot: + + 1. You return to Visual Studio Code's Chat view. + 1. You report the issue in the Chat view. For example: + + ```plaintext + I opened the application in the browser at http://localhost:5000. I was able to login as Ni Kang and navigate to the My Documents page. However, I encountered an issue where the application appears unresponsive with a "Loading documents..." message displayed in the UI. When I select the Upload Document button, nothing happens. I also tried logging out, but the application remains unresponsive and none of the buttons work. Can you help me troubleshoot this issue? + ``` + + When you report an issue, GitHub Copilot uses the information you provided to begin debugging. A detailed description, including what is working, helps GitHub Copilot understand the problem better. GitHub Copilot might need extra details, such as specific error messages to resolve some issues. Provide any additional information requested by GitHub Copilot to help diagnose (and resolve) the problem. + + Continue to provide assistance until the issue is resolved. Once the issue is resolved, GitHub Copilot should ask you to resume manual testing. + +1. Continue with the implement workflow until all tasks required for the MVP application are complete. + + GitHub Copilot should notify in the Chat view when the MVP implementation is complete. + +1. Review and accept all changes made to the project files. + + For this lab exercise, it's okay to accept all changes made by GitHub Copilot without a detailed review. However, in a production environment, it's important to review all code changes carefully to ensure they meet quality standards and align with project requirements. + +1. Take a few minutes to verify the acceptance scenarios for the MVP application. + + You can find the acceptance scenarios in the spec.md file. The acceptance scenarios listed under the **User Scenarios & Testing** section. The MVP application is usually associated with the first user story (US1) in the spec.md file. + + You can also ask GitHub Copilot for the steps required to perform manual testing of your MVP implementation. For example, you could enter the following prompt in the Chat view: + + ```plaintext + Can you provide the steps required to manually test the MVP implementation? + ``` + + Use Visual Studio Code to run the application, and then manually test the document upload and management functionality to ensure that it works as expected. + + For example, you can use the following steps to manually test document upload functionality: + + 1. Navigate to http://localhost:5000 + 1. Log in as Ni Kang (Employee). + 1. Select **Documents** from the navigation menu. + 1. Use the provided interface to open a file selection dialog. + 1. Locate and select a PDF file that's less than 25 MB, then fill the Title ("Test Document") and Category ("Personal Files") fields. + 1. Select the "Upload" option to start the upload process. + 1. Verify that an upload progress indicator appears. + 1. Verify that the document appears in your uploaded documents list. + +1. Report back to GitHub Copilot with the results of your manual testing. + + For example: + + - If your test succeeded, you can either continue to the next test or provide a report similar to the following example: + + "I opened the application in the browser at http://localhost:5000. I was able to login as Ni Kang and navigate to the My Documents page. I can upload a PDF file less than 25 MB with the Title 'Test Document' and Category 'Personal Files.' The upload progress indicator appeared, and the document shows up in my uploaded documents list. Task T041 passed successfully." + + - If your task failed, you need to report the issue to GitHub Copilot for assistance. + + For example: "I opened the application in the browser at http://localhost:5000. I was able to login as Ni Kang and navigate to the My Documents page. I can select a document and fill in the Title and Category fields, but there's an error when I try to upload the document. I see a progress indicator displayed on the Upload Document page, however, the My Documents page doesn't recognize that I uploaded a document. Can you help resolve the issue? + + GitHub Copilot can help you diagnose and fix issues, implement improvements to the user interface, or suggest next steps. + +1. Continue manual testing and reporting results until all acceptance scenarios for the MVP application pass successfully. + +1. After successfully testing your MVP application, commit and sync your implementation files. + +> **NOTE**: If time permits, you can continue implementing additional tasks beyond the MVP scope. You can either instruct GitHub Copilot to proceed with the next set of tasks or manually select specific tasks to implement next. + +Key Observations: + +- GitHub Copilot generates code that aligns with your spec because it references the *spec.md*, *plan.md*, and *tasks.md* files in your workspace. +- Detailed comments based on specification requirements guide GitHub Copilot to produce accurate implementations. +- The spec-driven development approach ensures you don't forget requirements (file size limits, supported types, etc.) because they're explicitly documented. +- Having clear acceptance criteria makes it easy to verify that your implementation meets requirements. + +In a full implementation, you would continue through all remaining tasks in the tasks.md file, using a phased approach to systematically build out the complete feature. The spec-driven development approach keeps you focused on requirements and prevents scope creep or missed functionality. + +## Clean up + +Now that you've finished the exercise, take a minute to ensure that you haven't made changes to your GitHub account or GitHub Copilot subscription that you don't want to keep. For example, you might want to delete the ContosoDashboard repository. If you're using a local PC as your lab environment, ensure that you want to keep any tools that might have installed during the exercise. You can archive or delete the local clone of the repository that you created for this exercise. diff --git a/Instructions/Labs/LAB_AK_15_configure_customize_github_copilot_vscode.md b/Instructions/Labs/LAB_AK_15_configure_customize_github_copilot_vscode.md new file mode 100644 index 0000000..c800800 --- /dev/null +++ b/Instructions/Labs/LAB_AK_15_configure_customize_github_copilot_vscode.md @@ -0,0 +1,902 @@ +--- +lab: + title: Exercise - Configure GitHub Copilot instructions and create custom agents + description: Learn how to configure a C# project to use custom GitHub Copilot instructions and create custom agents that collaborate through handoffs to complete multi-step development tasks. + level: 300 + duration: 50 minutes + islab: true + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Configure GitHub Copilot instructions and create custom agents + +GitHub Copilot provides powerful AI-assisted coding right out of the box, but its true potential emerges when you customize it to match your team's specific workflows and project requirements. By providing custom instructions and creating specialized agents, you can transform GitHub Copilot from a general-purpose assistant into a set of tailored AI collaborators that understand your codebase, follow your conventions, and handle multi-step development tasks. + +In this exercise, you configure the ContosoInventory C# Web API project to use custom GitHub Copilot instructions and create custom agents that collaborate through handoffs to complete a development task end-to-end. + +This exercise should take approximately **50** minutes to complete. + +> **IMPORTANT**: To complete this exercise, you must provide your own GitHub account and GitHub Copilot subscription. If you don't have a GitHub account, you can sign up for a free individual account and use a GitHub Copilot Free plan to complete the exercise. If you have access to a GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscription from within your lab environment, you can use your existing GitHub Copilot subscription to complete this exercise. + +## Before you start + +Your lab environment MUST include the following resources: + +- Git 2.48 or later. +- The .NET SDK version 8.0 or later. +- Access to a GitHub account with GitHub Copilot enabled. +- Visual Studio Code with the C# Dev Kit and GitHub Copilot Chat extensions. + +## Exercise scenario + +You're a software developer working for a consulting firm. The firm developed the ContosoInventory web application (a Blazor WebAssembly application with an ASP.NET Core backend) for Contoso's IT department. The application manages inventory categories for tracking equipment used across the organization. The client has specific coding standards, architectural patterns, and review processes, and they've asked you to add a Product Inventory management feature so individual products can be tracked within each category. + +You plan to use GitHub Copilot's customization features to accelerate development while ensuring that all code adheres to the client's standards. Your plan includes the following tasks: + +1. Create custom instruction files that embed the client's coding standards into GitHub Copilot's behavior so that all AI-generated code follows the established conventions. +1. Define custom agents for specific development roles—a "Planner" that designs implementation plans, an "Implementer" that writes code, and a "Reviewer" that checks code quality. +1. Chain these agents together using handoffs to create a structured multi-step workflow from planning through implementation to review. + +This exercise includes the following tasks: + +1. Review features of the ContosoInventory application. +1. Create repository-level custom instructions that enforce coding standards. +1. Create path-specific instruction files for targeted guidance. +1. Create a reusable prompt file for a common task. +1. Define a "Planner" custom agent with read-only tools. +1. Define an "Implementer" custom agent with editing capabilities. +1. Define a "Reviewer" custom agent for code quality checks. +1. Run the chained agents workflow to complete a development task end-to-end. + +## Review features of the ContosoInventory application + +Before adding GitHub Copilot customization files that enforce coding standards and guide development workflows, you need to download and review the ContosoInventory application. + +Use the following steps to complete this task: + +1. Open a browser window, navigate to the GitHub home page, and then log in to your GitHub account. + + You can log in to your GitHub account using the following URL: GitHub login. + +1. Sign in to your GitHub account, and then open your repositories tab. + + You can open your repositories tab by clicking on your profile icon in the top-right corner, then selecting **Repositories**. + +1. On the Repositories tab, select the **New** button. + +1. Under the **Create a new repository** section, select **Import a repository**. + +1. On the **Import your project to GitHub** page, under **Your source repository details**, enter the following URL for the source repository: + + ```plaintext + https://github.com/MicrosoftLearning/github-copilot-customization-starter-app + ``` + +1. Under the **Your new repository details** section, in the **Owner** dropdown, select your GitHub username. + +1. In the **Repository name** field, enter **ContosoInventory** + + GitHub automatically checks the availability of the repository name. If this name is already taken, append a unique suffix (for example, your initials or a random number) to the repository name to make it unique. + +1. To create your new ContosoInventory repository, select **Private**, and then select **Begin import**. + + GitHub uses the import process to create the new repository in your account. It can take a minute or two for the import process to finish. Wait for the import process to complete before proceeding. + + > **IMPORTANT**: If you're using the GitHub Copilot Free plan, you should create the repository using the **Public** option. When using GitHub Copilot's Free plan, some GitHub Copilot features are only available for public repositories. If you have a Pro, Pro+, Business, or Enterprise subscription, you can create the repository as **Private**. + + GitHub displays a progress indicator and notifies you when the import is complete. + +1. Once the import is complete, open your new repository. + + A link to your repository should be displayed. Your repository should be located at: `https://github.com/YOUR-USERNAME/ContosoInventory`. + +1. On your ContosoInventory repository page, select the **Code** button, and then copy the HTTPS URL. + + The URL should be similar to: `https://github.com/YOUR-USERNAME/ContosoInventory.git` + +1. Open a terminal window in your development environment, and then navigate to the folder location where you want to create a local clone of your repository. + + For example: + + ```powershell + cd C:\TrainingProjects + ``` + + Replace `C:\TrainingProjects` with your preferred location. You can use any directory where you have write permissions, and you can create a new folder location if needed. + +1. To clone your ContosoInventory repository, enter the following command: + + Be sure to replace `YOUR-USERNAME` with your actual GitHub username before running the command. + + ```powershell + git clone https://github.com/YOUR-USERNAME/ContosoInventory.git + ``` + + You might be prompted to authenticate using your GitHub credentials during the clone operation. You can authenticate using your browser. + +1. To open your ContosoInventory repository in Visual Studio Code, enter the following commands: + + ```powershell + cd ContosoInventory + code . + ``` + +1. In Visual Studio Code's EXPLORER view, expand the project folders. + + The ContosoInventory application uses a three-project architecture: + + - **ContosoInventory.Server**: ASP.NET Core Web API with Entity Framework Core, Identity authentication, and SQLite. + - **ContosoInventory.Client**: Blazor WebAssembly SPA that runs in the browser and calls the server API. + - **ContosoInventory.Shared**: Shared class library containing models, DTOs, and enums. + +1. Take a moment to review the project structure. + + Expand the project folders. You should see a folder structure similar to the following example: + + ```plaintext + ContosoInventory/ + ├── ContosoInventory.Server/ + │ ├── App_Data/ (SQLite database file location) + │ ├── Controllers/ + │ │ ├── AuthController.cs (Login, Logout, GetCurrentUser) + │ │ └── CategoriesController.cs (CRUD + toggle-active) + │ ├── Data/ + │ │ ├── InventoryContext.cs (EF Core DbContext) + │ │ ├── DbInitializer.cs (Database seeding) + │ │ └── Migrations/ (EF Core migrations) + │ ├── Models/ + │ │ └── Category.cs (Category entity) + │ ├── Services/ + │ │ ├── ICategoryService.cs (Category service interface) + │ │ └── CategoryService.cs (Category service implementation) + │ ├── Properties/ + │ │ └── launchSettings.json + │ ├── appsettings.json + │ ├── appsettings.Development.json + │ ├── ContosoInventory.Server.csproj + │ └── Program.cs + ├── ContosoInventory.Client/ + │ ├── Layout/ + │ │ ├── MainLayout.razor (Application shell with nav bar) + │ │ └── NavMenu.razor (Navigation menu component) + │ ├── Pages/ + │ │ ├── Home.razor (Dashboard with category stats) + │ │ ├── Login.razor (Login form) + │ │ └── Categories.razor (Category management page) + │ ├── Services/ + │ │ ├── CategoryApiService.cs (HTTP client for category API) + │ │ └── CookieAuthStateProvider.cs (Custom AuthenticationStateProvider) + │ ├── wwwroot/ + │ │ ├── css/ + │ │ │ └── app.css (Application styles) + │ │ └── index.html (Blazor host page) + │ ├── _Imports.razor + │ ├── App.razor (Root component with routing) + │ ├── ContosoInventory.Client.csproj + │ └── Program.cs + ├── ContosoInventory.Shared/ + │ ├── DTOs/ + │ │ ├── CategoryResponseDto.cs + │ │ ├── CreateCategoryDto.cs + │ │ ├── UpdateCategoryDto.cs + │ │ ├── LoginDto.cs + │ │ └── UserInfoDto.cs + │ ├── ContosoInventory.Shared.csproj + │ └── GlobalUsings.cs + └── ContosoInventory.sln + ``` + +1. Open the **ContosoInventory.Server/Program.cs** file and review the application configuration. + + Notice the following key configuration areas: + + - Entity Framework Core with SQLite for data access + - ASP.NET Core Identity for authentication with cookie-based sessions and account lockout + - Service registrations for `ICategoryService` + - Database seeding via `DbInitializer.InitializeAsync` at startup + - CORS, rate limiting, Cross-Site Request Forgery (CSRF) protection, and security headers middleware + +1. Open the **ContosoInventory.Server/Controllers/CategoriesController.cs** file and note the existing API endpoints. + + The categories controller provides the following endpoints: + + - `GetAllCategories`: Gets all categories ordered by display order + - `GetCategoryById`: Gets a specific category by ID + - `CreateCategory`: Creates a new category (Admin only) + - `UpdateCategory`: Updates an existing category (Admin only) + - `DeleteCategory`: Deletes a category (Admin only) + - `ToggleActive`: Toggles the active status of a category (Admin only) + +1. Open the **ContosoInventory.Server/Services/CategoryService.cs** file and review the service implementation. + + Notice the patterns used: async/await for all database operations, DTO mapping, structured logging, input validation, and error handling with try-catch blocks. The GitHub Copilot agents reference these patterns when creating a Product feature later in the exercise. + +1. Open a terminal in Visual Studio Code and build the solution. + + ```powershell + cd ContosoInventory/ContosoInventory.Server + dotnet build + ``` + + > **IMPORTANT**: The project uses .NET 8 by default. If you have the .NET 9 or .NET 10 SDK installed, but not .NET 8, you need to update the project to target the version of .NET that you have installed. For AI assistance with updating to a later version of .NET, open the GitHub Copilot Chat view and ask GitHub Copilot to update your project files to the version of .NET that you have installed in your environment. + + The build should complete successfully without errors. + +1. To start the server application, enter the following command in the terminal: + + ```powershell + dotnet run + ``` + + > **NOTE**: The first time you run the application, it may take a little extra time to apply database migrations and seed the database with sample data. You should see console output indicating that the database has been initialized and seeded. You should also see a message that the server starts listening on `http://localhost:5240`. + +1. Open a browser and navigate to `http://localhost:5240`. + + A login page should be displayed when the app opens. + + Just below the **Sign in** button, you see a **Lab Exercise — Test Accounts** information card that displays credentials for two test users. + + For the purposes of this lab exercise, you can test the application using the two test user accounts (Mateo Gomez and Megan Bowen): + + - Mateo has the Admin role and can perform all CRUD operations on categories. + - Megan has the Viewer role and can only view categories. + +1. Sign in using the Admin credentials (Mateo Gomez). + +1. Verify that the Home page displays category statistics. + + You should see a welcome message and statistics showing nine total categories, nine active, and zero inactive. + +1. Navigate to the **Categories** page and verify that all 9 categories are listed. + + You should see categories like Laptops & Desktops, Monitors & Displays, Networking Equipment, and others. As an Admin, you should also see Add Category, Edit, Deactivate/Activate, and Delete action buttons. + +1. Log out, and then sign in using the Viewer credentials (Megan Bowen). + +1. Navigate to the **Categories** page and verify that Admin-only action buttons (Add Category, Edit, Deactivate/Activate, Delete) aren't visible. + + When the app user is in a Viewer role, the user can see the category list but can't modify data. + +1. Log out from the application. + +1. To stop the application, return to the Visual Studio Code integrated terminal where the server is running, and then press **Ctrl+C**. + + > **NOTE**: You can leave the terminal open for the next task. + +## Create repository-level custom instructions that enforce coding standards + +In this task, you create a `.github/copilot-instructions.md` file that provides GitHub Copilot with always-on guidelines for the project. These instructions are automatically included in every GitHub Copilot Chat request within the workspace. + +Use the following steps to complete this task: + +1. Verify that GitHub Copilot is active in Visual Studio Code. + + Look for the GitHub Copilot icon in the status bar at the bottom of the Visual Studio Code window. The icon should be visible and not show any warnings. If GitHub Copilot isn't active, sign in to your GitHub account using the Accounts icon in the Activity Bar. + +1. Open GitHub Copilot's Chat view by selecting the **Toggle Chat** icon at the top of the Visual Studio Code window, or by pressing **Ctrl+Alt+I**. + + Verify that GitHub Copilot Chat opens successfully. You use GitHub Copilot Chat throughout this exercise. + +1. To see the custom instructions that GitHub Copilot generates automatically, enter the following command in the GitHub Copilot Chat view: + + ```plaintext + /init + ``` + + When you enter this command, GitHub Copilot analyzes your workspace—detecting the C# language, ASP.NET Core framework, Entity Framework Core, and the project structure—and generates a starter `copilot-instructions.md` file tailored to what it finds. + + > **NOTE**: It can take a few minutes for GitHub Copilot to analyze the codebase and generate the instructions. The generated instructions are a helpful starting point, but they won't capture your client's specific coding standards or architectural patterns. You'll customize the instructions in the next steps to ensure they reflect the exact requirements of your project. + +1. Review the generated instructions. + + Examine the copilot-instructions.md file. Notice that GitHub Copilot infers general best practices for the detected technology stack, but it doesn't know about the client's specific coding standards, such as the underscore prefix for private fields, the repository pattern requirement, or the xUnit testing conventions. + + The `/init` command generates a useful starting point, but you're going to replace the autogenerated content with manually crafted instructions that reflect the client's exact requirements. + +1. Delete the generated file so you can create one from scratch. + + If `/init` command created a `copilot-instructions.md` file in a `.github` folder, delete the file but keep the folder. + +1. Use Visual Studio Code's EXPLORER view to ensure that you have a **.github** folder in the root of your project. + + > **NOTE**: The folder name must start with a period. This is a convention used by GitHub for configuration files and is the standard location for GitHub Copilot instruction files. + +1. Right-click the **.github** folder and select **New File**. + +1. Name the file **copilot-instructions.md**. + +1. Add the following content to the **copilot-instructions.md** file: + + ```markdown + # Contoso Inventory API - Coding Standards + + ## Naming Conventions + - Use PascalCase for class names, public methods, and public properties. + - Use camelCase for local variables and method parameters. + - Prefix private fields with an underscore (e.g., _inventoryService). + - Suffix interface names with the interface prefix "I" (e.g., IInventoryService). + + ## Architecture Patterns + - Follow the repository pattern for all data access operations. + - Use dependency injection for all service dependencies. Register services in Program.cs. + - Separate business logic into service classes. Controllers should only handle HTTP concerns. + - Use DTOs (Data Transfer Objects) for API request and response payloads. Never expose database entities directly. + + ## Error Handling + - Use try-catch blocks for all external API calls and database operations. + - Return appropriate HTTP status codes (200 for success, 400 for bad requests, 404 for not found, 500 for server errors). + - Log all exceptions using ILogger with structured logging. + - Include meaningful error messages in API responses. + + ## Documentation + - Include XML documentation comments on all public methods and classes. + - Use inline comments only for complex business logic that is not self-explanatory. + + ## Testing + - Write unit tests using xUnit and Moq. + - Follow the Arrange-Act-Assert pattern in test methods. + - Name test methods using the pattern: MethodName_Scenario_ExpectedResult. + ``` + +1. Ensure that the file contents are left justified, and then save the file. + +1. Open Visual Studio Code Settings + + You can use the keyboard shortcut **Ctrl** + **,** to open settings, or you can select the gear icon in the lower-left corner and select **Settings** from the menu. + +1. In the **search settings** field, enter **Use Instruction Files**. + +1. Ensure that the checkbox "Code Generation: Use Instruction Files" is checked. + + This setting (github.copilot.chat.codeGeneration.useInstructionFiles) is enabled by default, but if it was previously toggled off, copilot-instructions.md won't be loaded. + +1. Close the Settings tab. + +1. To verify that GitHub Copilot is using your custom instructions, open the GitHub Copilot Chat view and enter the following prompt: + + ```plaintext + What naming convention should I use for private fields in this project? + ``` + +1. Take a moment to review the response from GitHub Copilot. + + GitHub Copilot should reference your custom instructions and respond with guidance to prefix private fields with an underscore. In the GitHub Copilot Chat response, look for a **References** section that cites `copilot-instructions.md` to verify the instructions are being applied. + + > **NOTE**: If you don't see the instructions being referenced, verify that the `chat.includeApplyingInstructions` setting is enabled in Visual Studio Code settings. This setting is on by default. + +1. To test another aspect of the instructions, enter the following prompt in the chat: + + ```plaintext + How should I structure data access in this project? What pattern should I follow? + ``` + +1. Take a moment to review the response from GitHub Copilot. + + The GitHub Copilot response should tell you to follow the repository pattern for data access. + +> **NOTE**: Visual Studio Code also recognizes `AGENTS.md` and `CLAUDE.md` files as always-on instructions. An `AGENTS.md` file placed in the root of your workspace works similarly to `copilot-instructions.md` and is useful when you work with multiple AI tools and want a single set of instructions recognized by all of them. Additionally, nested `AGENTS.md` files can be placed in subdirectories to provide context-specific instructions that apply only when GitHub Copilot operates on files within that directory or its children. For this exercise, you use the `.github/copilot-instructions.md` convention, which is the standard approach for GitHub Copilot customization. + +## Create path-specific instruction files for targeted guidance + +In this task, you create `.instructions.md` files that provide targeted coding guidelines for specific parts of the codebase. These files allow you to define different rules for controllers, services, and other areas of the project. + +Use the following steps to complete this task: + +1. In the **.github** folder, create a new folder named **instructions**. + +1. In the **.github/instructions/** folder, create a new file named **controllers.instructions.md**. + +1. Add the following content to **controllers.instructions.md**: + + ```markdown + --- + name: 'API Controller Standards' + description: 'Coding standards for ASP.NET Core API controllers' + applyTo: '**/Controllers/**/*.cs' + --- + # API Controller Standards + + - Inherit from ControllerBase and apply the [ApiController] attribute. + - Use attribute routing with [Route("api/[controller]")] on the class. + - Keep controller methods thin: delegate business logic to service classes. + - Use action-specific attributes: [HttpGet], [HttpPost], [HttpPut], [HttpDelete]. + - Return IActionResult or ActionResult from all action methods. + - Include [ProducesResponseType] attributes to document response types. + - Validate model state using data annotations on request DTOs. + - Inject services through the constructor, not directly in action methods. + ``` + + Notice the `applyTo` field in the frontmatter. This field specifies a glob pattern that determines which files these instructions apply to. In this case, the instructions apply only to C# files within any Controllers folder. When you ask GitHub Copilot for guidance while working in a controller file, it prioritizes these instructions in addition to the repository-level instructions. + +1. Ensure that the file contents are left justified, and then save the file. + +1. In the **.github/instructions/** folder, create a new file named **services.instructions.md**. + +1. Add the following content to **services.instructions.md**: + + ```markdown + --- + name: 'Service Layer Standards' + description: 'Coding standards for business logic service classes' + applyTo: '**/Services/**/*.cs' + --- + # Service Layer Standards + + - Define a corresponding interface for every service class (e.g., IProductService for ProductService). + - Use async/await for all I/O-bound operations (database calls, HTTP requests, file I/O). + - Accept and return DTOs, not database entities. + - Include comprehensive input validation at the start of each public method. + - Throw specific exception types (ArgumentException, InvalidOperationException) rather than generic Exception. + - Log significant operations using ILogger with structured logging parameters. + ``` + + Notice that the `applyTo` pattern targets C# files within any Services folder. When you ask GitHub Copilot for guidance while working in a service file, it prioritizes these instructions along with the repository-level instructions. + +1. Ensure that the file contents are left justified, and then save the file. + +1. To verify that your instruction files are loading correctly, right-click in the Chat view, and then select **Diagnostics**. + +1. Notice that GitHub Copilot Chat opens a new editor tab with diagnostic information about the current session. + +1. Take a minute to review the Chat Customization Diagnostics report. + + The "Instructions" section lists every instruction file found in your workspace's instructions, copilot-instructions.md, etc. — regardless of whether they're currently being applied. Under the "Instructions" section, the report should display "3 files loaded", meaning that GitHub Copilot has discovered and loaded three instruction files into its registry. + + > **NOTE**: Path-specific instructions are automatically merged with repository-wide instructions when GitHub Copilot works with files that match the `applyTo` pattern. The `description` field is also used for semantic matching, so GitHub Copilot may include these instructions when you ask about controllers or services even if a matching file isn't open. + + If you open the "ContosoInventory/ContosoInventory.Server/Controllers/CategoriesController.cs" file in the editor, and then ask GitHub Copilot for coding guidance, it should reference both `copilot-instructions.md` and `controllers.instructions.md` in its response. If you open a service file, it should reference `copilot-instructions.md` and `services.instructions.md`. You can use this approach to verify that path-specific instructions are being applied correctly based on the file context. + +## Create a reusable prompt file for a common task + +In this task, you create a prompt file that defines a reusable slash command for generating API endpoint documentation. Prompt files standardize common tasks so your team can execute them consistently. + +Use the following steps to complete this task: + +1. In the **.github** folder, create a new folder named **prompts**. + +1. In the **.github/prompts/** folder, create a new file named **generate-api-docs.prompt.md**. + +1. Add the following content to **generate-api-docs.prompt.md**: + + ```markdown + --- + description: 'Generate API documentation for the active controller file' + agent: 'copilot' + tools: ['search', 'read'] + --- + # Generate API Documentation + + Analyze the controller in ${file} and generate comprehensive API documentation. + + For each endpoint in the controller: + 1. List the HTTP method and route. + 2. Describe the purpose of the endpoint. + 3. Document the request parameters, body, and query string inputs. + 4. Document the possible response status codes and their meanings. + 5. Provide an example request and response in JSON format. + + Format the output as a Markdown document suitable for a developer wiki. + ``` + + The frontmatter at the top of the prompt file defines metadata about the prompt. In this case, the frontmatter includes three fields: `description`, `agent`, and `tools`. + + The `description` field provides a human-readable explanation of what the prompt does, which is displayed in GitHub Copilot Chat when you view or execute the prompt. This description helps users understand the purpose of the prompt and when to use it. + + The `agent` field in the frontmatter specifies which agent handles the prompt when the slash command is invoked. Here it's set to `copilot` (the default agent), but you could set it to a custom agent name—for example, after creating the **reviewer** agent later in this exercise, you could change this field to `agent: 'reviewer'` to have documentation generated through the **reviewer**'s lens. The frontmatter also supports an optional `model` field for specifying a preferred AI model. + + The `tools` field specifies which tools the agent can use when executing the prompt. In this case, the prompt allows the agent to use "search" and "read" tools to gather context from the codebase. + + The prompt section provides the actual instructions that are sent to the AI model when the prompt is executed. The `${file}` variable is a placeholder that resolves to the contents of the currently active file in the editor. This means that when you run this prompt while having a controller file open, GitHub Copilot will receive the full contents of that controller file as context, allowing it to generate accurate and relevant API documentation based on the specific code in that file. + +1. Ensure that the file contents are left justified, and then save the file. + +1. To verify that the prompt file is registered as a slash command, open the GitHub Copilot Chat view, and then type `/` in the chat input. + + You should see `generate-api-docs` appear in the list of available slash commands. + + > **NOTE**: If the prompt file doesn't appear in the slash command list, verify that the `chat.promptFiles` setting is enabled in Visual Studio Code settings. You can check this by opening Settings (**Ctrl** + **,**) and searching for "prompt files." + +1. To test the prompt file, open the **ContosoInventory.Server/Controllers/CategoriesController.cs** file in the editor, and then enter the following command in GitHub Copilot Chat: + + ```plaintext + /generate-api-docs + ``` + +1. Take a couple minutes to review the response from GitHub Copilot. + + GitHub Copilot should analyze the CategoriesController and produce formatted API documentation for all six endpoints (GET all, GET by ID, POST, PUT, DELETE, and toggle-active). + + This example demonstrates how prompt files can be used to standardize common tasks across the team. In this case, the prompt could save a significant amount of time in generating the documentation because the existing controller has multiple endpoints with different HTTP methods, authorization levels, and response types. + +1. Notice that the `${file}` variable resolved correctly. + + Near the top of GitHub Copilot's response, the documentation should specifically reference `CategoriesController.cs` — the file you had open in the editor. The `${file}` variable provided the controller's contents as context to the prompt. If you close all editor tabs and run `/generate-api-docs` again, GitHub Copilot won't have a specific controller to analyze and will produce a more generic or less accurate response, demonstrating that the variable provides meaningful context. + +## Define a "Planner" custom agent with read-only tools + +In this task, you create a custom agent that acts as a planning assistant. The Planner agent analyzes feature requirements and generates implementation plans without writing or editing any code. It uses only read-only tools to gather context from the codebase. + +Use the following steps to complete this task: + +1. In the **.github** folder, create a new folder named **agents**. + +1. In the **.github/agents/** folder, create a new file named **planner.agent.md**. + +1. Add the following content to **planner.agent.md**: + + ```markdown + --- + description: Analyzes feature requirements and generates implementation plans without writing code + tools: ['search', 'read', 'fetch'] + handoffs: + - label: Start Implementation + agent: implementer + prompt: "Implement the plan outlined above. Follow the project's custom instructions for coding standards. Create all necessary files including models, DTOs, services, interfaces, and controllers." + send: false + - label: Write Tests First + agent: implementer + prompt: "Before implementing the feature, write unit tests based on the plan outlined above. Use xUnit and Moq following the project's testing conventions. Create test classes that cover the service methods and controller actions described in the plan. Do not implement the production code yet—only the tests." + send: false + --- + # Planner + + You are a senior software architect working on a C# ASP.NET Core Web API project. When the user describes a feature or change, analyze the request and generate a detailed implementation plan. + + Before creating a plan, always search the existing codebase to understand the current project structure, coding patterns, and dependencies already in place. + + Your plan MUST include: + 1. **Summary**: A brief overview of the feature and its purpose. + 2. **Files to create or modify**: A complete list of files that need to be created or changed, with their full paths. + 3. **Implementation steps**: Step-by-step tasks in logical dependency order. Each step should specify what to do and which file to work in. + 4. **Models and DTOs**: Define the data structures needed, including property names and types. + 5. **Service interface and implementation**: Outline the service methods needed and their signatures. + 6. **Controller endpoints**: List the API endpoints to create, including HTTP methods, routes, request/response types, and status codes. + 7. **Dependency injection**: Specify any service registrations needed in Program.cs. + 8. **Risks and considerations**: Highlight potential issues, edge cases, or decisions that need input. + + IMPORTANT RULES: + - Do NOT write or modify any code. Focus on planning only. + - Do NOT create files. Your role is advisory. + - Follow the project's established architecture patterns and coding standards. + - Ask clarifying questions if the requirements are ambiguous. + - Reference existing code patterns in the project for consistency. + ``` + +1. Take a minute to review the content of the planner.agent.md file. + + The frontmatter defines the agent's description, the tools it can use (search, read, fetch), and two handoffs: "Start Implementation" and "Write Tests First." Both handoffs target the implementer agent but with different prompts—one for writing production code and one for writing tests first. This gives you the flexibility to choose your development approach (implementation-first or test-driven) after reviewing the plan. You'll see both handoff buttons appear after the Planner produces a response in a later task. + + > **TIP**: Each handoff entry also supports an optional `model` field that specifies a different AI model for that stage of the workflow. The format is `model: "Model Name (vendor)"` (for example, `model: "GPT-5.5 (OpenAI)"` or `model: "Claude Sonnet 4.6 (Anthropic)"`). This is useful when different workflow stages benefit from different model capabilities—for instance, using a reasoning model for planning and a faster model for implementation. The available models depend on your GitHub Copilot subscription. + + The prompt section provides detailed instructions for how the Planner should analyze feature requests and generate comprehensive implementation plans without writing any code. The Planner is designed to be a strategic advisor that helps developers understand the scope of work and the necessary steps before diving into implementation. The handoffs allow the developer to choose whether to start with implementation or write tests first, demonstrating flexibility in development approaches. + +1. Ensure that the file contents are left justified, and then save the file. + +1. Close any files that you have open in the editor. + +1. Verify that the Planner agent appears in the GitHub Copilot Chat agents dropdown. + + Use the GitHub Copilot Chat view to open the agents dropdown (it usually says "GitHub Copilot" or the name of the current agent). You should see **planner** listed among the available agents. + + > **TIP**: You can also type `/agents` in GitHub Copilot Chat to quickly view and switch between all available agents. + +1. Select the **planner** agent. + + Notice that the chat input placeholder text changes to "Analyzes feature requirements and generates implementation plans without writing code" — this is the description you defined in the agent's frontmatter. + +1. To test the **planner** agent, enter the following prompt: + + ```plaintext + Analyze the project structure and describe the current architecture, including any existing models, services, and controllers. Identify the patterns used in the Category feature implementation. + ``` + +1. Take a couple minutes to review the response from the **planner** agent. + + The **planner** agent should use the `search` and `read` tools to examine the project files and provide a detailed overview of the current architecture. It should identify the Category model, CategoryService, CategoriesController, the DTO pattern, dependency injection setup, and the overall three-project architecture. Since the agent only has read-only tools, it won't attempt to modify any files. + +1. Switch back to the default GitHub Copilot agent by opening the agents dropdown and selecting **Agent**. + +## Define an "Implementer" custom agent with editing capabilities + +In this task, you create a custom agent that implements code changes based on plans or instructions. The Implementer agent has editing capabilities and follows the project's custom instructions for coding standards. + +Use the following steps to complete this task: + +1. In the **.github/agents/** folder, create a new file named **implementer.agent.md**. + +1. Add the following content to **implementer.agent.md**: + + ```markdown + --- + description: Implements code changes based on plans, following the project coding standards + tools: ['search', 'read', 'edit', 'terminal'] + handoffs: + - label: Review Code + agent: reviewer + prompt: "Review the code changes made in the implementation above. Check for bugs, security issues, naming convention violations, and adherence to the project's coding standards defined in the custom instruction files." + send: false + --- + # Implementer + + You are an expert C# developer working on an ASP.NET Core Web API project. Your role is to implement code changes based on plans, feature requests, or bug fix descriptions. + + WORKFLOW: + 1. Read the plan or request carefully before writing any code. + 2. Search the existing codebase to understand current patterns, naming conventions, and dependencies. + 3. Implement changes following the project's established patterns and the coding standards defined in the custom instruction files. + 4. Create files in the correct project directories. + 5. After completing the implementation, provide a summary of all files created or modified. + + IMPLEMENTATION RULES: + - Follow the repository pattern for data access. Create an interface and implementation for each service. + - Use dependency injection. Register new services in Program.cs. + - Use DTOs for API request and response payloads. Never expose database entities directly. + - Include XML documentation comments on all public methods and classes. + - Prefix private fields with an underscore. + - Use PascalCase for public members, camelCase for local variables. + - Include proper error handling with try-catch blocks for database and external API calls. + - Use async/await for I/O-bound operations. + - Return appropriate HTTP status codes from controller actions. + ``` + +1. Take a minute to review the content of the implementer.agent.md file. + + Notice that the **implementer** agent has both read and edit tools, allowing it to make code changes. The prompt provides detailed instructions for how the **implementer** should approach coding tasks while adhering to the project's standards. The handoff to the **reviewer** agent allows for a code review step after implementation, demonstrating how agents can work together in a workflow. + +1. Ensure that the file contents are left justified, and then save the file. + +1. Verify that the **implementer** agent appears in the agents dropdown. + + Open the GitHub Copilot Chat agents dropdown. You should now see both **planner** and **implementer** listed. + +## Define a "Reviewer" custom agent for code quality checks + +In this task, you create a custom agent that reviews code for bugs, security issues, and adherence to coding standards. The **reviewer** agent uses only read-only tools, ensuring it doesn't accidentally modify code during its analysis. + +Use the following steps to complete this task: + +1. In the **.github/agents/** folder, create a new file named **reviewer.agent.md**. + +1. Add the following content to **reviewer.agent.md**: + + ```markdown + --- + description: Reviews code for bugs, security issues, and coding standards compliance + tools: ['search', 'read'] + handoffs: + - label: Fix Issues + agent: implementer + prompt: "Fix the issues identified in the code review above. Address each finding in order of severity, starting with Critical and High issues first." + send: false + --- + # Code Reviewer + + You are an experienced code reviewer specializing in C# and ASP.NET Core applications. When asked to review code, examine it thoroughly for issues across the following categories: + + ## Review Checklist + 1. **Bugs and logical errors**: Look for null reference risks, off-by-one errors, race conditions, and incorrect logic. + 2. **Security vulnerabilities**: Check for SQL injection, missing input validation, hardcoded secrets, missing authentication/authorization, and insecure data handling. + 3. **Naming convention violations**: Verify adherence to the project's naming standards (PascalCase for public members, underscore prefix for private fields, etc.). + 4. **Architecture compliance**: Confirm the code follows the repository pattern, uses dependency injection, and separates concerns properly. + 5. **Error handling**: Ensure try-catch blocks are present for external calls, appropriate HTTP status codes are returned, and exceptions are logged. + 6. **Missing documentation**: Flag public methods or classes that lack XML documentation comments. + 7. **Performance issues**: Identify unnecessary allocations, missing async/await, or inefficient queries. + + ## Output Format + Present your findings as a structured review: + - Group findings by severity: **Critical**, **High**, **Medium**, **Low** + - For each finding, include: + - The file and location + - A description of the issue + - A suggested fix + - End with an **Overall Assessment** summarizing the code quality and any patterns of concern. + + IMPORTANT: Do NOT modify any files. Your role is advisory only. + ``` + +1. Take a minute to review the content of the reviewer.agent.md file. + + The **reviewer** agent is designed to provide comprehensive code reviews without making any changes to the codebase. It uses only read-only tools to analyze the code and produce a structured report of findings. The handoff back to the **implementer** allows developers to address the identified issues, creating a feedback loop between implementation and review. + +1. Ensure that the file contents are left justified, and then save the file. + +1. Verify that all three agents appear in the agents dropdown. + + Open the GitHub Copilot Chat agents dropdown. You should now see **planner**, **implementer**, and **reviewer** listed alongside the built-in agents. + +1. Take a moment to consider the agent chain that you've configured between the **planner**, **implementer**, and **reviewer** agents. + + The handoff configuration creates the following workflow: + + - **planner** → (Start Implementation) → **implementer**: After the **planner** agent produces a plan, you can hand off to the **implementer** to write the code. + - **implementer** → (Review Code) → **reviewer**: After the **implementer** writes code, you can hand off to the **reviewer** to check the code quality. + - **reviewer** → (Fix Issues) → **implementer**: If the **reviewer** finds issues, you can hand off back to the **implementer** to apply fixes. + + Each handoff uses `send: false`, which means the prompt is prefilled for you to review and edit before submitting. This behavior keeps you in control at every step of the workflow. + + The `send` field controls whether the handoff prompt is submitted automatically. When set to `false` (the default), the prompt is prefilled in the chat input for you to review and optionally edit before sending. When set to `true`, the prompt is submitted immediately and the target agent begins working right away without waiting for your approval. All handoffs in this exercise use `send: false` to keep you in control at each transition. In fully automated pipelines where you trust the agent chain to operate without supervision, `send: true` can streamline the workflow—but it removes the human-in-the-loop checkpoint. + + > **NOTE**: By default, custom agents run on the client (inside Visual Studio Code). For long-running tasks—such as building an entire feature or running a comprehensive test suite—you can set `target: cloud` in the agent's YAML frontmatter to run the agent remotely. Cloud agents free your local Visual Studio Code instance while the agent processes in the background. Background agents are a related concept: they run independently without blocking the chat interface, allowing you to continue working while the agent completes its task. The available execution environments depend on your GitHub Copilot subscription. + +## Run the chained agents workflow to complete a development task end-to-end + +In this task, you run the full Planning → Implementation → Review workflow using your custom agents to add a new inventory management feature to the project. This task demonstrates how chaining agents creates a structured, multi-step development process. + +Use the following steps to complete this task: + +1. Open the GitHub Copilot Chat view and select the **planner** agent from the agents dropdown. + +1. Enter the following prompt to request a feature plan: + + ```plaintext + I need to add a Product Inventory management feature to this Web API, following the same patterns used by the existing Category feature. The feature should support: + - A Product model with: Id (int), Name (string), Sku (string), Description (string), Price (decimal), StockQuantity (int), CategoryId (int, foreign key to Category), CreatedDate (DateTime), LastUpdatedDate (DateTime). + - CRUD operations for products (Create, Read by ID, Read all with optional filtering by CategoryId, Update, Delete). + - A restock endpoint that increases the StockQuantity for a specific product. + - Input validation on all create/update operations. + - Authorization: Admin role required for create, update, delete, and restock operations. All authenticated users can read. + - Use Entity Framework Core with the existing SQLite database (add Product to the DbContext and create a migration). + - Follow the same service interface, DTO, and controller patterns used in the Category feature. + ``` + + The Planner agent should search the existing codebase, analyze the Category implementation patterns (model, DTOs, service interface, service implementation, controller), and produce a detailed implementation plan that mirrors the established architecture. + +1. Take a couple minutes to review the plan produced by the Planner agent. + + Verify that the plan includes the following elements: + + - Lists all files to be created (Product model, Product DTOs, IProductService interface, ProductService implementation, ProductsController). + - Follows the naming conventions from your custom instructions (PascalCase, underscore prefix for private fields). + - Uses the same service layer pattern established in the existing CategoryService. + - Includes DTOs for API payloads rather than exposing entities directly. + - Recognizes the CategoryId foreign key relationship between Product and Category. + - Specifies dependency injection registrations in Program.cs. + - Includes a migration step for adding the Product table to the SQLite database. + - Mentions authorization requirements (Admin-only for write operations). + + > **NOTE**: If the plan doesn't align with your custom instructions or the existing Category patterns, you can ask the Planner to revise it. For example: "Revise the plan to follow the same DTO patterns used in the Category feature, including separate CreateProductDto, UpdateProductDto, and ProductResponseDto types." + +1. When you're satisfied with the plan, select the **Start Implementation** handoff button. + + You should see two handoff buttons at the end of the Planner's response: **"Start Implementation"** and **"Write Tests First."** Select **"Start Implementation"** to proceed with the implementation-first approach. When you select it, GitHub Copilot Chat: + + - Switches to the **implementer** agent. + - Carries over the conversation history, including the full plan. + - Prefills the prompt text: "Implement your suggested plan ..." + + > **NOTE**: The "Write Tests First" button is available if you prefer a test-driven development approach. It sends a different prompt to the Implementer that focuses on creating unit tests before production code. For this exercise, you'll use "Start Implementation" to proceed with the main workflow. + +1. Review the prefilled prompt and select **Send** (or press **Enter**) to submit it. + + The **implementer** agent begins writing the code based on the planner's plan. It creates the models, DTOs, service interface, service implementation, and controller. + +1. Monitor the chat for updates from the **implementer** agent as it works through the implementation. + + Notice that the agent creates files and follows the coding standards from your custom instruction files. + + > **NOTE**: The implementation may take a few minutes depending on the complexity of the plan. The implementer agent uses the `edit` and `terminal` tools to create files and potentially run build commands. + +1. After the implementer agent finishes, review the summary of files created. + + The implementer should have created files similar to the following (exact names and locations might vary based on the plan): + + - **ContosoInventory.Server/Models/Product.cs**: The product entity with CategoryId foreign key. + - **ContosoInventory.Shared/DTOs/ProductResponseDto.cs** (or similar): Response DTO. + - **ContosoInventory.Shared/DTOs/CreateProductDto.cs** (or similar): Creation DTO with validation attributes. + - **ContosoInventory.Shared/DTOs/UpdateProductDto.cs** (or similar): Update DTO with validation attributes. + - **ContosoInventory.Server/Services/IProductService.cs**: The service interface. + - **ContosoInventory.Server/Services/ProductService.cs**: The service implementation using EF Core. + - **ContosoInventory.Server/Controllers/ProductsController.cs**: The API controller with CRUD, restock, and authorization. + - Updated **ContosoInventory.Server/Data/InventoryContext.cs**: Added `DbSet` and model configuration. + - Updated **ContosoInventory.Server/Program.cs**: Service registration via dependency injection. + - A new EF Core migration for the Product table. + +1. Build the project and verify that the generated code compiles. + + In the terminal, ensure you're in the **ContosoInventory.Server** directory, and then enter: + + ```powershell + dotnet build + ``` + + If the build has errors, you can ask the implementer agent to fix them. + + For example, complete the following steps: + + 1. Enter the following prompt in the chat: + + ```plaintext + The build has the following errors. Please fix them." + ``` + + 1. Paste the error output in the chat. + + 1. Submit the prompt and wait for the implementer to apply fixes. + +1. Select the **Review Code** handoff button to switch to the **reviewer** agent. + + The **reviewer** agent receives the full conversation context, including the plan and the implementation. The prefilled prompt asks it to review the code changes. + +1. Review the prefilled prompt and select **Send** to submit it. + + The **reviewer** agent examines all the created files, checking for bugs, security issues, naming convention violations, and compliance with the project's coding standards. It presents findings grouped by severity. + +1. Take a couple minutes to review the reviewer agent's findings. + + The review should check that: + + - Naming conventions match the custom instructions (PascalCase for public members, underscore prefix for private fields). + - The service layer pattern is followed consistently with the existing CategoryService (interface/implementation separation). + - DTOs are used for API payloads instead of entities. + - Error handling includes try-catch blocks and proper HTTP status codes. + - XML documentation comments are present on public methods. + - Input validation is implemented on create/update operations. + - Authorization attributes are applied correctly (Admin-only for write operations). + - The CategoryId foreign key relationship is properly configured. + + > **NOTE**: The **reviewer** may identify issues that the **implementer** missed. This is expected and demonstrates the value of having specialized agents review each other's work. The **reviewer** can compare the new Product code directly against the existing Category implementation for consistency. + +1. If the **reviewer** identifies issues, select the **Fix Issues** handoff button, and then monitor the chat. + + Selecting **Fix Issues** passes responsibility back to the **implementer** agent with a prompt to fix the issues identified in the review. The **implementer** addresses each finding in order of severity. + + > **IMPORTANT**: You need to monitor the chat as the **implementer** applies fixes based on the review feedback. GitHub Copilot may require assistance. + + The implementer should make necessary code changes to address critical and high-severity issues first, then move on to medium and low-severity findings. + + After the fixes are applied, use a `dotnet build` command to verify the project builds successfully. + +1. Optionally, test the API by running the application. + + Start the application: + + ```powershell + dotnet run + ``` + + If the application fails to start, you can ask the Implementer agent to help debug the issue. For example: "The application fails to start with the following error. Please help me fix it." Then paste the error output. + + If the application starts successfully, you can test the new Product endpoints using Swagger UI or a tool like Postman. + + To test the application using Swagger UI, complete the following steps: + + 1. Open a browser and navigate to the Swagger UI at `http://localhost:5240/swagger`. + + You can test the new Product endpoints alongside the existing Category endpoints. + + 1. To authenticate as the Admin user, use **POST /api/auth/login** with `mateo@contoso.com` / `Password123!`. + + 1. To test the Product endpoints, try the following operations: + + - **POST /api/products**: Create a new product with a JSON body. Use a valid `CategoryId` from the existing categories (for example, 1 for "Laptops & Desktops"). + - **GET /api/products**: List all products (try filtering by CategoryId if supported). + - **GET /api/products/{id}**: Get a specific product. + - **PUT /api/products/{id}**: Update a product. + - **DELETE /api/products/{id}**: Delete a product. + - **POST /api/products/{id}/restock** (or similar): Restock a product. + + When you're done testing, press **Ctrl+C** in the terminal to stop the application. + +## Summary + +In this exercise, you successfully configured and customized GitHub Copilot in Visual Studio Code for the ContosoInventory C# Web API project. You: + +- **Imported and reviewed the ContosoInventory starter application** to understand the existing three-project architecture (Server, Client, Shared), Category feature implementation, and security configuration with role-based authorization. +- **Explored the `/init` shortcut** to autogenerate a starter instruction file, then **created repository-level custom instructions** (`.github/copilot-instructions.md`) with the client's specific coding standards—naming conventions, architecture patterns, error handling, and documentation requirements—embedded into every GitHub Copilot Chat interaction. +- **Created path-specific instruction files** (`.github/instructions/*.instructions.md`) that provide targeted guidance for controllers and services, using `applyTo` glob patterns to match specific file locations. +- **Created a reusable prompt file** (`.github/prompts/generate-api-docs.prompt.md`) that standardizes API documentation generation as a slash command, using the `${file}` variable to pass the active editor file as context, and tested it against the existing CategoriesController. +- **Defined three custom agents** (Planner, Implementer, and Reviewer) with tailored instructions, tool permissions, and behavioral guidelines for each development role. +- **Configured handoffs** between agents—including **multiple handoffs** on the Planner agent offering both implementation-first and test-first paths—to create a structured Planning → Implementation → Review workflow with developer oversight at each transition. +- **Ran the complete chained workflow** to add a Product Inventory feature end-to-end, using the existing Category feature as a reference architecture. The agents collaborated to create a Product model with a CategoryId foreign key, DTOs, service interface and implementation, and a controller with authorization—all following the project's established coding standards. + +This pattern—embedding team knowledge in instruction files and orchestrating specialized agents through handoffs—is applicable to any development project. You can adapt these techniques for other scenarios such as Test-Driven Development (Test Writer → Implementer), debugging workflows (Debugger → Patcher → Tester), or migration projects (Analyzer → Upgrader → Reviewer). + +## Clean up + +Now that you've finished the exercise, take a minute to clean up your environment: + +- Stop the application if it's still running (press **Ctrl+C** in the terminal). +- Optionally archive or delete the project directory. +- If you created a private ContosoInventory repository in your GitHub account, you can delete it by going to the repository **Settings** tab and selecting **Delete this repository** at the bottom of the page. diff --git a/Instructions/Labs/LAB_AK_16_develop_ai_enabled_apps_github_copilot_sdk.md b/Instructions/Labs/LAB_AK_16_develop_ai_enabled_apps_github_copilot_sdk.md new file mode 100644 index 0000000..19b62be --- /dev/null +++ b/Instructions/Labs/LAB_AK_16_develop_ai_enabled_apps_github_copilot_sdk.md @@ -0,0 +1,2193 @@ +--- +lab: + title: Exercise - Integrate an AI Agent into existing apps using GitHub Copilot SDK + description: Learn how to integrate an AI Agent into existing applications using GitHub Copilot SDK to automate tasks and enhance functionality. + level: 300 + duration: 60 minutes + islab: true + primarytopics: + - GitHub + - Visual Studio Code +--- + +# Integrate an AI Agent into existing apps using GitHub Copilot SDK + +The GitHub Copilot SDK exposes the same engine behind GitHub Copilot CLI as a programmable SDK. It allows you to embed agentic AI workflows in your applications, including custom tools that let the AI call your code. + +In this exercise, you integrate an AI-powered customer support agent into the ContosoShop E-commerce Support Portal. By the end, the "Contact Support" page allows a user to ask questions (for example, "Where is my order?" or "I need to return an item") and receive helpful, automated answers from an AI agent. The agent uses backend tools (like checking order status or initiating a return) to resolve queries. + +This exercise should take approximately **60** minutes to complete. + +> **IMPORTANT**: To complete this exercise, you must provide your own GitHub account and GitHub Copilot subscription. If you don't have a GitHub account, you can sign up for a free individual account and use a GitHub Copilot Free plan to complete the exercise. If you have access to a GitHub Copilot Pro, GitHub Copilot Pro+, GitHub Copilot Business, or GitHub Copilot Enterprise subscription from within your lab environment, you can use your existing GitHub Copilot subscription to complete this exercise. + +## Before you start + +Your lab environment MUST include the following resources: + +- Git 2.48 or later. +- The .NET SDK version 8.0 or later. +- Access to a GitHub account with GitHub Copilot enabled. +- Visual Studio Code with the C# Dev Kit and GitHub Copilot Chat extensions. +- GitHub Copilot CLI installed and authenticated with your GitHub account. + +For help with configuring your lab environment, open the following link in a browser: Configure your GitHub Copilot SDK lab environment. + +## Exercise scenario + +You're a software developer working for a consulting firm. The firm developed the ContosoShop E-commerce Support Portal (a Blazor WebAssembly application with an ASP.NET Core backend) for a client. Application features enable a user to (manually) review their order history, track shipments, examine order details, and return items. The client asks you to add an AI-powered customer support agent to the "Contact Support" page. The agent needs to provide automated assistance for the customer, such as looking up order details and initiating returns. You decide to use the GitHub Copilot SDK to build a custom AI agent that can handle customer queries and perform actions on their behalf. + +The ContosoShop E-commerce Support Portal application uses a three-project architecture: + +- **ContosoShop.Server**: ASP.NET Core Web API with Entity Framework Core, Identity authentication, and SQLite. +- **ContosoShop.Client**: Blazor WebAssembly SPA that runs in the browser and calls the server API. +- **ContosoShop.Shared**: Shared class library containing models, DTOs, and enums. + +For the purposes of this lab exercise, you can test the application using two user accounts (Mateo Gomez and Megan Bowen). A total of 20 customer orders are split between the two user accounts. The customer orders are in various tracking stages (Processing, Shipped, Delivered, Returned, and Partially Returned). + +This exercise includes the following tasks: + +1. Review features of the ContosoShop application. +1. Install the GitHub Copilot SDK components. +1. Create the agent tools service. +1. Configure the GitHub Copilot SDK agent and expose an API endpoint. +1. Update the Blazor frontend to interact with the agent. +1. Test the end-to-end AI agent experience. + +## Review features of the ContosoShop application + +Before developing the AI customer support agent, you need to become familiar with the existing application features. + +Use the following steps to complete this task: + +1. Open a browser window and navigate to GitHub.com. + + You can log in to your GitHub account using the following URL: GitHub login. + +1. Sign in to your GitHub account, and then open your repositories tab. + + You can open your repositories tab by clicking on your profile icon in the top-right corner, then selecting **Repositories**. + +1. On the Repositories tab, select the **New** button. + +1. Under the **Create a new repository** section, select **Import a repository**. + +1. On the **Import your project to GitHub** page, under **Your source repository details**, enter the following URL for the source repository: + + ```plaintext + https://github.com/MicrosoftLearning/github-copilot-sdk-starter-app + ``` + +1. Under the **Your new repository details** section, in the **Owner** dropdown, select your GitHub username. + +1. In the **Repository name** field, enter **ContosoShop** + + GitHub automatically checks the availability of the repository name. If this name is already taken, append a unique suffix (for example, your initials or a random number) to the repository name to make it unique. + +1. To create a private repository, select **Private**, and then select **Begin import**. + + GitHub uses the import process to create the new repository in your account. It can take a minute or two for the import process to finish. Wait for the import process to complete. + + > **IMPORTANT**: If you're using the GitHub Copilot Free plan, you should create the repository as **Public** to ensure that you have access to GitHub Copilot features. If you have a Pro, Pro+, Business, or Enterprise subscription, you can create the repository as **Private**. + + GitHub displays a progress indicator and notifies you when the import is complete. + +1. Once the import is complete, open your new repository. + + A link to your repository should be displayed. Your repository should be located at: `https://github.com/YOUR-USERNAME/ContosoShop`. + + You can create a local clone of your ContosoShop repository and then initialize GitHub Spec Kit within the project directory. + +1. On your ContosoShop repository page, select the **Code** button, and then copy the HTTPS URL. + + The URL should be similar to: `https://github.com/YOUR-USERNAME/ContosoShop.git` + +1. Open a terminal window in your development environment, and then navigate to the location where you want to create the local clone of the repository. + + For example: + + Open a terminal window (Command Prompt, PowerShell, or Terminal), and then run: + + ```powershell + cd C:\TrainingProjects + ``` + + Replace `C:\TrainingProjects` with your preferred location. You can use any directory where you have write permissions, and you can create a new folder location if needed. + +1. To clone your ContosoShop repository, enter the following command: + + Be sure to replace `YOUR-USERNAME` with your actual GitHub username before running the command. + + ```powershell + git clone https://github.com/YOUR-USERNAME/ContosoShop.git + ``` + + You might be prompted to authenticate using your GitHub credentials during the clone operation. You can authenticate using your browser. + +1. To navigate into your ContosoShop directory and open it in Visual Studio Code, enter the following commands: + + ```powershell + cd ContosoShop + code . + ``` + +1. Take a moment to review the project structure. + + Use Visual Studio Code's EXPLORER view to expand the project folders. You should see a folder structure that's similar to the following example: + + ```plaintext + github-copilot-sdk-starter-app (root) + ├── ContosoShop.Client/ (Blazor WebAssembly frontend) + │ ├── Layout/ (MainLayout, NavMenu) + │ ├── Pages/ (Home, Login, Orders, OrderDetails, Support, Inventory) + │ ├── Services/ (OrderService, CookieAuthenticationStateProvider) + │ └── Shared/ (OrderStatusBadge) + ├── ContosoShop.Server/ (ASP.NET Core backend) + │ ├── App_Data/ (used for the SQLite database file) + │ ├── Controllers/ (AuthController, OrdersController, InventoryController) + │ ├── Data/ (ContosoContext, DbInitializer, Migrations) + │ ├── Services/ (OrderService, InventoryService, EmailServiceDev) + │ └── Program.cs (App configuration and middleware) + ├── ContosoShop.Shared/ (Shared class library) + │ ├── DTOs/ (InventorySummary, ReturnItemRequest) + │ └── Models/ (Order, OrderItem, Product, User, etc.) + └── ContosoShopSupportPortal.slnx (Solution file) + ``` + +1. Open the **ContosoShop.Server/Program.cs** file and review the application configuration. + + Notice the following key configuration areas: + + - Entity Framework Core with SQLite for data access + - ASP.NET Core Identity for authentication with cookie-based sessions + - Service registrations for `IEmailService`, `IInventoryService`, and `IOrderService` + - Database seeding via `DbInitializer.InitializeAsync` at startup + - CORS, rate limiting, cross-site request forgery (CSRF) protection, and security headers middleware + +1. Open the **ContosoShop.Server/Controllers/OrdersController.cs** file and note the existing API endpoints. + + The orders controller provides the following endpoints for managing orders: + + - `GetOrders`: Gets all orders for the authenticated user + - `GetOrder`: Gets a specific order with items (verifies ownership) + - `ReturnOrderItems`: Processes item-level returns for a delivered order + +1. Open the **ContosoShop.Server/Services/OrderService.cs** file and review the `ProcessItemReturnAsync` method. + + The ProcessItemReturnAsync method processes customer returns for order items. It performs several critical operations to ensure that returns are handled correctly while maintaining data integrity and providing a good customer experience. + + Key Operations: + + - Validates order exists and is returnable (Delivered/Returned/PartialReturn status) + - Verifies return quantities don't exceed available amounts + - Creates OrderItemReturn records with refund calculations + - Restores inventory stock via _inventoryService + - Updates order status (Returned or PartialReturn based on items) + - Sends email confirmation with refund details + +1. Open a terminal in the **ContosoShop.Server** directory and build the solution. + + ```powershell + cd ContosoShop.Server + dotnet build + ``` + + > **IMPORTANT**: The project uses .NET 8 by default. If you have the .NET 9 or .NET 10 SDK installed, but not .NET 8, you need to update the project to target the version of .NET that you have installed. For AI assistance with updating to a later version of .NET, open the GitHub Copilot Chat view and ask GitHub Copilot to update your project files to the version of .NET that you have installed in your environment. For example, you can ask: "I need to update this .NET 8 project to target .NET 10. Please: 1. Update all .csproj files to target net10.0; 2. Update all NuGet packages to .NET 10-compatible versions; 3. Update global.json (if present); 4. Address any breaking changes or deprecated APIs between .NET 8 and .NET 10; 5. Ensure ALL projects in the solution build successfully. Please explain any significant changes or potential issues I should be aware of." When you enter your prompt, the AI assistant should update your codebase and explain what was changed. + + The build should complete successfully without errors (there might be warnings). + +1. Start the server application. + + ```powershell + dotnet run + ``` + + > **NOTE**: The first time you run the application, it may take a little extra time to apply database migrations and seed the database with sample data. You should see console output indicating that the database has been initialized and seeded. You should also see a message that the server starts listening on `http://localhost:5266`. + +1. Open a browser and navigate to `http://localhost:5266`. + + The application should open to the ContosoShop login page. If necessary, accept any certificate warnings for the localhost development certificate. + +1. Sign in using the demo credentials. + + Enter `mateo@contoso.com` for the email and `Password123!` for the password, and then select **Login**. + +1. Verify that the My Orders page displays a list of orders. + + You should see 10 orders for Mateo with various statuses (Processing, Shipped, Delivered, Returned). + +1. On the My Orders page, select the **View Details** button for order #1004. + + The application should navigate to the order details page for the selected order. The page should display the order summary, including the order status, order date, total amount, and a list of items in the order. + +1. Take a moment to review the order details, and then select the **Return Items** button. + + The page should update to display **Return** and **Return Qty** columns. The **Return** column contains checkboxes, and the **Return Qty** column contains input fields for specifying the quantity to return. + +1. In the **Return** column, select the checkbox for the **Monitor** item, and then enter **1** in the corresponding **Return Qty** field. + + This selection indicates that you want to return one monitor item from the order. + +1. Select the **Submit Return (1 item)** button. + + The application should process the return request, display a success message, and update the order status to "Partial Return". The order details for the monitor item should show a "Returned 1 of 3" badge, and a Returned column should show that one monitor item was returned. + +1. To open a page that displays Contoso's product inventory, select **View Inventory** on the navigation menu. + + > **NOTE**: The Inventory Management page is included for lab purposes only, so that you can verify that a return has been processed correctly. The page should display a list of products with their available stock. + +1. Verify that the stock for the Monitor product has been replenished by one unit after processing the return in the previous steps. + + The **Returned** column for the **Monitor** product (Item Number: ITM-003) should show that one item has been returned. When an item is returned, the stock is replenished by the returned quantity. + +1. To open the Customer Support page, select **Contact Support** on the navigation menu. + + You should see contact information and a message that states "Interactive AI Chat Support Coming Soon". You'll update this Customer Support page in upcoming tasks. The corresponding project file is: **ContosoShop.Client/Pages/Support.razor**. + +1. To log out from the application, select **Logout** on the navigation menu. + + The application should log you out and navigate back to the Login page. + +1. To stop the application, return to the Visual Studio Code integrated terminal where the server is running, and then press **Ctrl+C**. + + > **NOTE**: You can leave the terminal open for the next task. + +## Install the GitHub Copilot SDK components + +In this task, you add the GitHub Copilot SDK NuGet package and the Microsoft.Extensions.AI package to the server project. The GitHub Copilot SDK provides the core components for building AI agents, while Microsoft.Extensions.AI provides types for defining custom tools that the agent can call. + +Use the following steps to complete this task: + +1. Ensure that you have Visual Studio Code's integrated terminal open and that you're located in the **ContosoShop.Server** directory. + +1. In terminal, to verify that the GitHub Copilot CLI is installed and authenticated, enter the following command: + + ```powershell + copilot --version + ``` + + You should see a version number (for example, `0.0.407`). If the command isn't found, use the following instructions to finish preparing the lab environment Configure your GitHub Copilot SDK lab environment. + + > **NOTE**: The GitHub Copilot SDK communicates with the Copilot CLI in server mode. The SDK manages the CLI process lifecycle automatically, but the CLI must be installed and accessible in your PATH. + +1. To configure the GitHub Copilot SDK NuGet package to your project, enter the following command: + + ```powershell + dotnet add package GitHub.Copilot.SDK --prerelease + ``` + + This command installs the latest preview version of the SDK. The SDK provides `CopilotClient`, `CopilotSession`, and related types for building AI agents. + + > **NOTE**: While the GitHub Copilot SDK is in Technical Preview, the `--prerelease` flag is required to install it. + +1. To add the `Microsoft.Extensions.AI` package to your project, enter the following command: + + ```powershell + dotnet add package Microsoft.Extensions.AI + ``` + + The GitHub Copilot SDK uses `Microsoft.Extensions.AI` for defining custom tools. This package provides the `AIFunctionFactory` and related types for creating tools that the AI agent can call. + +1. To verify the packages installed correctly, build the project: + + ```powershell + dotnet build + ``` + + The build should succeed without errors. + +## Create the agent tools service + +In this task, you create a new service class in the server project that implements the tools the AI agent uses to look up orders and process returns. This service will be registered in dependency injection and called by the AI agent when handling user queries. + +Use the following steps to complete this task: + +1. In Visual Studio Code's EXPLORER view, right-click the **ContosoShop.Server/Services** folder, and then select **New File**. + + You'll use this file to create the SupportAgentTools service class. + +1. Name the file **SupportAgentTools.cs**. + +1. Add the following code to the **SupportAgentTools.cs** file: + + ```csharp + using ContosoShop.Server.Data; + using ContosoShop.Shared.Models; + using ContosoShop.Shared.DTOs; + using Microsoft.EntityFrameworkCore; + + namespace ContosoShop.Server.Services; + + /// + /// Provides tool functions that the AI support agent can invoke + /// to look up order information and process returns. + /// + public class SupportAgentTools + { + private readonly ContosoContext _context; + private readonly IOrderService _orderService; + private readonly IEmailService _emailService; + private readonly ILogger _logger; + + public SupportAgentTools( + ContosoContext context, + IOrderService orderService, + IEmailService emailService, + ILogger logger) + { + _context = context; + _orderService = orderService; + _emailService = emailService; + _logger = logger; + } + + // add the `GetOrderDetailsAsync` method here + + + // add the `GetUserOrdersSummaryAsync` method here + + + // add the `ProcessReturnAsync` method here + + + // add the `SendCustomerEmailAsync` method here + + } + ``` + + This code sets up the class skeleton with dependency injection. The constructor receives four dependencies: + + - `ContosoContext`: the Entity Framework Core database context for querying orders and users directly. + - `IOrderService`: the existing service that handles return processing logic, inventory updates, and email confirmations. + - `IEmailService`: the service used to send follow-up emails to customers. + - `ILogger`: a logger for recording each tool invocation, which is useful for debugging and monitoring agent behavior. + + These dependencies allow the tools to access real data and use existing business logic rather than duplicating it. + +1. Inside the SupportAgentTools class (after the constructor's closing brace), add the following GetOrderDetailsAsync method: + + ```csharp + /// + /// Gets the status and details of a specific order by order ID. + /// The AI agent calls this tool when a user asks about their order status. + /// + public async Task GetOrderDetailsAsync(int orderId, int userId) + { + _logger.LogInformation("Agent tool invoked: GetOrderDetails for orderId {OrderId}, userId {UserId}", orderId, userId); + + var order = await _context.Orders + .Include(o => o.Items) + .FirstOrDefaultAsync(o => o.Id == orderId && o.UserId == userId); + + if (order == null) + { + return $"I could not find order #{orderId} associated with your account. Please double-check the order number."; + } + + var statusMessage = order.Status switch + { + OrderStatus.Processing => "is currently being processed and has not shipped yet", + OrderStatus.Shipped => order.ShipDate.HasValue + ? $"was shipped on {order.ShipDate.Value:MMMM dd, yyyy} and is on its way" + : "has been shipped and is on its way", + OrderStatus.Delivered => order.DeliveryDate.HasValue + ? $"was delivered on {order.DeliveryDate.Value:MMMM dd, yyyy}" + : "has been delivered", + OrderStatus.PartialReturn => "has been partially returned (some items have been returned, others are still with you)", + OrderStatus.Returned => "has been fully returned and a refund was issued", + _ => "has an unknown status" + }; + + var itemSummary = string.Join(", ", order.Items.Select(i => + { + var itemInfo = $"{i.ProductName} (Id: {i.Id}, qty: {i.Quantity}, ${i.Price:F2} each"; + if (i.ReturnedQuantity > 0) + { + itemInfo += $", {i.ReturnedQuantity} returned, {i.RemainingQuantity} remaining"; + } + itemInfo += ")"; + return itemInfo; + })); + + return $"Order #{order.Id} {statusMessage}. " + + $"Order date: {order.OrderDate:MMMM dd, yyyy}. " + + $"Total: ${order.TotalAmount:F2}. " + + $"Items: {itemSummary}."; + } + ``` + +1. Take a minute to review the GetOrderDetailsAsync method. + + This code snippet is the first agent tool. The AI agent calls this method when a customer asks about a specific order. The method queries the database for the order (including its items), verifies that the order belongs to the authenticated user via `userId`, and builds a natural language response. A C# `switch` expression translates the `OrderStatus` enum into human-readable phrases, including `PartialReturn` for orders where some items have been returned. The item summary lists each product with its database `Id`, quantity, and price; items that have been partially returned also show their `ReturnedQuantity` and `RemainingQuantity`. Including the item `Id` in the output is critical because the AI agent uses it when calling the `process_return` tool for partial returns. If the order isn't found, the method returns a friendly error message rather than throwing an exception, which is important because the AI agent presents the return value directly to the customer. + +1. On a code line below the GetOrderDetailsAsync method, add the following GetUserOrdersSummaryAsync method: + + ```csharp + /// + /// Gets a summary of all orders for a given user. + /// The AI agent calls this tool when a user asks about their orders + /// without specifying a particular order number. + /// + public async Task GetUserOrdersSummaryAsync(int userId) + { + _logger.LogInformation("Agent tool invoked: GetUserOrdersSummary for userId {UserId}", userId); + + var orders = await _context.Orders + .Where(o => o.UserId == userId) + .OrderByDescending(o => o.OrderDate) + .ToListAsync(); + + if (!orders.Any()) + { + return "You don't have any orders on file."; + } + + var summaries = orders.Select(o => + { + var status = o.Status switch + { + OrderStatus.Processing => "Processing", + OrderStatus.Shipped => "Shipped", + OrderStatus.Delivered => "Delivered", + OrderStatus.PartialReturn => "Partial Return", + OrderStatus.Returned => "Returned", + _ => "Unknown" + }; + return $"Order #{o.Id} - {status} - ${o.TotalAmount:F2} - Placed {o.OrderDate:MMM dd, yyyy}"; + }); + + return $"You have {orders.Count} orders:\n" + string.Join("\n", summaries); + } + ``` + +1. Take a minute to review the GetUserOrdersSummaryAsync method. + + This tool complements `GetOrderDetailsAsync` by handling cases where the customer asks about their orders without specifying a particular order number (for example, "What are my recent orders?"). It retrieves all orders for the user, sorted by date in descending order, and formats each one as a concise summary line showing the order number, status, total, and date. The AI agent uses this overview to help the customer identify the order they're interested in. + +1. On a code line below the GetUserOrdersSummaryAsync method, add the following ProcessReturnAsync method: + + ```csharp + /// + /// Processes a return for specific items in a delivered order. + /// The AI agent calls this tool when a user wants to return items. + /// Supports returning all items, specific items by ID, or specific quantities. + /// + /// The order ID to process returns for + /// The authenticated user ID + /// Optional: Specific order item IDs to return (comma-separated, e.g., "123,456"). If empty, returns all unreturned items. + /// Optional: Quantities for each item (comma-separated, e.g., "1,2" for items 123 and 456). Must match orderItemIds length. If empty, returns full remaining quantity for each item. + /// Optional: Reason for the return + public async Task ProcessReturnAsync( + int orderId, + int userId, + string orderItemIds = "", + string quantities = "", + string reason = "Customer requested return via AI support agent") + { + _logger.LogInformation("Agent tool invoked: ProcessReturn for orderId {OrderId}, userId {UserId}, items: {Items}", + orderId, userId, string.IsNullOrEmpty(orderItemIds) ? "all" : orderItemIds); + + var order = await _context.Orders + .Include(o => o.Items) + .FirstOrDefaultAsync(o => o.Id == orderId && o.UserId == userId); + + if (order == null) + { + return $"I could not find order #{orderId} associated with your account."; + } + + if (order.Status != OrderStatus.Delivered && order.Status != OrderStatus.Returned && order.Status != OrderStatus.PartialReturn) + { + return order.Status switch + { + OrderStatus.Processing => $"Order #{orderId} is still being processed and cannot be returned yet. It must be delivered first.", + OrderStatus.Shipped => $"Order #{orderId} is currently in transit and cannot be returned until it has been delivered.", + _ => $"Order #{orderId} has a status of {order.Status} and cannot be returned." + }; + } + + List returnItems; + + // Parse specific items if provided + if (!string.IsNullOrWhiteSpace(orderItemIds)) + { + var itemIdStrings = orderItemIds.Split(',', StringSplitOptions.RemoveEmptyEntries); + var itemIds = new List(); + + foreach (var idStr in itemIdStrings) + { + if (int.TryParse(idStr.Trim(), out int itemId)) + { + itemIds.Add(itemId); + } + else + { + return $"Invalid item ID format: '{idStr}'. Please provide valid item IDs."; + } + } + + // Parse quantities if provided + var itemQuantities = new List(); + if (!string.IsNullOrWhiteSpace(quantities)) + { + var quantityStrings = quantities.Split(',', StringSplitOptions.RemoveEmptyEntries); + foreach (var qtyStr in quantityStrings) + { + if (int.TryParse(qtyStr.Trim(), out int qty) && qty > 0) + { + itemQuantities.Add(qty); + } + else + { + return $"Invalid quantity format: '{qtyStr}'. Quantities must be positive numbers."; + } + } + + if (itemQuantities.Count != itemIds.Count) + { + return "The number of quantities must match the number of items."; + } + } + + // Build return items for specific items + returnItems = new List(); + for (int i = 0; i < itemIds.Count; i++) + { + var orderItem = order.Items.FirstOrDefault(item => item.Id == itemIds[i]); + if (orderItem == null) + { + return $"Item ID {itemIds[i]} was not found in order #{orderId}."; + } + + if (orderItem.RemainingQuantity <= 0) + { + return $"{orderItem.ProductName} has already been fully returned."; + } + + var quantityToReturn = itemQuantities.Count > 0 ? itemQuantities[i] : orderItem.RemainingQuantity; + + if (quantityToReturn > orderItem.RemainingQuantity) + { + return $"Cannot return {quantityToReturn} of {orderItem.ProductName}. Only {orderItem.RemainingQuantity} available to return."; + } + + returnItems.Add(new ReturnItem + { + OrderItemId = orderItem.Id, + Quantity = quantityToReturn, + Reason = reason + }); + } + } + else + { + // Return all unreturned items (original behavior) + returnItems = order.Items + .Where(i => i.RemainingQuantity > 0) + .Select(i => new ReturnItem + { + OrderItemId = i.Id, + Quantity = i.RemainingQuantity, + Reason = reason + }) + .ToList(); + } + + if (!returnItems.Any()) + { + return $"All items in order #{orderId} have already been returned."; + } + + var success = await _orderService.ProcessItemReturnAsync(orderId, returnItems); + + if (!success) + { + _logger.LogError("Failed to process return for orderId {OrderId}, userId {UserId}", orderId, userId); + return $"I was unable to process the return for order #{orderId}. Please contact our support team for assistance."; + } + + _logger.LogInformation("Successfully processed return for orderId {OrderId}, userId {UserId}, items: {ItemCount}", + orderId, userId, returnItems.Count); + + // Calculate refund amount for the items being returned + var refundAmount = returnItems.Sum(ri => + { + var item = order.Items.First(i => i.Id == ri.OrderItemId); + return item.Price * ri.Quantity; + }); + + // Build response message + var itemsSummary = string.Join(", ", returnItems.Select(ri => + { + var item = order.Items.First(i => i.Id == ri.OrderItemId); + return $"{item.ProductName} (qty: {ri.Quantity})"; + })); + + return $"I've successfully processed the return for the following items from order #{orderId}: {itemsSummary}. " + + $"A refund of ${refundAmount:F2} will be issued to your original payment method within 5-7 business days. " + + $"You will receive a confirmation email shortly. " + + $"To view the updated return status, please visit the Order Details page for order #{orderId}."; + } + ``` + +1. Take a minute to review the ProcessReturnAsync method. + + This code snippet is the most complex tool because it performs a state-changing operation with support for both full and partial returns. The method accepts three optional parameters: `orderItemIds` (comma-separated item IDs to return), `quantities` (comma-separated quantities for each item), and `reason`. When `orderItemIds` is empty, it returns all unreturned items (the default behavior). When specific item IDs are provided, it parses them and optionally their quantities, validates each item exists in the order and has remaining quantity, and builds targeted `ReturnItem` objects. The method includes several validation layers: it verifies the order exists and belongs to the user, checks that the order status is `Delivered`, `PartialReturn`, or `Returned`, validates item IDs and quantity formats, and confirms items haven't already been fully returned. If validation passes, it delegates the actual return processing to the existing `IOrderService.ProcessItemReturnAsync` method. The method calculates the refund amount based on the specific items being returned and includes a summary of returned items in the response. Each validation failure returns a specific, helpful message explaining why the return can't be processed. + +1. On a code line below the ProcessReturnAsync method, add the following SendCustomerEmailAsync method: + + ```csharp + /// + /// Sends a follow-up email to the customer regarding their order. + /// The AI agent calls this tool to send additional information by email. + /// + public async Task SendCustomerEmailAsync(int orderId, int userId, string message) + { + _logger.LogInformation("Agent tool invoked: SendCustomerEmail for orderId {OrderId}", orderId); + + var order = await _context.Orders + .FirstOrDefaultAsync(o => o.Id == orderId && o.UserId == userId); + + if (order == null) + { + return $"Could not find order #{orderId} to send an email about."; + } + + // Get the user's email from Identity + var user = await _context.Users.FindAsync(userId); + var email = user?.Email ?? "customer@contoso.com"; + + await _emailService.SendEmailAsync(email, $"Regarding your order #{orderId}", message); + + return $"I've sent an email to {email} with the details about order #{orderId}."; + } + ``` + + This tool enables the AI agent to send follow-up emails to customers. The method verifies that the order exists and belongs to the user, retrieves the user's email address from the Identity system, and sends the email using `IEmailService`. The `message` parameter is generated by the AI agent itself, allowing it to compose context-appropriate email content based on the conversation. A fallback email address is provided in case the user's email can't be retrieved. + +1. Your completed SupportAgentTools.cs file should look similar to the following code: + + ```csharp + using ContosoShop.Server.Data; + using ContosoShop.Shared.Models; + using ContosoShop.Shared.DTOs; + using Microsoft.EntityFrameworkCore; + + namespace ContosoShop.Server.Services; + + /// + /// Provides tool functions that the AI support agent can invoke + /// to look up order information and process returns. + /// + public class SupportAgentTools + { + private readonly ContosoContext _context; + private readonly IOrderService _orderService; + private readonly IEmailService _emailService; + private readonly ILogger _logger; + + public SupportAgentTools( + ContosoContext context, + IOrderService orderService, + IEmailService emailService, + ILogger logger) + { + _context = context; + _orderService = orderService; + _emailService = emailService; + _logger = logger; + } + + // add the `GetOrderDetailsAsync` method here + /// + /// Gets the status and details of a specific order by order ID. + /// The AI agent calls this tool when a user asks about their order status. + /// + public async Task GetOrderDetailsAsync(int orderId, int userId) + { + _logger.LogInformation("Agent tool invoked: GetOrderDetails for orderId {OrderId}, userId {UserId}", orderId, userId); + + var order = await _context.Orders + .Include(o => o.Items) + .FirstOrDefaultAsync(o => o.Id == orderId && o.UserId == userId); + + if (order == null) + { + return $"I could not find order #{orderId} associated with your account. Please double-check the order number."; + } + + var statusMessage = order.Status switch + { + OrderStatus.Processing => "is currently being processed and has not shipped yet", + OrderStatus.Shipped => order.ShipDate.HasValue + ? $"was shipped on {order.ShipDate.Value:MMMM dd, yyyy} and is on its way" + : "has been shipped and is on its way", + OrderStatus.Delivered => order.DeliveryDate.HasValue + ? $"was delivered on {order.DeliveryDate.Value:MMMM dd, yyyy}" + : "has been delivered", + OrderStatus.PartialReturn => "has been partially returned (some items have been returned, others are still with you)", + OrderStatus.Returned => "has been fully returned and a refund was issued", + _ => "has an unknown status" + }; + + var itemSummary = string.Join(", ", order.Items.Select(i => + { + var itemInfo = $"{i.ProductName} (Id: {i.Id}, qty: {i.Quantity}, ${i.Price:F2} each"; + if (i.ReturnedQuantity > 0) + { + itemInfo += $", {i.ReturnedQuantity} returned, {i.RemainingQuantity} remaining"; + } + itemInfo += ")"; + return itemInfo; + })); + + return $"Order #{order.Id} {statusMessage}. " + + $"Order date: {order.OrderDate:MMMM dd, yyyy}. " + + $"Total: ${order.TotalAmount:F2}. " + + $"Items: {itemSummary}."; + } + + // add the `GetUserOrdersSummaryAsync` method here + /// + /// Gets a summary of all orders for a given user. + /// The AI agent calls this tool when a user asks about their orders + /// without specifying a particular order number. + /// + public async Task GetUserOrdersSummaryAsync(int userId) + { + _logger.LogInformation("Agent tool invoked: GetUserOrdersSummary for userId {UserId}", userId); + + var orders = await _context.Orders + .Where(o => o.UserId == userId) + .OrderByDescending(o => o.OrderDate) + .ToListAsync(); + + if (!orders.Any()) + { + return "You don't have any orders on file."; + } + + var summaries = orders.Select(o => + { + var status = o.Status switch + { + OrderStatus.Processing => "Processing", + OrderStatus.Shipped => "Shipped", + OrderStatus.Delivered => "Delivered", + OrderStatus.PartialReturn => "Partial Return", + OrderStatus.Returned => "Returned", + _ => "Unknown" + }; + return $"Order #{o.Id} - {status} - ${o.TotalAmount:F2} - Placed {o.OrderDate:MMM dd, yyyy}"; + }); + + return $"You have {orders.Count} orders:\n" + string.Join("\n", summaries); + } + + // add the `ProcessReturnAsync` method here + /// + /// Processes a return for specific items in a delivered order. + /// The AI agent calls this tool when a user wants to return items. + /// Supports returning all items, specific items by ID, or specific quantities. + /// + /// The order ID to process returns for + /// The authenticated user ID + /// Optional: Specific order item IDs to return (comma-separated, e.g., "123,456"). If empty, returns all unreturned items. + /// Optional: Quantities for each item (comma-separated, e.g., "1,2" for items 123 and 456). Must match orderItemIds length. If empty, returns full remaining quantity for each item. + /// Optional: Reason for the return + public async Task ProcessReturnAsync( + int orderId, + int userId, + string orderItemIds = "", + string quantities = "", + string reason = "Customer requested return via AI support agent") + { + _logger.LogInformation("Agent tool invoked: ProcessReturn for orderId {OrderId}, userId {UserId}, items: {Items}", + orderId, userId, string.IsNullOrEmpty(orderItemIds) ? "all" : orderItemIds); + + var order = await _context.Orders + .Include(o => o.Items) + .FirstOrDefaultAsync(o => o.Id == orderId && o.UserId == userId); + + if (order == null) + { + return $"I could not find order #{orderId} associated with your account."; + } + + if (order.Status != OrderStatus.Delivered && order.Status != OrderStatus.Returned && order.Status != OrderStatus.PartialReturn) + { + return order.Status switch + { + OrderStatus.Processing => $"Order #{orderId} is still being processed and cannot be returned yet. It must be delivered first.", + OrderStatus.Shipped => $"Order #{orderId} is currently in transit and cannot be returned until it has been delivered.", + _ => $"Order #{orderId} has a status of {order.Status} and cannot be returned." + }; + } + + List returnItems; + + // Parse specific items if provided + if (!string.IsNullOrWhiteSpace(orderItemIds)) + { + var itemIdStrings = orderItemIds.Split(',', StringSplitOptions.RemoveEmptyEntries); + var itemIds = new List(); + + foreach (var idStr in itemIdStrings) + { + if (int.TryParse(idStr.Trim(), out int itemId)) + { + itemIds.Add(itemId); + } + else + { + return $"Invalid item ID format: '{idStr}'. Please provide valid item IDs."; + } + } + + // Parse quantities if provided + var itemQuantities = new List(); + if (!string.IsNullOrWhiteSpace(quantities)) + { + var quantityStrings = quantities.Split(',', StringSplitOptions.RemoveEmptyEntries); + foreach (var qtyStr in quantityStrings) + { + if (int.TryParse(qtyStr.Trim(), out int qty) && qty > 0) + { + itemQuantities.Add(qty); + } + else + { + return $"Invalid quantity format: '{qtyStr}'. Quantities must be positive numbers."; + } + } + + if (itemQuantities.Count != itemIds.Count) + { + return "The number of quantities must match the number of items."; + } + } + + // Build return items for specific items + returnItems = new List(); + for (int i = 0; i < itemIds.Count; i++) + { + var orderItem = order.Items.FirstOrDefault(item => item.Id == itemIds[i]); + if (orderItem == null) + { + return $"Item ID {itemIds[i]} was not found in order #{orderId}."; + } + + if (orderItem.RemainingQuantity <= 0) + { + return $"{orderItem.ProductName} has already been fully returned."; + } + + var quantityToReturn = itemQuantities.Count > 0 ? itemQuantities[i] : orderItem.RemainingQuantity; + + if (quantityToReturn > orderItem.RemainingQuantity) + { + return $"Cannot return {quantityToReturn} of {orderItem.ProductName}. Only {orderItem.RemainingQuantity} available to return."; + } + + returnItems.Add(new ReturnItem + { + OrderItemId = orderItem.Id, + Quantity = quantityToReturn, + Reason = reason + }); + } + } + else + { + // Return all unreturned items (original behavior) + returnItems = order.Items + .Where(i => i.RemainingQuantity > 0) + .Select(i => new ReturnItem + { + OrderItemId = i.Id, + Quantity = i.RemainingQuantity, + Reason = reason + }) + .ToList(); + } + + if (!returnItems.Any()) + { + return $"All items in order #{orderId} have already been returned."; + } + + var success = await _orderService.ProcessItemReturnAsync(orderId, returnItems); + + if (!success) + { + _logger.LogError("Failed to process return for orderId {OrderId}, userId {UserId}", orderId, userId); + return $"I was unable to process the return for order #{orderId}. Please contact our support team for assistance."; + } + + _logger.LogInformation("Successfully processed return for orderId {OrderId}, userId {UserId}, items: {ItemCount}", + orderId, userId, returnItems.Count); + + // Calculate refund amount for the items being returned + var refundAmount = returnItems.Sum(ri => + { + var item = order.Items.First(i => i.Id == ri.OrderItemId); + return item.Price * ri.Quantity; + }); + + // Build response message + var itemsSummary = string.Join(", ", returnItems.Select(ri => + { + var item = order.Items.First(i => i.Id == ri.OrderItemId); + return $"{item.ProductName} (qty: {ri.Quantity})"; + })); + + return $"I've successfully processed the return for the following items from order #{orderId}: {itemsSummary}. " + + $"A refund of ${refundAmount:F2} will be issued to your original payment method within 5-7 business days. " + + $"You will receive a confirmation email shortly. " + + $"To view the updated return status, please visit the Order Details page for order #{orderId}."; + } + + // add the `SendCustomerEmailAsync` method here + /// + /// Sends a follow-up email to the customer regarding their order. + /// The AI agent calls this tool to send additional information by email. + /// + public async Task SendCustomerEmailAsync(int orderId, int userId, string message) + { + _logger.LogInformation("Agent tool invoked: SendCustomerEmail for orderId {OrderId}", orderId); + + var order = await _context.Orders + .FirstOrDefaultAsync(o => o.Id == orderId && o.UserId == userId); + + if (order == null) + { + return $"Could not find order #{orderId} to send an email about."; + } + + // Get the user's email from Identity + var user = await _context.Users.FindAsync(userId); + var email = user?.Email ?? "customer@contoso.com"; + + await _emailService.SendEmailAsync(email, $"Regarding your order #{orderId}", message); + + return $"I've sent an email to {email} with the details about order #{orderId}."; + } + } + ``` + + The completed **SupportAgentTools.cs** file has the following structure: + + - The `using` statements, namespace, and class declaration at the top + - The constructor with four injected dependencies + - Four public methods: `GetOrderDetailsAsync`, `GetUserOrdersSummaryAsync`, `ProcessReturnAsync`, and `SendCustomerEmailAsync` + + All four methods follow a consistent design pattern: they accept a `userId` parameter for security verification, log the tool invocation, query the database, perform validation, and return human-readable strings that the AI agent presents directly to the customer. + +1. Open the **ContosoShop.Server/Program.cs** file. + + You'll use the Program.cs file to register SupportAgentTools in dependency injection. + +1. Scroll down to locate the service registration section. + + You can search for the following code comment: `// Register order business logic service`. + +1. Create a blank line after the code used to register the `OrderService`. + +1. To register the `SupportAgentTools` service, add the following code: + + ```csharp + // Register AI agent tools service + builder.Services.AddScoped(); + ``` + +1. Save your updated files. + +1. Build the ContosoShop.Server project and verify that there are no errors. + + For example, you can build the project by entering the following command in the terminal: + + ```powershell + dotnet build + ``` + + The build should succeed. If there are errors, ensure that your code matches the example code shown above. Review the SupportAgentTools.cs file to ensure all `using` statements and references are correct. Keep in mind that the GitHub Copilot SDK is in a Technical Preview phase that includes periodic updates. If necessary, you can point GitHub Copilot to the GitHub Copilot SDK repository (`https://github.com/github/copilot-sdk`) and ask the AI assistant to help you debug the issues. + +## Configure the GitHub Copilot SDK agent and expose an API endpoint + +In this task, you create a `CopilotClient` singleton, register it in dependency injection, and create a new API controller that accepts user questions and returns the AI agent's responses. + +Use the following steps to complete this task: + +1. Open the **ContosoShop.Server/Program.cs** file. + + You'll use the Program.cs file to register CopilotClient as a singleton in dependency injection. + +1. Add the following `using` statement at the top of the file, after the existing `using` statements: + + ```csharp + using GitHub.Copilot.SDK; + ``` + +1. Locate the service registration section. + + You can search for the code comment that you added earlier: `// Register AI agent tools service`. + +1. Create a blank line after the code used to register the SupportAgentTools service. + + This location is where you'll add the code to register the `CopilotClient` singleton. + +1. To create and register a CopilotClient singleton, add the following code: + + ```csharp + // Register GitHub Copilot SDK client as a singleton + builder.Services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + return new CopilotClient(new CopilotClientOptions + { + AutoStart = true, + LogLevel = "info" + }); + }); + ``` + + The `CopilotClient` manages the Copilot CLI process lifecycle. Setting `AutoStart = true` means the CLI server starts automatically when the first session is created. + +1. Scroll down to locate the following code line: + + ```csharp + var app = builder.Build(); + ``` + +1. Create a blank code line between `var app = builder.Build();` and the database initialization block. + +1. To initialize and start the GitHub Copilot SDK client, add the following code: + + ```csharp + // Ensure CopilotClient is started + var copilotClient = app.Services.GetRequiredService(); + await copilotClient.StartAsync(); + ``` + + In addition to initializing and starting the CopilotClient, this code also ensures that it's properly disposed when the application shuts down. + +1. Save the file. + +1. In Visual Studio Code's EXPLORER view, right-click the **ContosoShop.Shared/Models** folder, and then select **New File**. + +1. Name the file **SupportQuery.cs**. + +1. Add the following code to the **SupportQuery.cs** file: + + ```csharp + using System.ComponentModel.DataAnnotations; + + namespace ContosoShop.Shared.Models; + + /// + /// Represents a support question submitted by the user to the AI agent. + /// + public class SupportQuery + { + /// + /// The user's question or message for the AI support agent. + /// + [Required] + [StringLength(1000, MinimumLength = 1)] + public string Question { get; set; } = string.Empty; + } + + /// + /// Represents the AI agent's response to a support query. + /// + public class SupportResponse + { + /// + /// The AI agent's answer to the user's question. + /// + public string Answer { get; set; } = string.Empty; + } + ``` + +1. Take a minute to review the **SupportQuery** and **SupportResponse** models. + + This file defines data transfer models for AI support agent communication: + + **SupportQuery** + + - Represents customer questions sent to the AI support agent + - Contains a Question property with validation: required, 1-1000 characters + - Used as the request payload from client to server + + **SupportResponse** + + - Represents AI agent responses back to the customer + - Contains an Answer property with the agent's reply + - Used as the response payload from server to client + + These models are lightweight DTOs for the support chat interface, enabling structured communication between the Blazor client and the AI-powered support endpoint. The simple design focuses on text-based question-and-answer exchanges with basic input validation. + +1. In Visual Studio Code's EXPLORER view, right-click the **ContosoShop.Server/Controllers** folder, and then select **New File**. + +1. Name the file **SupportAgentController.cs**. + +1. Add the following code to the **SupportAgentController.cs** file: + + ```csharp + using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.AI; + using GitHub.Copilot.SDK; + using ContosoShop.Server.Services; + using ContosoShop.Shared.Models; + using System.ComponentModel; + using System.Security.Claims; + + namespace ContosoShop.Server.Controllers; + + /// + /// API controller that handles AI support agent queries. + /// Accepts user questions, creates a Copilot SDK session with custom tools, + /// and returns the agent's response. + /// + [ApiController] + [Route("api/[controller]")] + [Authorize] + public class SupportAgentController : ControllerBase + { + private readonly CopilotClient _copilotClient; + private readonly SupportAgentTools _agentTools; + private readonly ILogger _logger; + + public SupportAgentController( + CopilotClient copilotClient, + SupportAgentTools agentTools, + ILogger logger) + { + _copilotClient = copilotClient; + _agentTools = agentTools; + _logger = logger; + } + + /// + /// Accepts a support question from the user and returns the AI agent's response. + /// POST /api/supportagent/ask + /// + [HttpPost("ask")] + public async Task AskQuestion([FromBody] SupportQuery query) + { + if (query == null || string.IsNullOrWhiteSpace(query.Question)) + { + return BadRequest(new SupportResponse { Answer = "Please enter a question." }); + } + + // Get the authenticated user's ID from claims + var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + if (!int.TryParse(userIdClaim, out int userId)) + { + return Unauthorized(new SupportResponse { Answer = "Unable to identify user." }); + } + + _logger.LogInformation("Support agent query from user {UserId}: {Question}", userId, query.Question); + } + } + + ``` + +1. Take a minute to review the SupportAgentController code. + + This code establishes the controller skeleton. Key design decisions in this code: + + - The `[Authorize]` attribute ensures only authenticated users can reach the endpoint, which is critical since the agent accesses user-specific order data. + - The `[ApiController]` and `[Route("api/[controller]")]` attributes configure the endpoint at `POST /api/supportagent/ask`. + - The constructor injects three dependencies: `CopilotClient` (the SDK client for creating AI sessions), `SupportAgentTools` (the tools service you created earlier), and `ILogger` for diagnostics. + - The method starts by validating the input and extracting the authenticated user's ID from the claims. The `userId` is extracted once and then passed to each tool call. This process ensures the agent can only access the current user's data, preventing cross-user data leaks. + +1. Inside the `AskQuestion` method, after the logging statement, add the following code: + + > **NOTE**: The following code doesn't include the entire `try` block — you will add more code in the following steps. + + ```csharp + + try + { + // Define the tools the AI agent can use + var tools = new[] + { + AIFunctionFactory.Create( + async ([Description("The order ID number")] int orderId) => + await _agentTools.GetOrderDetailsAsync(orderId, userId), + "get_order_details", + "Look up the status and details of a specific order by its order number. Returns order status, items, dates, and total amount."), + + AIFunctionFactory.Create( + async () => + await _agentTools.GetUserOrdersSummaryAsync(userId), + "get_user_orders", + "Get a summary list of all orders for the current user. Use this when the user asks about their orders without specifying an order number."), + + AIFunctionFactory.Create( + async ( + [Description("The order ID number")] int orderId, + [Description("Optional: Specific order item IDs to return (comma-separated, e.g. '123,456'). Leave empty to return all items.")] string orderItemIds = "", + [Description("Optional: Quantities for each item (comma-separated, e.g. '1,2'). Must match orderItemIds count. Leave empty to return full quantity.")] string quantities = "", + [Description("Optional: Reason for return")] string reason = "Customer requested return via AI support agent") => + await _agentTools.ProcessReturnAsync(orderId, userId, orderItemIds, quantities, reason), + "process_return", + "Process a return for specific items from a delivered order. Can return all items, specific items by ID, or specific quantities of items. Accepts comma-separated item IDs and quantities. Works for orders with Delivered, PartialReturn, or Returned status."), + + AIFunctionFactory.Create( + async ( + [Description("The order ID number")] int orderId, + [Description("The email message content")] string message) => + await _agentTools.SendCustomerEmailAsync(orderId, userId, message), + "send_customer_email", + "Send a follow-up email to the customer with additional information about their order.") + }; + + ``` + +1. Take a minute to review the tool definitions that you just added. + + This code snippet is where the AI agent's capabilities are defined. The code uses `AIFunctionFactory` from `Microsoft.Extensions.AI` to wrap each `SupportAgentTools` method as a callable AI tool. Each call to `AIFunctionFactory.Create` wraps a `SupportAgentTools` method as a tool the AI model can invoke. For each tool, you provide: + + - A **lambda delegate** that calls the corresponding method. Notice that `userId` is captured from the outer scope so the AI model never needs to know or guess the user's identity. + - A **tool name** (like `"get_order_details"`) that the model uses when deciding which tool to call. + - A **description** that helps the model understand when and how to use the tool. + - `[Description]` attributes on parameters that tell the model what values to provide. + + The `get_user_orders` tool takes no parameters from the model (the `userId` is captured automatically). The `process_return` tool has three optional parameters (`orderItemIds`, `quantities`, and `reason`) that enable partial returns by specifying which items and quantities to return (omitting them returns all items). The `send_customer_email` tool takes two model-provided parameters (`orderId` and `message`). This design keeps the user context secure while giving the model flexibility to handle various return scenarios and compose email content. + +1. Just below the tool definitions, to create a Copilot SDK session with a system prompt and tools, add the following code: + + ```csharp + + // Create a Copilot session with the system prompt and tools + await using var session = await _copilotClient.CreateSessionAsync(new SessionConfig + { + Model = "gpt-4.1", + SystemMessage = new SystemMessageConfig + { + Mode = SystemMessageMode.Replace, + Content = @"You are ContosoShop's AI customer support assistant. Your role is to help customers with their order inquiries. + + CAPABILITIES: + - Look up order status and details using the get_order_details tool + - List all customer orders using the get_user_orders tool + - Process returns for delivered orders using the process_return tool (supports full or partial returns) + - Send follow-up emails using the send_customer_email tool + + RETURN PROCESSING WORKFLOW: + 1. When customer wants to return an item, first call get_order_details to see items and their IDs + 2. Parse the customer's request carefully: + - Extract the product name they mentioned (e.g., 'Headphones', 'Desk Lamp', 'Monitor') + - Check if they specified a quantity (e.g., '1 Desk Lamp', '2 monitors', 'one laptop') + - Number words: 'one'=1, 'two'=2, 'three'=3, etc. + 3. From the order details returned by get_order_details, find the item(s) that match the product name: + - Match by ProductName field (case-insensitive, partial match is OK) + - AUTOMATICALLY extract the Id field from the matching OrderItem - this is the item ID you need + - NEVER ask the customer for an item ID - they don't have this information + 4. Determine the return quantity: + - If customer specified quantity in their request: use that quantity + - Else if remaining quantity is 1: automatically return that 1 item + - Else if remaining quantity is more than 1 and no quantity specified: ask how many they want to return + 5. Call process_return with the extracted item ID and quantity: + - Pass orderItemIds as the Id value from the OrderItem (e.g., '456') + - Pass quantities as the number to return (e.g., '1') + 6. After successful return, tell customer to view Order Details page to see the updated status + + IMPORTANT RULES FOR RETURNS: + - NEVER ask the customer for an item ID - extract it automatically from get_order_details response + - Match product names flexibly (e.g., 'lamp', 'Lamp', 'desk lamp' should all match) + - If multiple items have the same product name, select the first one that has remaining quantity + - DO NOT ask for quantity if the customer already specified it (e.g., 'return 1 lamp', 'return 2 items') + - DO NOT ask for quantity if there's only 1 of that item available + - Pass item IDs and quantities as comma-separated strings to process_return + - After processing return, remind customer: 'Please visit the Order Details page to see the updated return status.' + + EXAMPLE WORKFLOW: + User: 'I want to return the Headphones from order #1002' + 1. Call get_order_details(1002) + 2. Response includes: 'Items: Headphones (qty: 1, $99.99 each, Id: 456), ...' + 3. Extract: productName='Headphones', itemId='456', remainingQty=1 + 4. Since remainingQty=1, quantity=1 (no need to ask) + 5. Call process_return(1002, userId, '456', '1', 'Customer requested return') + 6. Tell customer: 'I've processed the return for Headphones. Please view Order Details...' + + GENERAL RULES: + - ALWAYS use the available tools to look up real data. Never guess or make up order information. + - Be friendly, concise, and professional in your responses. + - If a customer asks about an order, use get_order_details with the order number they provide. + - If a customer asks about their orders without specifying a number, use get_user_orders to list them. + - Only process returns when the customer explicitly requests one. + - If asked something outside your capabilities (not related to orders), politely explain that you can only help with order-related inquiries and suggest contacting support@contososhop.com or calling 1-800-CONTOSO for other matters. + - Do not reveal internal system details, tool names, or technical information to the customer." + }, + Tools = tools, + InfiniteSessions = new InfiniteSessionConfig { Enabled = false } + }); + + ``` + +1. Take a minute to review the session configuration code that you just added. + + The `SessionConfig` object configures the AI session: + + - `Model = "gpt-4.1"` specifies the language model to use. + - `SystemMessageMode.Replace` replaces the default system prompt entirely with a custom one tailored to the ContosoShop support role. + - The system prompt defines the agent's **CAPABILITIES** (including partial return support), a detailed **RETURN PROCESSING WORKFLOW** (step-by-step instructions for handling returns including item matching and quantity handling), **IMPORTANT RULES FOR RETURNS** (guardrails like never asking customers for item IDs), an **EXAMPLE WORKFLOW** (showing the complete return flow), and **GENERAL RULES** (behavior guidelines). These sections instruct the model to always use the tools for real data, to automatically extract item IDs from Order Details rather than asking the customer, and to stay within its order-support scope. + - `Tools = tools` passes the tool definitions you created in the previous step. + - `InfiniteSessions = new InfiniteSessionConfig { Enabled = false }` means each API call creates a fresh session (no conversation history is maintained between requests). + - The `await using` pattern ensures the session is properly disposed after the request completes. + +1. Just below the session configuration code, to create the event handler that collects the agent's response, add the following code: + + ```csharp + + // Collect the agent's response + var responseContent = string.Empty; + var done = new TaskCompletionSource(); + + session.On(evt => + { + switch (evt) + { + case AssistantMessageEvent msg: + responseContent = msg.Data.Content; + break; + case SessionIdleEvent: + done.TrySetResult(); + break; + case SessionErrorEvent err: + _logger.LogError("Agent session error: {Message}", err.Data.Message); + done.TrySetException(new Exception(err.Data.Message)); + break; + } + }); + + ``` + +1. Take a minute to review the event handler code that you just added. + + The Copilot SDK uses an event-driven model for communication. The `session.On` method registers a callback that handles three event types: + + - `AssistantMessageEvent`: Fired when the AI model produces a response. The message content is captured in `responseContent`. + - `SessionIdleEvent`: Fired when the session is finished processing (including any tool calls). This signals that the response is complete by resolving the `TaskCompletionSource`. + - `SessionErrorEvent`: Fired if something goes wrong during the session. The error is logged and propagated as an exception via `done.TrySetException`. + + The `TaskCompletionSource` pattern converts the event-driven flow into an awaitable task, allowing the controller to wait for the agent to finish before returning the HTTP response. + +1. Just below the event handler code, to send the user's question, wait for the response with a timeout, and return the result, add the following code: + + ```csharp + + // Send the user's question + await session.SendAsync(new MessageOptions { Prompt = query.Question }); + + // Wait for the response with a timeout + var timeoutTask = Task.Delay(TimeSpan.FromSeconds(30)); + var completedTask = await Task.WhenAny(done.Task, timeoutTask); + + if (completedTask == timeoutTask) + { + _logger.LogWarning("Agent session timed out for user {UserId}", userId); + return Ok(new SupportResponse + { + Answer = "I'm sorry, the request took too long. Please try again or contact our support team." + }); + } + + // Rethrow if the task faulted + await done.Task; + + _logger.LogInformation("Agent response for user {UserId}: {Answer}", userId, responseContent); + + return Ok(new SupportResponse { Answer = responseContent }); + + ``` + +1. Take a minute to review the code that manages communication with the AI agent. + + This code sends the customer's question and handles the asynchronous response: + + - `session.SendAsync` dispatches the user's question to the AI model, which may invoke zero or more tools before composing a final response. + - A **30-second timeout** protects against long-running requests. If the agent takes too long (for example, due to multiple tool calls or network delays), the user gets a friendly timeout message rather than the request hanging indefinitely. + - `Task.WhenAny` races the agent's completion against the timeout. If the `done.Task` completes first, `await done.Task` is called again to propagate any exception that might have been set by `SessionErrorEvent`. + - The successful response is wrapped in a `SupportResponse` DTO and returned as HTTP 200. + +1. Just below the code that manages communication with the AI agent, to complete the `try-catch` block, add the following code: + + ```csharp + + } + + catch (Exception ex) + { + _logger.LogError(ex, "Error processing support agent query for user {UserId}", userId); + return StatusCode(500, new SupportResponse + { + Answer = "I'm sorry, I encountered an error processing your request. Please try again or contact our support team at support@contososhop.com." + }); + } + + ``` + +1. Take a minute to review the error handling code in the `catch` block. + + The `catch` block provides a safety net for any unhandled exceptions—including errors from the Copilot SDK, tool execution failures, or network issues. Rather than exposing a raw error to the customer, it logs the full exception for debugging and returns a friendly error message with a fallback contact option. This error handling ensures the API always returns a valid `SupportResponse` regardless of what goes wrong internally. + +1. Your completed **SupportAgentController.cs** file should look like the following code: + + ```csharp + using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.AI; + using GitHub.Copilot.SDK; + using ContosoShop.Server.Services; + using ContosoShop.Shared.Models; + using System.ComponentModel; + using System.Security.Claims; + + namespace ContosoShop.Server.Controllers; + + /// + /// API controller that handles AI support agent queries. + /// Accepts user questions, creates a Copilot SDK session with custom tools, + /// and returns the agent's response. + /// + [ApiController] + [Route("api/[controller]")] + [Authorize] + public class SupportAgentController : ControllerBase + { + private readonly CopilotClient _copilotClient; + private readonly SupportAgentTools _agentTools; + private readonly ILogger _logger; + + public SupportAgentController( + CopilotClient copilotClient, + SupportAgentTools agentTools, + ILogger logger) + { + _copilotClient = copilotClient; + _agentTools = agentTools; + _logger = logger; + } + + /// + /// Accepts a support question from the user and returns the AI agent's response. + /// POST /api/supportagent/ask + /// + [HttpPost("ask")] + public async Task AskQuestion([FromBody] SupportQuery query) + { + if (query == null || string.IsNullOrWhiteSpace(query.Question)) + { + return BadRequest(new SupportResponse { Answer = "Please enter a question." }); + } + + // Get the authenticated user's ID from claims + var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + if (!int.TryParse(userIdClaim, out int userId)) + { + return Unauthorized(new SupportResponse { Answer = "Unable to identify user." }); + } + + _logger.LogInformation("Support agent query from user {UserId}: {Question}", userId, query.Question); + + try + { + // Define the tools the AI agent can use + var tools = new[] + { + AIFunctionFactory.Create( + async ([Description("The order ID number")] int orderId) => + await _agentTools.GetOrderDetailsAsync(orderId, userId), + "get_order_details", + "Look up the status and details of a specific order by its order number. Returns order status, items, dates, and total amount."), + + AIFunctionFactory.Create( + async () => + await _agentTools.GetUserOrdersSummaryAsync(userId), + "get_user_orders", + "Get a summary list of all orders for the current user. Use this when the user asks about their orders without specifying an order number."), + + AIFunctionFactory.Create( + async ( + [Description("The order ID number")] int orderId, + [Description("Optional: Specific order item IDs to return (comma-separated, e.g. '123,456'). Leave empty to return all items.")] string orderItemIds = "", + [Description("Optional: Quantities for each item (comma-separated, e.g. '1,2'). Must match orderItemIds count. Leave empty to return full quantity.")] string quantities = "", + [Description("Optional: Reason for return")] string reason = "Customer requested return via AI support agent") => + await _agentTools.ProcessReturnAsync(orderId, userId, orderItemIds, quantities, reason), + "process_return", + "Process a return for specific items from a delivered order. Can return all items, specific items by ID, or specific quantities of items. Accepts comma-separated item IDs and quantities. Works for orders with Delivered, PartialReturn, or Returned status."), + + + AIFunctionFactory.Create( + async ( + [Description("The order ID number")] int orderId, + [Description("The email message content")] string message) => + await _agentTools.SendCustomerEmailAsync(orderId, userId, message), + "send_customer_email", + "Send a follow-up email to the customer with additional information about their order.") + }; + + // Create a Copilot session with the system prompt and tools + await using var session = await _copilotClient.CreateSessionAsync(new SessionConfig + { + Model = "gpt-4.1", + SystemMessage = new SystemMessageConfig + { + Mode = SystemMessageMode.Replace, + Content = @"You are ContosoShop's AI customer support assistant. Your role is to help customers with their order inquiries. + + CAPABILITIES: + - Look up order status and details using the get_order_details tool + - List all customer orders using the get_user_orders tool + - Process returns for delivered orders using the process_return tool (supports full or partial returns) + - Send follow-up emails using the send_customer_email tool + + RETURN PROCESSING WORKFLOW: + 1. When customer wants to return an item, first call get_order_details to see items and their IDs + 2. Parse the customer's request carefully: + - Extract the product name they mentioned (e.g., 'Headphones', 'Desk Lamp', 'Monitor') + - Check if they specified a quantity (e.g., '1 Desk Lamp', '2 monitors', 'one laptop') + - Number words: 'one'=1, 'two'=2, 'three'=3, etc. + 3. From the order details returned by get_order_details, find the item(s) that match the product name: + - Match by ProductName field (case-insensitive, partial match is OK) + - AUTOMATICALLY extract the Id field from the matching OrderItem - this is the item ID you need + - NEVER ask the customer for an item ID - they don't have this information + 4. Determine the return quantity: + - If customer specified quantity in their request: use that quantity + - Else if remaining quantity is 1: automatically return that 1 item + - Else if remaining quantity is more than 1 and no quantity specified: ask how many they want to return + 5. Call process_return with the extracted item ID and quantity: + - Pass orderItemIds as the Id value from the OrderItem (e.g., '456') + - Pass quantities as the number to return (e.g., '1') + 6. After successful return, tell customer to view Order Details page to see the updated status + + IMPORTANT RULES FOR RETURNS: + - NEVER ask the customer for an item ID - extract it automatically from get_order_details response + - Match product names flexibly (e.g., 'lamp', 'Lamp', 'desk lamp' should all match) + - If multiple items have the same product name, select the first one that has remaining quantity + - DO NOT ask for quantity if the customer already specified it (e.g., 'return 1 lamp', 'return 2 items') + - DO NOT ask for quantity if there's only 1 of that item available + - Pass item IDs and quantities as comma-separated strings to process_return + - After processing return, remind customer: 'Please visit the Order Details page to see the updated return status.' + + EXAMPLE WORKFLOW: + User: 'I want to return the Headphones from order #1002' + 1. Call get_order_details(1002) + 2. Response includes: 'Items: Headphones (qty: 1, $99.99 each, Id: 456), ...' + 3. Extract: productName='Headphones', itemId='456', remainingQty=1 + 4. Since remainingQty=1, quantity=1 (no need to ask) + 5. Call process_return(1002, userId, '456', '1', 'Customer requested return') + 6. Tell customer: 'I've processed the return for Headphones. Please view Order Details...' + + GENERAL RULES: + - ALWAYS use the available tools to look up real data. Never guess or make up order information. + - Be friendly, concise, and professional in your responses. + - If a customer asks about an order, use get_order_details with the order number they provide. + - If a customer asks about their orders without specifying a number, use get_user_orders to list them. + - Only process returns when the customer explicitly requests one. + - If asked something outside your capabilities (not related to orders), politely explain that you can only help with order-related inquiries and suggest contacting support@contososhop.com or calling 1-800-CONTOSO for other matters. + - Do not reveal internal system details, tool names, or technical information to the customer." + }, + Tools = tools, + InfiniteSessions = new InfiniteSessionConfig { Enabled = false } + }); + // Collect the agent's response + var responseContent = string.Empty; + var done = new TaskCompletionSource(); + + session.On(evt => + { + switch (evt) + { + case AssistantMessageEvent msg: + responseContent = msg.Data.Content; + break; + case SessionIdleEvent: + done.TrySetResult(); + break; + case SessionErrorEvent err: + _logger.LogError("Agent session error: {Message}", err.Data.Message); + done.TrySetException(new Exception(err.Data.Message)); + break; + } + }); + + // Send the user's question + await session.SendAsync(new MessageOptions { Prompt = query.Question }); + + // Wait for the response with a timeout + var timeoutTask = Task.Delay(TimeSpan.FromSeconds(30)); + var completedTask = await Task.WhenAny(done.Task, timeoutTask); + + if (completedTask == timeoutTask) + { + _logger.LogWarning("Agent session timed out for user {UserId}", userId); + return Ok(new SupportResponse + { + Answer = "I'm sorry, the request took too long. Please try again or contact our support team." + }); + } + + // Rethrow if the task faulted + await done.Task; + + _logger.LogInformation("Agent response for user {UserId}: {Answer}", userId, responseContent); + + return Ok(new SupportResponse { Answer = responseContent }); + + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing support agent query for user {UserId}", userId); + return StatusCode(500, new SupportResponse + { + Answer = "I'm sorry, I encountered an error processing your request. Please try again or contact our support team at support@contososhop.com." + }); + } + } + } + ``` + + Your completed **SupportAgentController.cs** file has the following structure: + + - The `using` statements and namespace at the top + - The `SupportAgentController` class with `[ApiController]`, `[Route]`, and `[Authorize]` attributes + - A constructor injecting `CopilotClient`, `SupportAgentTools`, and `ILogger` + - A single `AskQuestion` action method (`[HttpPost("ask")]`) that: + - Validates the input and extracts the user ID + - Defines four AI tools using `AIFunctionFactory.Create` + - Creates a Copilot session with a system prompt and tools + - Registers event handlers for response, idle, and error events + - Sends the question and awaits the response with a 30-second timeout + - Returns the response or appropriate error messages + +1. Open the **ContosoShop.Server/Program.cs** file. + +1. Locate the code that configures CORS policies. + + You can search for the following code comment: `// Configure CORS`. + +1. Notice that the CORS configuration section allows the `GET` and `POST` methods required by the API endpoint you just created. + + The existing configuration allows `GET` and `POST` methods, which is sufficient. + + ```csharp + .WithMethods("GET", "POST") // Only required methods + ``` + +1. To build the project, enter the following command in the terminal: + + ```powershell + dotnet build + ``` + + The build should succeed without errors. If you see errors related to `GitHub.Copilot.SDK` types, verify that the NuGet package was installed correctly. + +## Update the Blazor frontend to interact with the agent + +In this task, you create a client-side service to call the agent API and update the Support.razor page with an interactive chat interface. + +Use the following steps to complete this task: + +1. In Visual Studio Code's EXPLORER view, right-click the **ContosoShop.Client/Services** folder, and then select **New File**. + +1. Name the file **SupportAgentService.cs**. + +1. Add the following code: + + ```csharp + using System.Net.Http.Json; + using ContosoShop.Shared.Models; + + namespace ContosoShop.Client.Services; + + /// + /// Client-side service for communicating with the AI support agent API. + /// + public class SupportAgentService + { + private readonly HttpClient _http; + + public SupportAgentService(HttpClient http) + { + _http = http; + } + + /// + /// Sends a question to the AI support agent and returns the response. + /// + /// The user's question + /// The agent's response text + public async Task AskAsync(string question) + { + var query = new SupportQuery { Question = question }; + + var response = await _http.PostAsJsonAsync("api/supportagent/ask", query); + + if (!response.IsSuccessStatusCode) + { + var errorText = await response.Content.ReadAsStringAsync(); + throw new HttpRequestException( + $"Support agent returned {response.StatusCode}: {errorText}"); + } + + var result = await response.Content.ReadFromJsonAsync(); + return result?.Answer ?? "I'm sorry, I didn't receive a response. Please try again."; + } + } + ``` + +1. Take a minute to review the `SupportAgentService` code. + + This code snippet is a client-side HTTP service that interfaces with the AI support agent backend. Key features: + + Simple API Wrapper: + + - Single method AskAsync(string question) - sends user questions to the support agent API endpoint + - Posts to POST /api/supportagent/ask on the server + + Communication Handling: + + - Wraps the question in a SupportQuery DTO + - Uses HttpClient.PostAsJsonAsync for automatic JSON serialization + - Deserializes the response into a SupportResponse object + + Error Management: + + - Checks HTTP status codes for failures + - Throws HttpRequestException with detailed error information on nonsuccess responses + - Provides fallback message if response parsing fails + + Design Pattern: + + - Thin client wrapper following the service layer pattern + - Injected HttpClient for testability and proper lifetime management + - Used by Blazor components (like Support.razor) to interact with the AI agent without handling HTTP details directly + + This service abstracts away the HTTP communication complexity, providing a clean interface for Blazor components to ask questions to the AI support agent. + +1. Open the **ContosoShop.Client/Program.cs** file. + +1. Locate the service registration section of the file. + + You can search for `// Register application services` + +1. Create a blank code line below the existing service registrations, and then add the following code: + + ```csharp + + // Register AI support agent service + builder.Services.AddScoped(sp => + new SupportAgentService(sp.GetRequiredService())); + + ``` + + This code registers the `SupportAgentService` as a scoped service in Blazor's dependency injection container, allowing it to be injected into components. The `HttpClient` is injected into the service constructor, ensuring proper lifetime management and configuration. The `using ContosoShop.Client.Services;` statement should already be present at the top of the file. + +1. Save the file. + +1. Open the **ContosoShop.Client/Pages/Support.razor** file. + + You replace the existing content of this file to create a new support chat interface that interacts with the AI agent. + +1. Select and then delete the existing content of the file. + +1. To begin the construction of the new file, add the following code: + + > **NOTE**: You'll build the Support.razor file in stages. Don't autoformat (Format Document) the file until you've added all the code snippets. + + ```cshtml + @page "/support" + @using ContosoShop.Shared.Models + @using ContosoShop.Client.Services + @attribute [Microsoft.AspNetCore.Authorization.Authorize] + @inject SupportAgentService AgentService + + Contact Support - ContosoShop Support Portal + +
+
+
+

Contact Support

+ + +
+
+
+ AI Chat Support +
+
+
+ +
+ @if (!conversations.Any()) + { +
+ +

Ask me about your orders! For example:

+
    +
  • "What is the status of order #1001?"
  • +
  • "Show me all my orders"
  • +
  • "I want to return order #1005"
  • +
+
+ } + @foreach (var entry in conversations) + { +
+
+ You + @entry.Question +
+ @if (!string.IsNullOrEmpty(entry.Answer)) + { +
+ Agent + @entry.Answer +
+ } +
+ } + @if (isLoading) + { +
+ Agent + Thinking... +
+ } +
+ + ``` + +1. Take a minute to review the HTML code that you just added. + + This first section establishes the page structure: + + - The `@page "/support"` directive maps this component to the `/support` URL route. + - The `@attribute [Authorize]` ensures only authenticated users can access the page. + - The `@inject SupportAgentService AgentService` injects the client-side service you created in the previous step, giving the page access to the AI agent API. + - The chat messages area is a scrollable `div` (300-500px height) that displays the conversation history. When there are no messages yet, it shows helpful example prompts to guide the user. Each conversation entry shows the user's question with a "You" badge and the agent's response with an "Agent" badge. The `white-space: pre-line` style preserves line breaks in the agent's responses (for example, when listing multiple orders). A "Thinking..." indicator appears while the agent is processing a request. + +1. After the chat messages area `
`, to add the input area and close the AI Chat card, enter the following code: + + ```cshtml + + +
+ + +
+ + @if (!string.IsNullOrEmpty(errorMessage)) + { +
+ @errorMessage +
+ } +
+
+ + ``` + +1. Take a minute to review the input area code that you just added. + + The input area uses Bootstrap's `input-group` for a clean text field with attached send button. Key interaction details: + + - `@bind="currentQuestion"` with `@bind:event="oninput"` provides real-time two-way binding — the `currentQuestion` variable updates as the user types (not just on blur). + - `@onkeydown="HandleKeyDown"` enables the Enter key shortcut for submitting questions. + - Both the input and button are disabled while `isLoading` is true, preventing duplicate submissions during agent processing. + - The button is also disabled when the input is empty (`string.IsNullOrWhiteSpace(currentQuestion)`). + - An error alert conditionally appears below the input when `errorMessage` is set, providing user-friendly feedback if something goes wrong. + +1. After the AI Chat card, to add the Contact Information card, enter the following code: + + ```cshtml + + +
+
+
+ Get in Touch +
+
+
+
+
+
Email Support
+

+ + support@contososhop.com +

+ Response time: 24-48 hours +
+
+
Phone Support
+

+ + 1-800-CONTOSO +

+ Mon-Fri 9AM-5PM EST +
+
+
+
+ + ``` + +1. Take a minute to review the Contact Information card code that you just added. + + This card provides traditional contact methods as a fallback when the AI agent can't fully resolve a customer's issue. The two-column layout (using Bootstrap's grid) shows email and phone support side by side on medium+ screens, each with response time expectations. This is consistent with the system prompt you configured earlier, which tells the AI agent to direct customers to `support@contososhop.com` or `1-800-CONTOSO` for nonorder matters. + +1. After the Contact Information card, to add the Quick Links card and the closing `
` tags for the page layout, enter the following code: + + ```cshtml + + +
+
+
+ Need Help With Your Order? +
+
+
+
    +
  • + + View Your Orders + +
  • +
  • + + Return a delivered order from the Order Details page +
  • +
  • + + Track shipment status and delivery updates +
  • +
+
+
+
+ + + + ``` + +1. Take a minute to review the Quick Links card code that you just added. + + The Quick Links card provides navigation shortcuts to other parts of the application. The "View Your Orders" link navigates to the `/orders` page where customers can see their full order list. The remaining items describe self-service actions available elsewhere in the app. The three closing `` tags close the `col-lg-8`, `row`, and `container` elements that wrap the entire page layout. + +1. On a line below the HTML code, to add the `@code` block that contains the component's state management and event handling logic, enter the following code: + + ```cshtml + + @code { + private class ConversationEntry + { + public string Question { get; set; } = string.Empty; + public string Answer { get; set; } = string.Empty; + } + + private List conversations = new(); + private string currentQuestion = string.Empty; + private bool isLoading = false; + private string errorMessage = string.Empty; + + private async Task HandleKeyDown(KeyboardEventArgs e) + { + if (e.Key == "Enter" && !isLoading && !string.IsNullOrWhiteSpace(currentQuestion)) + { + await SubmitQuestion(); + } + } + + private async Task SubmitQuestion() + { + if (string.IsNullOrWhiteSpace(currentQuestion) || isLoading) + return; + + errorMessage = string.Empty; + var question = currentQuestion.Trim(); + currentQuestion = string.Empty; + + var entry = new ConversationEntry { Question = question }; + conversations.Add(entry); + + try + { + isLoading = true; + StateHasChanged(); + + var answer = await AgentService.AskAsync(question); + entry.Answer = answer; + } + catch (Exception ex) + { + errorMessage = "Sorry, something went wrong. Please try again or contact our support team."; + Console.Error.WriteLine($"Agent error: {ex.Message}"); + } + finally + { + isLoading = false; + StateHasChanged(); + } + } + } + ``` + +1. Take a minute to review the `@code` block you just added. + + The `@code` block contains all of the component's logic: + + - `ConversationEntry` is a simple inner class that pairs each user question with the agent's answer, forming the chat history. + - The component state consists of four fields: `conversations` (the full chat history), `currentQuestion` (the text input binding), `isLoading` (prevents duplicate submissions and shows the "Thinking..." indicator), and `errorMessage` (displays errors below the input). + - `HandleKeyDown` enables submitting questions by pressing Enter—it checks the same guards as the send button (not loading, not empty). + - `SubmitQuestion` orchestrates the full send flow: it clears the error state, captures and clears the input text, adds a new conversation entry immediately (so the user's question appears right away), then calls `AgentService.AskAsync` to get the agent's response. The `StateHasChanged()` calls force Blazor to re-render the UI—once when "Thinking..." appears and again when the response arrives or an error occurs. The `try/finally` pattern ensures `isLoading` is always reset, even if the API call fails. + +1. Verify that your completed **Support.razor** file has the following structure: + + - Page directives (`@page`, `@using`, `@attribute`, `@inject`) at the top + - A container layout with a centered column + - Three cards: AI Chat Support (with messages area, input area, and error display), Contact Information (email and phone), and Quick Links (navigation shortcuts) + - An `@code` block with `ConversationEntry` class, state fields, `HandleKeyDown`, and `SubmitQuestion` methods + +1. Open the ContosoShop.Server directory in the terminal, and then enter the following command: + + ```powershell + dotnet build + ``` + + The build should succeed without errors. + +## Test the end-to-end AI agent experience + +In this task, you run the application and test the AI agent with various support queries to verify it functions correctly. + +Use the following steps to complete this task: + +1. To start the server application from the terminal, enter the following command: + + ```powershell + dotnet run + ``` + + Watch the console output for any errors during startup. You should see the application listening on an HTTP port. + + If you see errors, verify that you completed each step in the previous tasks and that you entered the code correctly. If you still have errors after verifying your code, you can point GitHub Copilot to the GitHub Copilot SDK repository (`https://github.com/github/copilot-sdk`) and ask the AI assistant to help you debug the issues. + +1. Open a browser and navigate to the specified HTML port. + + For example, you might see output like `Now listening on: http://localhost:5266`. In that case, open `http://localhost:5266` in a browser window. You should see the ContosoShop E-commerce Support Portal login page. + +1. Log in with the demo credentials for Mateo. + + Enter `mateo@contoso.com` for the email and `Password123!` for the password, and then select **Login**. + +1. Navigate to the **Contact Support** page. + +1. Take a moment to review the page. + + You should now see the interactive AI Chat Support interface instead of the "Coming Soon" placeholder. The chat area displays example prompts to help you get started. + +1. To test the agent's ability to **Check order status**, enter the following prompt and select **Send** (or press Enter): + + ```plaintext + What's the status of order #1001? + ``` + + The agent should respond with details about order #1001, such as the delivery date and the items delivered. The response should reflect the actual data in the database. + + Verify the response matches what you see on the Orders page for order #1001. + +1. To test the agent's ability to **List all orders**, enter the following prompt: + + ```plaintext + Show me all my orders + ``` + + The agent should use the `get_user_orders` tool and return a summary list of all 10 of Mateo's orders with their statuses and amounts. + +1. To test the agent's ability to **Process a return**, enter the following prompt: + + ```plaintext + I want to return order #1008 + ``` + + The agent should process the return for order #1008 (which was Delivered) and confirm the refund amount. + + After the AI response is displayed: + + - Navigate to the **Orders** page and verify that order #1008 now shows a "Returned" status. + +1. To test the agent's ability to **Process a return for a single item within an order**, enter the following prompt: + + ```plaintext + I want to return 1 Desk Lamp from order #1005 + ``` + + The agent should process the return for the specified item within order #1005 and confirm the refund amount. + + After the AI response is displayed: + + - Navigate to the **Orders** page and verify that order #1005 now shows a "Partial Return" status. + - Open the order details for order #1005 and verify that a "Returned" status is shown for one Desk Lamp. + +1. To test the agent's ability to **Handle an order that can't be returned**, enter the following prompt: + + ```plaintext + I want to return order #1010. + ``` + + Order #1010 has "Processing" status and can't be returned. The agent should explain that the order must be delivered before it can be returned. + +1. To test the agent's ability to **Handle a non-existent order**, enter the following prompt: + + ```plaintext + Where is my order #9999? + ``` + + The agent should respond that it couldn't find order #9999 associated with the user's account. + +1. To test the agent's ability to **Handle an off-topic question**, enter the following prompt: + + ```plaintext + What's the weather like today? + ``` + + The agent should politely explain that it can only help with order-related inquiries and suggest contacting support through other channels. + +1. When you're done testing, return to the terminal and press **Ctrl+C** to stop the application. + +## Summary + +In this exercise, you successfully integrated an AI-powered customer support agent into the ContosoShop E-commerce Support Portal using the GitHub Copilot SDK. You: + +- **Created backend tools** (`SupportAgentTools`) that the AI agent can invoke to look up orders and process returns, using the existing application services. +- **Configured the Copilot SDK** with a `CopilotClient` singleton and created sessions with a custom system prompt and tool definitions using `AIFunctionFactory.Create`. +- **Built an API endpoint** (`SupportAgentController`) that accepts user questions, creates agent sessions, and returns AI-generated responses. +- **Updated the Blazor frontend** with an interactive chat interface on the Support page. +- **Tested the integration** with real-world scenarios including order lookups, returns, error handling, and off-topic deflection. + +This pattern—defining business logic as tools, registering them with an AI agent runtime, and exposing the agent via an API—is applicable to many domains beyond e-commerce support. You can apply the same approach to IT helpdesk automation, CRM assistants, or any scenario where an AI agent needs to take actions on behalf of users. + +## Clean up + +Now that you've finished the exercise, take a minute to clean up your environment: + +- Stop the server application if it's still running (press **Ctrl+C** in the terminal). +- Ensure that you haven't made changes to your GitHub account or GitHub Copilot subscription that you don't want to keep. +- Optionally archive or delete the local clone of the repository. diff --git a/Instructions/Labs/Media/edge-copilot.png b/Instructions/Labs/Media/edge-copilot.png new file mode 100644 index 0000000..62ad58f Binary files /dev/null and b/Instructions/Labs/Media/edge-copilot.png differ diff --git a/Instructions/Labs/Media/launch-exercise.png b/Instructions/Labs/Media/launch-exercise.png new file mode 100644 index 0000000..9f1b88d Binary files /dev/null and b/Instructions/Labs/Media/launch-exercise.png differ diff --git a/Instructions/Labs/Media/m00-github-copilot-extensions-vscode.png b/Instructions/Labs/Media/m00-github-copilot-extensions-vscode.png new file mode 100644 index 0000000..b714160 Binary files /dev/null and b/Instructions/Labs/Media/m00-github-copilot-extensions-vscode.png differ diff --git a/Instructions/Labs/Media/m00-github-copilot-setup.png b/Instructions/Labs/Media/m00-github-copilot-setup.png new file mode 100644 index 0000000..c6c96c2 Binary files /dev/null and b/Instructions/Labs/Media/m00-github-copilot-setup.png differ diff --git a/Instructions/Labs/Media/m01-github-copilot-chat-extension-settings-preview-experimental.png b/Instructions/Labs/Media/m01-github-copilot-chat-extension-settings-preview-experimental.png new file mode 100644 index 0000000..0bd79d1 Binary files /dev/null and b/Instructions/Labs/Media/m01-github-copilot-chat-extension-settings-preview-experimental.png differ diff --git a/Instructions/Labs/Media/m01-github-copilot-chat-extension-settings.png b/Instructions/Labs/Media/m01-github-copilot-chat-extension-settings.png new file mode 100644 index 0000000..6fb363e Binary files /dev/null and b/Instructions/Labs/Media/m01-github-copilot-chat-extension-settings.png differ diff --git a/Instructions/Labs/Media/m01-github-copilot-chat-view-interface.png b/Instructions/Labs/Media/m01-github-copilot-chat-view-interface.png new file mode 100644 index 0000000..7c4cc87 Binary files /dev/null and b/Instructions/Labs/Media/m01-github-copilot-chat-view-interface.png differ diff --git a/Instructions/Labs/Media/m01-github-copilot-chat-view-response-ask-mode.png b/Instructions/Labs/Media/m01-github-copilot-chat-view-response-ask-mode.png new file mode 100644 index 0000000..dcb6094 Binary files /dev/null and b/Instructions/Labs/Media/m01-github-copilot-chat-view-response-ask-mode.png differ diff --git a/Instructions/Labs/Media/m01-github-copilot-chat-view-response-edit-mode.png b/Instructions/Labs/Media/m01-github-copilot-chat-view-response-edit-mode.png new file mode 100644 index 0000000..8900c81 Binary files /dev/null and b/Instructions/Labs/Media/m01-github-copilot-chat-view-response-edit-mode.png differ diff --git a/Instructions/Labs/Media/m01-github-copilot-code-completion-1.png b/Instructions/Labs/Media/m01-github-copilot-code-completion-1.png new file mode 100644 index 0000000..6968aaa Binary files /dev/null and b/Instructions/Labs/Media/m01-github-copilot-code-completion-1.png differ diff --git a/Instructions/Labs/Media/m01-github-copilot-config-settings.png b/Instructions/Labs/Media/m01-github-copilot-config-settings.png new file mode 100644 index 0000000..8515794 Binary files /dev/null and b/Instructions/Labs/Media/m01-github-copilot-config-settings.png differ diff --git a/Instructions/Labs/Media/m01-github-copilot-enable-disable.png b/Instructions/Labs/Media/m01-github-copilot-enable-disable.png new file mode 100644 index 0000000..9cde500 Binary files /dev/null and b/Instructions/Labs/Media/m01-github-copilot-enable-disable.png differ diff --git a/Instructions/Labs/Media/m01-github-copilot-extension-settings-advanced-options.png b/Instructions/Labs/Media/m01-github-copilot-extension-settings-advanced-options.png new file mode 100644 index 0000000..1d91b04 Binary files /dev/null and b/Instructions/Labs/Media/m01-github-copilot-extension-settings-advanced-options.png differ diff --git a/Instructions/Labs/Media/m01-github-copilot-extension-settings-change-completions-model-menu.png b/Instructions/Labs/Media/m01-github-copilot-extension-settings-change-completions-model-menu.png new file mode 100644 index 0000000..8dda13e Binary files /dev/null and b/Instructions/Labs/Media/m01-github-copilot-extension-settings-change-completions-model-menu.png differ diff --git a/Instructions/Labs/Media/m01-github-copilot-extension-settings-change-completions-model.png b/Instructions/Labs/Media/m01-github-copilot-extension-settings-change-completions-model.png new file mode 100644 index 0000000..24c0b23 Binary files /dev/null and b/Instructions/Labs/Media/m01-github-copilot-extension-settings-change-completions-model.png differ diff --git a/Instructions/Labs/Media/m01-github-copilot-extension-settings-enable-languages.png b/Instructions/Labs/Media/m01-github-copilot-extension-settings-enable-languages.png new file mode 100644 index 0000000..adf33c5 Binary files /dev/null and b/Instructions/Labs/Media/m01-github-copilot-extension-settings-enable-languages.png differ diff --git a/Instructions/Labs/Media/m01-github-copilot-extension-settings.png b/Instructions/Labs/Media/m01-github-copilot-extension-settings.png new file mode 100644 index 0000000..8b73bdf Binary files /dev/null and b/Instructions/Labs/Media/m01-github-copilot-extension-settings.png differ diff --git a/Instructions/Labs/Media/m01-github-copilot-inline-chat-response.png b/Instructions/Labs/Media/m01-github-copilot-inline-chat-response.png new file mode 100644 index 0000000..4073c8f Binary files /dev/null and b/Instructions/Labs/Media/m01-github-copilot-inline-chat-response.png differ diff --git a/Instructions/Labs/Media/m01-github-copilot-menu-chat-options.png b/Instructions/Labs/Media/m01-github-copilot-menu-chat-options.png new file mode 100644 index 0000000..65bc0ca Binary files /dev/null and b/Instructions/Labs/Media/m01-github-copilot-menu-chat-options.png differ diff --git a/Instructions/Labs/Media/m01-github-copilot-menu.png b/Instructions/Labs/Media/m01-github-copilot-menu.png new file mode 100644 index 0000000..ad46619 Binary files /dev/null and b/Instructions/Labs/Media/m01-github-copilot-menu.png differ diff --git a/Instructions/Labs/Media/m01-github-copilot-popup-menu.png b/Instructions/Labs/Media/m01-github-copilot-popup-menu.png new file mode 100644 index 0000000..e0e9d21 Binary files /dev/null and b/Instructions/Labs/Media/m01-github-copilot-popup-menu.png differ diff --git a/Instructions/Labs/Media/m01-github-copilot-smart-action-explain.png b/Instructions/Labs/Media/m01-github-copilot-smart-action-explain.png new file mode 100644 index 0000000..81e5313 Binary files /dev/null and b/Instructions/Labs/Media/m01-github-copilot-smart-action-explain.png differ diff --git a/Instructions/Labs/Media/m01-github-copilot-smart-action-generate-docs.png b/Instructions/Labs/Media/m01-github-copilot-smart-action-generate-docs.png new file mode 100644 index 0000000..44d8494 Binary files /dev/null and b/Instructions/Labs/Media/m01-github-copilot-smart-action-generate-docs.png differ diff --git a/Instructions/Labs/Media/m01-github-copilot-toggle-chat.png b/Instructions/Labs/Media/m01-github-copilot-toggle-chat.png new file mode 100644 index 0000000..b57ce38 Binary files /dev/null and b/Instructions/Labs/Media/m01-github-copilot-toggle-chat.png differ diff --git a/Instructions/Labs/Media/m02-github-copilot-toggle-chat.png b/Instructions/Labs/Media/m02-github-copilot-toggle-chat.png new file mode 100644 index 0000000..b57ce38 Binary files /dev/null and b/Instructions/Labs/Media/m02-github-copilot-toggle-chat.png differ diff --git a/Instructions/Labs/Media/m03-github-copilot-chat-view-response-ask-mode.png b/Instructions/Labs/Media/m03-github-copilot-chat-view-response-ask-mode.png new file mode 100644 index 0000000..d336128 Binary files /dev/null and b/Instructions/Labs/Media/m03-github-copilot-chat-view-response-ask-mode.png differ diff --git a/Instructions/Labs/Media/m03-github-copilot-commit-message-python.png b/Instructions/Labs/Media/m03-github-copilot-commit-message-python.png new file mode 100644 index 0000000..a9b8557 Binary files /dev/null and b/Instructions/Labs/Media/m03-github-copilot-commit-message-python.png differ diff --git a/Instructions/Labs/Media/m03-github-copilot-commit-message.png b/Instructions/Labs/Media/m03-github-copilot-commit-message.png new file mode 100644 index 0000000..6ff858e Binary files /dev/null and b/Instructions/Labs/Media/m03-github-copilot-commit-message.png differ diff --git a/Instructions/Labs/Media/m03-github-copilot-pull-request-summary.png b/Instructions/Labs/Media/m03-github-copilot-pull-request-summary.png new file mode 100644 index 0000000..0bcbffc Binary files /dev/null and b/Instructions/Labs/Media/m03-github-copilot-pull-request-summary.png differ diff --git a/Instructions/Labs/Media/m04-fix-pytest-test-failure-py.png b/Instructions/Labs/Media/m04-fix-pytest-test-failure-py.png new file mode 100644 index 0000000..e551751 Binary files /dev/null and b/Instructions/Labs/Media/m04-fix-pytest-test-failure-py.png differ diff --git a/Instructions/Labs/Media/m04-github-copilot-agent-mode-code-editor-update.png b/Instructions/Labs/Media/m04-github-copilot-agent-mode-code-editor-update.png new file mode 100644 index 0000000..63aa3da Binary files /dev/null and b/Instructions/Labs/Media/m04-github-copilot-agent-mode-code-editor-update.png differ diff --git a/Instructions/Labs/Media/m04-github-copilot-agent-mode-terminal-command-mkdir-py.png b/Instructions/Labs/Media/m04-github-copilot-agent-mode-terminal-command-mkdir-py.png new file mode 100644 index 0000000..9d92c68 Binary files /dev/null and b/Instructions/Labs/Media/m04-github-copilot-agent-mode-terminal-command-mkdir-py.png differ diff --git a/Instructions/Labs/Media/m04-github-copilot-agent-mode-terminal-command-mkdir.png b/Instructions/Labs/Media/m04-github-copilot-agent-mode-terminal-command-mkdir.png new file mode 100644 index 0000000..0c3699d Binary files /dev/null and b/Instructions/Labs/Media/m04-github-copilot-agent-mode-terminal-command-mkdir.png differ diff --git a/Instructions/Labs/Media/m04-pytest-configure-results-py.png b/Instructions/Labs/Media/m04-pytest-configure-results-py.png new file mode 100644 index 0000000..b0f8d43 Binary files /dev/null and b/Instructions/Labs/Media/m04-pytest-configure-results-py.png differ diff --git a/Instructions/Labs/Media/m04-pytest-flask-py.png b/Instructions/Labs/Media/m04-pytest-flask-py.png new file mode 100644 index 0000000..0bc8be3 Binary files /dev/null and b/Instructions/Labs/Media/m04-pytest-flask-py.png differ diff --git a/Instructions/Labs/Media/m04-pytest-results-py.png b/Instructions/Labs/Media/m04-pytest-results-py.png new file mode 100644 index 0000000..259b389 Binary files /dev/null and b/Instructions/Labs/Media/m04-pytest-results-py.png differ diff --git a/Instructions/Labs/Media/m04-python-configure-tests-py.png b/Instructions/Labs/Media/m04-python-configure-tests-py.png new file mode 100644 index 0000000..7260cd2 Binary files /dev/null and b/Instructions/Labs/Media/m04-python-configure-tests-py.png differ diff --git a/Instructions/Labs/Media/m04-refresh-pytest-py.png b/Instructions/Labs/Media/m04-refresh-pytest-py.png new file mode 100644 index 0000000..8fa2cb4 Binary files /dev/null and b/Instructions/Labs/Media/m04-refresh-pytest-py.png differ diff --git a/Instructions/readme.md b/Instructions/readme.md new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE b/LICENSE index 9ac2500..8863003 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Sidney Andrews +Copyright (c) 2024 Microsoft Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/index.md b/index.md index eac3f41..baea103 100644 --- a/index.md +++ b/index.md @@ -1,25 +1,21 @@ --- -title: Online Hosted Instructions +title: Exercise Instructions permalink: index.html layout: home --- -# Content Directory +# GitHub Copilot Exercises -Hyperlinks to each of the lab exercises and demos are listed below. +The following quickstart exercises are designed to provide you with a hands-on learning experience in which you'll explore the capabilities of GitHub Copilot. Each exercise includes a set of tasks that you can complete in your lab environment. -## Labs +## Quickstart exercises +
{% assign labs = site.pages | where_exp:"page", "page.url contains '/Instructions/Labs'" %} -| Module | Lab | -| --- | --- | -{% for activity in labs %}| {{ activity.lab.module }} | [{{ activity.lab.title }}{% if activity.lab.type %} - {{ activity.lab.type }}{% endif %}]({{ site.github.url }}{{ activity.url }}) | -{% endfor %} -## Demos +{% for activity in labs %} -{% assign demos = site.pages | where_exp:"page", "page.url contains '/Instructions/Demos'" %} -| Module | Demo | -| --- | --- | -{% for activity in demos %}| {{ activity.demo.module }} | [{{ activity.demo.title }}]({{ site.github.url }}{{ activity.url }}) | +### [{{ activity.lab.title }}]({{ site.github.url }}{{ activity.url }}) +{{ activity.lab.description }} +
{% endfor %} diff --git a/readme.md b/readme.md index b62c842..f4b1186 100644 --- a/readme.md +++ b/readme.md @@ -1,35 +1,14 @@ -# INF99X: Sample Course +# Microsoft Lab Exercises + -- **[Download Latest Student Handbook and AllFiles Content](../../releases/latest)** -- **Are you a MCT?** - Have a look at our [GitHub User Guide for MCTs](https://microsoftlearning.github.io/MCT-User-Guide/) -- **Need to manually build the lab instructions?** - Instructions are available in the [MicrosoftLearning/Docker-Build](https://github.com/MicrosoftLearning/Docker-Build) repository +This repo contains exercises and supporting files for Microsoft skilling content. -## What are we doing? +The exercises may be used in both self-paced skilling experiences on [Microsoft Learn](https://learn.microsoft.com) and in Microsoft authorized instructor-led training. + -- To support this course, we will need to make frequent updates to the course content to keep it current with the Azure services used in the course. We are publishing the lab instructions and lab files on GitHub to allow for open contributions between the course authors and MCTs to keep the content current with changes in the Azure platform. +## Information for MCTs + -- We hope that this brings a sense of collaboration to the labs like we've never had before - when Azure changes and you find it first during a live delivery, go ahead and make an enhancement right in the lab source. Help your fellow MCTs. +**Are you an MCT?** - Have a look at our [GitHub User Guide for MCTs](https://microsoftlearning.github.io/MCT-User-Guide/) -## How should I use these files relative to the released MOC files? - -- The instructor handbook and PowerPoints are still going to be your primary source for teaching the course content. - -- These files on GitHub are designed to be used in conjunction with the student handbook, but are in GitHub as a central repository so MCTs and course authors can have a shared source for the latest lab files. - -- It will be recommended that for every delivery, trainers check GitHub for any changes that may have been made to support the latest Azure services, and get the latest files for their delivery. - -## What about changes to the student handbook? - -- We will review the student handbook on a quarterly basis and update through the normal MOC release channels as needed. - -## How do I contribute? - -- Any MCT can submit a pull request to the code or content in the GitHub repro, Microsoft and the course author will triage and include content and lab code changes as needed. - -- You can submit bugs, changes, improvement and ideas. Find a new Azure feature before we have? Submit a new demo! - -## Notes - -### Classroom Materials - -It is strongly recommended that MCTs and Partners access these materials and in turn, provide them separately to students. Pointing students directly to GitHub to access Lab steps as part of an ongoing class will require them to access yet another UI as part of the course, contributing to a confusing experience for the student. An explanation to the student regarding why they are receiving separate Lab instructions can highlight the nature of an always-changing cloud-based interface and platform. Microsoft Learning support for accessing files on GitHub and support for navigation of the GitHub site is limited to MCTs teaching this course only. +Any MCT (Microsoft Certified Trainer) can submit a pull request to the code or content in the GitHub repro. Microsoft and the course author will then triage and include content and lab code changes as needed. You can submit bugs, changes, improvement, and ideas. Find a new Azure or Microsoft 365 feature before we have? Submit a new demo!