From 710a9fca8ae9fd54988686e65108109e2ca5abc5 Mon Sep 17 00:00:00 2001 From: fanyang7265-commits Date: Tue, 28 Jul 2026 17:53:16 -0400 Subject: [PATCH 1/5] Unit Test Module Completed --- .../JsonLoanRepository/GetLoan.cs | 66 +++++++++++++++++++ .../tests/UnitTests/UnitTests.csproj | 8 +++ 2 files changed, 74 insertions(+) create mode 100644 LabFiles/04-develop-unit-tests-xunit/AccelerateDevGHCopilot/tests/UnitTests/Infrastructure/JsonLoanRepository/GetLoan.cs diff --git a/LabFiles/04-develop-unit-tests-xunit/AccelerateDevGHCopilot/tests/UnitTests/Infrastructure/JsonLoanRepository/GetLoan.cs b/LabFiles/04-develop-unit-tests-xunit/AccelerateDevGHCopilot/tests/UnitTests/Infrastructure/JsonLoanRepository/GetLoan.cs new file mode 100644 index 0000000..1ae1eb4 --- /dev/null +++ b/LabFiles/04-develop-unit-tests-xunit/AccelerateDevGHCopilot/tests/UnitTests/Infrastructure/JsonLoanRepository/GetLoan.cs @@ -0,0 +1,66 @@ + 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); + } + + + [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); + } + +} diff --git a/LabFiles/04-develop-unit-tests-xunit/AccelerateDevGHCopilot/tests/UnitTests/UnitTests.csproj b/LabFiles/04-develop-unit-tests-xunit/AccelerateDevGHCopilot/tests/UnitTests/UnitTests.csproj index a156d8f..c3dfc74 100644 --- a/LabFiles/04-develop-unit-tests-xunit/AccelerateDevGHCopilot/tests/UnitTests/UnitTests.csproj +++ b/LabFiles/04-develop-unit-tests-xunit/AccelerateDevGHCopilot/tests/UnitTests/UnitTests.csproj @@ -23,6 +23,14 @@ + + + + Json\%(RecursiveDir)%(FileName)%(Extension) + PreserveNewest + + + From b79e3fb2d11b9b7bebb27fe028d80cb24123c16c Mon Sep 17 00:00:00 2001 From: fanyang7265-commits Date: Wed, 29 Jul 2026 17:49:37 -0400 Subject: [PATCH 2/5] Module5 lab complete --- .../Enums/EnumHelper.cs | 54 +++++++++--- .../Library.Infrastructure/Data/JsonData.cs | 85 +++++-------------- .../Data/JsonLoanRepository.cs | 24 ++---- .../Data/JsonPatronRepository.cs | 39 +++------ 4 files changed, 80 insertions(+), 122 deletions(-) diff --git a/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/EnumHelper.cs b/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/EnumHelper.cs index 5369856..22a33a9 100644 --- a/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/EnumHelper.cs +++ b/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/EnumHelper.cs @@ -1,27 +1,53 @@ -using System.ComponentModel; -using System.Reflection; +using System.Collections.Generic; namespace Library.ApplicationCore.Enums; public static class EnumHelper { + private static readonly IReadOnlyDictionary LoanExtensionStatusDescriptions = + new Dictionary + { + [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 IReadOnlyDictionary LoanReturnStatusDescriptions = + new Dictionary + { + [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 IReadOnlyDictionary MembershipRenewalStatusDescriptions = + new Dictionary + { + [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(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 switch { - return value.ToString(); - } + LoanExtensionStatus loanExtensionStatus when LoanExtensionStatusDescriptions.TryGetValue(loanExtensionStatus, out var extensionDescription) + => extensionDescription, + LoanReturnStatus loanReturnStatus when LoanReturnStatusDescriptions.TryGetValue(loanReturnStatus, out var returnDescription) + => returnDescription, + MembershipRenewalStatus membershipRenewalStatus when MembershipRenewalStatusDescriptions.TryGetValue(membershipRenewalStatus, out var membershipDescription) + => membershipDescription, + _ => value.ToString() + }; } } \ No newline at end of file diff --git a/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonData.cs b/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonData.cs index 7af26a7..54f7490 100644 --- a/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonData.cs +++ b/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonData.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Linq; +using System.Text.Json; using Library.ApplicationCore.Entities; using Microsoft.Extensions.Configuration; @@ -97,108 +98,66 @@ public List GetPopulatedPatrons(IEnumerable patrons) public Patron GetPopulatedPatron(Patron p) { - Patron populated = new Patron + return new Patron { Id = p.Id, Name = p.Name, ImageName = p.ImageName, MembershipStart = p.MembershipStart, MembershipEnd = p.MembershipEnd, - Loans = new List() + Loans = Loans? + .Where(loan => loan.PatronId == p.Id) + .Select(GetPopulatedLoan) + .ToList() ?? 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 + return new Loan { Id = l.Id, BookItemId = l.BookItemId, PatronId = l.PatronId, LoanDate = l.LoanDate, DueDate = l.DueDate, - ReturnDate = l.ReturnDate + ReturnDate = l.ReturnDate, + BookItem = GetPopulatedBookItem(BookItems!.Single(bi => bi.Id == l.BookItemId)), + Patron = Patrons!.Single(p => p.Id == l.PatronId) }; - - 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 + return new BookItem { Id = bi.Id, BookId = bi.BookId, AcquisitionDate = bi.AcquisitionDate, - Condition = bi.Condition + Condition = bi.Condition, + Book = GetPopulatedBook(Books!.Single(b => b.Id == bi.BookId)) }; - - 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 + return 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 + ImageName = b.ImageName, + Author = Authors! + .Where(a => a.Id == b.AuthorId) + .Select(a => new Author { Id = a.Id, Name = a.Name - }; - break; - } - } - - return populated; + }) + .First() + }; } private async Task LoadJson(string filePath) diff --git a/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonLoanRepository.cs b/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonLoanRepository.cs index 2683283..cd51cc4 100644 --- a/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonLoanRepository.cs +++ b/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonLoanRepository.cs @@ -16,28 +16,16 @@ public JsonLoanRepository(JsonData jsonData) { await _jsonData.EnsureDataLoaded(); - foreach (Loan loan in _jsonData.Loans!) - { - if (loan.Id == id) - { - Loan populated = _jsonData.GetPopulatedLoan(loan); - return populated; - } - } - return null; + return _jsonData.Loans! + .Where(loan => loan.Id == id) + .Select(_jsonData.GetPopulatedLoan) + .FirstOrDefault(); } public async Task UpdateLoan(Loan loan) { - Loan? existingLoan = null; - foreach (Loan l in _jsonData.Loans!) - { - if (l.Id == loan.Id) - { - existingLoan = l; - break; - } - } + Loan? existingLoan = _jsonData.Loans! + .FirstOrDefault(l => l.Id == loan.Id); if (existingLoan != null) { diff --git a/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonPatronRepository.cs b/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonPatronRepository.cs index efb05f8..a52ce09 100644 --- a/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonPatronRepository.cs +++ b/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonPatronRepository.cs @@ -1,5 +1,6 @@ using Library.ApplicationCore; using Library.ApplicationCore.Entities; +using System.Linq; namespace Library.Infrastructure.Data; @@ -16,15 +17,10 @@ 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)); + List searchResults = _jsonData.Patrons! + .Where(patron => patron.Name.Contains(searchInput)) + .OrderBy(patron => patron.Name) + .ToList(); searchResults = _jsonData.GetPopulatedPatrons(searchResults); @@ -35,30 +31,19 @@ public async Task> SearchPatrons(string searchInput) { await _jsonData.EnsureDataLoaded(); - foreach (Patron patron in _jsonData.Patrons!) - { - if (patron.Id == id) - { - Patron populated = _jsonData.GetPopulatedPatron(patron); - return populated; - } - } - return null; + Patron? patron = _jsonData.Patrons! + .FirstOrDefault(p => p.Id == id); + + return patron == null ? null : _jsonData.GetPopulatedPatron(patron); } 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; - } - } + Patron? existingPatron = patrons + .FirstOrDefault(p => p.Id == patron.Id); + if (existingPatron != null) { existingPatron.Name = patron.Name; From 3c05d8aced311bf284bd52ddd55cb67839bc3010 Mon Sep 17 00:00:00 2001 From: fanyang7265-commits Date: Wed, 29 Jul 2026 22:00:17 -0400 Subject: [PATCH 3/5] Updated readme --- readme.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index f4b1186..ec049a8 100644 --- a/readme.md +++ b/readme.md @@ -1,8 +1,11 @@ -# Microsoft Lab Exercises +# Microsoft Github Copilot Lab Exercises + This repo contains exercises and supporting files for Microsoft skilling content. + + 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. From 1b2c8ebbab4f333a8911fa916bee9006a1c89753 Mon Sep 17 00:00:00 2001 From: fanyang7265-commits Date: Fri, 31 Jul 2026 14:06:45 -0400 Subject: [PATCH 4/5] updated CommonActions and ConsoleApp --- .../src/Library.Console/CommonActions.cs | 3 ++- .../src/Library.Console/ConsoleApp.cs | 24 ++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/CommonActions.cs b/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/CommonActions.cs index 681f95c..a002da6 100644 --- a/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/CommonActions.cs +++ b/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/CommonActions.cs @@ -9,5 +9,6 @@ public enum CommonActions SearchPatrons = 4, RenewPatronMembership = 8, ReturnLoanedBook = 16, - ExtendLoanedBook = 32 + ExtendLoanedBook = 32, + SearchBooks = 64 } diff --git a/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs b/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs index 9fc9750..7c65d37 100644 --- a/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs +++ b/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs @@ -136,6 +136,7 @@ static CommonActions ReadInputOptions(CommonActions options, out int optionNumbe { "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, @@ -170,6 +171,10 @@ static void WriteInputOptions(CommonActions options) { 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"); @@ -193,7 +198,7 @@ async Task PatronDetails() loanNumber++; } - CommonActions options = CommonActions.SearchPatrons | CommonActions.Quit | CommonActions.Select | CommonActions.RenewPatronMembership; + CommonActions options = CommonActions.SearchPatrons | CommonActions.SearchBooks | CommonActions.Quit | CommonActions.Select | CommonActions.RenewPatronMembership; CommonActions action = ReadInputOptions(options, out int selectedLoanNumber); if (action == CommonActions.Select) { @@ -225,10 +230,27 @@ async Task PatronDetails() 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."); } + Task SearchBooks() + { + string? bookTitle = null; + while (string.IsNullOrWhiteSpace(bookTitle)) + { + Console.Write("Enter a book title to search for: "); + bookTitle = Console.ReadLine(); + } + + return Task.FromResult(ConsoleState.PatronDetails); + } + + async Task LoanDetails() { Console.WriteLine($"Book title: {selectedLoanDetails.BookItem!.Book!.Title}"); From cb80fc41e9ec3dbaf8662991cfee795383a689b6 Mon Sep 17 00:00:00 2001 From: fanyang7265-commits Date: Fri, 31 Jul 2026 14:23:56 -0400 Subject: [PATCH 5/5] Add book search functionality to ConsoleApp --- .../src/Library.Console/ConsoleApp.cs | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs b/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs index 7c65d37..214d748 100644 --- a/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs +++ b/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs @@ -1,6 +1,7 @@ using Library.ApplicationCore; using Library.ApplicationCore.Entities; using Library.ApplicationCore.Enums; +using Library.Infrastructure.Data; using Library.Console; public class ConsoleApp @@ -16,13 +17,15 @@ public class ConsoleApp ILoanRepository _loanRepository; ILoanService _loanService; IPatronService _patronService; + JsonData _jsonData; - public ConsoleApp(ILoanService loanService, IPatronService patronService, IPatronRepository patronRepository, ILoanRepository loanRepository) + 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() @@ -238,7 +241,7 @@ async Task PatronDetails() throw new InvalidOperationException("An input option is not handled."); } - Task SearchBooks() + async Task SearchBooks() { string? bookTitle = null; while (string.IsNullOrWhiteSpace(bookTitle)) @@ -247,7 +250,33 @@ Task SearchBooks() bookTitle = Console.ReadLine(); } - return Task.FromResult(ConsoleState.PatronDetails); + 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; }