diff --git a/.gitignore b/.gitignore index dfcfd56..30fbdce 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ *.userosscache *.sln.docstates +# directories +.idea + # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/BankingSoftware/BankingSoftware.csproj b/BankingSoftware/BankingSoftware.csproj new file mode 100644 index 0000000..d7fa852 --- /dev/null +++ b/BankingSoftware/BankingSoftware.csproj @@ -0,0 +1,16 @@ + + + + Exe + net7.0 + disable + enable + BankingSoftware + + + + + + + + diff --git a/BankingSoftware/Common/AddSymbol.cs b/BankingSoftware/Common/AddSymbol.cs new file mode 100644 index 0000000..a727510 --- /dev/null +++ b/BankingSoftware/Common/AddSymbol.cs @@ -0,0 +1,22 @@ +using System; + +namespace BankingSoftware.Common; + +public static class AddSymbol +{ + public static void AddBreakLine(int symbolLength = 80, string symbol = "-") + { + for (var i = 0; i < symbolLength; i++) + Console.Write(symbol); + + Console.WriteLine(""); + } + + public static void AddBreakLines(int symbolLength = 80, int lineLength = 3, string symbol = "-") + { + for (var k = 0; k < lineLength; k++) + AddBreakLine(symbolLength, symbol); + + Console.WriteLine("\n"); + } +} \ No newline at end of file diff --git a/BankingSoftware/Common/Hash.cs b/BankingSoftware/Common/Hash.cs new file mode 100644 index 0000000..c5cd96a --- /dev/null +++ b/BankingSoftware/Common/Hash.cs @@ -0,0 +1,29 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace BankingSoftware.Common; + +// to hash input values +public class Hash +{ + private const int keySize = 16; + private const int iterations = 350000; + private readonly HashAlgorithmName hashAlgorithm = HashAlgorithmName.SHA256; + + public string HashValues(string[] values) + { + var joinedValues = string.Join("", values); + + var salt = new UTF8Encoding(true).GetBytes(joinedValues); + + var hash = Rfc2898DeriveBytes.Pbkdf2( + Encoding.UTF8.GetBytes(joinedValues), + salt, + iterations, + hashAlgorithm, + keySize); + + return Convert.ToHexString(hash); + } +} \ No newline at end of file diff --git a/BankingSoftware/Common/VerifyInput.cs b/BankingSoftware/Common/VerifyInput.cs new file mode 100644 index 0000000..2d7a1cb --- /dev/null +++ b/BankingSoftware/Common/VerifyInput.cs @@ -0,0 +1,130 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; + +namespace BankingSoftware.Common; + +// verify input by user +public static partial class VerifyInput +{ + public static string VerifyForStringOnly(string input) + { + while ( + string.IsNullOrEmpty(input) + || string.IsNullOrWhiteSpace(input) + || input.Any(char.IsNumber) + || input.Any(c => + { + char.IsNumber(c); + char.IsSymbol(c); + char.IsPunctuation(c); + char.IsSeparator(c); + + return false; + }) + ) + { + Console.WriteLine("Input must not contain any number or symbol"); + Console.Write("Please input new value: "); + input = Console.ReadLine(); + } + + Console.WriteLine("Input is valid"); + return input; + } + + public static string VerifyForStringAndNumber(string stringNumber) + { + while ( + string.IsNullOrEmpty(stringNumber) + || string.IsNullOrWhiteSpace(stringNumber) + || stringNumber.Any(c => + { + char.IsLetter(c); + char.IsPunctuation(c); + char.IsSymbol(c); + + return false; + }) + ) + { + Console.WriteLine("Input must not contain any number or symbol"); + Console.Write("Please input new value: "); + stringNumber = Console.ReadLine(); + } + + Console.WriteLine("Input is valid"); + return stringNumber; + } + + public static int VerifyNumber(int number) + { + while ( + string.IsNullOrEmpty(number.ToString()) + || string.IsNullOrWhiteSpace(number.ToString()) + || number.ToString().Any(c => + { + char.IsSymbol(c); + char.IsPunctuation(c); + char.IsLetter(c); + + + return false; + }) + ) + { + Console.WriteLine("Input must not contain any letter or symbol"); + Console.Write("Please input your value: "); + number = int.Parse(Console.ReadLine()); + } + + Console.WriteLine("Number is valid"); + return number; + } + + public static string VerifyPassword(string password) + { + while ( + string.IsNullOrEmpty(password) + || string.IsNullOrWhiteSpace(password) + ) + { + Console.WriteLine("Input must not contain any space"); + Console.Write("Please input new value: "); + password = Console.ReadLine(); + } + + Console.WriteLine("Password is valid"); + return password; + } + + [GeneratedRegex("^\\(?([0-9]{3})\\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$")] + private static partial Regex MyRegex(); + + public static string VerifyPhoneNumber(string phoneNumber) + { + while (!MyRegex().IsMatch(phoneNumber)) + { + Console.WriteLine("Input must not contain any letter or symbol"); + Console.Write("Please input new value: "); + phoneNumber = Console.ReadLine(); + } + + Console.WriteLine("Phone number is valid"); + return phoneNumber; + } + + public static string VerifyEmail(string email) + { + var regex = @"^[^@\s]+@[^@\s]+\.(com|net|org|gov)$"; + + while (!Regex.IsMatch(email, regex, RegexOptions.IgnoreCase)) + { + Console.Write("Input a valid email: "); + email = Console.ReadLine(); + } + + Console.WriteLine("Email is valid."); + return email; + } +} \ No newline at end of file diff --git a/BankingSoftware/Common/WriteFile.cs b/BankingSoftware/Common/WriteFile.cs new file mode 100644 index 0000000..faf9e37 --- /dev/null +++ b/BankingSoftware/Common/WriteFile.cs @@ -0,0 +1,24 @@ +using System.IO; +using System.Text; + +namespace BankingSoftware.Common; + +// for writing to text files +public static class WriteFile +{ + public static void WriteLine(string lineText, string fileName) + { + using var fs = File.AppendText(fileName); + fs.Write(lineText); + } + + public static void WriteLines(string[] linesTexts, string fileName) + { + using var fs = File.OpenWrite(fileName); + for (var i = 0; i < linesTexts.Length; i++) + { + var info = new UTF8Encoding(true).GetBytes(linesTexts[i] + "\n"); + fs.Write(info, 0, info.Length); + } + } +} \ No newline at end of file diff --git a/BankingSoftware/Modules/AccountModule.cs b/BankingSoftware/Modules/AccountModule.cs new file mode 100644 index 0000000..f5bd39d --- /dev/null +++ b/BankingSoftware/Modules/AccountModule.cs @@ -0,0 +1,120 @@ +using System; +using System.IO; +using BankingSoftware.Common; + +namespace BankingSoftware.Modules; + +// Module to handle account operations +public class AccountModule +{ + private static string GetAccountFile(in string userLogin, string accountStoragePath) + { + var charFileName = userLogin.Split(); + var accountFileName = new Hash().HashValues(charFileName); + var accountFilenameWithPath = accountStoragePath + accountFileName + ".txt"; + + return accountFilenameWithPath; + } + + private static string[] GetAccountSummary(string accountFilename) + { + var accountInfo = File.ReadAllLines(accountFilename); + + return accountInfo; + } + + public static void Deposit(in string userLogin, string accountStoragePath) + { + var accountFilename = GetAccountFile(userLogin, accountStoragePath); + + var accountInfo = GetAccountSummary(accountFilename); + foreach (var info in accountInfo) Console.WriteLine(info); + + Console.Write("Please enter the amount you want to deposit: "); + var depositAmount = int.Parse(Console.ReadLine()); + + while (depositAmount is string && depositAmount < 0) depositAmount = int.Parse(Console.ReadLine()); + + var depositAmountLine = accountInfo[2].Split(": $"); + var depositTimeLine = accountInfo[3].Split(": "); + depositAmountLine[1] = depositAmount.ToString(); + depositTimeLine[1] = DateTime.Now.ToString(); + + accountInfo[2] = string.Join(": $", depositAmountLine); + accountInfo[3] = string.Join(": ", depositTimeLine); + + var totalLine = accountInfo[4].Split(": $"); + + var totalAmount = int.Parse(totalLine[1]) + depositAmount; + totalLine[1] = totalAmount.ToString(); + accountInfo[4] = string.Join(": $", totalLine); + + Console.WriteLine("You've deposited ${0} to your account", depositAmount); + + Console.WriteLine("\nYour total amount is ${0}", totalAmount); + + WriteFile.WriteLines(accountInfo, accountFilename); + } + + public static void Withdraw(in string userLogin, string accountStoragePath) + { + var accountFilename = GetAccountFile(userLogin, accountStoragePath); + + var accountInfo = GetAccountSummary(accountFilename); + var totalLine = accountInfo[4].Split(": $"); + + Console.Write("Please enter the amount you want to withdraw: "); + var withdrawAmount = int.Parse(Console.ReadLine()); + var currentTotal = int.Parse(totalLine[1]); + var finalTotalAmount = currentTotal - withdrawAmount; + + while (withdrawAmount is string && withdrawAmount < 0) + { + Console.WriteLine("Your withdraw amount should be greater than Zero"); + withdrawAmount = int.Parse(Console.ReadLine()); + finalTotalAmount = currentTotal - withdrawAmount; + } + + if (finalTotalAmount < 0) + { + Console.WriteLine("Insufficient balance."); + } + else + { + var withdrawAmountLine = accountInfo[0].Split(": $"); + var withdrawTimeLine = accountInfo[1].Split(": "); + withdrawAmountLine[1] = withdrawAmount.ToString(); + withdrawTimeLine[1] = DateTime.Now.ToString(); + + accountInfo[0] = string.Join(": ", withdrawAmountLine); + accountInfo[1] = string.Join(": ", withdrawTimeLine); + + totalLine[1] = finalTotalAmount.ToString(); + accountInfo[4] = string.Join(": $", totalLine); + + Console.WriteLine("You've withdrawn ${0} from your account", withdrawAmount); + + Console.WriteLine("\nYour have ${0} left in your account", finalTotalAmount); + + WriteFile.WriteLines(accountInfo, accountFilename); + } + } + + public static void ViewAccountBalance(in string userLogin, string accountStoragePath) + { + var accountFilename = GetAccountFile(userLogin, accountStoragePath); + + var accountBalance = GetAccountSummary(accountFilename)[4]; + + Console.WriteLine("Your account balance is {0}", accountBalance); + } + + public static void ViewAccountSummary(in string userLogin, string accountStoragePath) + { + var accountFilename = GetAccountFile(userLogin, accountStoragePath); + + var accountInfo = GetAccountSummary(accountFilename); + + foreach (var info in accountInfo) Console.WriteLine(info); + } +} \ No newline at end of file diff --git a/BankingSoftware/Modules/AuthModule.cs b/BankingSoftware/Modules/AuthModule.cs new file mode 100644 index 0000000..46193c3 --- /dev/null +++ b/BankingSoftware/Modules/AuthModule.cs @@ -0,0 +1,42 @@ +using System; +using System.IO; +using BankingSoftware.Common; + +namespace BankingSoftware.Modules; + +// Module to handle authentication +public static class AuthModule +{ + public static string SignIn(string userStoragePath) + { + Console.Write("Please enter your username: "); + var username = Console.ReadLine(); + + Console.Write("Please enter your password: "); + var password = Console.ReadLine(); + var hashedPassword = new Hash().HashValues(password.Split()); + + var userLogin = new Hash().HashValues(new[] { username, hashedPassword }); + var hashedUsername = new Hash().HashValues(username.Split()); + var userExist = File.Exists(userStoragePath + hashedUsername + ".txt"); + + while (!userExist) + { + Console.WriteLine("Incorrect username or password, please try again"); + Console.Write("Please enter your username: "); + username = Console.ReadLine(); + + Console.Write("Please enter your password: "); + password = Console.ReadLine(); + hashedPassword = new Hash().HashValues(password.Split()); + + userLogin = new Hash().HashValues(new[] { username, hashedPassword }); + hashedUsername = new Hash().HashValues(username.Split()); + userExist = File.Exists(userStoragePath + hashedUsername + ".txt"); + } + + Console.WriteLine("Welcome {0}", username); + + return userLogin; + } +} \ No newline at end of file diff --git a/BankingSoftware/Modules/SignupModule.cs b/BankingSoftware/Modules/SignupModule.cs new file mode 100644 index 0000000..695f0d2 --- /dev/null +++ b/BankingSoftware/Modules/SignupModule.cs @@ -0,0 +1,99 @@ +using System; +using BankingSoftware.Common; + +namespace BankingSoftware.Modules; + +// Module to handle signin and creation of account +public class SignupModule +{ + //const string StoragePath = + + public static string Signup(string userStoragePath) + { + string[] userKeys = + { + "First Name: ", "Last Name: ", "Email: ", "Username: ", "Age: ", "Phone Number: ", + "HashedPassword: " + }; + var userValues = new string[userKeys.Length + 1]; + + Console.WriteLine("Please answer the questions below to signup..."); + AddSymbol.AddBreakLine(symbol: " "); + + Console.Write("Enter your first name: "); + var firstName = Console.ReadLine(); + VerifyInput.VerifyForStringOnly(firstName); + userValues[0] = userKeys[0] + firstName; + AddSymbol.AddBreakLine(); + + Console.Write("Enter your last name: "); + var lastName = Console.ReadLine(); + VerifyInput.VerifyForStringOnly(lastName); + userValues[1] = userKeys[1] + lastName; + AddSymbol.AddBreakLine(); + + Console.Write("Enter your email: "); + var email = Console.ReadLine(); + email = VerifyInput.VerifyEmail(email); + userValues[2] = userKeys[2] + email; + AddSymbol.AddBreakLine(); + + Console.Write("Enter your username: "); + var username = Console.ReadLine(); + username = VerifyInput.VerifyForStringAndNumber(username); + userValues[3] = userKeys[3] + username; + AddSymbol.AddBreakLine(); + + Console.Write("Enter your age: "); + var age = int.Parse(Console.ReadLine()); + age = VerifyInput.VerifyNumber(age); + while (age < 18 || age > 100) + { + Console.WriteLine("Age should be between 18 and 100"); + Console.Write("Input new value"); + Console.ReadLine(); + + age = int.Parse(Console.ReadLine()); + VerifyInput.VerifyNumber(age); + } + + userValues[4] = userKeys[4] + age; + AddSymbol.AddBreakLine(); + + Console.Write("Enter your phone number: "); + var phoneNumber = Console.ReadLine(); + phoneNumber = VerifyInput.VerifyPhoneNumber(phoneNumber); + userValues[5] = userKeys[5] + phoneNumber; + AddSymbol.AddBreakLine(); + + Console.Write("Enter your password: "); + var password = Console.ReadLine(); + password = VerifyInput.VerifyPassword(password); + var hashedPassword = new Hash().HashValues(password.Split()); + userValues[6] = userKeys[6] + hashedPassword; + AddSymbol.AddBreakLines(lineLength: 2); + + var hashedUsername = new Hash().HashValues(username.Split()); + userStoragePath += "\\" + hashedUsername + ".txt"; + WriteFile.WriteLines(userValues, userStoragePath); + + var credentials = new Hash().HashValues(new[] { username, hashedPassword }); + + return credentials; + } + + public static void CreateAccount(in string userLogin, string accountStoragePath) + { + string[] accountValues = + { + "Last withdrawal amount: $0", "Last withdrawal time: ", "Last deposit amount: $0", + "Last deposit time: ", "Total: $0" + }; + + var charFileName = userLogin.Split(); + var hashedFileName = new Hash().HashValues(charFileName); + + var accountFilename = accountStoragePath + hashedFileName + ".txt"; + WriteFile.WriteLines(accountValues, accountFilename); + } +} \ No newline at end of file diff --git a/BankingSoftware/Program.cs b/BankingSoftware/Program.cs new file mode 100644 index 0000000..41a5e87 --- /dev/null +++ b/BankingSoftware/Program.cs @@ -0,0 +1,110 @@ +using System; +using BankingSoftware.Common; +using BankingSoftware.Modules; + +namespace BankingSoftware; + +internal class Program +{ + private const string userStoragePath = + @"C:\Users\rexxr\Documents\projectss\CodeBenders\BankingSoftware\Storage\Users\"; + + private const string accountStoragePath = + @"C:\Users\rexxr\Documents\projectss\CodeBenders\BankingSoftware\Storage\Accounts\"; + + public static void Auth(out string userLogin) + { + userLogin = ""; + Console.WriteLine("\nWelcome to our console banking app"); + AddSymbol.AddBreakLines(symbol: "*"); + + Console.WriteLine("Please login or signup. \n"); + Console.WriteLine("Press 1 to login or 2 to signup"); + Console.Write(">>>>> "); + var loginSignup = int.Parse(Console.ReadLine()); + Console.WriteLine("\n"); + + while (loginSignup is string || loginSignup > 2) + { + Console.WriteLine("Input the correct value..."); + Console.Write(">>>>> "); + loginSignup = int.Parse(Console.ReadLine()); + } + + switch (loginSignup) + { + case 1: + userLogin = AuthModule.SignIn(userStoragePath); + break; + case 2: + userLogin = SignupModule.Signup(userStoragePath); + SignupModule.CreateAccount(userLogin, accountStoragePath); + break; + } + + AddSymbol.AddBreakLine(); + } + + public static void Account(in string userLogin) + { + Console.WriteLine("What action do you want to perform"); + Console.WriteLine("Press 1 to VIEW ACCOUNT SUMMARY"); + Console.WriteLine("Press 2 to VIEW BALANCE"); + Console.WriteLine("Press 3 to DEPOSIT"); + Console.WriteLine("Press 4 to WITHDRAW"); + + Console.Write(">>>>> "); + var action = int.Parse(Console.ReadLine()); + + switch (action) + { + case 1: + AccountModule.ViewAccountSummary(in userLogin, accountStoragePath); + break; + case 2: + AccountModule.ViewAccountBalance(in userLogin, accountStoragePath); + break; + case 3: + AccountModule.Deposit(in userLogin, accountStoragePath); + break; + case 4: + AccountModule.Withdraw(in userLogin, accountStoragePath); + break; + default: + Console.WriteLine("Input the correct value"); + break; + } + + Console.WriteLine("Do you want to perform another transaction"); + Console.WriteLine("............ Press 1 to continue"); + Console.WriteLine("............ Press 2 to quit"); + Console.Write(">>>>> "); + var anotherAction = int.Parse(Console.ReadLine()); + + while (anotherAction > 2) + { + Console.WriteLine("Please input the correct value."); + Console.Write(">>>>> "); + anotherAction = int.Parse(Console.ReadLine()); + } + + if (anotherAction == 1) + { + Account(in userLogin); + } + else if(anotherAction == 2) + { + Console.WriteLine("Thank you for banking with us..."); + Console.WriteLine("Goodbye."); + } + + AddSymbol.AddBreakLines(symbol: "*"); + } + + private static void Main() + { + Auth(out var userLogin); + + Account(in userLogin); + } +} \ No newline at end of file diff --git a/BankingSoftware/Storage/Accounts/086591F1B63CEC976440DC2952AC4138.txt b/BankingSoftware/Storage/Accounts/086591F1B63CEC976440DC2952AC4138.txt new file mode 100644 index 0000000..2f3dfb6 --- /dev/null +++ b/BankingSoftware/Storage/Accounts/086591F1B63CEC976440DC2952AC4138.txt @@ -0,0 +1,5 @@ +Last withdrawal amount: $0 +Last withdrawal time: +Last deposit amount: $0 +Last deposit time: +Total: $0 diff --git a/BankingSoftware/Storage/Accounts/0E84027714179DA860EC36BE3ABC2CCE.txt b/BankingSoftware/Storage/Accounts/0E84027714179DA860EC36BE3ABC2CCE.txt new file mode 100644 index 0000000..3df7b09 --- /dev/null +++ b/BankingSoftware/Storage/Accounts/0E84027714179DA860EC36BE3ABC2CCE.txt @@ -0,0 +1,5 @@ +Last withdrawal amount: 20000 +Last withdrawal time: 3/3/2023 9:01:46 AM +Last deposit amount: 60000 +Last deposit time: 3/3/2023 9:01:34 AM +Total: $40000 diff --git a/BankingSoftware/Storage/Accounts/1BBCFB82D95A66785DABF16FE16DD079.txt b/BankingSoftware/Storage/Accounts/1BBCFB82D95A66785DABF16FE16DD079.txt new file mode 100644 index 0000000..f7a2f63 --- /dev/null +++ b/BankingSoftware/Storage/Accounts/1BBCFB82D95A66785DABF16FE16DD079.txt @@ -0,0 +1,5 @@ +Last withdrawal amount: 50000 +Last withdrawal time: 3/3/2023 8:55:01 AM +Last deposit amount: 80000 +Last deposit time: 3/3/2023 8:54:50 AM +Total: $30000 diff --git a/BankingSoftware/Storage/Accounts/5FE5F846BD5D98FFF20F1384D0DFC710.txt b/BankingSoftware/Storage/Accounts/5FE5F846BD5D98FFF20F1384D0DFC710.txt new file mode 100644 index 0000000..9f6b71d --- /dev/null +++ b/BankingSoftware/Storage/Accounts/5FE5F846BD5D98FFF20F1384D0DFC710.txt @@ -0,0 +1,5 @@ +Last withdrawal amount: 20000 +Last withdrawal time: 3/1/2023 10:02:54 AM +Last deposit amount: 90000 +Last deposit time: 3/1/2023 10:02:43 AM +Total: $70000 diff --git a/BankingSoftware/Storage/Accounts/7445BDA0F89FE36B4BC831F76CB0533A.txt b/BankingSoftware/Storage/Accounts/7445BDA0F89FE36B4BC831F76CB0533A.txt new file mode 100644 index 0000000..2f3dfb6 --- /dev/null +++ b/BankingSoftware/Storage/Accounts/7445BDA0F89FE36B4BC831F76CB0533A.txt @@ -0,0 +1,5 @@ +Last withdrawal amount: $0 +Last withdrawal time: +Last deposit amount: $0 +Last deposit time: +Total: $0 diff --git a/BankingSoftware/Storage/Accounts/A91CCAE139380AA1368393C8FC2953C1.txt b/BankingSoftware/Storage/Accounts/A91CCAE139380AA1368393C8FC2953C1.txt new file mode 100644 index 0000000..15c2903 --- /dev/null +++ b/BankingSoftware/Storage/Accounts/A91CCAE139380AA1368393C8FC2953C1.txt @@ -0,0 +1,5 @@ +Last withdrawal amount: 20000 +Last withdrawal time: 3/3/2023 8:47:41 AM +Last deposit amount: 80000 +Last deposit time: 3/3/2023 8:47:30 AM +Total: $60000 diff --git a/BankingSoftware/Storage/Accounts/B56FD7BF4AB39D813239C006C4FF5813.txt b/BankingSoftware/Storage/Accounts/B56FD7BF4AB39D813239C006C4FF5813.txt new file mode 100644 index 0000000..4c63b6b --- /dev/null +++ b/BankingSoftware/Storage/Accounts/B56FD7BF4AB39D813239C006C4FF5813.txt @@ -0,0 +1,5 @@ +Last withdrawal amount: 30000 +Last withdrawal time: 3/3/2023 9:09:01 AM +Last deposit amount: $60000 +Last deposit time: 3/3/2023 9:08:49 AM +Total: $30000 diff --git a/BankingSoftware/Storage/Users/2947CB1B92BB13C8F373E78C76FCA765.txt b/BankingSoftware/Storage/Users/2947CB1B92BB13C8F373E78C76FCA765.txt new file mode 100644 index 0000000..8cb0584 --- /dev/null +++ b/BankingSoftware/Storage/Users/2947CB1B92BB13C8F373E78C76FCA765.txt @@ -0,0 +1,8 @@ +First Name: Ade +Last Name: Bendel +Email: ade@bendel.com +Username: ade +Age: 78 +Phone Number: 9834563247 +HashedPassword: D84BA9FED904447C1BE566032AF633CD + diff --git a/BankingSoftware/Storage/Users/6A3B12DA60987AF16E55F3FCEC2A6EED.txt b/BankingSoftware/Storage/Users/6A3B12DA60987AF16E55F3FCEC2A6EED.txt new file mode 100644 index 0000000..a8c4c60 --- /dev/null +++ b/BankingSoftware/Storage/Users/6A3B12DA60987AF16E55F3FCEC2A6EED.txt @@ -0,0 +1,8 @@ +First Name: Kunle +Last Name: Afod +Email: kunle@afod.com +Username: kunle +Age: 67 +Phone Number: 8734652345 +HashedPassword: F1A3AB9E8E8AB970167FEEE7A731F3E9 + diff --git a/BankingSoftware/Storage/Users/7A362B30780DD15E6D710EC44AA0E023.txt b/BankingSoftware/Storage/Users/7A362B30780DD15E6D710EC44AA0E023.txt new file mode 100644 index 0000000..bc3adbb --- /dev/null +++ b/BankingSoftware/Storage/Users/7A362B30780DD15E6D710EC44AA0E023.txt @@ -0,0 +1,8 @@ +First Name: Usher +Last Name: Ray +Email: usher@ray.com +Username: usher +Age: 87 +Phone Number: 8975346523 +HashedPassword: BA4AFECD23BE0B3121EA708831022CEF + diff --git a/BankingSoftware/Storage/Users/C19291F87C625505E0CB012E128C3E33.txt b/BankingSoftware/Storage/Users/C19291F87C625505E0CB012E128C3E33.txt new file mode 100644 index 0000000..738de81 --- /dev/null +++ b/BankingSoftware/Storage/Users/C19291F87C625505E0CB012E128C3E33.txt @@ -0,0 +1,8 @@ +First Name: Hal +Last Name: Finley +Email: hal@finley.com +Username: hal +Age: 87 +Phone Number: 9874523876 +HashedPassword: 8E3F0695D2604D26F5B64C23C485E3BF + diff --git a/BankingSoftware/Storage/Users/FA2111BDFC79125C0CF1D96B53B88FEB.txt b/BankingSoftware/Storage/Users/FA2111BDFC79125C0CF1D96B53B88FEB.txt new file mode 100644 index 0000000..7ebdd13 --- /dev/null +++ b/BankingSoftware/Storage/Users/FA2111BDFC79125C0CF1D96B53B88FEB.txt @@ -0,0 +1,8 @@ +First Name: John +Last Name: Camark +Email: john@camark.com +Username: john +Age: 67 +Phone Number: 9834562346 +HashedPassword: E813ECD4F0196D88949044C1FA1B568D + diff --git a/BankingSoftware/Storage/Users/FE6FC17D485932CC797BB376BB501D44.txt b/BankingSoftware/Storage/Users/FE6FC17D485932CC797BB376BB501D44.txt new file mode 100644 index 0000000..d548215 --- /dev/null +++ b/BankingSoftware/Storage/Users/FE6FC17D485932CC797BB376BB501D44.txt @@ -0,0 +1,8 @@ +First Name: Hailey +Last Name: Bieber +Email: hailey@bieber.com +Username: hailey +Age: 78 +Phone Number: 8777345623 +HashedPassword: 588BE3DDB98C4DB0FB1E484105399596 + diff --git a/CodeBenders.sln b/CodeBenders.sln new file mode 100644 index 0000000..c0017fd --- /dev/null +++ b/CodeBenders.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BankingSoftware", "BankingSoftware\BankingSoftware.csproj", "{335BF223-9311-4DF9-A534-4B3831D2279E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibraryManagementApp", "LibraryManagementApp\LibraryManagementApp.csproj", "{787924DD-178E-4142-BAA5-3E68A646B481}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {335BF223-9311-4DF9-A534-4B3831D2279E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {335BF223-9311-4DF9-A534-4B3831D2279E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {335BF223-9311-4DF9-A534-4B3831D2279E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {335BF223-9311-4DF9-A534-4B3831D2279E}.Release|Any CPU.Build.0 = Release|Any CPU + {787924DD-178E-4142-BAA5-3E68A646B481}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {787924DD-178E-4142-BAA5-3E68A646B481}.Debug|Any CPU.Build.0 = Debug|Any CPU + {787924DD-178E-4142-BAA5-3E68A646B481}.Release|Any CPU.ActiveCfg = Release|Any CPU + {787924DD-178E-4142-BAA5-3E68A646B481}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/LibraryManagementApp/ApiResponse/ApiResult.cs b/LibraryManagementApp/ApiResponse/ApiResult.cs new file mode 100644 index 0000000..e91825b --- /dev/null +++ b/LibraryManagementApp/ApiResponse/ApiResult.cs @@ -0,0 +1,152 @@ +using System.Linq.Dynamic.Core; +using System.Reflection; +using EFCore.BulkExtensions; +using Microsoft.EntityFrameworkCore; + +namespace LibraryManagementApp.ApiResponse; + +public class ApiResult +{ + /// + /// Private constructor called by the CreateAsync method. + /// + private ApiResult( + List data, + int count, + int pageIndex, + int pageSize, + string? filterQuery) + { + Data = data; + PageIndex = pageIndex; + PageSize = pageSize; + TotalCount = count; + TotalPages = (int)Math.Ceiling(count / (double)pageSize); + FilterQuery = filterQuery; + } + + #region Methods + + /// + /// Pages, sorts and/or filters a IQueryable source. + /// + /// An IQueryable source of generic + /// type + /// Zero-based current page index + /// (0 = first page) + /// The actual size of + /// each page + /// The filtering query (value to + /// lookup) + /// + /// A object containing the IQueryable paged/sorted/filtered + /// result + /// and all the relevant paging/sorting/filtering navigation + /// info. + /// + public static async Task> CreateAsync( + IQueryable source, + int pageIndex, + int pageSize, + string? filterQuery = null) + { + if (!string.IsNullOrEmpty(filterQuery)) + { + source = source.Where(filterQuery); + } + + var count = await source.CountAsync(); + + source = source + .Skip(pageIndex * pageSize) + .Take(pageSize); + +#if DEBUG + // retrieve the SQL query (for debug purposes) + var sql = source.ToParametrizedSql(); +#endif + + var data = await source.ToListAsync(); + + return new ApiResult( + data, + count, + pageIndex, + pageSize, + filterQuery); + } + + /// + /// Checks if the given property name exists + /// to protect against SQL injection attacks + /// + public static bool IsValidProperty( + string propertyName, + bool throwExceptionIfNotFound = true) + { + var prop = typeof(T).GetProperty( + propertyName, + BindingFlags.IgnoreCase | + BindingFlags.Public | + BindingFlags.Static | + BindingFlags.Instance); + if (prop == null && throwExceptionIfNotFound) + throw new NotSupportedException($"ERROR: Property '{propertyName}' does not exist."); + return prop != null; + } + + #endregion + + #region Properties + + /// + /// IQueryable data result to return. + /// + public List Data { get; private set; } + + /// + /// Zero-based index of current page. + /// + public int PageIndex { get; private set; } + + /// + /// Number of items contained in each page. + /// + public int PageSize { get; private set; } + + /// + /// Total items count + /// + public int TotalCount { get; private set; } + + /// + /// Total pages count + /// + public int TotalPages { get; private set; } + + /// + /// TRUE if the current page has a previous page, + /// FALSE otherwise. + /// + public bool HasPreviousPage + { + get { return (PageIndex > 0); } + } + + /// + /// TRUE if the current page has a next page, FALSE otherwise. + /// + public bool HasNextPage + { + get { return ((PageIndex + 1) < TotalPages); } + } + + + /// + /// Filter Query string + /// (to be used within the given FilterColumn) + /// + public string? FilterQuery { get; set; } + + #endregion +} \ No newline at end of file diff --git a/LibraryManagementApp/Contracts/RepositoryContracts/IAuthorRepository.cs b/LibraryManagementApp/Contracts/RepositoryContracts/IAuthorRepository.cs new file mode 100644 index 0000000..2253c59 --- /dev/null +++ b/LibraryManagementApp/Contracts/RepositoryContracts/IAuthorRepository.cs @@ -0,0 +1,8 @@ +using LibraryManagementApp.Domain; + +namespace LibraryManagementApp.Contracts.RepositoryContracts; + +public interface IAuthorRepository: IRepositoryBase +{ + +} \ No newline at end of file diff --git a/LibraryManagementApp/Contracts/RepositoryContracts/IBookRepository.cs b/LibraryManagementApp/Contracts/RepositoryContracts/IBookRepository.cs new file mode 100644 index 0000000..e6bac7d --- /dev/null +++ b/LibraryManagementApp/Contracts/RepositoryContracts/IBookRepository.cs @@ -0,0 +1,8 @@ +using LibraryManagementApp.Domain; + +namespace LibraryManagementApp.Contracts.RepositoryContracts; + +public interface IBookRepository: IRepositoryBase +{ + +} \ No newline at end of file diff --git a/LibraryManagementApp/Contracts/RepositoryContracts/ICategoryRepository.cs b/LibraryManagementApp/Contracts/RepositoryContracts/ICategoryRepository.cs new file mode 100644 index 0000000..9575d32 --- /dev/null +++ b/LibraryManagementApp/Contracts/RepositoryContracts/ICategoryRepository.cs @@ -0,0 +1,8 @@ +using LibraryManagementApp.Domain; + +namespace LibraryManagementApp.Contracts.RepositoryContracts; + +public interface ICategoryRepository: IRepositoryBase +{ + +} \ No newline at end of file diff --git a/LibraryManagementApp/Contracts/RepositoryContracts/IPublisherRepository.cs b/LibraryManagementApp/Contracts/RepositoryContracts/IPublisherRepository.cs new file mode 100644 index 0000000..b7aa2f2 --- /dev/null +++ b/LibraryManagementApp/Contracts/RepositoryContracts/IPublisherRepository.cs @@ -0,0 +1,8 @@ +using LibraryManagementApp.Domain; + +namespace LibraryManagementApp.Contracts.RepositoryContracts; + +public interface IPublisherRepository: IRepositoryBase +{ + +} \ No newline at end of file diff --git a/LibraryManagementApp/Contracts/RepositoryContracts/IRepositoryBase.cs b/LibraryManagementApp/Contracts/RepositoryContracts/IRepositoryBase.cs new file mode 100644 index 0000000..032d48e --- /dev/null +++ b/LibraryManagementApp/Contracts/RepositoryContracts/IRepositoryBase.cs @@ -0,0 +1,15 @@ +using System.Linq.Expressions; + +namespace LibraryManagementApp.Contracts.RepositoryContracts; + +public interface IRepositoryBase where T: class +{ + T FindById(Guid id); + IQueryable FindByCondition(Expression> expression, bool trackChanges); + IQueryable FindAll(bool trackChanges); + void Add(T entity); + void AddRange(IEnumerable entities); + void Update(T entity); + void Remove(T entity); + void RemoveRange(IEnumerable entities); +} \ No newline at end of file diff --git a/LibraryManagementApp/Contracts/RepositoryContracts/IUnitOfWork.cs b/LibraryManagementApp/Contracts/RepositoryContracts/IUnitOfWork.cs new file mode 100644 index 0000000..49bc705 --- /dev/null +++ b/LibraryManagementApp/Contracts/RepositoryContracts/IUnitOfWork.cs @@ -0,0 +1,11 @@ +namespace LibraryManagementApp.Contracts.RepositoryContracts; + +public interface IUnitOfWork: IDisposable +{ + IAuthorRepository Authors { get; } + IBookRepository Books { get; } + IPublisherRepository Publishers { get; } + ICategoryRepository Categories { get; } + + int Complete(); +} \ No newline at end of file diff --git a/LibraryManagementApp/Controllers/AuthorController.cs b/LibraryManagementApp/Controllers/AuthorController.cs new file mode 100644 index 0000000..b39220c --- /dev/null +++ b/LibraryManagementApp/Controllers/AuthorController.cs @@ -0,0 +1,122 @@ +using LibraryManagementApp.ApiResponse; +using LibraryManagementApp.Contracts.RepositoryContracts; +using LibraryManagementApp.Domain; +using LibraryManagementApp.Dtos; +using LibraryManagementApp.Exceptions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LibraryManagementApp.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AuthorController: ControllerBase +{ + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + + public AuthorController(ILogger logger, IUnitOfWork unitOfWork) + { + _logger = logger; + _unitOfWork = unitOfWork; + } + + [HttpGet] + public async Task>> GetAuthors( + bool trackChanges, + int pageIndex = 0, + int pageSize = 10, + string? filterQuery = null) + { + var authors = await ApiResult.CreateAsync( + _unitOfWork.Authors.FindAll(trackChanges).Select( + a => new AuthorDto() + { + Id = a.Id, + FirstName = a.FirstName, + LastName = a.LastName, + PublisherName = a.Publisher.Name, + AvatarUrl = a.AvatarUrl + }), + pageIndex, + pageSize, + filterQuery); + + return authors; + } + + [HttpGet("{id}")] + public async Task> GetAuthorById(Guid id, bool trackChanges) + { + var entity = await _unitOfWork.Authors + .FindByCondition(a => a.Id.Equals(id), trackChanges) + .Select(a => new AuthorDto() + { + Id = a.Id, + FirstName = a.FirstName, + LastName = a.LastName, + PublisherName = a.Publisher.Name, + AvatarUrl = a.AvatarUrl + }) + .FirstOrDefaultAsync(); + + if (entity == null) + throw new NotFoundException(); + + return entity; + } + + [HttpPost] + public ActionResult CreateAuthor(AuthorDto author) + { + var entity = new Author + { + FirstName = author.FirstName, + LastName = author.LastName, + AvatarUrl = author.AvatarUrl, + PublisherId = author.PublisherId + }; + + _unitOfWork.Authors.Add(entity); + _unitOfWork.Complete(); + _unitOfWork.Dispose(); + + return Ok(); + } + + [HttpPut("{id:guid}")] + public ActionResult UpdateAuthor(AuthorDto author, Guid id) + { + var entity = _unitOfWork.Authors.FindById(id); + + if (entity == null) + throw new NotFoundException(); + + entity.FirstName = author.FirstName; + entity.LastName = author.LastName; + entity.AvatarUrl = author.AvatarUrl; + entity.PublisherId = author.PublisherId; + + + _unitOfWork.Authors.Update(entity); + _unitOfWork.Complete(); + _unitOfWork.Dispose(); + + return Ok(); + } + + [HttpDelete("{id:guid}")] + public ActionResult DeleteAuthor(Guid id) + { + var entity = _unitOfWork.Authors.FindById(id); + + if (entity == null) + throw new NotFoundException(); + + _unitOfWork.Authors.Remove(entity); + _unitOfWork.Complete(); + _unitOfWork.Dispose(); + + return Ok(); + } +} \ No newline at end of file diff --git a/LibraryManagementApp/Controllers/BookController.cs b/LibraryManagementApp/Controllers/BookController.cs new file mode 100644 index 0000000..fb3ba4b --- /dev/null +++ b/LibraryManagementApp/Controllers/BookController.cs @@ -0,0 +1,134 @@ +using LibraryManagementApp.ApiResponse; +using LibraryManagementApp.Contracts.RepositoryContracts; +using LibraryManagementApp.Domain; +using LibraryManagementApp.Dtos; +using LibraryManagementApp.Exceptions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LibraryManagementApp.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class BookController : ControllerBase +{ + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + + public BookController(ILogger logger, IUnitOfWork unitOfWork) + { + _logger = logger; + _unitOfWork = unitOfWork; + } + + [HttpGet] + public async Task>> GetBooks( + bool trackChanges, + int pageIndex = 0, + int pageSize = 10, + string? filterQuery = null) + { + var books = await ApiResult.CreateAsync( + _unitOfWork.Books.FindAll(trackChanges).Select( + b => new BookDto() + { + Id = b.Id, + Title = b.Title, + CopyrightNotice = b.CopyrightNotice, + ISBN = b.ISBN, + AuthorName = b.Author.FirstName, + PageLength = b.PageLength, + Language = b.Language, + ReleaseYear = b.ReleaseYear + }), + pageIndex, + pageSize, + filterQuery); + + return books; + } + + [HttpGet("{id}")] + public async Task> GetBookById(Guid id, bool trackChanges) + { + var entity = await _unitOfWork.Books + .FindByCondition(a => a.Id.Equals(id), trackChanges) + .Select(b => new BookDto() + { + Id = b.Id, + Title = b.Title, + CopyrightNotice = b.CopyrightNotice, + ISBN = b.ISBN, + AuthorName = b.Author.FirstName, + PageLength = b.PageLength, + Language = b.Language, + ReleaseYear = b.ReleaseYear + }) + .FirstOrDefaultAsync(); + + if (entity == null) + throw new NotFoundException(); + + return entity; + } + + [HttpPost] + public ActionResult CreateBook(BookDto book) + { + var author = _unitOfWork.Authors.FindById(book.AuthourId); + var entity = new Book + { + Title = book.Title, + CopyrightNotice = book.CopyrightNotice, + ISBN = book.ISBN, + AuthorId = author.Id, + PageLength = book.PageLength, + Language = book.Language, + ReleaseYear = book.ReleaseYear + }; + + _unitOfWork.Books.Add(entity); + _unitOfWork.Complete(); + _unitOfWork.Dispose(); + + return Ok(); + } + + [HttpPut("{id:guid}")] + public ActionResult UpdateBook(BookDto book, Guid id) + { + var entity = _unitOfWork.Books.FindById(id); + + if (entity == null) + throw new NotFoundException(); + + entity.Title = book.Title; + entity.CopyrightNotice = book.CopyrightNotice; + entity.ISBN = book.ISBN; + entity.PageLength = book.PageLength; + entity.Language = book.Language; + entity.ReleaseYear = book.ReleaseYear; + + + _unitOfWork.Books.Update(entity); + _unitOfWork.Complete(); + _unitOfWork.Dispose(); + + return Ok(); + } + + [HttpDelete("{id:guid}")] + public ActionResult DeleteBook(Guid id) + { + var entity = _unitOfWork.Books.FindById(id); + + if (entity == null) + throw new NotFoundException(); + + _unitOfWork.Books.Remove(entity); + _unitOfWork.Complete(); + _unitOfWork.Dispose(); + + return Ok(); + } +} \ No newline at end of file diff --git a/LibraryManagementApp/Controllers/CategoryController.cs b/LibraryManagementApp/Controllers/CategoryController.cs new file mode 100644 index 0000000..0392adf --- /dev/null +++ b/LibraryManagementApp/Controllers/CategoryController.cs @@ -0,0 +1,113 @@ +using LibraryManagementApp.ApiResponse; +using LibraryManagementApp.Contracts.RepositoryContracts; +using LibraryManagementApp.Domain; +using LibraryManagementApp.Dtos; +using LibraryManagementApp.Exceptions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LibraryManagementApp.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class CategoryController: ControllerBase +{ + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + + public CategoryController(ILogger logger, IUnitOfWork unitOfWork) + { + _logger = logger; + _unitOfWork = unitOfWork; + } + + [HttpGet] + public async Task>> GetPublishers( + bool trackChanges, + int pageIndex = 0, + int pageSize = 10, + string? filterQuery = null) + { + var categories = await ApiResult.CreateAsync( + _unitOfWork.Categories.FindAll(trackChanges).Select( + c => new CategoryDto() + { + Id = c.Id, + Name = c.Name, + Description = c.Description + }), + pageIndex, + pageSize, + filterQuery); + + return categories; + } + + [HttpGet("{id}")] + public async Task> GetCategoryById(Guid id, bool trackChanges) + { + var entity = await _unitOfWork.Categories + .FindByCondition(a => a.Id.Equals(id), trackChanges) + .Select(c => new CategoryDto() + { + Id = c.Id, + Name = c.Name, + Description = c.Description + }) + .FirstOrDefaultAsync(); + + if (entity == null) + throw new NotFoundException(); + + return entity; + } + + [HttpPost] + public ActionResult CreateCategory(CategoryDto category) + { + var entity = new Category + { + Name = category.Name, + Description = category.Description, + }; + + _unitOfWork.Categories.Add(entity); + _unitOfWork.Complete(); + _unitOfWork.Dispose(); + + return Ok(); + } + + [HttpPut("{id:guid}")] + public ActionResult UpdateCategory(CategoryDto category, Guid id) + { + var entity = _unitOfWork.Categories.FindById(id); + + if (entity == null) + throw new NotFoundException(); + + entity.Name = category.Name; + entity.Description = category.Description; + + _unitOfWork.Categories.Update(entity); + _unitOfWork.Complete(); + _unitOfWork.Dispose(); + + return Ok(); + } + + [HttpDelete("{id:guid}")] + public ActionResult DeleteCategory(Guid id) + { + var entity = _unitOfWork.Categories.FindById(id); + + if (entity == null) + throw new NotFoundException(); + + _unitOfWork.Categories.Remove(entity); + _unitOfWork.Complete(); + _unitOfWork.Dispose(); + + return Ok(); + } +} \ No newline at end of file diff --git a/LibraryManagementApp/Controllers/PublisherController.cs b/LibraryManagementApp/Controllers/PublisherController.cs new file mode 100644 index 0000000..16c6f78 --- /dev/null +++ b/LibraryManagementApp/Controllers/PublisherController.cs @@ -0,0 +1,114 @@ +using LibraryManagementApp.ApiResponse; +using LibraryManagementApp.Contracts.RepositoryContracts; +using LibraryManagementApp.Domain; +using LibraryManagementApp.Dtos; +using LibraryManagementApp.Exceptions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LibraryManagementApp.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class PublisherController : ControllerBase +{ + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + + public PublisherController(ILogger logger, IUnitOfWork unitOfWork) + { + _logger = logger; + _unitOfWork = unitOfWork; + } + + [HttpGet] + public async Task>> GetPublishers( + bool trackChanges, + int pageIndex = 0, + int pageSize = 10, + string? filterQuery = null) + { + var publishers = await ApiResult.CreateAsync( + _unitOfWork.Publishers.FindAll(trackChanges).Select( + p => new PublisherDto() + { + Id = p.Id, + Name = p.Name, + LogoUrl = p.LogoUrl, + }), + pageIndex, + pageSize, + filterQuery); + + return publishers; + } + + [HttpGet("{id}")] + public async Task> GetPublisherById(Guid id, bool trackChanges) + { + var entity = await _unitOfWork.Publishers + .FindByCondition(p => p.Id.Equals(id), trackChanges) + .Select(p => new PublisherDto + { + Name = p.Name, + LogoUrl = p.LogoUrl, + TotalBooks = p.TotBooks, + TotalAuthors = p.TotAuthors + }) + .FirstOrDefaultAsync(); + + if (entity == null) + throw new NotFoundException(); + + return entity; + } + + [HttpPost] + public ActionResult CreatePublisher(PublisherDto publisher) + { + var entity = new Publisher + { + Name = publisher.Name, + LogoUrl = publisher.LogoUrl, + }; + + _unitOfWork.Publishers.Add(entity); + _unitOfWork.Complete(); + _unitOfWork.Dispose(); + + return Ok(); + } + + [HttpPut("{id:guid}")] + public ActionResult UpdatePublisher(PublisherDto publisher, Guid id) + { + var entity = _unitOfWork.Publishers.FindById(id); + + if (entity == null) + throw new NotFoundException(); + + entity.Name = publisher.Name; + entity.LogoUrl = publisher.LogoUrl; + + _unitOfWork.Publishers.Update(entity); + _unitOfWork.Complete(); + _unitOfWork.Dispose(); + + return Ok(); + } + + [HttpDelete("{id:guid}")] + public ActionResult DeletePublisher(Guid id) + { + var entity = _unitOfWork.Publishers.FindById(id); + + if (entity == null) + throw new NotFoundException(); + + _unitOfWork.Publishers.Remove(entity); + _unitOfWork.Complete(); + _unitOfWork.Dispose(); + + return Ok(); + } +} \ No newline at end of file diff --git a/LibraryManagementApp/Domain/Author.cs b/LibraryManagementApp/Domain/Author.cs new file mode 100644 index 0000000..39a2f54 --- /dev/null +++ b/LibraryManagementApp/Domain/Author.cs @@ -0,0 +1,40 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace LibraryManagementApp.Domain +{ + public class Author: BaseEntity + { + public Author() + { + + } + + public Author(string firstName, string lastName, string avatarUrl, Guid publisherId) + { + FirstName = firstName; + LastName = lastName; + AvatarUrl = avatarUrl; + PublisherId = publisherId; + } + + public string FirstName{get; set;} + public string LastName{get; set;} + public string AvatarUrl{get; set;} + public Guid PublisherId{get; set;} + public Publisher Publisher { get; set; } + public ICollection Books{get; set;} + + [NotMapped] + public int TotBooks + { + get + { + return (Books != null) + ? Books.Count + : _TotBooks; + } + set => _TotBooks = value; + } + private int _TotBooks; + } +} \ No newline at end of file diff --git a/LibraryManagementApp/Domain/BaseEntity.cs b/LibraryManagementApp/Domain/BaseEntity.cs new file mode 100644 index 0000000..5f3c4ea --- /dev/null +++ b/LibraryManagementApp/Domain/BaseEntity.cs @@ -0,0 +1,23 @@ +namespace LibraryManagementApp.Domain; + +public class BaseEntity +{ + public BaseEntity() + { + + } + + public BaseEntity(Guid id, DateTime createdOn) + { + Id = new Guid(); + CreatedOn = createdOn; + } + + public Guid Id { get; init; } + public DateTime CreatedOn { get; init; } + public Guid CreatedBy { get; init; } + public DateTime ModifiedOn { get; set; } + public Guid ModifiedBy { get; set; } + public DateTime DeletedOn { get; init; } + public Guid DeletedBy { get; init; } +} \ No newline at end of file diff --git a/LibraryManagementApp/Domain/Book.cs b/LibraryManagementApp/Domain/Book.cs new file mode 100644 index 0000000..a480777 --- /dev/null +++ b/LibraryManagementApp/Domain/Book.cs @@ -0,0 +1,26 @@ +namespace LibraryManagementApp.Domain +{ + public class Book: BaseEntity + { + public Book() + { + + } + + public Book(string title, string copyrightNotice, Guid authorId) + { + Title = title; + CopyrightNotice = copyrightNotice; + AuthorId = authorId; + } + + public string Title{get; set;} + public string CopyrightNotice{get; set;} + public int ISBN{get; set;} + public Guid AuthorId{get; set;} + public Author Author { get; set; } + public int PageLength{get; set;} + public Language Language{get; set;} + public DateOnly ReleaseYear{get; set;} + } +} \ No newline at end of file diff --git a/LibraryManagementApp/Domain/Category.cs b/LibraryManagementApp/Domain/Category.cs new file mode 100644 index 0000000..74672ae --- /dev/null +++ b/LibraryManagementApp/Domain/Category.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace LibraryManagementApp.Domain +{ + public class Category: BaseEntity + { + public Category() + { + + } + public Category(string name, string description) + { + Name = name; + Description = description; + } + + public string Name{get; set;} + public string Description{get; set;} + public ICollection Books{get; set;} + } +} \ No newline at end of file diff --git a/LibraryManagementApp/Domain/Language.cs b/LibraryManagementApp/Domain/Language.cs new file mode 100644 index 0000000..8ad94e9 --- /dev/null +++ b/LibraryManagementApp/Domain/Language.cs @@ -0,0 +1,11 @@ +namespace LibraryManagementApp.Domain +{ + public enum Language + { + English = 10, + Yoruba = 20, + Igbo = 30, + Hausa = 40, + French = 50, + } +} \ No newline at end of file diff --git a/LibraryManagementApp/Domain/Publisher.cs b/LibraryManagementApp/Domain/Publisher.cs new file mode 100644 index 0000000..48105de --- /dev/null +++ b/LibraryManagementApp/Domain/Publisher.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Threading.Tasks; + +namespace LibraryManagementApp.Domain +{ + public class Publisher: BaseEntity + { + public Publisher() + { + + } + public Publisher(string name, string logoUrl) + { + Name = name; + LogoUrl = logoUrl; + } + + public string Name{get; set;} + public string LogoUrl{get; set;} + public ICollection Books { get; set; } + public ICollection Authors { get; set; } + + [NotMapped] + public int TotBooks + { + get + { + return (Books != null) + ? Books.Count + : _TotBooks; + } + set => _TotBooks = value; + } + private int _TotBooks; + + [NotMapped] + public int TotAuthors + { + get + { + return (Books != null) + ? Books.Count + : _TotBooks; + } + set => _TotBooks = value; + } + private int _TotAuthors; + } +} \ No newline at end of file diff --git a/LibraryManagementApp/Dtos/AuthorDto.cs b/LibraryManagementApp/Dtos/AuthorDto.cs new file mode 100644 index 0000000..981bbeb --- /dev/null +++ b/LibraryManagementApp/Dtos/AuthorDto.cs @@ -0,0 +1,11 @@ +namespace LibraryManagementApp.Dtos; + +public record AuthorDto +{ + public Guid Id { get; init; } + public string FirstName{get; init;} + public string LastName{get; init;} + public string AvatarUrl{get; init;} + public string PublisherName{get; init;} + public Guid PublisherId {get; init;} +} \ No newline at end of file diff --git a/LibraryManagementApp/Dtos/BookDto.cs b/LibraryManagementApp/Dtos/BookDto.cs new file mode 100644 index 0000000..f4799a6 --- /dev/null +++ b/LibraryManagementApp/Dtos/BookDto.cs @@ -0,0 +1,16 @@ +using LibraryManagementApp.Domain; + +namespace LibraryManagementApp.Dtos; + +public record BookDto +{ + public Guid Id { get; init; } + public string Title{get; init;} + public string CopyrightNotice{get; init;} + public int ISBN{get; init;} + public string AuthorName { get; init; } + public Guid AuthourId { get; init; } + public int PageLength{get; init;} + public Language Language{get; init;} + public DateOnly ReleaseYear{get; init;} +} \ No newline at end of file diff --git a/LibraryManagementApp/Dtos/CategoryDto.cs b/LibraryManagementApp/Dtos/CategoryDto.cs new file mode 100644 index 0000000..fb156db --- /dev/null +++ b/LibraryManagementApp/Dtos/CategoryDto.cs @@ -0,0 +1,8 @@ +namespace LibraryManagementApp.Dtos; + +public record CategoryDto +{ + public Guid Id { get; init; } + public string Name{get; set;} + public string Description{get; set;} +} \ No newline at end of file diff --git a/LibraryManagementApp/Dtos/PublisherDto.cs b/LibraryManagementApp/Dtos/PublisherDto.cs new file mode 100644 index 0000000..e701323 --- /dev/null +++ b/LibraryManagementApp/Dtos/PublisherDto.cs @@ -0,0 +1,10 @@ +namespace LibraryManagementApp.Dtos; + +public record PublisherDto +{ + public Guid Id { get; init; } + public string Name{get; init;} + public string LogoUrl{get; init;} + public int? TotalAuthors { get; init; } + public int? TotalBooks { get; init; } +} \ No newline at end of file diff --git a/LibraryManagementApp/Exceptions/NotFoundException.cs b/LibraryManagementApp/Exceptions/NotFoundException.cs new file mode 100644 index 0000000..fcdabbe --- /dev/null +++ b/LibraryManagementApp/Exceptions/NotFoundException.cs @@ -0,0 +1,24 @@ +namespace LibraryManagementApp.Exceptions; + +public class NotFoundException : Exception +{ + public NotFoundException() + : base() + { + } + + public NotFoundException(string message) + : base(message) + { + } + + public NotFoundException(string message, Exception innerException) + : base(message, innerException) + { + } + + public NotFoundException(string name, object key) + : base($"Entity \"{name}\" ({key}) was not found.") + { + } +} diff --git a/LibraryManagementApp/LibraryManagementApp.csproj b/LibraryManagementApp/LibraryManagementApp.csproj new file mode 100644 index 0000000..5e13825 --- /dev/null +++ b/LibraryManagementApp/LibraryManagementApp.csproj @@ -0,0 +1,23 @@ + + + + net7.0 + enable + enable + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/LibraryManagementApp/Migrations/20230309042120_Initial.Designer.cs b/LibraryManagementApp/Migrations/20230309042120_Initial.Designer.cs new file mode 100644 index 0000000..bacd982 --- /dev/null +++ b/LibraryManagementApp/Migrations/20230309042120_Initial.Designer.cs @@ -0,0 +1,262 @@ +// +using System; +using LibraryManagementApp.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LibraryManagementApp.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230309042120_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("BookCategory", b => + { + b.Property("BooksId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("BooksId", "CategoryId"); + + b.HasIndex("CategoryId"); + + b.ToTable("BookCategory"); + }); + + modelBuilder.Entity("LibraryManagementApp.Domain.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AvatarUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ModifiedBy") + .HasColumnType("uuid"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("PublisherId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Authors"); + }); + + modelBuilder.Entity("LibraryManagementApp.Domain.Book", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorId") + .HasColumnType("uuid"); + + b.Property("CopyrightNotice") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("ISBN") + .HasColumnType("integer"); + + b.Property("Language") + .HasColumnType("integer"); + + b.Property("ModifiedBy") + .HasColumnType("uuid"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("PageLength") + .HasColumnType("integer"); + + b.Property("PublisherId") + .HasColumnType("uuid"); + + b.Property("ReleaseYear") + .HasColumnType("date"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("PublisherId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LibraryManagementApp.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ModifiedBy") + .HasColumnType("uuid"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("LibraryManagementApp.Domain.Publisher", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("LogoUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("ModifiedBy") + .HasColumnType("uuid"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Publishers"); + }); + + modelBuilder.Entity("BookCategory", b => + { + b.HasOne("LibraryManagementApp.Domain.Book", null) + .WithMany() + .HasForeignKey("BooksId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LibraryManagementApp.Domain.Category", null) + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LibraryManagementApp.Domain.Book", b => + { + b.HasOne("LibraryManagementApp.Domain.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LibraryManagementApp.Domain.Publisher", null) + .WithMany("Books") + .HasForeignKey("PublisherId"); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("LibraryManagementApp.Domain.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LibraryManagementApp.Domain.Publisher", b => + { + b.Navigation("Books"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LibraryManagementApp/Migrations/20230309042120_Initial.cs b/LibraryManagementApp/Migrations/20230309042120_Initial.cs new file mode 100644 index 0000000..af9489d --- /dev/null +++ b/LibraryManagementApp/Migrations/20230309042120_Initial.cs @@ -0,0 +1,168 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LibraryManagementApp.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Authors", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + FirstName = table.Column(type: "text", nullable: false), + LastName = table.Column(type: "text", nullable: false), + AvatarUrl = table.Column(type: "text", nullable: false), + PublisherId = table.Column(type: "uuid", nullable: false), + CreatedOn = table.Column(type: "timestamp with time zone", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: false), + ModifiedOn = table.Column(type: "timestamp with time zone", nullable: false), + ModifiedBy = table.Column(type: "uuid", nullable: false), + DeletedOn = table.Column(type: "timestamp with time zone", nullable: false), + DeletedBy = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Authors", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Categories", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: false), + CreatedOn = table.Column(type: "timestamp with time zone", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: false), + ModifiedOn = table.Column(type: "timestamp with time zone", nullable: false), + ModifiedBy = table.Column(type: "uuid", nullable: false), + DeletedOn = table.Column(type: "timestamp with time zone", nullable: false), + DeletedBy = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Publishers", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + LogoUrl = table.Column(type: "text", nullable: false), + CreatedOn = table.Column(type: "timestamp with time zone", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: false), + ModifiedOn = table.Column(type: "timestamp with time zone", nullable: false), + ModifiedBy = table.Column(type: "uuid", nullable: false), + DeletedOn = table.Column(type: "timestamp with time zone", nullable: false), + DeletedBy = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Publishers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Books", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Title = table.Column(type: "text", nullable: false), + CopyrightNotice = table.Column(type: "text", nullable: false), + ISBN = table.Column(type: "integer", nullable: false), + AuthorId = table.Column(type: "uuid", nullable: false), + PageLength = table.Column(type: "integer", nullable: false), + Language = table.Column(type: "integer", nullable: false), + ReleaseYear = table.Column(type: "date", nullable: false), + PublisherId = table.Column(type: "uuid", nullable: true), + CreatedOn = table.Column(type: "timestamp with time zone", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: false), + ModifiedOn = table.Column(type: "timestamp with time zone", nullable: false), + ModifiedBy = table.Column(type: "uuid", nullable: false), + DeletedOn = table.Column(type: "timestamp with time zone", nullable: false), + DeletedBy = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Books", x => x.Id); + table.ForeignKey( + name: "FK_Books_Authors_AuthorId", + column: x => x.AuthorId, + principalTable: "Authors", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Books_Publishers_PublisherId", + column: x => x.PublisherId, + principalTable: "Publishers", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "BookCategory", + columns: table => new + { + BooksId = table.Column(type: "uuid", nullable: false), + CategoryId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BookCategory", x => new { x.BooksId, x.CategoryId }); + table.ForeignKey( + name: "FK_BookCategory_Books_BooksId", + column: x => x.BooksId, + principalTable: "Books", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_BookCategory_Categories_CategoryId", + column: x => x.CategoryId, + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_BookCategory_CategoryId", + table: "BookCategory", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_Books_AuthorId", + table: "Books", + column: "AuthorId"); + + migrationBuilder.CreateIndex( + name: "IX_Books_PublisherId", + table: "Books", + column: "PublisherId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "BookCategory"); + + migrationBuilder.DropTable( + name: "Books"); + + migrationBuilder.DropTable( + name: "Categories"); + + migrationBuilder.DropTable( + name: "Authors"); + + migrationBuilder.DropTable( + name: "Publishers"); + } + } +} diff --git a/LibraryManagementApp/Migrations/ApplicationDbContextModelSnapshot.cs b/LibraryManagementApp/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..d011046 --- /dev/null +++ b/LibraryManagementApp/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,259 @@ +// +using System; +using LibraryManagementApp.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LibraryManagementApp.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("BookCategory", b => + { + b.Property("BooksId") + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.HasKey("BooksId", "CategoryId"); + + b.HasIndex("CategoryId"); + + b.ToTable("BookCategory"); + }); + + modelBuilder.Entity("LibraryManagementApp.Domain.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AvatarUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ModifiedBy") + .HasColumnType("uuid"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("PublisherId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Authors"); + }); + + modelBuilder.Entity("LibraryManagementApp.Domain.Book", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorId") + .HasColumnType("uuid"); + + b.Property("CopyrightNotice") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("ISBN") + .HasColumnType("integer"); + + b.Property("Language") + .HasColumnType("integer"); + + b.Property("ModifiedBy") + .HasColumnType("uuid"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("PageLength") + .HasColumnType("integer"); + + b.Property("PublisherId") + .HasColumnType("uuid"); + + b.Property("ReleaseYear") + .HasColumnType("date"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("PublisherId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LibraryManagementApp.Domain.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ModifiedBy") + .HasColumnType("uuid"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("LibraryManagementApp.Domain.Publisher", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("LogoUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("ModifiedBy") + .HasColumnType("uuid"); + + b.Property("ModifiedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Publishers"); + }); + + modelBuilder.Entity("BookCategory", b => + { + b.HasOne("LibraryManagementApp.Domain.Book", null) + .WithMany() + .HasForeignKey("BooksId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LibraryManagementApp.Domain.Category", null) + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LibraryManagementApp.Domain.Book", b => + { + b.HasOne("LibraryManagementApp.Domain.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LibraryManagementApp.Domain.Publisher", null) + .WithMany("Books") + .HasForeignKey("PublisherId"); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("LibraryManagementApp.Domain.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LibraryManagementApp.Domain.Publisher", b => + { + b.Navigation("Books"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LibraryManagementApp/Persistence/ApplicationDbContext.cs b/LibraryManagementApp/Persistence/ApplicationDbContext.cs new file mode 100644 index 0000000..365bfea --- /dev/null +++ b/LibraryManagementApp/Persistence/ApplicationDbContext.cs @@ -0,0 +1,26 @@ +using LibraryManagementApp.Domain; +using LibraryManagementApp.Persistence.Configurations; +using Microsoft.EntityFrameworkCore; + +namespace LibraryManagementApp.Persistence; + +public class ApplicationDbContext: DbContext +{ + public ApplicationDbContext(DbContextOptions options) : base(options) + { + + } + + public DbSet Authors => Set(); + public DbSet Books => Set(); + public DbSet Categories => Set(); + public DbSet Publishers => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new AuthorConfiguration()); + modelBuilder.ApplyConfiguration(new BookConfiguration()); + modelBuilder.ApplyConfiguration(new CategoryConfiguration()); + modelBuilder.ApplyConfiguration(new PublisherConfiguration()); + } +} \ No newline at end of file diff --git a/LibraryManagementApp/Persistence/Configurations/AuthorConfiguration.cs b/LibraryManagementApp/Persistence/Configurations/AuthorConfiguration.cs new file mode 100644 index 0000000..daa1236 --- /dev/null +++ b/LibraryManagementApp/Persistence/Configurations/AuthorConfiguration.cs @@ -0,0 +1,15 @@ +using LibraryManagementApp.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace LibraryManagementApp.Persistence.Configurations; + +public class AuthorConfiguration: IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.Property(a => a.Id) + .IsRequired(); + builder.HasMany(a => a.Books); + } +} \ No newline at end of file diff --git a/LibraryManagementApp/Persistence/Configurations/BookConfiguration.cs b/LibraryManagementApp/Persistence/Configurations/BookConfiguration.cs new file mode 100644 index 0000000..64d2bd8 --- /dev/null +++ b/LibraryManagementApp/Persistence/Configurations/BookConfiguration.cs @@ -0,0 +1,17 @@ +using LibraryManagementApp.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace LibraryManagementApp.Persistence.Configurations; + +public class BookConfiguration: IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + /*builder.Property(b => b.Id) + .IsRequired(); + builder.HasOne(b => b.Author) + .WithMany(a => a.Books) + .HasForeignKey(b => b.AuthorId);*/ + } +} \ No newline at end of file diff --git a/LibraryManagementApp/Persistence/Configurations/CategoryConfiguration.cs b/LibraryManagementApp/Persistence/Configurations/CategoryConfiguration.cs new file mode 100644 index 0000000..30cd565 --- /dev/null +++ b/LibraryManagementApp/Persistence/Configurations/CategoryConfiguration.cs @@ -0,0 +1,16 @@ +using LibraryManagementApp.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace LibraryManagementApp.Persistence.Configurations; + +public class CategoryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.Property(b => b.Id) + .IsRequired(); + builder.HasMany(b => b.Books) + .WithMany(); + } +} \ No newline at end of file diff --git a/LibraryManagementApp/Persistence/Configurations/PublisherConfiguration.cs b/LibraryManagementApp/Persistence/Configurations/PublisherConfiguration.cs new file mode 100644 index 0000000..7a1223a --- /dev/null +++ b/LibraryManagementApp/Persistence/Configurations/PublisherConfiguration.cs @@ -0,0 +1,16 @@ +using LibraryManagementApp.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace LibraryManagementApp.Persistence.Configurations; + +public class PublisherConfiguration: IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.Property(p => p.Id) + .IsRequired(); + builder.HasMany(p => p.Books) + .WithOne(); + } +} \ No newline at end of file diff --git a/LibraryManagementApp/Persistence/Repository/AuthorRepository.cs b/LibraryManagementApp/Persistence/Repository/AuthorRepository.cs new file mode 100644 index 0000000..0884e18 --- /dev/null +++ b/LibraryManagementApp/Persistence/Repository/AuthorRepository.cs @@ -0,0 +1,11 @@ +using LibraryManagementApp.Contracts.RepositoryContracts; +using LibraryManagementApp.Domain; + +namespace LibraryManagementApp.Persistence.Repository; + +public class AuthorRepository: RepositoryBase, IAuthorRepository +{ + public AuthorRepository(ApplicationDbContext context) : base(context) + { + } +} \ No newline at end of file diff --git a/LibraryManagementApp/Persistence/Repository/BookRepository.cs b/LibraryManagementApp/Persistence/Repository/BookRepository.cs new file mode 100644 index 0000000..4e151c0 --- /dev/null +++ b/LibraryManagementApp/Persistence/Repository/BookRepository.cs @@ -0,0 +1,11 @@ +using LibraryManagementApp.Contracts.RepositoryContracts; +using LibraryManagementApp.Domain; + +namespace LibraryManagementApp.Persistence.Repository; + +public class BookRepository: RepositoryBase, IBookRepository +{ + public BookRepository(ApplicationDbContext context) : base(context) + { + } +} \ No newline at end of file diff --git a/LibraryManagementApp/Persistence/Repository/CategoryRepository.cs b/LibraryManagementApp/Persistence/Repository/CategoryRepository.cs new file mode 100644 index 0000000..7d5ed2b --- /dev/null +++ b/LibraryManagementApp/Persistence/Repository/CategoryRepository.cs @@ -0,0 +1,11 @@ +using LibraryManagementApp.Contracts.RepositoryContracts; +using LibraryManagementApp.Domain; + +namespace LibraryManagementApp.Persistence.Repository; + +public class CategoryRepository: RepositoryBase, ICategoryRepository +{ + public CategoryRepository(ApplicationDbContext context) : base(context) + { + } +} \ No newline at end of file diff --git a/LibraryManagementApp/Persistence/Repository/PublisherRepository.cs b/LibraryManagementApp/Persistence/Repository/PublisherRepository.cs new file mode 100644 index 0000000..822a685 --- /dev/null +++ b/LibraryManagementApp/Persistence/Repository/PublisherRepository.cs @@ -0,0 +1,11 @@ +using LibraryManagementApp.Contracts.RepositoryContracts; +using LibraryManagementApp.Domain; + +namespace LibraryManagementApp.Persistence.Repository; + +public class PublisherRepository: RepositoryBase, IPublisherRepository +{ + public PublisherRepository(ApplicationDbContext context) : base(context) + { + } +} \ No newline at end of file diff --git a/LibraryManagementApp/Persistence/Repository/RepositoryBase.cs b/LibraryManagementApp/Persistence/Repository/RepositoryBase.cs new file mode 100644 index 0000000..706136e --- /dev/null +++ b/LibraryManagementApp/Persistence/Repository/RepositoryBase.cs @@ -0,0 +1,45 @@ +using System.Linq.Expressions; +using LibraryManagementApp.Contracts.RepositoryContracts; +using Microsoft.EntityFrameworkCore; + +namespace LibraryManagementApp.Persistence.Repository; + +public class RepositoryBase: IRepositoryBase where T: class +{ + protected readonly ApplicationDbContext _context; + + public RepositoryBase(ApplicationDbContext context) => + _context = context; + + public T FindById(Guid id) => + _context.Set().Find(id); + + public IQueryable FindByCondition(Expression> expression, bool trackChanges) => + !trackChanges ? + _context.Set() + .Where(expression) + .AsNoTracking() : + _context.Set() + .Where(expression); + + public IQueryable FindAll(bool trackChanges) => + !trackChanges ? + _context.Set() + .AsNoTracking() : + _context.Set(); + + public void Add(T entity) => + _context.Set().Add(entity); + + public void AddRange(IEnumerable entities) => + _context.Set().AddRange(entities); + + public void Update(T entity) => + _context.Set().Update(entity); + + public void Remove(T entity) => + _context.Set().Remove(entity); + + public void RemoveRange(IEnumerable entities) => + _context.Set().RemoveRange(entities); +} \ No newline at end of file diff --git a/LibraryManagementApp/Persistence/Repository/UnitOfWork.cs b/LibraryManagementApp/Persistence/Repository/UnitOfWork.cs new file mode 100644 index 0000000..8b64d33 --- /dev/null +++ b/LibraryManagementApp/Persistence/Repository/UnitOfWork.cs @@ -0,0 +1,28 @@ +using LibraryManagementApp.Contracts.RepositoryContracts; + +namespace LibraryManagementApp.Persistence.Repository; + +public class UnitOfWork: IUnitOfWork +{ + private readonly ApplicationDbContext _context; + + public UnitOfWork(ApplicationDbContext context) + { + _context = context; + Authors = new AuthorRepository(_context); + Books = new BookRepository(_context); + Publishers = new PublisherRepository(_context); + Categories = new CategoryRepository(_context); + } + + public IAuthorRepository Authors { get; private set; } + public IBookRepository Books { get; private set; } + public IPublisherRepository Publishers { get; private set; } + public ICategoryRepository Categories { get; private set; } + + public int Complete() => + _context.SaveChanges(); + + public void Dispose() => + _context.Dispose(); +} \ No newline at end of file diff --git a/LibraryManagementApp/Program.cs b/LibraryManagementApp/Program.cs new file mode 100644 index 0000000..1f7d868 --- /dev/null +++ b/LibraryManagementApp/Program.cs @@ -0,0 +1,41 @@ +using LibraryManagementApp.Contracts.RepositoryContracts; +using LibraryManagementApp.Persistence; +using LibraryManagementApp.Persistence.Repository; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); +var connectionString = builder.Configuration.GetConnectionString("PostgresqlConnectionString"); + +// AddDbContext +builder.Services.AddDbContext(options => + options.UseNpgsql(connectionString)); + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +// Add Services +builder.Services.AddTransient(typeof(IRepositoryBase<>), typeof(RepositoryBase<>)); +builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/LibraryManagementApp/Properties/launchSettings.json b/LibraryManagementApp/Properties/launchSettings.json new file mode 100644 index 0000000..a7f8d21 --- /dev/null +++ b/LibraryManagementApp/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:20043", + "sslPort": 44351 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5293", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7119;http://localhost:5293", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/LibraryManagementApp/appsettings.Development.json b/LibraryManagementApp/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/LibraryManagementApp/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/LibraryManagementApp/appsettings.json b/LibraryManagementApp/appsettings.json new file mode 100644 index 0000000..9223b41 --- /dev/null +++ b/LibraryManagementApp/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "PostgresqlConnectionString": "User ID =postgres;Password=ologuneru;Host=localhost;Port=5433;Database=librarydb; Integrated Security=true;Pooling=true;" + }, + "AllowedHosts": "*" +}