diff --git a/MyCloset/Controllers/MyClosetController.cs b/MyCloset/Controllers/MyClosetController.cs index 84ed299..e7df72c 100644 --- a/MyCloset/Controllers/MyClosetController.cs +++ b/MyCloset/Controllers/MyClosetController.cs @@ -1,28 +1,43 @@ using Microsoft.AspNetCore.Mvc; using MyCloset.Data; using Microsoft.EntityFrameworkCore; +using Azure.Storage.Blobs; +using MyCloset.Models; +using MyCloset.ViewModels; namespace MyCloset.Controllers { public class MyClosetController : Controller { + // Database context private readonly Context _context; - // Database context - public MyClosetController(Context context) + // Blob Account Container and Stoage + private const string ContainerName = "mycloset"; + private readonly BlobServiceClient _blobServiceClient; + private readonly BlobContainerClient _containerClient; + + // Constructor + public MyClosetController(Context context, BlobServiceClient blobServiceClient) { _context = context; + + _blobServiceClient = blobServiceClient; + _containerClient = _blobServiceClient.GetBlobContainerClient(ContainerName); } - // Retrieve all clothing items from the database - // Use asynchronous action to retrieve data from the database + // Build the default closet view public async Task Closet() { - // Clothing Items database set - var ClothingItems = await _context.ClothingItems.ToListAsync(); - // Wait until we get data back before returning the view - return View(ClothingItems); + var Closet = await _context.ClothingItems.ToListAsync(); + + // Create the MyCloset View Model + var MyCloset = new ViewModels.MyClosetViewModel(); + MyCloset.ClothingItems = Closet; + MyCloset.BlobContainerUri = _containerClient.Uri; + + return View(MyCloset); } } } diff --git a/MyCloset/Data/Context.cs b/MyCloset/Data/Context.cs index 466b0a4..00da738 100644 --- a/MyCloset/Data/Context.cs +++ b/MyCloset/Data/Context.cs @@ -5,7 +5,6 @@ namespace MyCloset.Data { public class Context : DbContext { - public Context() { } public Context(DbContextOptions options) : base(options) { diff --git a/MyCloset/Migrations/20250227182612_CroppedImageUrl-New-Col.Designer.cs b/MyCloset/Migrations/20250227182612_CroppedImageUrl-New-Col.Designer.cs new file mode 100644 index 0000000..2628267 --- /dev/null +++ b/MyCloset/Migrations/20250227182612_CroppedImageUrl-New-Col.Designer.cs @@ -0,0 +1,61 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyCloset.Data; + +#nullable disable + +namespace MyCloset.Migrations +{ + [DbContext(typeof(Context))] + [Migration("20250227182612_CroppedImageUrl-New-Col")] + partial class CroppedImageUrlNewCol + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("MyCloset.Models.ClothingItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CroppedImageUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ImageUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StackType") + .HasColumnType("int"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("ClothingItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyCloset/Migrations/20250227182612_CroppedImageUrl-New-Col.cs b/MyCloset/Migrations/20250227182612_CroppedImageUrl-New-Col.cs new file mode 100644 index 0000000..236689a --- /dev/null +++ b/MyCloset/Migrations/20250227182612_CroppedImageUrl-New-Col.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyCloset.Migrations +{ + /// + public partial class CroppedImageUrlNewCol : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CroppedImageUrl", + table: "ClothingItems", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CroppedImageUrl", + table: "ClothingItems"); + } + } +} diff --git a/MyCloset/Migrations/20250301024623_Removing URL Columns.Designer.cs b/MyCloset/Migrations/20250301024623_Removing URL Columns.Designer.cs new file mode 100644 index 0000000..769cad0 --- /dev/null +++ b/MyCloset/Migrations/20250301024623_Removing URL Columns.Designer.cs @@ -0,0 +1,53 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MyCloset.Data; + +#nullable disable + +namespace MyCloset.Migrations +{ + [DbContext(typeof(Context))] + [Migration("20250301024623_Removing URL Columns")] + partial class RemovingURLColumns + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("MyCloset.Models.ClothingItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StackType") + .HasColumnType("int"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("ClothingItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MyCloset/Migrations/20250301024623_Removing URL Columns.cs b/MyCloset/Migrations/20250301024623_Removing URL Columns.cs new file mode 100644 index 0000000..167a31a --- /dev/null +++ b/MyCloset/Migrations/20250301024623_Removing URL Columns.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MyCloset.Migrations +{ + /// + public partial class RemovingURLColumns : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CroppedImageUrl", + table: "ClothingItems"); + + migrationBuilder.DropColumn( + name: "ImageUrl", + table: "ClothingItems"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CroppedImageUrl", + table: "ClothingItems", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "ImageUrl", + table: "ClothingItems", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + } + } +} diff --git a/MyCloset/Migrations/ContextModelSnapshot.cs b/MyCloset/Migrations/ContextModelSnapshot.cs index a06089c..03345ee 100644 --- a/MyCloset/Migrations/ContextModelSnapshot.cs +++ b/MyCloset/Migrations/ContextModelSnapshot.cs @@ -29,10 +29,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - b.Property("ImageUrl") - .IsRequired() - .HasColumnType("nvarchar(max)"); - b.Property("Name") .IsRequired() .HasColumnType("nvarchar(max)"); diff --git a/MyCloset/Models/ClothingItem.cs b/MyCloset/Models/ClothingItem.cs index b639744..3bbf5c1 100644 --- a/MyCloset/Models/ClothingItem.cs +++ b/MyCloset/Models/ClothingItem.cs @@ -6,6 +6,5 @@ public class ClothingItem public string Name { get; set; } public string Type { get; set; } public int StackType { get; set; } - public string ImageUrl { get; set; } } } diff --git a/MyCloset/Models/Doll.cs b/MyCloset/Models/Doll.cs new file mode 100644 index 0000000..c1bc2b3 --- /dev/null +++ b/MyCloset/Models/Doll.cs @@ -0,0 +1,19 @@ +namespace MyCloset.Models +{ + // There is only one instance of the doll object + public class Doll + { + // List of the clothing items currently displayed on the doll + // only one of each clothing type will be selected at each time, enforced by the selector functions + public List ClothingItems { get; set; } + + // Default Constructor + public Doll() + { + // TODO: add default clothing + ClothingItems = new List(); + } + + // Function to select / replace clothing items + } +} diff --git a/MyCloset/MyCloset.csproj b/MyCloset/MyCloset.csproj index 8c66ced..99f3266 100644 --- a/MyCloset/MyCloset.csproj +++ b/MyCloset/MyCloset.csproj @@ -7,6 +7,8 @@ + + all @@ -17,6 +19,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/MyCloset/Program.cs b/MyCloset/Program.cs index 88e9f28..3bfa18c 100644 --- a/MyCloset/Program.cs +++ b/MyCloset/Program.cs @@ -1,5 +1,9 @@ +using Azure.Identity; using Microsoft.EntityFrameworkCore; using MyCloset.Data; +using Microsoft.Extensions.Azure; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; /************************************ * Documentation: @@ -11,21 +15,30 @@ // Add services to the container. builder.Services.AddControllersWithViews(); -var connection = String.Empty; +var Connection = String.Empty; if (builder.Environment.IsDevelopment()) { // Set the Azure SQL Database connection string builder.Configuration.AddEnvironmentVariables().AddJsonFile("appsettings.Development.json"); - connection = builder.Configuration.GetConnectionString("AZURE_SQL_CONNECTIONSTRING"); + Connection = builder.Configuration.GetConnectionString("AZURE_SQL_CONNECTIONSTRING"); } else { - connection = Environment.GetEnvironmentVariable("AZURE_SQL_CONNECTIONSTRING"); + // Uses Environment variable set in the Azure App Service + Connection = Environment.GetEnvironmentVariable("AZURE_SQL_CONNECTIONSTRING"); } // Use the connection string to register the EF Core DbContext class builder.Services.AddDbContext(options => - options.UseSqlServer(connection)); + options.UseSqlServer(Connection)); + +// Connect to Azure Blob Storage Account using default azure credential +builder.Services.AddAzureClients(clientBuilder => +{ + clientBuilder.AddBlobServiceClient( + new Uri("https://mycloset.blob.core.windows.net")); + clientBuilder.UseCredential(new DefaultAzureCredential()); +}); var app = builder.Build(); @@ -33,6 +46,7 @@ if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } @@ -48,5 +62,4 @@ name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); - app.Run(); diff --git a/MyCloset/Properties/ServiceDependencies/my-closet - Web Deploy/profile.arm.json b/MyCloset/Properties/ServiceDependencies/my-closet - Web Deploy/profile.arm.json new file mode 100644 index 0000000..215c242 --- /dev/null +++ b/MyCloset/Properties/ServiceDependencies/my-closet - Web Deploy/profile.arm.json @@ -0,0 +1,113 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_dependencyType": "compute.appService.windows" + }, + "parameters": { + "resourceGroupName": { + "type": "string", + "defaultValue": "MyCloset", + "metadata": { + "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." + } + }, + "resourceGroupLocation": { + "type": "string", + "defaultValue": "centralus", + "metadata": { + "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support." + } + }, + "resourceName": { + "type": "string", + "defaultValue": "my-closet", + "metadata": { + "description": "Name of the main resource to be created by this template." + } + }, + "resourceLocation": { + "type": "string", + "defaultValue": "[parameters('resourceGroupLocation')]", + "metadata": { + "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." + } + } + }, + "variables": { + "appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", + "appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]" + }, + "resources": [ + { + "type": "Microsoft.Resources/resourceGroups", + "name": "[parameters('resourceGroupName')]", + "location": "[parameters('resourceGroupLocation')]", + "apiVersion": "2019-10-01" + }, + { + "type": "Microsoft.Resources/deployments", + "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", + "resourceGroup": "[parameters('resourceGroupName')]", + "apiVersion": "2019-10-01", + "dependsOn": [ + "[parameters('resourceGroupName')]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "location": "[parameters('resourceLocation')]", + "name": "[parameters('resourceName')]", + "type": "Microsoft.Web/sites", + "apiVersion": "2015-08-01", + "tags": { + "[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty" + }, + "dependsOn": [ + "[variables('appServicePlan_ResourceId')]" + ], + "kind": "app", + "properties": { + "name": "[parameters('resourceName')]", + "kind": "app", + "httpsOnly": true, + "reserved": false, + "serverFarmId": "[variables('appServicePlan_ResourceId')]", + "siteConfig": { + "metadata": [ + { + "name": "CURRENT_STACK", + "value": "dotnetcore" + } + ] + } + }, + "identity": { + "type": "SystemAssigned" + } + }, + { + "location": "[parameters('resourceLocation')]", + "name": "[variables('appServicePlan_name')]", + "type": "Microsoft.Web/serverFarms", + "apiVersion": "2015-08-01", + "sku": { + "name": "S1", + "tier": "Standard", + "family": "S", + "size": "S1" + }, + "properties": { + "name": "[variables('appServicePlan_name')]" + } + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/MyCloset/Properties/launchSettings.json b/MyCloset/Properties/launchSettings.json index c0e6f41..01533f9 100644 --- a/MyCloset/Properties/launchSettings.json +++ b/MyCloset/Properties/launchSettings.json @@ -1,31 +1,23 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:1915", - "sslPort": 44357 - } - }, +{ "profiles": { "http": { "commandName": "Project", - "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "http://localhost:5224", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5224" }, "https": { "commandName": "Project", - "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "https://localhost:7073;http://localhost:5224", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "AZURE_TENANT_ID": "825c59ea-5dda-4f5a-aada-104ab02f7910" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7073;http://localhost:5224" }, "IIS Express": { "commandName": "IISExpress", @@ -34,5 +26,14 @@ "ASPNETCORE_ENVIRONMENT": "Development" } } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:1915", + "sslPort": 44357 + } } -} +} \ No newline at end of file diff --git a/MyCloset/ViewModels/MyClosetViewModel.cs b/MyCloset/ViewModels/MyClosetViewModel.cs new file mode 100644 index 0000000..6ddd9db --- /dev/null +++ b/MyCloset/ViewModels/MyClosetViewModel.cs @@ -0,0 +1,13 @@ +using MyCloset.Models; +using System.Reflection.Metadata.Ecma335; + +namespace MyCloset.ViewModels +{ + public class MyClosetViewModel + { + public Uri BlobContainerUri { get; set; } + public List ClothingItems { get; set; } + public Doll Doll { get; set; } + + } +} diff --git a/MyCloset/Views/MyCloset/Closet.cshtml b/MyCloset/Views/MyCloset/Closet.cshtml index 18b5470..11f991c 100644 --- a/MyCloset/Views/MyCloset/Closet.cshtml +++ b/MyCloset/Views/MyCloset/Closet.cshtml @@ -1,4 +1,5 @@ -@model List +@using MyCloset.ViewModels +@model MyCloset.ViewModels.MyClosetViewModel @{ ViewData["Title"] = "Closet"; } @@ -14,13 +15,16 @@ - @foreach(var ClothingItem in Model) + @foreach(var ClothingItem in Model.ClothingItems) { + var ClothingImagePath = Model.BlobContainerUri + "/" + ClothingItem.Name + "-cropped.png"; + Console.WriteLine(ClothingImagePath); + @ClothingItem.Name @ClothingItem.Type @ClothingItem.StackType - @ClothingItem.ImageUrl + Sample Image }