From c390b4869d885692d593e1ac0a6186517c6a1da1 Mon Sep 17 00:00:00 2001 From: Xuyang Cao Date: Thu, 14 May 2026 11:08:27 +0800 Subject: [PATCH 01/12] =?UTF-8?q?Code=20migration=20completed:=20ASP.NET?= =?UTF-8?q?=20MVC=205=20(.NET=20Framework=204.8)=20to=20ASP.NET=20Core=20(?= =?UTF-8?q?.NET=2010)=20-=20SDK-style=20csproj,=20Program.cs,=20appsetting?= =?UTF-8?q?s.json,=20EF=20Core=209,=20MSMQ=E2=86=92in-memory=20queue,=20al?= =?UTF-8?q?l=20controllers/views=20updated?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modernize/ContosoUniversity/plan.md | 281 ++++++++ .../modernize/ContosoUniversity/tasks.json | 315 +++++++++ ContosoUniversity/App_Start/BundleConfig.cs | 27 - ContosoUniversity/App_Start/FilterConfig.cs | 14 - ContosoUniversity/App_Start/RouteConfig.cs | 19 - ContosoUniversity/ContosoUniversity.csproj | 354 +--------- .../Controllers/BaseController.cs | 11 +- .../Controllers/CoursesController.cs | 127 ++-- .../Controllers/DepartmentsController.cs | 64 +- .../Controllers/HomeController.cs | 19 +- .../Controllers/InstructorsController.cs | 76 ++- .../Controllers/NotificationsController.cs | 15 +- .../Controllers/StudentsController.cs | 39 +- .../Data/SchoolContextFactory.cs | 7 +- ContosoUniversity/Global.asax | 1 - ContosoUniversity/Global.asax.cs | 37 -- ContosoUniversity/Program.cs | 47 ++ ContosoUniversity/Properties/AssemblyInfo.cs | 19 - .../Services/NotificationService.cs | 57 +- ContosoUniversity/Views/Courses/Create.cshtml | 2 +- ContosoUniversity/Views/Courses/Edit.cshtml | 2 +- .../Views/Departments/Create.cshtml | 2 +- .../Views/Departments/Edit.cshtml | 2 +- .../Views/Instructors/Create.cshtml | 2 +- .../Views/Instructors/Edit.cshtml | 2 +- ContosoUniversity/Views/Shared/Error.cshtml | 13 +- ContosoUniversity/Views/Shared/_Layout.cshtml | 30 +- .../Shared/_ValidationScriptsPartial.cshtml | 2 + .../Views/Students/Create.cshtml | 2 +- ContosoUniversity/Views/Students/Edit.cshtml | 2 +- ContosoUniversity/Views/Web.config | 50 -- ContosoUniversity/Views/_ViewImports.cshtml | 4 + ContosoUniversity/Web.config | 134 ---- .../appsettings.Development.json | 8 + ContosoUniversity/appsettings.json | 12 + ContosoUniversity/packages.config | 48 -- .../wwwroot/css/notifications.css | 96 +++ ContosoUniversity/wwwroot/css/site.css | 628 ++++++++++++++++++ ContosoUniversity/wwwroot/js/notifications.js | 153 +++++ 39 files changed, 1784 insertions(+), 939 deletions(-) create mode 100644 ContosoUniversity/.github/modernize/ContosoUniversity/plan.md create mode 100644 ContosoUniversity/.github/modernize/ContosoUniversity/tasks.json delete mode 100644 ContosoUniversity/App_Start/BundleConfig.cs delete mode 100644 ContosoUniversity/App_Start/FilterConfig.cs delete mode 100644 ContosoUniversity/App_Start/RouteConfig.cs delete mode 100644 ContosoUniversity/Global.asax delete mode 100644 ContosoUniversity/Global.asax.cs create mode 100644 ContosoUniversity/Program.cs delete mode 100644 ContosoUniversity/Properties/AssemblyInfo.cs create mode 100644 ContosoUniversity/Views/Shared/_ValidationScriptsPartial.cshtml delete mode 100644 ContosoUniversity/Views/Web.config create mode 100644 ContosoUniversity/Views/_ViewImports.cshtml delete mode 100644 ContosoUniversity/Web.config create mode 100644 ContosoUniversity/appsettings.Development.json create mode 100644 ContosoUniversity/appsettings.json delete mode 100644 ContosoUniversity/packages.config create mode 100644 ContosoUniversity/wwwroot/css/notifications.css create mode 100644 ContosoUniversity/wwwroot/css/site.css create mode 100644 ContosoUniversity/wwwroot/js/notifications.js diff --git a/ContosoUniversity/.github/modernize/ContosoUniversity/plan.md b/ContosoUniversity/.github/modernize/ContosoUniversity/plan.md new file mode 100644 index 00000000..c08c4eb6 --- /dev/null +++ b/ContosoUniversity/.github/modernize/ContosoUniversity/plan.md @@ -0,0 +1,281 @@ +# Modernization Plan: ContosoUniversity + +## Overview + +This plan modernizes the **ContosoUniversity** ASP.NET MVC application from **.NET Framework 4.8** to **.NET 10** and migrates it to Azure cloud services. The plan is generated from the AppCAT assessment report (`report-20260514104511`) which identified **6 issue categories** with **25 total incidents** across the codebase, plus a user-requested .NET version upgrade. + +| Property | Value | +|---|---| +| **Application** | ContosoUniversity | +| **Current Framework** | .NET Framework 4.8 (ASP.NET MVC) | +| **Target Framework** | .NET 10 (ASP.NET Core) | +| **Language** | C# | +| **Build Tool** | MSBuild | +| **Total Issues** | 7 (6 from assessment + 1 user-requested) | +| **Total Incidents** | 25+ | +| **Estimated Effort** | 75+ story points | + +--- + +## Task Summary + +| # | Task | Category | Severity | Incidents | Skill | +|---|---|---|---|---|---| +| 1 | Upgrade .NET Framework 4.8 β†’ .NET 10 | Framework Upgrade | mandatory | β€” | `dotnet-upgrade` | +| 2 | Migrate MSMQ to Azure Service Bus | Queue | mandatory | 12 | `dotnet-azure-servicebus` | +| 3 | Migrate Windows Auth to Managed Identity | Identity | mandatory | 1 | `dotnet-managed-identity` | +| 4 | Migrate local file I/O to Azure Storage | Local | potential | 8 | `dotnet-azure-storage-blob` | +| 5 | Migrate secrets to Azure Key Vault | Security | optional | 2 | `dotnet-azure-keyvault-secret` | +| 6 | Configure Azure SQL Database | Database | potential | 1 | `dotnet-azure-sql-database` | +| 7 | Move static content to Azure CDN | Scale | optional | 1 | `manual` | + +--- + +## Task Details + +### Task 1: Upgrade .NET Framework 4.8 to .NET 10 + +**Priority:** πŸ”΄ Mandatory β€” Must be completed first +**Effort:** High +**Category:** Framework Upgrade (user-requested) + +**Description:** +Upgrade the application from .NET Framework 4.8 (ASP.NET MVC 5) to .NET 10 (ASP.NET Core). This is a prerequisite for all other modernization tasks, as the Azure SDK libraries and modern authentication patterns target .NET Core/.NET 5+. + +**Scope:** +- Convert `ContosoUniversity.csproj` from legacy format to SDK-style project +- Migrate from `packages.config` to `` format +- Replace ASP.NET MVC 5 controllers/views with ASP.NET Core MVC patterns +- Replace `Web.config` with `appsettings.json` and `Program.cs` configuration +- Replace `Global.asax` with ASP.NET Core middleware pipeline +- Update Entity Framework 6 to Entity Framework Core +- Update all NuGet packages to .NET 10-compatible versions +- Replace `System.Web` dependencies with ASP.NET Core equivalents +- Update Razor views for ASP.NET Core Tag Helpers + +**Files Affected:** +- `ContosoUniversity.csproj` β€” Project file conversion +- `packages.config` β€” Remove (migrate to PackageReference) +- `Web.config` β€” Replace with `appsettings.json` +- `Global.asax` / `Global.asax.cs` β€” Replace with `Program.cs` +- `Controllers/*.cs` β€” Update base classes and namespaces +- `Views/**/*.cshtml` β€” Update Razor syntax +- `Models/*.cs` β€” Update EF annotations +- `Data/*.cs` β€” Migrate to EF Core DbContext +- `App_Start/*.cs` β€” Migrate to middleware configuration + +**Success Criteria:** +- Application compiles on .NET 10 +- All existing functionality preserved +- EF Core migrations work against the database + +--- + +### Task 2: Migrate MSMQ to Azure Service Bus + +**Priority:** πŸ”΄ Mandatory +**Effort:** 3 story points per incident Γ— 12 incidents = 36 story points +**Category:** Queue +**Rule:** Queue.0003 β€” MSMQ usage detected + +**Description:** +The application uses `System.Messaging.MessageQueue` (MSMQ) for notification processing. MSMQ is not supported on Azure App Service or containerized deployments. Migrate to Azure Service Bus for reliable cloud-native messaging. + +**Scope:** +- Replace `System.Messaging.MessageQueue` with `Azure.Messaging.ServiceBus.ServiceBusClient` +- Replace `System.Messaging.XmlMessageFormatter` with JSON serialization +- Replace `System.Messaging.MessageQueueAccessRights` with Azure Service Bus RBAC +- Configure Service Bus connection via Managed Identity (DefaultAzureCredential) +- Update notification send/receive patterns to use Service Bus SDK + +**Files Affected:** +- `Services/NotificationService.cs` β€” Lines 19, 21, 22, 26, 30 (all 12 MSMQ incidents) + +**References:** +- [Azure Service Bus queues](https://go.microsoft.com/fwlink/?LinkID=2243266) +- [Azure Queue Storage vs Service Bus comparison](https://go.microsoft.com/fwlink/?LinkID=2249263) + +**Success Criteria:** +- All MSMQ references removed +- Notifications sent and received via Azure Service Bus +- Authentication uses DefaultAzureCredential + +--- + +### Task 3: Migrate Authentication to Managed Identity + +**Priority:** πŸ”΄ Mandatory +**Effort:** 3 story points +**Category:** Identity +**Rule:** Identity.0002 β€” Windows authentication detected + +**Description:** +The application uses connection-string-based authentication which includes Windows authentication patterns not supported on Azure App Service, ACA, or AKS. Migrate to Azure Managed Identity with `DefaultAzureCredential` for all Azure service connections. + +**Scope:** +- Replace connection string authentication with `DefaultAzureCredential` +- Configure Azure SQL connection to use Managed Identity token-based auth +- Ensure all Azure SDK clients (Service Bus, Storage, Key Vault) use `DefaultAzureCredential` + +**Files Affected:** +- `Web.config` β†’ `appsettings.json` β€” Connection string configuration + +**References:** +- [Azure App Service authentication](https://go.microsoft.com/fwlink/?LinkID=2242883) + +**Success Criteria:** +- No passwords or secrets in connection strings +- All Azure services accessed via Managed Identity + +--- + +### Task 4: Migrate Local File I/O to Azure Storage + +**Priority:** 🟑 Potential +**Effort:** 3 story points per incident Γ— 8 incidents = 24 story points +**Category:** Local +**Rule:** Local.0003 β€” Local or network IO operations detected + +**Description:** +The application uses `System.IO.File` and `System.IO.Directory` for file operations (teaching material uploads). Local file system access may not be persistent or scalable on Azure App Service. Migrate to Azure Blob Storage or Azure Storage mount paths. + +**Scope:** +- Replace `System.IO.Directory` calls with Azure Blob Storage container operations or mounted storage paths +- Replace `System.IO.File` calls with blob upload/download operations +- Update file upload handling in `CoursesController` to use `BlobServiceClient` +- Configure storage access via Managed Identity + +**Files Affected:** +- `Controllers/CoursesController.cs` β€” Lines 76, 78, 159, 161 (`System.IO.Directory`) +- `Controllers/CoursesController.cs` β€” Lines 172, 174, 229, 233 (`System.IO.File`) + +**References:** +- [Azure Blob Storage](https://go.microsoft.com/fwlink/?linkid=2250574) +- [Azure File Shares](https://go.microsoft.com/fwlink/?LinkID=2242591) +- [Storage mounts for Managed Instance](https://go.microsoft.com/fwlink/?linkid=2346952) + +**Success Criteria:** +- All file operations use Azure Blob Storage or mounted storage +- File uploads persist across app restarts and scale-out +- Authentication uses DefaultAzureCredential + +--- + +### Task 5: Migrate Secrets to Azure Key Vault + +**Priority:** 🟒 Optional (recommended) +**Effort:** 3 story points per incident Γ— 2 incidents = 6 story points +**Category:** Security +**Rule:** Security.0002 β€” Connection strings without configuration builders detected + +**Description:** +The application stores connection strings and app settings directly in `Web.config` without configuration builders. This is a security risk and violates compliance standards (PCI DSS, GDPR). Migrate secrets to Azure Key Vault. + +**Scope:** +- Move `` values to Azure Key Vault +- Move `` secrets to Azure Key Vault +- Configure ASP.NET Core to load secrets from Key Vault via `Azure.Extensions.AspNetCore.Configuration.Secrets` +- Access Key Vault via Managed Identity + +**Files Affected:** +- `Web.config` β†’ `appsettings.json` β€” `` section +- `Web.config` β†’ `appsettings.json` β€” `` section + +**References:** +- [Configuration builders](https://go.microsoft.com/fwlink/?LinkID=2250915) +- [Storing application secrets](https://go.microsoft.com/fwlink/?LinkID=2250916) +- [Centralized app configuration and security](https://go.microsoft.com/fwlink/?LinkID=2250733) + +**Success Criteria:** +- No secrets stored in configuration files or source code +- All secrets retrieved from Azure Key Vault at runtime +- Key Vault access uses DefaultAzureCredential + +--- + +### Task 6: Configure Azure SQL Database + +**Priority:** 🟑 Potential +**Effort:** 3 story points +**Category:** Database +**Rule:** Database.0002 β€” SQL database connection detected + +**Description:** +Ensure the SQL database is available on Azure. Migrate the on-premises SQL Server database to Azure SQL Database and update connection configuration for cloud deployment. + +**Scope:** +- Update `DefaultConnection` connection string for Azure SQL Database +- Configure Entity Framework Core to connect to Azure SQL with Managed Identity +- Validate database schema compatibility with Azure SQL + +**Files Affected:** +- `Web.config` β†’ `appsettings.json` β€” `DefaultConnection` connection string +- `Data/*.cs` β€” DbContext configuration + +**References:** +- [Migrate SQL Server database to Azure](https://go.microsoft.com/fwlink/?LinkID=2251731) +- [Azure SQL Managed Instance](https://go.microsoft.com/fwlink/?LinkID=2251613) +- [Azure Migrate](https://go.microsoft.com/fwlink/?linkid=2252410) + +**Success Criteria:** +- Application connects to Azure SQL Database +- Connection uses Managed Identity (no password in connection string) +- All queries and migrations work on Azure SQL + +--- + +### Task 7: Move Static Content to Azure CDN + +**Priority:** 🟒 Optional +**Effort:** 3 story points +**Category:** Scale +**Rule:** Scale.0001 β€” Static content detected + +**Description:** +The application bundles 16 static files (CSS, JavaScript, images) directly in the project. Serving static content from the application increases costs, reduces performance, and requires redeployment for content changes. Consider offloading to Azure Blob Storage with Azure CDN. + +**Scope:** +- Move static files (Content/, Scripts/, favicon.ico) to Azure Blob Storage +- Configure Azure CDN for global content delivery +- Update Razor views to reference CDN URLs +- Keep local fallback for development + +**Files Affected:** +- `Content/bootstrap.css`, `Content/bootstrap.min.css`, `Content/Site.css` +- `Scripts/bootstrap.js`, `Scripts/bootstrap.min.js` +- `Scripts/jquery-*.js`, `Scripts/jquery.validate*.js` +- `Scripts/modernizr-2.6.2.js`, `Scripts/respond.js`, `Scripts/respond.min.js` +- `favicon.ico` +- `Uploads/TeachingMaterials/*` + +**References:** +- [Azure Blob Storage](https://go.microsoft.com/fwlink/?linkid=2250574) +- [Azure CDN](https://go.microsoft.com/fwlink/?linkid=2250392) + +**Success Criteria:** +- Static files served via CDN +- Improved load times for end users +- Content updates possible without app redeployment + +--- + +## Execution Order + +The tasks should be executed in the following dependency order: + +``` +Task 1: .NET Upgrade (prerequisite for all Azure SDK tasks) + β”œβ”€β”€ Task 2: MSMQ β†’ Azure Service Bus (mandatory) + β”œβ”€β”€ Task 3: Managed Identity (mandatory, enables other Azure tasks) + β”‚ β”œβ”€β”€ Task 4: Local I/O β†’ Azure Storage + β”‚ β”œβ”€β”€ Task 5: Secrets β†’ Azure Key Vault + β”‚ └── Task 6: Azure SQL Database + └── Task 7: Static Content β†’ CDN (independent, optional) +``` + +## Assessment Source + +- **Report:** `.github/modernize/assessment/reports/report-20260514104511/report.json` +- **Producer:** .NET AppCAT CLI v1.0.0 +- **Analysis Date:** 2026-05-14 +- **Target Platforms:** Azure App Service, AKS, ACA, App Service Container, App Service Managed Instance diff --git a/ContosoUniversity/.github/modernize/ContosoUniversity/tasks.json b/ContosoUniversity/.github/modernize/ContosoUniversity/tasks.json new file mode 100644 index 00000000..87e79e9d --- /dev/null +++ b/ContosoUniversity/.github/modernize/ContosoUniversity/tasks.json @@ -0,0 +1,315 @@ +{ + "tasks": [ + { + "id": "task-1", + "type": "upgrade", + "title": "Upgrade .NET Framework 4.8 to .NET 10", + "description": "Convert the ASP.NET MVC 5 application from .NET Framework 4.8 to ASP.NET Core on .NET 10. Includes project file conversion, package migration, middleware pipeline setup, EF Core migration, and Razor view updates.", + "skill": "builtin:dotnet-upgrade", + "category": "Framework Upgrade", + "severity": "mandatory", + "target": { + "current_framework": ".NETFramework,Version=v4.8", + "target_framework": "net10.0" + }, + "incidents": [], + "filesAffected": [ + "ContosoUniversity.csproj", + "packages.config", + "Web.config", + "Global.asax", + "Global.asax.cs", + "Controllers/*.cs", + "Views/**/*.cshtml", + "Models/*.cs", + "Data/*.cs", + "App_Start/*.cs" + ], + "dependencies": [], + "successCriteria": "Application compiles and runs on .NET 10 with all existing functionality preserved", + "status": "pending" + }, + { + "id": "task-2", + "type": "cloud-migration", + "title": "Migrate MSMQ to Azure Service Bus", + "description": "Replace System.Messaging.MessageQueue (MSMQ) usage in NotificationService with Azure.Messaging.ServiceBus. Replace XmlMessageFormatter with JSON serialization and configure Service Bus access via DefaultAzureCredential.", + "skill": "dotnet-azure-servicebus", + "category": "Queue", + "severity": "mandatory", + "ruleId": "Queue.0003", + "target": { + "from": "System.Messaging.MessageQueue (MSMQ)", + "to": "Azure.Messaging.ServiceBus" + }, + "incidents": [ + { + "incidentId": "efa216b2-6628-463b-8a8d-d29eb8011944", + "location": "Services/NotificationService.cs", + "line": 21, + "snippet": "System.Messaging.MessageQueue" + }, + { + "incidentId": "8193c7d5-9eff-4feb-82bb-546821534b32", + "location": "Services/NotificationService.cs", + "line": 26, + "snippet": "System.Messaging.MessageQueue" + }, + { + "incidentId": "e34bba0e-418d-4faf-b1b4-71ecbc734765", + "location": "Services/NotificationService.cs", + "line": 30, + "snippet": "System.Messaging.XmlMessageFormatter" + }, + { + "incidentId": "63ed7e4f-3185-40b0-97f5-2f3e3e130709", + "location": "Services/NotificationService.cs", + "line": 22, + "snippet": "System.Messaging.MessageQueueAccessRights" + }, + { + "incidentId": "52843c11-b423-4efe-a607-1028b921cacb", + "location": "Services/NotificationService.cs", + "line": 19, + "snippet": "System.Messaging.MessageQueue" + } + ], + "filesAffected": [ + "Services/NotificationService.cs" + ], + "dependencies": [ + "task-1" + ], + "successCriteria": "All MSMQ references removed; notifications sent/received via Azure Service Bus with DefaultAzureCredential", + "status": "pending" + }, + { + "id": "task-3", + "type": "cloud-migration", + "title": "Migrate Authentication to Managed Identity", + "description": "Replace connection-string-based and Windows authentication with Azure Managed Identity using DefaultAzureCredential. Configure all Azure service connections to use token-based authentication.", + "skill": "dotnet-managed-identity", + "category": "Identity", + "severity": "mandatory", + "ruleId": "Identity.0002", + "target": { + "from": "Windows authentication / connection string auth", + "to": "Azure Managed Identity (DefaultAzureCredential)" + }, + "incidents": [ + { + "incidentId": "10c96015-cf9a-4e3b-85bb-4a17b0e053d8", + "location": "Web.config", + "snippet": "" + } + ], + "filesAffected": [ + "Web.config" + ], + "dependencies": [ + "task-1" + ], + "successCriteria": "No passwords or secrets in connection strings; all Azure services accessed via Managed Identity", + "status": "pending" + }, + { + "id": "task-4", + "type": "cloud-migration", + "title": "Migrate Local File I/O to Azure Storage", + "description": "Replace System.IO.File and System.IO.Directory usage in CoursesController with Azure Blob Storage or mounted storage paths. Update file upload/download handling to use BlobServiceClient with DefaultAzureCredential.", + "skill": "dotnet-azure-storage-blob", + "category": "Local", + "severity": "potential", + "ruleId": "Local.0003", + "target": { + "from": "System.IO.File / System.IO.Directory (local file system)", + "to": "Azure Blob Storage or Azure Storage mount" + }, + "incidents": [ + { + "incidentId": "a0c9305c-f7ab-4215-acac-a4e7e97380e3", + "location": "Controllers/CoursesController.cs", + "line": 76, + "snippet": "System.IO.Directory" + }, + { + "incidentId": "cdf5de15-2709-46ca-89ad-612ac7d493c9", + "location": "Controllers/CoursesController.cs", + "line": 78, + "snippet": "System.IO.Directory" + }, + { + "incidentId": "8e42661a-16ee-4e47-a4ce-ba0dad6c9e46", + "location": "Controllers/CoursesController.cs", + "line": 159, + "snippet": "System.IO.Directory" + }, + { + "incidentId": "7514c406-42ec-4fc6-94a4-d6ea973cfa51", + "location": "Controllers/CoursesController.cs", + "line": 161, + "snippet": "System.IO.Directory" + }, + { + "incidentId": "8e33f3b8-d19b-41a0-a48d-7ca952ac622b", + "location": "Controllers/CoursesController.cs", + "line": 172, + "snippet": "System.IO.File" + }, + { + "incidentId": "4dbc4e16-071c-4659-b7a0-ccb8695b7100", + "location": "Controllers/CoursesController.cs", + "line": 174, + "snippet": "System.IO.File" + }, + { + "incidentId": "f35a8b1d-69e0-4bc1-b660-615b41d5ab9d", + "location": "Controllers/CoursesController.cs", + "line": 229, + "snippet": "System.IO.File" + }, + { + "incidentId": "df2634d6-58ed-469b-8094-bd5897ab5fbb", + "location": "Controllers/CoursesController.cs", + "line": 233, + "snippet": "System.IO.File" + } + ], + "filesAffected": [ + "Controllers/CoursesController.cs" + ], + "dependencies": [ + "task-1", + "task-3" + ], + "successCriteria": "All file operations use Azure Blob Storage or mounted storage; files persist across app restarts and scale-out", + "status": "pending" + }, + { + "id": "task-5", + "type": "cloud-migration", + "title": "Migrate Secrets to Azure Key Vault", + "description": "Move connection strings and app settings from Web.config to Azure Key Vault. Configure ASP.NET Core to load secrets from Key Vault via Azure.Extensions.AspNetCore.Configuration.Secrets with DefaultAzureCredential.", + "skill": "dotnet-azure-keyvault-secret", + "category": "Security", + "severity": "optional", + "ruleId": "Security.0002", + "target": { + "from": "Secrets in Web.config (appSettings, connectionStrings)", + "to": "Azure Key Vault" + }, + "incidents": [ + { + "incidentId": "6c136b01-e007-4147-ba6d-ee5362412ee8", + "location": "Web.config", + "snippet": "" + }, + { + "incidentId": "517dbf2e-9f4e-4e10-9121-f828907d94fc", + "location": "Web.config", + "snippet": "" + } + ], + "filesAffected": [ + "Web.config" + ], + "dependencies": [ + "task-1", + "task-3" + ], + "successCriteria": "No secrets in configuration files or source code; all secrets retrieved from Azure Key Vault at runtime", + "status": "pending" + }, + { + "id": "task-6", + "type": "cloud-migration", + "title": "Configure Azure SQL Database", + "description": "Migrate the SQL Server database connection to Azure SQL Database. Update DefaultConnection configuration and configure Entity Framework Core to use Managed Identity for Azure SQL authentication.", + "skill": "dotnet-azure-sql-database", + "category": "Database", + "severity": "potential", + "ruleId": "Database.0002", + "target": { + "from": "On-premises SQL Server with connection string", + "to": "Azure SQL Database with Managed Identity" + }, + "incidents": [ + { + "incidentId": "764b27bd-807d-44bb-ba6f-add7904229d8", + "location": "Web.config", + "snippet": "" + } + ], + "filesAffected": [ + "Web.config", + "Data/SchoolContext.cs" + ], + "dependencies": [ + "task-1", + "task-3" + ], + "successCriteria": "Application connects to Azure SQL Database via Managed Identity; all queries and migrations work on Azure SQL", + "status": "pending" + }, + { + "id": "task-7", + "type": "optimization", + "title": "Move Static Content to Azure CDN", + "description": "Move static files (CSS, JavaScript, images) from the application to Azure Blob Storage with Azure CDN for improved performance, reduced costs, and independent content updates.", + "skill": "manual", + "category": "Scale", + "severity": "optional", + "ruleId": "Scale.0001", + "target": { + "from": "Static files bundled in project (Content/, Scripts/)", + "to": "Azure Blob Storage + Azure CDN" + }, + "incidents": [ + { + "incidentId": "f0e3c2b1-0a47-4046-b5bd-ff21956d2f4f", + "location": "ContosoUniversity.csproj", + "snippet": "16 static files detected (CSS, JS, images)" + } + ], + "filesAffected": [ + "Content/bootstrap.css", + "Content/bootstrap.min.css", + "Content/Site.css", + "Scripts/bootstrap.js", + "Scripts/bootstrap.min.js", + "Scripts/jquery-3.4.1.js", + "Scripts/jquery-3.4.1.min.js", + "Scripts/jquery.validate.js", + "Scripts/jquery.validate.min.js", + "Scripts/jquery.validate.unobtrusive.js", + "Scripts/jquery.validate.unobtrusive.min.js", + "Scripts/modernizr-2.6.2.js", + "Scripts/respond.js", + "Scripts/respond.min.js", + "favicon.ico" + ], + "dependencies": [ + "task-1" + ], + "successCriteria": "Static files served via CDN with improved load times", + "status": "pending" + } + ], + "metadata": { + "language": "dotnet", + "planName": "ContosoUniversity Modernization Plan", + "projectName": "ContosoUniversity", + "sourceFramework": ".NETFramework,Version=v4.8", + "targetFramework": "net10.0", + "assessmentReport": ".github/modernize/assessment/reports/report-20260514104511/report.json", + "createdAt": "2026-05-14T10:49:00Z", + "version": "1.0", + "totalTasks": 7, + "totalIncidents": 25, + "severityBreakdown": { + "mandatory": 3, + "potential": 2, + "optional": 2 + } + } +} diff --git a/ContosoUniversity/App_Start/BundleConfig.cs b/ContosoUniversity/App_Start/BundleConfig.cs deleted file mode 100644 index 6f68ffce..00000000 --- a/ContosoUniversity/App_Start/BundleConfig.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Web.Optimization; - -namespace ContosoUniversity -{ - public class BundleConfig - { - public static void RegisterBundles(BundleCollection bundles) - { - bundles.Add(new ScriptBundle("~/bundles/jquery").Include( - "~/Scripts/jquery-{version}.js")); - - bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include( - "~/Scripts/jquery.validate*")); - - bundles.Add(new ScriptBundle("~/bundles/modernizr").Include( - "~/Scripts/modernizr-*")); - - bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include( - "~/Scripts/bootstrap.js", - "~/Scripts/respond.js")); - - bundles.Add(new StyleBundle("~/Content/css").Include( - "~/Content/bootstrap.css", - "~/Content/site.css")); - } - } -} diff --git a/ContosoUniversity/App_Start/FilterConfig.cs b/ContosoUniversity/App_Start/FilterConfig.cs deleted file mode 100644 index f4645d93..00000000 --- a/ContosoUniversity/App_Start/FilterConfig.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Web.Mvc; - -namespace ContosoUniversity -{ - public class FilterConfig - { - public static void RegisterGlobalFilters(GlobalFilterCollection filters) - { - filters.Add(new HandleErrorAttribute()); - // Remove the global authorization filter since we're implementing role-based authorization - // filters.Add(new AuthorizeAttribute()); // Require authentication for all controllers - } - } -} diff --git a/ContosoUniversity/App_Start/RouteConfig.cs b/ContosoUniversity/App_Start/RouteConfig.cs deleted file mode 100644 index b3be05f2..00000000 --- a/ContosoUniversity/App_Start/RouteConfig.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Web.Mvc; -using System.Web.Routing; - -namespace ContosoUniversity -{ - public class RouteConfig - { - public static void RegisterRoutes(RouteCollection routes) - { - routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); - - routes.MapRoute( - name: "Default", - url: "{controller}/{action}/{id}", - defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } - ); - } - } -} diff --git a/ContosoUniversity/ContosoUniversity.csproj b/ContosoUniversity/ContosoUniversity.csproj index 8f49c50d..e0125d8b 100644 --- a/ContosoUniversity/ContosoUniversity.csproj +++ b/ContosoUniversity/ContosoUniversity.csproj @@ -1,350 +1,22 @@ - - - + - Debug - AnyCPU - - - 2.0 - {8F1F2C91-8D72-4F8B-9A4A-3B2C5D6E7F8A} - {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} - Library - Properties + net10.0 ContosoUniversity ContosoUniversity - v4.8 - false - true - - - disabled - enabled - - - - - + enable + disable - - true - full - false - bin\ - DEBUG;TRACE - prompt - 4 - - - true - pdbonly - true - bin\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - - - True - - - True - packages\Microsoft.Web.Infrastructure.2.0.1\lib\net40\Microsoft.Web.Infrastructure.dll - - - - - - - True - packages\Microsoft.AspNet.WebPages.3.2.9\lib\net45\System.Web.Helpers.dll - - - True - packages\Microsoft.AspNet.Mvc.5.2.9\lib\net45\System.Web.Mvc.dll - - - packages\Microsoft.AspNet.Web.Optimization.1.1.3\lib\net40\System.Web.Optimization.dll - True - - - True - packages\Microsoft.AspNet.Razor.3.2.9\lib\net45\System.Web.Razor.dll - - - True - packages\Microsoft.AspNet.WebPages.3.2.9\lib\net45\System.Web.WebPages.dll - - - True - packages\Microsoft.AspNet.WebPages.3.2.9\lib\net45\System.Web.WebPages.Deployment.dll - - - True - packages\Microsoft.AspNet.WebPages.3.2.9\lib\net45\System.Web.WebPages.Razor.dll - - - packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll - True - - - - True - packages\WebGrease.1.5.2\lib\WebGrease.dll - - - True - packages\Antlr.3.4.1.9004\lib\Antlr3.Runtime.dll - - - packages\Microsoft.EntityFrameworkCore.3.1.32\lib\netstandard2.0\Microsoft.EntityFrameworkCore.dll - True - - - packages\Microsoft.EntityFrameworkCore.Abstractions.3.1.32\lib\netstandard2.0\Microsoft.EntityFrameworkCore.Abstractions.dll - True - - - packages\Microsoft.EntityFrameworkCore.SqlServer.3.1.32\lib\netstandard2.0\Microsoft.EntityFrameworkCore.SqlServer.dll - True - - - packages\Microsoft.EntityFrameworkCore.Relational.3.1.32\lib\netstandard2.0\Microsoft.EntityFrameworkCore.Relational.dll - True - - - packages\Microsoft.Data.SqlClient.2.1.4\lib\net46\Microsoft.Data.SqlClient.dll - True - - - packages\Microsoft.Bcl.AsyncInterfaces.1.1.1\lib\netstandard2.0\Microsoft.Bcl.AsyncInterfaces.dll - True - - - packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll - True - - - packages\Microsoft.Extensions.DependencyInjection.Abstractions.3.1.32\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll - True - - - packages\Microsoft.Extensions.DependencyInjection.3.1.32\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.dll - True - - - packages\Microsoft.Extensions.Caching.Abstractions.3.1.32\lib\netstandard2.0\Microsoft.Extensions.Caching.Abstractions.dll - True - - - packages\Microsoft.Extensions.Caching.Memory.3.1.32\lib\netstandard2.0\Microsoft.Extensions.Caching.Memory.dll - True - - - packages\Microsoft.Extensions.Configuration.3.1.32\lib\netstandard2.0\Microsoft.Extensions.Configuration.dll - True - - - packages\Microsoft.Extensions.Configuration.Abstractions.3.1.32\lib\netstandard2.0\Microsoft.Extensions.Configuration.Abstractions.dll - True - - - packages\Microsoft.Extensions.Configuration.Binder.3.1.32\lib\netstandard2.0\Microsoft.Extensions.Configuration.Binder.dll - True - - - packages\Microsoft.Extensions.Logging.3.1.32\lib\netstandard2.0\Microsoft.Extensions.Logging.dll - True - - - packages\Microsoft.Extensions.Logging.Abstractions.3.1.32\lib\netstandard2.0\Microsoft.Extensions.Logging.Abstractions.dll - True - - - packages\Microsoft.Extensions.Options.3.1.32\lib\netstandard2.0\Microsoft.Extensions.Options.dll - True - - - packages\Microsoft.Extensions.Primitives.3.1.32\lib\netstandard2.0\Microsoft.Extensions.Primitives.dll - True - - - packages\System.Diagnostics.DiagnosticSource.4.7.1\lib\net46\System.Diagnostics.DiagnosticSource.dll - True - - - packages\System.Collections.Immutable.1.7.1\lib\netstandard2.0\System.Collections.Immutable.dll - True - - - packages\System.ComponentModel.Annotations.4.7.0\lib\net461\System.ComponentModel.Annotations.dll - True - - - packages\Microsoft.Identity.Client.4.21.1\lib\net461\Microsoft.Identity.Client.dll - True - - - - - packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.2.0.1\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll - True - - - - - - - - - - - - - - - - - Global.asax - - - - - - - - - - - - - - - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + - - - - - - - - - - - - - - - - - - - - - - - - - Web.config - - - Web.config - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - - - - - - - - - - - - - - - - - True - True - 58801 - / - https://localhost:44300/ - False - False - - - False - - - - + \ No newline at end of file diff --git a/ContosoUniversity/Controllers/BaseController.cs b/ContosoUniversity/Controllers/BaseController.cs index 5e46cefb..fd158928 100644 --- a/ContosoUniversity/Controllers/BaseController.cs +++ b/ContosoUniversity/Controllers/BaseController.cs @@ -1,5 +1,5 @@ using System; -using System.Web.Mvc; +using Microsoft.AspNetCore.Mvc; using ContosoUniversity.Services; using ContosoUniversity.Models; using ContosoUniversity.Data; @@ -9,11 +9,12 @@ namespace ContosoUniversity.Controllers public abstract class BaseController : Controller { protected SchoolContext db; - protected NotificationService notificationService = new NotificationService(); + protected NotificationService notificationService; - public BaseController() + public BaseController(SchoolContext context, NotificationService notificationSvc) { - db = SchoolContextFactory.Create(); + db = context; + notificationService = notificationSvc; } protected void SendEntityNotification(string entityType, string entityId, EntityOperation operation) @@ -40,9 +41,9 @@ protected override void Dispose(bool disposing) if (disposing) { db?.Dispose(); - notificationService?.Dispose(); } base.Dispose(disposing); } } } + diff --git a/ContosoUniversity/Controllers/CoursesController.cs b/ContosoUniversity/Controllers/CoursesController.cs index 32706841..ebc59ee3 100644 --- a/ContosoUniversity/Controllers/CoursesController.cs +++ b/ContosoUniversity/Controllers/CoursesController.cs @@ -1,216 +1,213 @@ using System; using Microsoft.EntityFrameworkCore; using System.Linq; -using System.Net; -using System.Web.Mvc; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Hosting; using System.IO; -using System.Web; using ContosoUniversity.Data; using ContosoUniversity.Models; +using ContosoUniversity.Services; namespace ContosoUniversity.Controllers { public class CoursesController : BaseController { + private readonly IWebHostEnvironment _webHostEnvironment; + + public CoursesController(SchoolContext context, NotificationService notificationService, IWebHostEnvironment webHostEnvironment) + : base(context, notificationService) + { + _webHostEnvironment = webHostEnvironment; + } + // GET: Courses - public ActionResult Index() + public IActionResult Index() { var courses = db.Courses.Include(c => c.Department); return View(courses.ToList()); } // GET: Courses/Details/5 - public ActionResult Details(int? id) + public IActionResult Details(int? id) { if (id == null) { - return new HttpStatusCodeResult(HttpStatusCode.BadRequest); + return BadRequest(); } Course course = db.Courses.Include(c => c.Department).Where(c => c.CourseID == id).Single(); if (course == null) { - return HttpNotFound(); + return NotFound(); } return View(course); } // GET: Courses/Create - public ActionResult Create() + public IActionResult Create() { - ViewBag.DepartmentID = new SelectList(db.Departments, "DepartmentID", "Name"); + ViewBag.DepartmentID = new Microsoft.AspNetCore.Mvc.Rendering.SelectList(db.Departments, "DepartmentID", "Name"); return View(new Course()); } // POST: Courses/Create [HttpPost] [ValidateAntiForgeryToken] - public ActionResult Create([Bind(Include = "CourseID,Title,Credits,DepartmentID,TeachingMaterialImagePath")] Course course, HttpPostedFileBase teachingMaterialImage) + public IActionResult Create([Bind("CourseID,Title,Credits,DepartmentID,TeachingMaterialImagePath")] Course course, IFormFile teachingMaterialImage) { if (ModelState.IsValid) { // Handle file upload if an image is provided - if (teachingMaterialImage != null && teachingMaterialImage.ContentLength > 0) + if (teachingMaterialImage != null && teachingMaterialImage.Length > 0) { - // Validate file type var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".bmp" }; var fileExtension = Path.GetExtension(teachingMaterialImage.FileName).ToLower(); - + if (!allowedExtensions.Contains(fileExtension)) { ModelState.AddModelError("teachingMaterialImage", "Please upload a valid image file (jpg, jpeg, png, gif, bmp)."); - ViewBag.DepartmentID = new SelectList(db.Departments, "DepartmentID", "Name", course.DepartmentID); + ViewBag.DepartmentID = new Microsoft.AspNetCore.Mvc.Rendering.SelectList(db.Departments, "DepartmentID", "Name", course.DepartmentID); return View(course); } - // Validate file size (max 5MB) - if (teachingMaterialImage.ContentLength > 5 * 1024 * 1024) + if (teachingMaterialImage.Length > 5 * 1024 * 1024) { ModelState.AddModelError("teachingMaterialImage", "File size must be less than 5MB."); - ViewBag.DepartmentID = new SelectList(db.Departments, "DepartmentID", "Name", course.DepartmentID); + ViewBag.DepartmentID = new Microsoft.AspNetCore.Mvc.Rendering.SelectList(db.Departments, "DepartmentID", "Name", course.DepartmentID); return View(course); } try { - // Create uploads directory if it doesn't exist - var uploadsPath = Server.MapPath("~/Uploads/TeachingMaterials/"); + var uploadsPath = Path.Combine(_webHostEnvironment.WebRootPath, "Uploads", "TeachingMaterials"); if (!Directory.Exists(uploadsPath)) { Directory.CreateDirectory(uploadsPath); } - // Generate unique filename var fileName = $"course_{course.CourseID}_{Guid.NewGuid()}{fileExtension}"; var filePath = Path.Combine(uploadsPath, fileName); - // Save file - teachingMaterialImage.SaveAs(filePath); - course.TeachingMaterialImagePath = $"~/Uploads/TeachingMaterials/{fileName}"; + using (var stream = new FileStream(filePath, FileMode.Create)) + { + teachingMaterialImage.CopyTo(stream); + } + course.TeachingMaterialImagePath = $"/Uploads/TeachingMaterials/{fileName}"; } catch (Exception ex) { ModelState.AddModelError("teachingMaterialImage", "Error uploading file: " + ex.Message); - ViewBag.DepartmentID = new SelectList(db.Departments, "DepartmentID", "Name", course.DepartmentID); + ViewBag.DepartmentID = new Microsoft.AspNetCore.Mvc.Rendering.SelectList(db.Departments, "DepartmentID", "Name", course.DepartmentID); return View(course); } } db.Courses.Add(course); db.SaveChanges(); - - // Send notification for course creation SendEntityNotification("Course", course.CourseID.ToString(), course.Title, EntityOperation.CREATE); - return RedirectToAction("Index"); } - ViewBag.DepartmentID = new SelectList(db.Departments, "DepartmentID", "Name", course.DepartmentID); + ViewBag.DepartmentID = new Microsoft.AspNetCore.Mvc.Rendering.SelectList(db.Departments, "DepartmentID", "Name", course.DepartmentID); return View(course); } // GET: Courses/Edit/5 - public ActionResult Edit(int? id) + public IActionResult Edit(int? id) { if (id == null) { - return new HttpStatusCodeResult(HttpStatusCode.BadRequest); + return BadRequest(); } Course course = db.Courses.Find(id); if (course == null) { - return HttpNotFound(); + return NotFound(); } - ViewBag.DepartmentID = new SelectList(db.Departments, "DepartmentID", "Name", course.DepartmentID); + ViewBag.DepartmentID = new Microsoft.AspNetCore.Mvc.Rendering.SelectList(db.Departments, "DepartmentID", "Name", course.DepartmentID); return View(course); } // POST: Courses/Edit/5 [HttpPost] [ValidateAntiForgeryToken] - public ActionResult Edit([Bind(Include = "CourseID,Title,Credits,DepartmentID,TeachingMaterialImagePath")] Course course, HttpPostedFileBase teachingMaterialImage) + public IActionResult Edit([Bind("CourseID,Title,Credits,DepartmentID,TeachingMaterialImagePath")] Course course, IFormFile teachingMaterialImage) { if (ModelState.IsValid) { - // Handle file upload if a new image is provided - if (teachingMaterialImage != null && teachingMaterialImage.ContentLength > 0) + if (teachingMaterialImage != null && teachingMaterialImage.Length > 0) { - // Validate file type var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".bmp" }; var fileExtension = Path.GetExtension(teachingMaterialImage.FileName).ToLower(); - + if (!allowedExtensions.Contains(fileExtension)) { ModelState.AddModelError("teachingMaterialImage", "Please upload a valid image file (jpg, jpeg, png, gif, bmp)."); - ViewBag.DepartmentID = new SelectList(db.Departments, "DepartmentID", "Name", course.DepartmentID); + ViewBag.DepartmentID = new Microsoft.AspNetCore.Mvc.Rendering.SelectList(db.Departments, "DepartmentID", "Name", course.DepartmentID); return View(course); } - // Validate file size (max 5MB) - if (teachingMaterialImage.ContentLength > 5 * 1024 * 1024) + if (teachingMaterialImage.Length > 5 * 1024 * 1024) { ModelState.AddModelError("teachingMaterialImage", "File size must be less than 5MB."); - ViewBag.DepartmentID = new SelectList(db.Departments, "DepartmentID", "Name", course.DepartmentID); + ViewBag.DepartmentID = new Microsoft.AspNetCore.Mvc.Rendering.SelectList(db.Departments, "DepartmentID", "Name", course.DepartmentID); return View(course); } try { - // Create uploads directory if it doesn't exist - var uploadsPath = Server.MapPath("~/Uploads/TeachingMaterials/"); + var uploadsPath = Path.Combine(_webHostEnvironment.WebRootPath, "Uploads", "TeachingMaterials"); if (!Directory.Exists(uploadsPath)) { Directory.CreateDirectory(uploadsPath); } - // Generate unique filename var fileName = $"course_{course.CourseID}_{Guid.NewGuid()}{fileExtension}"; var filePath = Path.Combine(uploadsPath, fileName); - // Delete old file if exists if (!string.IsNullOrEmpty(course.TeachingMaterialImagePath)) { - var oldFilePath = Server.MapPath(course.TeachingMaterialImagePath); + var oldFilePath = Path.Combine(_webHostEnvironment.WebRootPath, course.TeachingMaterialImagePath.TrimStart('/')); if (System.IO.File.Exists(oldFilePath)) { System.IO.File.Delete(oldFilePath); } } - // Save new file - teachingMaterialImage.SaveAs(filePath); - course.TeachingMaterialImagePath = $"~/Uploads/TeachingMaterials/{fileName}"; + using (var stream = new FileStream(filePath, FileMode.Create)) + { + teachingMaterialImage.CopyTo(stream); + } + course.TeachingMaterialImagePath = $"/Uploads/TeachingMaterials/{fileName}"; } catch (Exception ex) { ModelState.AddModelError("teachingMaterialImage", "Error uploading file: " + ex.Message); - ViewBag.DepartmentID = new SelectList(db.Departments, "DepartmentID", "Name", course.DepartmentID); + ViewBag.DepartmentID = new Microsoft.AspNetCore.Mvc.Rendering.SelectList(db.Departments, "DepartmentID", "Name", course.DepartmentID); return View(course); } } db.Entry(course).State = EntityState.Modified; db.SaveChanges(); - - // Send notification for course update SendEntityNotification("Course", course.CourseID.ToString(), course.Title, EntityOperation.UPDATE); - return RedirectToAction("Index"); } - ViewBag.DepartmentID = new SelectList(db.Departments, "DepartmentID", "Name", course.DepartmentID); + ViewBag.DepartmentID = new Microsoft.AspNetCore.Mvc.Rendering.SelectList(db.Departments, "DepartmentID", "Name", course.DepartmentID); return View(course); } // GET: Courses/Delete/5 - public ActionResult Delete(int? id) + public IActionResult Delete(int? id) { if (id == null) { - return new HttpStatusCodeResult(HttpStatusCode.BadRequest); + return BadRequest(); } Course course = db.Courses.Include(c => c.Department).Where(c => c.CourseID == id).Single(); if (course == null) { - return HttpNotFound(); + return NotFound(); } return View(course); } @@ -218,15 +215,14 @@ public ActionResult Delete(int? id) // POST: Courses/Delete/5 [HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] - public ActionResult DeleteConfirmed(int id) + public IActionResult DeleteConfirmed(int id) { Course course = db.Courses.Find(id); var courseTitle = course.Title; - - // Delete associated image file if it exists + if (!string.IsNullOrEmpty(course.TeachingMaterialImagePath)) { - var filePath = Server.MapPath(course.TeachingMaterialImagePath); + var filePath = Path.Combine(_webHostEnvironment.WebRootPath, course.TeachingMaterialImagePath.TrimStart('/')); if (System.IO.File.Exists(filePath)) { try @@ -235,19 +231,14 @@ public ActionResult DeleteConfirmed(int id) } catch (Exception ex) { - // Log the error but don't prevent deletion of the course - // In a production application, you would log this error properly System.Diagnostics.Debug.WriteLine($"Error deleting file: {ex.Message}"); } } } - + db.Courses.Remove(course); db.SaveChanges(); - - // Send notification for course deletion SendEntityNotification("Course", id.ToString(), courseTitle, EntityOperation.DELETE); - return RedirectToAction("Index"); } @@ -255,7 +246,7 @@ protected override void Dispose(bool disposing) { if (disposing) { - // Base class will dispose db and notificationService + // Base class will dispose db } base.Dispose(disposing); } diff --git a/ContosoUniversity/Controllers/DepartmentsController.cs b/ContosoUniversity/Controllers/DepartmentsController.cs index bfd2afee..63a6e5b1 100644 --- a/ContosoUniversity/Controllers/DepartmentsController.cs +++ b/ContosoUniversity/Controllers/DepartmentsController.cs @@ -1,84 +1,86 @@ using System; using Microsoft.EntityFrameworkCore; using System.Linq; -using System.Net; -using System.Web.Mvc; +using Microsoft.AspNetCore.Mvc; using ContosoUniversity.Data; using ContosoUniversity.Models; +using ContosoUniversity.Services; namespace ContosoUniversity.Controllers { public class DepartmentsController : BaseController { + public DepartmentsController(SchoolContext context, NotificationService notificationService) + : base(context, notificationService) + { + } + // GET: Departments - All roles can view - public ActionResult Index() + public IActionResult Index() { var departments = db.Departments.Include(d => d.Administrator); return View(departments.ToList()); } // GET: Departments/Details/5 - public ActionResult Details(int? id) + public IActionResult Details(int? id) { if (id == null) { - return new HttpStatusCodeResult(HttpStatusCode.BadRequest); + return BadRequest(); } Department department = db.Departments.Find(id); if (department == null) { - return HttpNotFound(); + return NotFound(); } return View(department); } // GET: Departments/Create - public ActionResult Create() + public IActionResult Create() { - ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName"); + ViewBag.InstructorID = new Microsoft.AspNetCore.Mvc.Rendering.SelectList(db.Instructors, "ID", "FullName"); return View(); } // POST: Departments/Create [HttpPost] [ValidateAntiForgeryToken] - public ActionResult Create([Bind(Include = "Name,Budget,StartDate,InstructorID")] Department department) + public IActionResult Create([Bind("Name,Budget,StartDate,InstructorID")] Department department) { if (ModelState.IsValid) { db.Departments.Add(department); db.SaveChanges(); - - // Send notification for department creation SendEntityNotification("Department", department.DepartmentID.ToString(), department.Name, EntityOperation.CREATE); - return RedirectToAction("Index"); } - ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", department.InstructorID); + ViewBag.InstructorID = new Microsoft.AspNetCore.Mvc.Rendering.SelectList(db.Instructors, "ID", "FullName", department.InstructorID); return View(department); } // GET: Departments/Edit/5 - public ActionResult Edit(int? id) + public IActionResult Edit(int? id) { if (id == null) { - return new HttpStatusCodeResult(HttpStatusCode.BadRequest); + return BadRequest(); } Department department = db.Departments.Find(id); if (department == null) { - return HttpNotFound(); + return NotFound(); } - ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", department.InstructorID); + ViewBag.InstructorID = new Microsoft.AspNetCore.Mvc.Rendering.SelectList(db.Instructors, "ID", "FullName", department.InstructorID); return View(department); } // POST: Departments/Edit/5 [HttpPost] [ValidateAntiForgeryToken] - public ActionResult Edit([Bind(Include = "DepartmentID,Name,Budget,StartDate,InstructorID,RowVersion")] Department department) + public IActionResult Edit([Bind("DepartmentID,Name,Budget,StartDate,InstructorID,RowVersion")] Department department) { try { @@ -86,10 +88,7 @@ public ActionResult Edit([Bind(Include = "DepartmentID,Name,Budget,StartDate,Ins { db.Entry(department).State = EntityState.Modified; db.SaveChanges(); - - // Send notification for department update SendEntityNotification("Department", department.DepartmentID.ToString(), department.Name, EntityOperation.UPDATE); - return RedirectToAction("Index"); } } @@ -98,7 +97,7 @@ public ActionResult Edit([Bind(Include = "DepartmentID,Name,Budget,StartDate,Ins var entry = ex.Entries.Single(); var clientValues = (Department)entry.Entity; var databaseEntry = entry.GetDatabaseValues(); - + if (databaseEntry == null) { ModelState.AddModelError(string.Empty, "Unable to save changes. The department was deleted by another user."); @@ -106,7 +105,7 @@ public ActionResult Edit([Bind(Include = "DepartmentID,Name,Budget,StartDate,Ins else { var databaseValues = (Department)databaseEntry.ToObject(); - + if (databaseValues.Name != clientValues.Name) ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}"); if (databaseValues.Budget != clientValues.Budget) @@ -118,32 +117,32 @@ public ActionResult Edit([Bind(Include = "DepartmentID,Name,Budget,StartDate,Ins var instructor = db.Instructors.Find(databaseValues.InstructorID); ModelState.AddModelError("InstructorID", $"Current value: {instructor?.FullName}"); } - + ModelState.AddModelError(string.Empty, "The record you attempted to edit " + "was modified by another user after you got the original value. The " + "edit operation was canceled and the current values in the database " + "have been displayed. If you still want to edit this record, click " + "the Save button again. Otherwise click the Back to List hyperlink."); - + department.RowVersion = databaseValues.RowVersion; } } - - ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", department.InstructorID); + + ViewBag.InstructorID = new Microsoft.AspNetCore.Mvc.Rendering.SelectList(db.Instructors, "ID", "FullName", department.InstructorID); return View(department); } // GET: Departments/Delete/5 - public ActionResult Delete(int? id) + public IActionResult Delete(int? id) { if (id == null) { - return new HttpStatusCodeResult(HttpStatusCode.BadRequest); + return BadRequest(); } Department department = db.Departments.Find(id); if (department == null) { - return HttpNotFound(); + return NotFound(); } return View(department); } @@ -151,16 +150,13 @@ public ActionResult Delete(int? id) // POST: Departments/Delete/5 [HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] - public ActionResult DeleteConfirmed(int id) + public IActionResult DeleteConfirmed(int id) { Department department = db.Departments.Find(id); var departmentName = department.Name; db.Departments.Remove(department); db.SaveChanges(); - - // Send notification for department deletion SendEntityNotification("Department", id.ToString(), departmentName, EntityOperation.DELETE); - return RedirectToAction("Index"); } diff --git a/ContosoUniversity/Controllers/HomeController.cs b/ContosoUniversity/Controllers/HomeController.cs index c6fd682c..45750fe9 100644 --- a/ContosoUniversity/Controllers/HomeController.cs +++ b/ContosoUniversity/Controllers/HomeController.cs @@ -1,19 +1,25 @@ using System.Collections.Generic; using System.Linq; -using System.Web.Mvc; +using Microsoft.AspNetCore.Mvc; using ContosoUniversity.Data; using ContosoUniversity.Models.SchoolViewModels; +using ContosoUniversity.Services; namespace ContosoUniversity.Controllers { public class HomeController : BaseController { - public ActionResult Index() + public HomeController(SchoolContext context, NotificationService notificationService) + : base(context, notificationService) + { + } + + public IActionResult Index() { return View(); } - public ActionResult About() + public IActionResult About() { IQueryable data = from student in db.Students @@ -26,22 +32,23 @@ group student by student.EnrollmentDate into dateGroup return View(data.ToList()); } - public ActionResult Contact() + public IActionResult Contact() { ViewBag.Message = "Your contact page."; return View(); } - public ActionResult Error() + public IActionResult Error() { return View(); } - public ActionResult Unauthorized() + public IActionResult Unauthorized() { ViewBag.Message = "You don't have permission to access this resource."; return View(); } } } + diff --git a/ContosoUniversity/Controllers/InstructorsController.cs b/ContosoUniversity/Controllers/InstructorsController.cs index 894fa928..8919791b 100644 --- a/ContosoUniversity/Controllers/InstructorsController.cs +++ b/ContosoUniversity/Controllers/InstructorsController.cs @@ -2,18 +2,23 @@ using System.Collections.Generic; using Microsoft.EntityFrameworkCore; using System.Linq; -using System.Net; -using System.Web.Mvc; +using Microsoft.AspNetCore.Mvc; using ContosoUniversity.Data; using ContosoUniversity.Models; using ContosoUniversity.Models.SchoolViewModels; +using ContosoUniversity.Services; namespace ContosoUniversity.Controllers { public class InstructorsController : BaseController { + public InstructorsController(SchoolContext context, NotificationService notificationService) + : base(context, notificationService) + { + } + // GET: Instructors - All roles can view - public ActionResult Index(int? id, int? courseID) + public IActionResult Index(int? id, int? courseID) { var viewModel = new InstructorIndexData(); viewModel.Instructors = db.Instructors @@ -41,22 +46,22 @@ public ActionResult Index(int? id, int? courseID) } // GET: Instructors/Details/5 - All roles can view details - public ActionResult Details(int? id) + public IActionResult Details(int? id) { if (id == null) { - return new HttpStatusCodeResult(HttpStatusCode.BadRequest); + return BadRequest(); } Instructor instructor = db.Instructors.Find(id); if (instructor == null) { - return HttpNotFound(); + return NotFound(); } return View(instructor); } // GET: Instructors/Create - public ActionResult Create() + public IActionResult Create() { var instructor = new Instructor(); instructor.CourseAssignments = new List(); @@ -67,7 +72,7 @@ public ActionResult Create() // POST: Instructors/Create [HttpPost] [ValidateAntiForgeryToken] - public ActionResult Create([Bind(Include = "LastName,FirstMidName,HireDate,OfficeAssignment")] Instructor instructor, string[] selectedCourses) + public IActionResult Create([Bind("LastName,FirstMidName,HireDate,OfficeAssignment")] Instructor instructor, string[] selectedCourses) { if (selectedCourses != null) { @@ -82,10 +87,7 @@ public ActionResult Create([Bind(Include = "LastName,FirstMidName,HireDate,Offic { db.Instructors.Add(instructor); db.SaveChanges(); - - // Send notification for instructor creation SendEntityNotification("Instructor", instructor.ID.ToString(), EntityOperation.CREATE); - return RedirectToAction("Index"); } PopulateAssignedCourseData(instructor); @@ -93,11 +95,11 @@ public ActionResult Create([Bind(Include = "LastName,FirstMidName,HireDate,Offic } // GET: Instructors/Edit/5 - public ActionResult Edit(int? id) + public IActionResult Edit(int? id) { if (id == null) { - return new HttpStatusCodeResult(HttpStatusCode.BadRequest); + return BadRequest(); } Instructor instructor = db.Instructors .Include(i => i.OfficeAssignment) @@ -108,7 +110,7 @@ public ActionResult Edit(int? id) PopulateAssignedCourseData(instructor); if (instructor == null) { - return HttpNotFound(); + return NotFound(); } return View(instructor); } @@ -133,11 +135,11 @@ private void PopulateAssignedCourseData(Instructor instructor) // POST: Instructors/Edit/5 [HttpPost] [ValidateAntiForgeryToken] - public ActionResult Edit(int? id, string[] selectedCourses) + public IActionResult Edit(int? id, string[] selectedCourses, string LastName, string FirstMidName, DateTime HireDate, string OfficeLocation) { if (id == null) { - return new HttpStatusCodeResult(HttpStatusCode.BadRequest); + return BadRequest(); } var instructorToUpdate = db.Instructors .Include(i => i.OfficeAssignment) @@ -146,23 +148,31 @@ public ActionResult Edit(int? id, string[] selectedCourses) .Where(i => i.ID == id) .Single(); - if (TryUpdateModel(instructorToUpdate, "", - new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" })) + // Manually update instructor fields + instructorToUpdate.LastName = LastName; + instructorToUpdate.FirstMidName = FirstMidName; + instructorToUpdate.HireDate = HireDate; + + if (String.IsNullOrWhiteSpace(OfficeLocation)) { - try + instructorToUpdate.OfficeAssignment = null; + } + else + { + if (instructorToUpdate.OfficeAssignment == null) { - if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location)) - { - instructorToUpdate.OfficeAssignment = null; - } + instructorToUpdate.OfficeAssignment = new OfficeAssignment { InstructorID = instructorToUpdate.ID }; + } + instructorToUpdate.OfficeAssignment.Location = OfficeLocation; + } + if (ModelState.IsValid) + { + try + { UpdateInstructorCourses(selectedCourses, instructorToUpdate); - db.SaveChanges(); - - // Send notification for instructor update SendEntityNotification("Instructor", instructorToUpdate.ID.ToString(), EntityOperation.UPDATE); - return RedirectToAction("Index"); } catch (Exception) @@ -196,7 +206,6 @@ private void UpdateInstructorCourses(string[] selectedCourses, Instructor instru } else { - if (instructorCourses.Contains(course.CourseID)) { CourseAssignment courseToRemove = instructorToUpdate.CourseAssignments.SingleOrDefault(i => i.CourseID == course.CourseID); @@ -207,16 +216,16 @@ private void UpdateInstructorCourses(string[] selectedCourses, Instructor instru } // GET: Instructors/Delete/5 - public ActionResult Delete(int? id) + public IActionResult Delete(int? id) { if (id == null) { - return new HttpStatusCodeResult(HttpStatusCode.BadRequest); + return BadRequest(); } Instructor instructor = db.Instructors.Find(id); if (instructor == null) { - return HttpNotFound(); + return NotFound(); } return View(instructor); } @@ -224,7 +233,7 @@ public ActionResult Delete(int? id) // POST: Instructors/Delete/5 - Only admins can delete instructors [HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] - public ActionResult DeleteConfirmed(int id) + public IActionResult DeleteConfirmed(int id) { Instructor instructor = db.Instructors .Include(i => i.OfficeAssignment) @@ -242,10 +251,7 @@ public ActionResult DeleteConfirmed(int id) } db.SaveChanges(); - - // Send notification for instructor deletion SendEntityNotification("Instructor", id.ToString(), EntityOperation.DELETE); - return RedirectToAction("Index"); } diff --git a/ContosoUniversity/Controllers/NotificationsController.cs b/ContosoUniversity/Controllers/NotificationsController.cs index 3e177e12..84df5c1b 100644 --- a/ContosoUniversity/Controllers/NotificationsController.cs +++ b/ContosoUniversity/Controllers/NotificationsController.cs @@ -1,13 +1,19 @@ using System; using System.Collections.Generic; -using System.Web.Mvc; +using Microsoft.AspNetCore.Mvc; using ContosoUniversity.Services; using ContosoUniversity.Models; +using ContosoUniversity.Data; namespace ContosoUniversity.Controllers { public class NotificationsController : BaseController { + public NotificationsController(SchoolContext context, NotificationService notificationService) + : base(context, notificationService) + { + } + // GET: api/notifications - Get pending notifications for admin [HttpGet] public JsonResult GetNotifications() @@ -30,14 +36,14 @@ public JsonResult GetNotifications() catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"Error retrieving notifications: {ex.Message}"); - return Json(new { success = false, message = "Error retrieving notifications" }, JsonRequestBehavior.AllowGet); + return Json(new { success = false, message = "Error retrieving notifications" }); } return Json(new { success = true, notifications = notifications, count = notifications.Count - }, JsonRequestBehavior.AllowGet); + }); } // POST: api/notifications/mark-read @@ -57,9 +63,10 @@ public JsonResult MarkAsRead(int id) } // GET: Notifications/Index - Admin notification dashboard - public ActionResult Index() + public IActionResult Index() { return View(); } } } + diff --git a/ContosoUniversity/Controllers/StudentsController.cs b/ContosoUniversity/Controllers/StudentsController.cs index 05fcad57..98718122 100644 --- a/ContosoUniversity/Controllers/StudentsController.cs +++ b/ContosoUniversity/Controllers/StudentsController.cs @@ -1,18 +1,23 @@ using System; using Microsoft.EntityFrameworkCore; using System.Linq; -using System.Net; -using System.Web.Mvc; +using Microsoft.AspNetCore.Mvc; using ContosoUniversity.Data; using ContosoUniversity.Models; +using ContosoUniversity.Services; using System.Diagnostics; namespace ContosoUniversity.Controllers { public class StudentsController : BaseController { + public StudentsController(SchoolContext context, NotificationService notificationService) + : base(context, notificationService) + { + } + // GET: Students - Admins and Teachers can view - public ActionResult Index(string sortOrder, string currentFilter, string searchString, int? page) + public IActionResult Index(string sortOrder, string currentFilter, string searchString, int? page) { ViewBag.CurrentSort = sortOrder; ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "name_desc" : ""; @@ -60,11 +65,11 @@ public ActionResult Index(string sortOrder, string currentFilter, string searchS } // GET: Students/Details/5 - Admins and Teachers can view details - public ActionResult Details(int? id) + public IActionResult Details(int? id) { if (id == null) { - return new HttpStatusCodeResult(HttpStatusCode.BadRequest); + return BadRequest(); } Student student = db.Students .Include(s => s.Enrollments) @@ -72,13 +77,13 @@ public ActionResult Details(int? id) .Where(s => s.ID == id).Single(); if (student == null) { - return HttpNotFound(); + return NotFound(); } return View(student); } // GET: Students/Create - public ActionResult Create() + public IActionResult Create() { var student = new Student { @@ -90,7 +95,7 @@ public ActionResult Create() // POST: Students/Create [HttpPost] [ValidateAntiForgeryToken] - public ActionResult Create([Bind(Include = "LastName,FirstMidName,EnrollmentDate")] Student student) + public IActionResult Create([Bind("LastName,FirstMidName,EnrollmentDate")] Student student) { try { @@ -127,16 +132,16 @@ public ActionResult Create([Bind(Include = "LastName,FirstMidName,EnrollmentDate } // GET: Students/Edit/5 - public ActionResult Edit(int? id) + public IActionResult Edit(int? id) { if (id == null) { - return new HttpStatusCodeResult(HttpStatusCode.BadRequest); + return BadRequest(); } Student student = db.Students.Find(id); if (student == null) { - return HttpNotFound(); + return NotFound(); } return View(student); } @@ -144,7 +149,7 @@ public ActionResult Edit(int? id) // POST: Students/Edit/5 [HttpPost] [ValidateAntiForgeryToken] - public ActionResult Edit([Bind(Include = "ID,LastName,FirstMidName,EnrollmentDate")] Student student) + public IActionResult Edit([Bind("ID,LastName,FirstMidName,EnrollmentDate")] Student student) { try { @@ -181,16 +186,16 @@ public ActionResult Edit([Bind(Include = "ID,LastName,FirstMidName,EnrollmentDat } // GET: Students/Delete/5 - public ActionResult Delete(int? id) + public IActionResult Delete(int? id) { if (id == null) { - return new HttpStatusCodeResult(HttpStatusCode.BadRequest); + return BadRequest(); } Student student = db.Students.Find(id); if (student == null) { - return HttpNotFound(); + return NotFound(); } return View(student); } @@ -198,7 +203,7 @@ public ActionResult Delete(int? id) // POST: Students/Delete/5 [HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] - public ActionResult DeleteConfirmed(int id) + public IActionResult DeleteConfirmed(int id) { try { @@ -224,7 +229,7 @@ protected override void Dispose(bool disposing) { if (disposing) { - // Base class will dispose db and notificationService + // Base class will dispose db } base.Dispose(disposing); } diff --git a/ContosoUniversity/Data/SchoolContextFactory.cs b/ContosoUniversity/Data/SchoolContextFactory.cs index e07eeca9..ce5be7ea 100644 --- a/ContosoUniversity/Data/SchoolContextFactory.cs +++ b/ContosoUniversity/Data/SchoolContextFactory.cs @@ -1,13 +1,13 @@ using Microsoft.EntityFrameworkCore; -using System.Configuration; +using Microsoft.Extensions.Configuration; namespace ContosoUniversity.Data { public static class SchoolContextFactory { - public static SchoolContext Create() + public static SchoolContext Create(IConfiguration configuration) { - var connectionString = ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString; + var connectionString = configuration.GetConnectionString("DefaultConnection"); var optionsBuilder = new DbContextOptionsBuilder(); optionsBuilder.UseSqlServer(connectionString); @@ -15,3 +15,4 @@ public static SchoolContext Create() } } } + diff --git a/ContosoUniversity/Global.asax b/ContosoUniversity/Global.asax deleted file mode 100644 index 7b65e2c8..00000000 --- a/ContosoUniversity/Global.asax +++ /dev/null @@ -1 +0,0 @@ -<%@ Application Codebehind="Global.asax.cs" Inherits="ContosoUniversity.MvcApplication" Language="C#" %> diff --git a/ContosoUniversity/Global.asax.cs b/ContosoUniversity/Global.asax.cs deleted file mode 100644 index e4c4ad10..00000000 --- a/ContosoUniversity/Global.asax.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Web; -using System.Web.Mvc; -using System.Web.Optimization; -using System.Web.Routing; -using Microsoft.EntityFrameworkCore; -using ContosoUniversity.Data; -using Microsoft.Extensions.DependencyInjection; - -namespace ContosoUniversity -{ - public class MvcApplication : HttpApplication - { - protected void Application_Start() - { - AreaRegistration.RegisterAllAreas(); - FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); - RouteConfig.RegisterRoutes(RouteTable.Routes); - BundleConfig.RegisterBundles(BundleTable.Bundles); - - // Initialize database with EF Core - InitializeDatabase(); - } - - private void InitializeDatabase() - { - var connectionString = System.Configuration.ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString; - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseSqlServer(connectionString); - - using (var context = new SchoolContext(optionsBuilder.Options)) - { - DbInitializer.Initialize(context); - } - } - } -} diff --git a/ContosoUniversity/Program.cs b/ContosoUniversity/Program.cs new file mode 100644 index 00000000..944b595e --- /dev/null +++ b/ContosoUniversity/Program.cs @@ -0,0 +1,47 @@ +using ContosoUniversity.Data; +using ContosoUniversity.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Configuration; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container +builder.Services.AddControllersWithViews(); + +// Register EF Core DbContext +builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); + +// Register NotificationService as a singleton (in-memory queue) +builder.Services.AddSingleton(); + +var app = builder.Build(); + +// Initialize the database +using (var scope = app.Services.CreateScope()) +{ + var context = scope.ServiceProvider.GetRequiredService(); + DbInitializer.Initialize(context); +} + +// Configure the HTTP request pipeline +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Home/Error"); + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.UseStaticFiles(); +app.UseRouting(); +app.UseAuthorization(); + +app.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + +app.Run(); diff --git a/ContosoUniversity/Properties/AssemblyInfo.cs b/ContosoUniversity/Properties/AssemblyInfo.cs deleted file mode 100644 index 629a4aff..00000000 --- a/ContosoUniversity/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("ContosoUniversity")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("ContosoUniversity")] -[assembly: AssemblyCopyright("Copyright Β© 2024")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -[assembly: ComVisible(false)] - -[assembly: Guid("8f1f2c91-8d72-4f8b-9a4a-3b2c5d6e7f8a")] - -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/ContosoUniversity/Services/NotificationService.cs b/ContosoUniversity/Services/NotificationService.cs index 32dac411..ac368ede 100644 --- a/ContosoUniversity/Services/NotificationService.cs +++ b/ContosoUniversity/Services/NotificationService.cs @@ -1,6 +1,5 @@ using System; -using System.Messaging; -using System.Configuration; +using System.Collections.Concurrent; using ContosoUniversity.Models; using Newtonsoft.Json; @@ -8,27 +7,11 @@ namespace ContosoUniversity.Services { public class NotificationService { - private readonly string _queuePath; - private readonly MessageQueue _queue; + private readonly ConcurrentQueue _queue = new ConcurrentQueue(); + private int _nextId = 1; public NotificationService() { - // Get queue path from configuration or use default - _queuePath = ConfigurationManager.AppSettings["NotificationQueuePath"] ?? @".\Private$\ContosoUniversityNotifications"; - - // Ensure the queue exists - if (!MessageQueue.Exists(_queuePath)) - { - _queue = MessageQueue.Create(_queuePath); - _queue.SetPermissions("Everyone", MessageQueueAccessRights.FullControl); - } - else - { - _queue = new MessageQueue(_queuePath); - } - - // Configure queue formatter - _queue.Formatter = new XmlMessageFormatter(new Type[] { typeof(string) }); } public void SendNotification(string entityType, string entityId, EntityOperation operation, string userName = null) @@ -42,6 +25,7 @@ public void SendNotification(string entityType, string entityId, string entityDi { var notification = new Notification { + Id = System.Threading.Interlocked.Increment(ref _nextId), EntityType = entityType, EntityId = entityId, Operation = operation.ToString(), @@ -51,14 +35,7 @@ public void SendNotification(string entityType, string entityId, string entityDi IsRead = false }; - var jsonMessage = JsonConvert.SerializeObject(notification); - var message = new Message(jsonMessage) - { - Label = $"{entityType} {operation}", - Priority = MessagePriority.Normal - }; - - _queue.Send(message); + _queue.Enqueue(notification); } catch (Exception ex) { @@ -69,22 +46,11 @@ public void SendNotification(string entityType, string entityId, string entityDi public Notification ReceiveNotification() { - try - { - var message = _queue.Receive(TimeSpan.FromSeconds(1)); - var jsonContent = message.Body.ToString(); - return JsonConvert.DeserializeObject(jsonContent); - } - catch (MessageQueueException ex) when (ex.MessageQueueErrorCode == MessageQueueErrorCode.IOTimeout) + if (_queue.TryDequeue(out var notification)) { - // No messages available - return null; - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"Failed to receive notification: {ex.Message}"); - return null; + return notification; } + return null; } public void MarkAsRead(int notificationId) @@ -95,8 +61,8 @@ public void MarkAsRead(int notificationId) private string GenerateMessage(string entityType, string entityId, string entityDisplayName, EntityOperation operation) { - var displayText = !string.IsNullOrWhiteSpace(entityDisplayName) - ? $"{entityType} '{entityDisplayName}'" + var displayText = !string.IsNullOrWhiteSpace(entityDisplayName) + ? $"{entityType} '{entityDisplayName}'" : $"{entityType} (ID: {entityId})"; switch (operation) @@ -114,7 +80,8 @@ private string GenerateMessage(string entityType, string entityId, string entity public void Dispose() { - _queue?.Dispose(); + // Nothing to dispose for in-memory queue } } } + diff --git a/ContosoUniversity/Views/Courses/Create.cshtml b/ContosoUniversity/Views/Courses/Create.cshtml index 5629ec31..eca202fd 100644 --- a/ContosoUniversity/Views/Courses/Create.cshtml +++ b/ContosoUniversity/Views/Courses/Create.cshtml @@ -68,5 +68,5 @@ @section Scripts { - @Scripts.Render("~/bundles/jqueryval") + } diff --git a/ContosoUniversity/Views/Courses/Edit.cshtml b/ContosoUniversity/Views/Courses/Edit.cshtml index 20a53d2c..381fbe2f 100644 --- a/ContosoUniversity/Views/Courses/Edit.cshtml +++ b/ContosoUniversity/Views/Courses/Edit.cshtml @@ -78,5 +78,5 @@ @section Scripts { - @Scripts.Render("~/bundles/jqueryval") + } diff --git a/ContosoUniversity/Views/Departments/Create.cshtml b/ContosoUniversity/Views/Departments/Create.cshtml index 1820f058..e160f48b 100644 --- a/ContosoUniversity/Views/Departments/Create.cshtml +++ b/ContosoUniversity/Views/Departments/Create.cshtml @@ -51,5 +51,5 @@ @section Scripts { - @Scripts.Render("~/bundles/jqueryval") + } diff --git a/ContosoUniversity/Views/Departments/Edit.cshtml b/ContosoUniversity/Views/Departments/Edit.cshtml index 5f9eda02..5dce16b5 100644 --- a/ContosoUniversity/Views/Departments/Edit.cshtml +++ b/ContosoUniversity/Views/Departments/Edit.cshtml @@ -54,5 +54,5 @@ @section Scripts { - @Scripts.Render("~/bundles/jqueryval") + } diff --git a/ContosoUniversity/Views/Instructors/Create.cshtml b/ContosoUniversity/Views/Instructors/Create.cshtml index 5f97499e..9de2cf20 100644 --- a/ContosoUniversity/Views/Instructors/Create.cshtml +++ b/ContosoUniversity/Views/Instructors/Create.cshtml @@ -87,5 +87,5 @@ @section Scripts { - @Scripts.Render("~/bundles/jqueryval") + } diff --git a/ContosoUniversity/Views/Instructors/Edit.cshtml b/ContosoUniversity/Views/Instructors/Edit.cshtml index 4f5676f9..f2dd9d8a 100644 --- a/ContosoUniversity/Views/Instructors/Edit.cshtml +++ b/ContosoUniversity/Views/Instructors/Edit.cshtml @@ -89,5 +89,5 @@ @section Scripts { - @Scripts.Render("~/bundles/jqueryval") + } diff --git a/ContosoUniversity/Views/Shared/Error.cshtml b/ContosoUniversity/Views/Shared/Error.cshtml index 7d30b816..b4fdca64 100644 --- a/ContosoUniversity/Views/Shared/Error.cshtml +++ b/ContosoUniversity/Views/Shared/Error.cshtml @@ -1,5 +1,3 @@ -@model System.Web.Mvc.HandleErrorInfo - @{ ViewBag.Title = "Error"; } @@ -7,15 +5,10 @@

Error.

An error occurred while processing your request.

-@if (Model != null && HttpContext.Current.IsDebuggingEnabled) +@if (System.Diagnostics.Debugger.IsAttached) {

- Exception Details: @Model.Exception.Message -

-

- Controller: @Model.ControllerName -

-

- Action: @Model.ActionName + An unhandled exception occurred during the execution of this request. + Please check the application logs for more details.

} diff --git a/ContosoUniversity/Views/Shared/_Layout.cshtml b/ContosoUniversity/Views/Shared/_Layout.cshtml index 98b5062e..882e4eee 100644 --- a/ContosoUniversity/Views/Shared/_Layout.cshtml +++ b/ContosoUniversity/Views/Shared/_Layout.cshtml @@ -4,24 +4,24 @@ @ViewBag.Title - Contoso University - @Styles.Render("~/Content/css") - - @Scripts.Render("~/bundles/modernizr") + + +