diff --git a/ContosoUniversity/ContosoUniversity.csproj b/ContosoUniversity/ContosoUniversity.csproj index 8f49c50d..66ae2904 100644 --- a/ContosoUniversity/ContosoUniversity.csproj +++ b/ContosoUniversity/ContosoUniversity.csproj @@ -45,11 +45,59 @@ 4 + + packages\Azure.Storage.Blobs.12.24.0\lib\netstandard2.0\Azure.Storage.Blobs.dll + True + + + packages\Azure.Core.1.44.1\lib\netstandard2.0\Azure.Core.dll + True + + + packages\Microsoft.Bcl.AsyncInterfaces.6.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll + True + + + packages\System.ClientModel.1.1.0\lib\netstandard2.0\System.ClientModel.dll + True + + + packages\System.Memory.Data.6.0.0\lib\netstandard2.0\System.Memory.Data.dll + True + + + packages\Azure.Storage.Common.12.23.0\lib\netstandard2.0\Azure.Storage.Common.dll + True + + + packages\System.Diagnostics.DiagnosticSource.6.0.1\lib\net461\System.Diagnostics.DiagnosticSource.dll + True + + + packages\System.IO.Hashing.6.0.0\lib\net461\System.IO.Hashing.dll + True + + + packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + True + + + packages\System.Text.Encodings.Web.6.0.1\lib\net461\System.Text.Encodings.Web.dll + True + + + packages\System.Text.Json.6.0.11\lib\net461\System.Text.Json.dll + True + + + packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll + True + @@ -135,10 +183,6 @@ 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 @@ -187,10 +231,6 @@ 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 @@ -199,10 +239,6 @@ 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 - @@ -242,6 +278,7 @@ + diff --git a/ContosoUniversity/Controllers/BaseController.cs b/ContosoUniversity/Controllers/BaseController.cs index 5e46cefb..ca4f4707 100644 --- a/ContosoUniversity/Controllers/BaseController.cs +++ b/ContosoUniversity/Controllers/BaseController.cs @@ -10,6 +10,7 @@ public abstract class BaseController : Controller { protected SchoolContext db; protected NotificationService notificationService = new NotificationService(); + protected BlobStorageService blobStorageService = new BlobStorageService(); public BaseController() { @@ -41,6 +42,7 @@ protected override void Dispose(bool disposing) { db?.Dispose(); notificationService?.Dispose(); + blobStorageService?.Dispose(); } base.Dispose(disposing); } diff --git a/ContosoUniversity/Controllers/CoursesController.cs b/ContosoUniversity/Controllers/CoursesController.cs index 32706841..43e87e77 100644 --- a/ContosoUniversity/Controllers/CoursesController.cs +++ b/ContosoUniversity/Controllers/CoursesController.cs @@ -54,7 +54,7 @@ public ActionResult Create([Bind(Include = "CourseID,Title,Credits,DepartmentID, // 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)."); @@ -72,20 +72,16 @@ public ActionResult Create([Bind(Include = "CourseID,Title,Credits,DepartmentID, try { - // Create uploads directory if it doesn't exist - var uploadsPath = Server.MapPath("~/Uploads/TeachingMaterials/"); - if (!Directory.Exists(uploadsPath)) - { - Directory.CreateDirectory(uploadsPath); - } + // Generate unique blob name + var blobName = $"course_{course.CourseID}_{Guid.NewGuid()}{fileExtension}"; - // Generate unique filename - var fileName = $"course_{course.CourseID}_{Guid.NewGuid()}{fileExtension}"; - var filePath = Path.Combine(uploadsPath, fileName); + // Upload to Azure Blob Storage + var blobUri = blobStorageService.UploadBlobAsync( + teachingMaterialImage.InputStream, + blobName, + teachingMaterialImage.ContentType).Result; - // Save file - teachingMaterialImage.SaveAs(filePath); - course.TeachingMaterialImagePath = $"~/Uploads/TeachingMaterials/{fileName}"; + course.TeachingMaterialImagePath = blobUri; } catch (Exception ex) { @@ -97,10 +93,10 @@ public ActionResult Create([Bind(Include = "CourseID,Title,Credits,DepartmentID, db.Courses.Add(course); db.SaveChanges(); - + // Send notification for course creation SendEntityNotification("Course", course.CourseID.ToString(), course.Title, EntityOperation.CREATE); - + return RedirectToAction("Index"); } @@ -137,7 +133,7 @@ public ActionResult Edit([Bind(Include = "CourseID,Title,Credits,DepartmentID,Te // 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)."); @@ -155,30 +151,22 @@ public ActionResult Edit([Bind(Include = "CourseID,Title,Credits,DepartmentID,Te try { - // Create uploads directory if it doesn't exist - var uploadsPath = Server.MapPath("~/Uploads/TeachingMaterials/"); - if (!Directory.Exists(uploadsPath)) + // Delete old blob if exists + if (!string.IsNullOrEmpty(course.TeachingMaterialImagePath)) { - Directory.CreateDirectory(uploadsPath); + blobStorageService.DeleteBlobAsync(course.TeachingMaterialImagePath).Wait(); } - // Generate unique filename - var fileName = $"course_{course.CourseID}_{Guid.NewGuid()}{fileExtension}"; - var filePath = Path.Combine(uploadsPath, fileName); + // Generate unique blob name + var blobName = $"course_{course.CourseID}_{Guid.NewGuid()}{fileExtension}"; - // Delete old file if exists - if (!string.IsNullOrEmpty(course.TeachingMaterialImagePath)) - { - var oldFilePath = Server.MapPath(course.TeachingMaterialImagePath); - if (System.IO.File.Exists(oldFilePath)) - { - System.IO.File.Delete(oldFilePath); - } - } + // Upload new blob to Azure Blob Storage + var blobUri = blobStorageService.UploadBlobAsync( + teachingMaterialImage.InputStream, + blobName, + teachingMaterialImage.ContentType).Result; - // Save new file - teachingMaterialImage.SaveAs(filePath); - course.TeachingMaterialImagePath = $"~/Uploads/TeachingMaterials/{fileName}"; + course.TeachingMaterialImagePath = blobUri; } catch (Exception ex) { @@ -190,10 +178,10 @@ public ActionResult Edit([Bind(Include = "CourseID,Title,Credits,DepartmentID,Te 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); @@ -223,22 +211,17 @@ public ActionResult DeleteConfirmed(int id) Course course = db.Courses.Find(id); var courseTitle = course.Title; - // Delete associated image file if it exists + // Delete associated image from Azure Blob Storage if it exists if (!string.IsNullOrEmpty(course.TeachingMaterialImagePath)) { - var filePath = Server.MapPath(course.TeachingMaterialImagePath); - if (System.IO.File.Exists(filePath)) + try { - try - { - System.IO.File.Delete(filePath); - } - 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}"); - } + blobStorageService.DeleteBlobAsync(course.TeachingMaterialImagePath).Wait(); + } + catch (Exception ex) + { + // Log the error but don't prevent deletion of the course + System.Diagnostics.Debug.WriteLine($"Error deleting blob: {ex.Message}"); } } diff --git a/ContosoUniversity/Services/BlobStorageService.cs b/ContosoUniversity/Services/BlobStorageService.cs new file mode 100644 index 00000000..b88867f0 --- /dev/null +++ b/ContosoUniversity/Services/BlobStorageService.cs @@ -0,0 +1,169 @@ +using System; +using System.Configuration; +using System.IO; +using System.Threading.Tasks; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; + +namespace ContosoUniversity.Services +{ + public class BlobStorageService : IDisposable + { + private readonly BlobServiceClient _blobServiceClient; + private readonly string _containerName; + + public BlobStorageService() + { + // Get configuration from Web.config + string connectionString = ConfigurationManager.AppSettings["AzureStorageBlob:ConnectionString"]; + _containerName = ConfigurationManager.AppSettings["AzureStorageBlob:ContainerName"]; + + if (string.IsNullOrEmpty(connectionString)) + { + throw new InvalidOperationException("AzureStorageBlob:ConnectionString configuration is missing in Web.config"); + } + + if (string.IsNullOrEmpty(_containerName)) + { + throw new InvalidOperationException("AzureStorageBlob:ContainerName configuration is missing in Web.config"); + } + + // Create BlobServiceClient using connection string + _blobServiceClient = new BlobServiceClient(connectionString); + + // Ensure container exists + EnsureContainerExists(); + } + + private void EnsureContainerExists() + { + try + { + var containerClient = _blobServiceClient.GetBlobContainerClient(_containerName); + + // Create container if it doesn't exist with public read access for blobs + containerClient.CreateIfNotExists(PublicAccessType.Blob); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error ensuring container exists: {ex.Message}"); + // Don't throw - container might already exist or will be created on first upload + } + } + + /// + /// Uploads a file to Azure Blob Storage + /// + /// File stream to upload + /// Name for the blob (e.g., "course_1045_guid.jpg") + /// Content type of the file (e.g., "image/jpeg") + /// URI of the uploaded blob + public async Task UploadBlobAsync(Stream stream, string blobName, string contentType) + { + try + { + var containerClient = _blobServiceClient.GetBlobContainerClient(_containerName); + var blobClient = containerClient.GetBlobClient(blobName); + + // Set blob upload options with content type + var uploadOptions = new BlobUploadOptions + { + HttpHeaders = new BlobHttpHeaders + { + ContentType = contentType + } + }; + + // Upload the blob + await blobClient.UploadAsync(stream, uploadOptions); + + // Return the blob URI + return blobClient.Uri.ToString(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error uploading blob {blobName}: {ex.Message}"); + throw new ApplicationException($"Failed to upload file to blob storage: {ex.Message}", ex); + } + } + + /// + /// Deletes a blob from Azure Blob Storage + /// + /// Full URI of the blob to delete + /// True if deleted, false if not found + public async Task DeleteBlobAsync(string blobUri) + { + if (string.IsNullOrEmpty(blobUri)) + { + return false; + } + + try + { + // Extract blob name from URI + var uri = new Uri(blobUri); + var blobName = uri.Segments[uri.Segments.Length - 1]; + + var containerClient = _blobServiceClient.GetBlobContainerClient(_containerName); + var blobClient = containerClient.GetBlobClient(blobName); + + // Delete the blob if it exists + var response = await blobClient.DeleteIfExistsAsync(); + return response.Value; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error deleting blob {blobUri}: {ex.Message}"); + // Return false instead of throwing to avoid breaking course deletion + return false; + } + } + + /// + /// Gets the URI for a blob + /// + /// Name of the blob + /// URI of the blob + public string GetBlobUri(string blobName) + { + var containerClient = _blobServiceClient.GetBlobContainerClient(_containerName); + var blobClient = containerClient.GetBlobClient(blobName); + return blobClient.Uri.ToString(); + } + + /// + /// Checks if a blob exists + /// + /// Full URI of the blob + /// True if exists, false otherwise + public async Task BlobExistsAsync(string blobUri) + { + if (string.IsNullOrEmpty(blobUri)) + { + return false; + } + + try + { + var uri = new Uri(blobUri); + var blobName = uri.Segments[uri.Segments.Length - 1]; + + var containerClient = _blobServiceClient.GetBlobContainerClient(_containerName); + var blobClient = containerClient.GetBlobClient(blobName); + + var response = await blobClient.ExistsAsync(); + return response.Value; + } + catch + { + return false; + } + } + + public void Dispose() + { + // BlobServiceClient doesn't require explicit disposal + } + } +} diff --git a/ContosoUniversity/Web.config b/ContosoUniversity/Web.config index f9257e0e..b1522a23 100644 --- a/ContosoUniversity/Web.config +++ b/ContosoUniversity/Web.config @@ -17,6 +17,9 @@ + + +