Repository: ITU-ISE2025-GROUP8-PT/Annotations
Pull request #50: Refactor/implement handlers pt80
Author: login: nalm2812
Created: 2025-04-30T14:40:44Z | Merged: 2025-05-04T15:48:42Z

Theresia and I have done some work today on refactoring the backend. We would like some feedback. We are also making this pull request to try to make smaller pull requests
This is a list of what we have done:

- Renamed ImageGroup to ImageEndpoints
- Made the ImageService class
- Made Some functions for code duplications
- Made handlers for the endpoints inside the ImageEndpoint class so documentations can be added
- Made some methods in ImageService so ImageEndpoint is shorter. Methods were made when a specific "action" was done, such as images being deleted. Another example is to have a specific method to get a dataset that is used by the "images/datasets/{datasetid}" endpoint


We are considering if the refactor portion of our ticket is done, and that all we are missing is adding the needed documentation/comments. However, there are some specfic portions of the code that would be nice to get some feedback on:

- Is ImageService too long? A lot of the code from ImageGroup has just been pasted into ImageService
- Are the uses of records in ImageService alright?
- For the endpoint that gets a single image, the endpoint functionality has been changed a bit. This was done to reuse some of the code for "images/datasets/{datasets}". The change was that instead of returning a IResult as a file, a string is returned. This mea status code 422 will not be returned if the image cannot be found. Instead, a status code 200 will be returned (inside swagger), but the body will say "Cannot retrieve image because it doesn't exist". This can easily be changed before the ticket is closed
- In general, any feedback on the methods themselves would be appreciated :)


Some things worth nothing when testing the branch:

- Other than the change listed above with getting a single image, functionality should be the same
- Comments have not been changed. The current comments are the ones that existed before. We pasted the comments into the methods. However, we have not checked if the comments still match up. Please dont let this confuse you. If the code is hard to understand, please write to either me or Theresia
- The documentation for AddImagesToDatasets is a large comment that we put into a documentation for less clutter. This piece of documentation is not done
- We focused on keeping the code (almost) exact to how it was previously to focus on the ticket and also to prevent issues. There are inconsistencies as to how images are retrieved for use. We could technically be more efficient and prevent code duplication if we were more consistent. This will be delved into right below.


We also had some ideas for future improvements of the backend when we finish the ticket/if we have time:
- As listed above, be more consistent with retrieving images. To be more exact, have a method that we call to retrieve images. This should ideally be the only way images are retrieved (if we change this method, all places where images are retrieved will be changed). Currently, image retrieval for "images/filter/{category}" does not use the "GetImage" method
- (As listed above) Split ImageService up. To be more exact, split it up so that there is one class for methods that is used multiple places and another class where we have methods that are only used once (i.e. the ones that were only made to simplify code/documentation)
- Make all endpoints return IResult so the correct error code will be thrown
- Hardcode less things (I know this is not that specific)


Diff:
diff --git a/Annotations.API/Groups/ImageEndpoints.cs b/Annotations.API/Groups/ImageEndpoints.cs
new file mode 100644
index 00000000..3b962286
--- /dev/null
+++ b/Annotations.API/Groups/ImageEndpoints.cs
@@ -0,0 +1,159 @@
+using Annotations.API.Services;
+using Annotations.Core.Models;
+using Microsoft.AspNetCore.Mvc;
+using Azure.Storage.Blobs;
+using Microsoft.Extensions.Azure;
+
+
+namespace Annotations.API.Groups;
+
+public static class ImageEndpoints
+{
+    private static int _counter = 0;//change this - it gets reset every time the program resets
+ 
+    
+    
+    public static void MapEndpoints(RouteGroupBuilder pathBuilder)
+    {
+        pathBuilder.RequireAuthorization().DisableAntiforgery();
+        
+        pathBuilder.MapPost("/upload", UploadImageHandler);
+        
+        pathBuilder.MapGet("/{imageId}", RetrieveImageHandler);
+
+        pathBuilder.MapDelete("/{imageId}", DeleteImageHandler);
+
+        pathBuilder.MapGet("/filter/{category}", FilterImagesHandler);
+
+        pathBuilder.MapGet("/datasets", RetrieveDatasetHandler);
+
+        pathBuilder.MapGet("/datasets/{dataset}", RetrieveImagesFromDatasetHandler);
+        
+        pathBuilder.MapGet("/exception",
+            () =>
+            {
+                throw new InvalidOperationException(
+                    "Exception has been raised in the API. Look for further details in the log.");
+            });
+
+        pathBuilder.MapGet("/APITest", () =>
+        {
+            return new string[] { "1", "2", "3", "Dette er en prøve" };
+        });
+    }
+
+    
+    
+    //This is the upload endpoint where the image first gets validated,
+    //and then gets uploaded into your local Azurite BlobStorage as a JSON file
+    public static async Task<IResult> UploadImageHandler(IFormFile image, string category, AnnotationsDbContext context, 
+         [FromServices] IImageService _imageService)
+    {
+        ValidationResponse response = _imageService.ValidateImage(image);
+        if (!response.Success)
+        {
+            _imageService.UploadImageError(response);
+            return Results.StatusCode(422);
+            //TODO: how should the end user see this?
+        } 
+        await _imageService.UploadingImage(image,_counter, category);
+        _counter++;
+        return Results.StatusCode(200);
+    }
+
+    
+    
+    public static async Task<IResult> DeleteImageHandler(string imageId, [FromServices] IAzureClientFactory<BlobServiceClient> clientFactory, 
+        [FromServices] IImageService _imageService)
+    {
+
+        bool deleting = await _imageService.DeleteImage(imageId);
+        if (deleting)
+        {
+            return Results.StatusCode(204);
+        }
+            
+        Console.WriteLine("Cannot delete image because it doesn't exist");
+        return Results.StatusCode(404);
+    }
+    
+    
+    
+    public static async Task<string> RetrieveImageHandler([FromRoute] string imageId, [FromServices] IAzureClientFactory<BlobServiceClient> clientFactory, 
+        [FromServices] IImageService _imageService)
+    {
+        //insert password restrictions here 🐿️
+        
+        var getImageResult = await _imageService.GetImage(imageId);
+
+        if (getImageResult.Success)
+        {
+            return getImageResult.image; 
+        }
+        
+        //will only reach here if it cannot find an image with the id of the correct file type,
+        //or else the request will terminate inside the for-loop
+        Console.WriteLine("Cannot retrieve image because it doesn't exist");
+        return "Cannot retrieve image because it doesn't exist";
+    }
+    
+    
+    
+    public static async Task<string[]> FilterImagesHandler(string category, [FromServices] IAzureClientFactory<BlobServiceClient> clientFactory, 
+        [FromServices] IImageService _imageService)
+    {
+
+        HashSet<string> collection = await _imageService.Filter(category);
+
+        if (collection.Count() == 0)
+        {
+            Console.WriteLine("No pictures have this category");
+            //TODO return proper error code
+            //return Results.StatusCode(404);
+        }
+        
+        return collection.ToArray();//returns array of the JSON files as strings
+
+    }
+    
+    
+    
+    public static async Task<DatasetModel[]> RetrieveDatasetHandler(AnnotationsDbContext context, [FromServices] IImageService _imageService)
+    {
+        return await _imageService.GetAllDatasets();
+
+    }
+    
+    
+    
+    public static async Task<string[]> RetrieveImagesFromDatasetHandler(string dataset, AnnotationsDbContext context, 
+        [FromServices] IAzureClientFactory<BlobServiceClient> clientFactory, [FromServices] IImageService _imageService)
+    {
+         //TODO: almost identical code as "/filter/{category}" - remove the code duplication
+
+            var datasetModel = await _imageService.GetDataset(dataset);
+            
+            HashSet<string> collection = new HashSet<string>();
+
+            //foreach (DatasetModel datasetModel in datasets)//guaranteed to only be one dataset in "datasets", so this is not linear time. 
+            //there is a better way of doing this
+            
+                foreach (int ids in datasetModel.ImageIds)
+                {
+                    Console.WriteLine(ids);
+                    var getImageResult = await _imageService.GetImage(ids.ToString());
+                    if (getImageResult.Success)
+                    {
+                        collection.Add(getImageResult.image);
+                    }
+                    else
+                    {
+                        Console.WriteLine("Cannot retrieve image because it doesn't exist");
+                        break;
+                    }
+                }
+            
+            return collection.ToArray();//returns array of all images as JSON strings
+            
+    }
+}
\ No newline at end of file
diff --git a/Annotations.API/Groups/ImagesGroup.cs b/Annotations.API/Groups/ImagesGroup.cs
deleted file mode 100644
index c56cb2d9..00000000
--- a/Annotations.API/Groups/ImagesGroup.cs
+++ /dev/null
@@ -1,314 +0,0 @@
-using Annotations.Core.Models;
-using Microsoft.AspNetCore.Http.Extensions;
-using Annotations.Core.Entities;
-using Azure;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.EntityFrameworkCore;
-using Azure.Storage.Blobs;
-using Azure.Storage.Blobs.Models;
-using Microsoft.AspNetCore.Http.HttpResults;
-using Microsoft.Extensions.Azure;
-using SQLitePCL;
-
-namespace Annotations.API.Groups;
-
-public static class ImagesGroup
-{
-    private static int _counter = 0;//change this - it gets reset every time the program resets
-    private record ValidationResponse(bool Success, string Message);
-    
-    private static string[] _arrayOfFileExtension = {"png", "jpg", "jpeg"};
-
-
-
-    /// <summary>
-    /// Helping method that validates an image based on type, size, and also checks if it even contains anything
-    /// Images can be JPEG, PNG and JPG, and everything else gets rejected
-    /// </summary>
-    /// <param name="file"></param>
-    /// <returns>ValidationResponse - a record that contains a boolean of whether the image is validated, and a message that describes either what went wrong, or that it was successful</returns>
-    private static ValidationResponse ValidateImage(IFormFile file)//temporary location
-    {
-        
-        if (file.Length > 50 * 1024 * 1024) {//50MB
-            return new ValidationResponse(false, "File is too large.");
-        }
-        if (file.Length == 0) 
-        { 
-            return new ValidationResponse(false, "File doesn't exist.");
-        }
-        foreach (string fileExtension in _arrayOfFileExtension)
-        {
-            if (file.ContentType.Contains(fileExtension))
-            {
-                //if the image is the correct type, it will be uploaded, since it fulfills the other criterias
-                //upload image to db
-                return new ValidationResponse(true ,"Uploaded image successfully.");
-            }
-        }
-        //if the code reaches this point, then the file type is none of the permitted file types, so an error is thrown
-        return new ValidationResponse(false, "File is not a valid image.");
-        
-    }
-    public static void MapEndpoints(RouteGroupBuilder pathBuilder)
-    {
-        pathBuilder.RequireAuthorization();
-        
-        //This is the upload endpoint where the image first gets validated, and then gets uploaded into your local Azurite BlobStorage as a JSON file
-        pathBuilder.MapPost("/upload",
-            async (IFormFile image, string category, AnnotationsDbContext context, [FromServices] IAzureClientFactory<BlobServiceClient> clientFactory) =>
-            {//TODO: more paramters for the other fields in images?
-
-                ValidationResponse response = ValidateImage(image);
-                if (!response.Success)
-                {
-                    Console.WriteLine("rejecting image");
-                    Console.WriteLine(response.Message);
-                    return Results.StatusCode(422);
-                    //TODO: how should the end user see this?
-                }
-                else
-                {
-                    var blobServiceClient = clientFactory.CreateClient("Default");
-                    BlobContainerClient containerClient = blobServiceClient.GetBlobContainerClient("images");
-
-                    //fileExtension will always be a proper fileExtension because of the ValidateImage method
-
-
-                    using (MemoryStream ms = new MemoryStream())
-                    {
-                        await image.OpenReadStream().CopyToAsync(ms);
-                        var thisImage = new ImageModel()//TODO: dont do this - the title, description and datasetsId are hardcoded
-                        {
-                            Id = _counter,
-                            Title = "idk",
-                            Description = "description",
-                            ImageData = ms.ToArray(),
-                            Category = category,
-                            DatasetsIds = new List<int>(){1, 2},
-                        };
-                        /*
-                        Below functionality of cross-adding the image to the assigned datasets
-                        is overriden by the hard-code creation of datasets in Api, Program.cs
-                        The idea for future reference is that when uploading an image, the user
-                        inputs the names (ids) of datasets, and we can use below code (without
-                        hard-coded ids) to cross-add the images to the relevant datasets.
-                        */
-                        var neededDataset = context.Datasets.Select(Dataset => Dataset).
-                        Where(Dataset => Dataset.Id == 1 || Dataset.Id == 2);//TODO dont do this - this is hardcoded for testing
-                        
-                        foreach (Dataset dataset in neededDataset)
-                            
-                        {
-                            dataset.ImageIds.Add(thisImage.Id);//adds images to the datasets
-                            await context.SaveChangesAsync();
-                        }
-
-                        string jsonString = System.Text.Json.JsonSerializer.Serialize(thisImage);//objects becomes JSON string
-                        var byteContent = System.Text.Encoding.UTF8.GetBytes(jsonString);//JSON string becomes byte array
-
-                        BlobClient thisImageBlobClient = containerClient.GetBlobClient($"{_counter}.json");
-                        _counter++;
-
-                        var blobHeaders = new BlobHttpHeaders
-                        {
-                            ContentType = "application/json"
-                        };
-
-                        // Trigger the upload function to push the data to blob
-                        await thisImageBlobClient.UploadAsync(new MemoryStream(byteContent), blobHeaders);//uploaded as byte array
-                        //Should it be uploaded as a string instead? So far all endpoints retrieve the JSON files as strings?
-                    }
-                   
-                    
-
-                    return Results.StatusCode(200);
-                }
-
-
-            }).DisableAntiforgery();
-        //ellers ved billedfil brug da
-        pathBuilder.MapGet("/{imageId}",
-            async ([FromRoute] string imageId, [FromServices] IAzureClientFactory<BlobServiceClient> clientFactory) =>
-            {
-                //insert password restrictions here 🐿️
-                var cts = new CancellationTokenSource(5000);
-
-                var blobServiceClient = clientFactory.CreateClient("Default");
-                BlobContainerClient containerClient = blobServiceClient.GetBlobContainerClient("images");//enters images
-
-               
-                BlobClient blobClient = containerClient.GetBlobClient(imageId + ".json");
-                if (!blobClient.Exists(cts.Token).ToString()
-                        .Contains("404")) //checks if the blobClient is empty/couldn't find the image of that format
-                {
-                    using var memoryStream = new MemoryStream();
-                    await blobClient.DownloadToAsync(memoryStream);
-                    return Results.File(memoryStream.ToArray(),
-                        "application/json"); //because it is a return statement the for-loop will not continue after finding the image
-                }
-
-                
-
-                //will only reach here if it cannot find an image with the id of the correct file type, or else the request will terminate inside the for-loop
-                Console.WriteLine("Cannot retrieve image because it doesn't exist");
-                return Results.StatusCode(404);
-
-            }).DisableAntiforgery();
-        
-        pathBuilder.MapDelete("/{imageId}", async (string imageId, [FromServices] IAzureClientFactory<BlobServiceClient> clientFactory) =>
-        {
-            var cts = new CancellationTokenSource(5000);
-
-            var blobServiceClient = clientFactory.CreateClient("Default");
-            BlobContainerClient containerClient = blobServiceClient.GetBlobContainerClient("images");
-
-            BlobClient blobClient = containerClient.GetBlobClient(imageId + ".json");
-            if (!blobClient.Exists(cts.Token).ToString().Contains("404"))
-            {
-                /*A snapshot is a read-only version of a blob that's taken at a point in time. 
-                As of right now, we do not make snapshots of blobs, but it is still possible to manually create.*/
-                await blobClient.DeleteAsync(snapshotsOption: DeleteSnapshotsOption.IncludeSnapshots);
-                Console.WriteLine("image deleted successfully");
-                return Results.StatusCode(204);
-            }
-
-            
-            Console.WriteLine("Cannot delete image because it doesn't exist");
-            return Results.StatusCode(404);
-
-        }).DisableAntiforgery();
-
-        pathBuilder.MapGet("/filter/{category}",//returns all images of a certain category
-            async (string category, [FromServices] IAzureClientFactory<BlobServiceClient> clientFactory) =>
-            {
-                var cts = new CancellationTokenSource(5000);
-
-                var blobServiceClient = clientFactory.CreateClient("Default");
-                BlobContainerClient containerClient = blobServiceClient.GetBlobContainerClient("images");
-                var listOfFiles = containerClient.GetBlobsAsync().AsPages();//all data inside of blobContainer
-              
-                HashSet<string> collection = new HashSet<string>();
-                await foreach (Page<BlobItem> blobPage in listOfFiles)
-                {
-                    foreach (BlobItem blobItem in blobPage.Values)//every image found
-                    {
-                        //goes through all images and check for the category
-                        var BlobClient = containerClient.GetBlobClient(blobItem.Name);
-                        using var memoryStream = new MemoryStream();
-                        await BlobClient.DownloadToAsync(memoryStream);
-                        var jsonString = System.Text.Encoding.UTF8.GetString(memoryStream.ToArray());//JSON file as string
-                        var imageObject = System.Text.Json.JsonSerializer.Deserialize<ImageModel>(jsonString);//deserialize so it becomes imageModel
-                        if (imageObject == null)
-                        {
-                            throw new Exception("image object is null");}
-                        if (imageObject.Category == category)
-                        {
-                            collection.Add(jsonString);
-                        }
-                    }
-
-                }
-
-                if (collection.Count() == 0)
-                {
-                    Console.WriteLine("No pictures have this category");
-                    //TODO return proper error code
-                    //return Results.StatusCode(404);
-                }
-                return collection.ToArray();//returns array of the JSON files as strings
-
-
-               
-
-
-
-            }).DisableAntiforgery();
-        
-        pathBuilder.MapGet("/datasets", async (AnnotationsDbContext context) =>//returns all existing datasets
-            {
-                var cts = new CancellationTokenSource(5000);
-
-                //Datasets from DBContext are transformed to DatasetModels
-                var datasets = await context.Datasets
-                    .Select(u => new DatasetModel()
-                    {
-                        Id = u.Id,
-                        ImageIds = u.ImageIds,
-                        Category = u.Category,
-                        AnnotatedBy= u.AnnotatedBy,
-                        ReviewedBy = u.ReviewedBy
-                    })
-                    .ToListAsync();
-                
-                //DatasetModel list is converted to Array for sending to Blazor front-end
-                DatasetModel[] datasetsarray = datasets.ToArray();
-
-                return datasetsarray;
-
-            }).DisableAntiforgery();
-        
-        pathBuilder.MapGet("/datasets/{dataset}", async (string dataset, AnnotationsDbContext context, [FromServices] IAzureClientFactory<BlobServiceClient> clientFactory) =>
-        {//access images of specific dataset
-            //TODO: almost identical code as "/filter/{category}" - remove the code duplication
-            var cts = new CancellationTokenSource(5000);
-
-            var datasets = context.Datasets
-                .Select(u => new DatasetModel()
-                {
-                    Id = u.Id,
-                    ImageIds = u.ImageIds,
-                    Category = u.Category,
-                    AnnotatedBy= u.AnnotatedBy,
-                    ReviewedBy = u.ReviewedBy
-                }).Where(DatasetModel => DatasetModel.Id == Int32.Parse(dataset));
-                //there is only one dataset with a certain Id, so no point of taking more
-            var datasetModel = await datasets.FirstOrDefaultAsync();
-
-            if (datasetModel == null)
-            {
-                throw new Exception("No dataset found");
-            };
-            
-            HashSet<string> collection = new HashSet<string>();
-            var blobServiceClient = clientFactory.CreateClient("Default");
-            BlobContainerClient containerClient = blobServiceClient.GetBlobContainerClient("images");
-            //foreach (DatasetModel datasetModel in datasets)//guaranteed to only be one dataset in "datasets", so this is not linear time. 
-            //{//there is a better way of doing this
-            
-                foreach (int ids in datasetModel.ImageIds)
-                {
-                    Console.WriteLine(ids);
-                    BlobClient blobClient = containerClient.GetBlobClient(ids + ".json");
-                    if (!blobClient.Exists(cts.Token).ToString()
-                            .Contains("404")) //checks if the blobClient is empty/couldn't find the image of that format
-                    {
-                        using var memoryStream = new MemoryStream();
-                        await blobClient.DownloadToAsync(memoryStream);
-                        var jsonString = System.Text.Encoding.UTF8.GetString(memoryStream.ToArray());
-                        collection.Add(jsonString);
-                    }
-                    else
-                    {
-                        Console.WriteLine("Cannot retrieve image because it doesn't exist");
-                        break;
-                    }
-                }
-            //}
-            return collection.ToArray();//returns array of all images as JSON strings
-            
-
-        }).DisableAntiforgery();        
-        pathBuilder.MapGet("/exception",
-            () =>
-            {
-                throw new InvalidOperationException(
-                    "Exception has been raised in the API. Look for further details in the log.");
-            });
-
-        pathBuilder.MapGet("/APITest", () =>
-        {
-            return new string[] { "1", "2", "3", "Dette er en prøve" };
-        });
-    }
-}
\ No newline at end of file
diff --git a/Annotations.API/Groups/UsersGroup.cs b/Annotations.API/Groups/UserEndpoints.cs
similarity index 98%
rename from Annotations.API/Groups/UsersGroup.cs
rename to Annotations.API/Groups/UserEndpoints.cs
index 56a43c7a..3447bf35 100644
--- a/Annotations.API/Groups/UsersGroup.cs
+++ b/Annotations.API/Groups/UserEndpoints.cs
@@ -4,7 +4,7 @@
 
 namespace Annotations.API.Groups;
 
-public static class UsersGroup
+public static class UserEndpoints
 {
     public static void MapEndpoints(RouteGroupBuilder pathBuilder)
     {
diff --git a/Annotations.API/Program.cs b/Annotations.API/Program.cs
index 88606f73..1f4b0347 100644
--- a/Annotations.API/Program.cs
+++ b/Annotations.API/Program.cs
@@ -1,6 +1,7 @@
 using System.Net.Http.Headers;
 using Annotations.API;
 using Annotations.API.Groups;
+using Annotations.API.Services;
 using Annotations.Core.Entities;
 using Microsoft.AspNetCore.Authentication.JwtBearer;
 using Microsoft.AspNetCore.Authorization;
@@ -79,11 +80,17 @@
     clientBuilder.AddQueueServiceClient(builder.Configuration["AzureStorageConnection"]!);
     clientBuilder.AddTableServiceClient(builder.Configuration["AzureStorageConnection"]!);
 });
-var app = builder.Build();
 
+builder.Services.AddScoped<IImageService, ImageService>();
+var app = builder.Build();
+using (var scope = app.Services.CreateScope())
+{
+    using var context = scope.ServiceProvider.GetRequiredService<AnnotationsDbContext>();
+    context.Database.Migrate();
+}
 
-UsersGroup.MapEndpoints(app.MapGroup("/users").RequireAuthorization());
-ImagesGroup.MapEndpoints(app.MapGroup("/images").RequireAuthorization());//TODO    DONT DO THIS. REMOVE 
+UserEndpoints.MapEndpoints(app.MapGroup("/users").RequireAuthorization());
+ImageEndpoints.MapEndpoints(app.MapGroup("/images").RequireAuthorization());//TODO    DONT DO THIS. REMOVE 
 
 app.MapGet("/error", () => "Dette er en 400-599 eller værre");
 
@@ -145,7 +152,7 @@ void InitializeTempDatabase()
     //this is only for testing/showcasing
     
     /*
-    Due to the hard-coding of database elements below, we override code from ImagesGroup
+    Due to the hard-coding of database elements below, we override code from ImageEndpoints
     image-upload-functionality, that adds an image to the dataset.
     */  
 context.Add(new Dataset//different images compared to the other 5 datasets
diff --git a/Annotations.API/Services/ImageService.cs b/Annotations.API/Services/ImageService.cs
new file mode 100644
index 00000000..b4a764c6
--- /dev/null
+++ b/Annotations.API/Services/ImageService.cs
@@ -0,0 +1,291 @@
+using Annotations.Core.Entities;
+using Annotations.Core.Models;
+using Azure;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Azure;
+
+namespace Annotations.API.Services;
+
+public record ValidationResponse(bool Success, string Message);
+public record ImageData(ImageModel Image, string JSONString);
+
+public record GetImageResult(bool Success, string image);
+
+
+public interface IImageService
+{
+    ValidationResponse ValidateImage(IFormFile file);
+    Task UploadingImage(IFormFile image, int counter, string category);
+    void UploadImageError(ValidationResponse response);
+    Task<HashSet<string>> Filter(string category);
+    Task<DatasetModel> GetDataset(string dataset);
+    Task<GetImageResult> GetImage(string imageId);
+    Task<DatasetModel[]> GetAllDatasets();
+    Task<bool> DeleteImage(string imageId);
+}
+
+
+
+public class ImageService: IImageService
+{
+    private static string[] _arrayOfFileExtension = {"png", "jpg", "jpeg"};
+    private readonly IAzureClientFactory<BlobServiceClient> _clientFactory;
+    private readonly AnnotationsDbContext _DbContext;
+    private readonly BlobContainerClient _containerClient;
+
+    
+    
+    public ImageService(IAzureClientFactory<BlobServiceClient> clientFactory , AnnotationsDbContext context)
+    {
+        _clientFactory = clientFactory;
+        _DbContext = context;
+        var blobServiceClient = _clientFactory.CreateClient("Default");
+        _containerClient = blobServiceClient.GetBlobContainerClient("images");
+    }
+    
+    
+    
+    /// <summary>
+    /// Helping method that validates an image based on type, size, and also checks if it even contains anything
+    /// Images can be JPEG, PNG and JPG, and everything else gets rejected
+    /// </summary>
+    /// <param name="file">The file to be validated</param>
+    /// <returns>ValidationResponse - a record that contains a boolean of whether the image is validated, and a message that describes either what went wrong, or that it was successful</returns>
+    public ValidationResponse ValidateImage(IFormFile file)
+    {
+        
+        if (file.Length > 50 * 1024 * 1024) {//50MB
+            return new ValidationResponse(false, "File is too large.");
+        }
+        if (file.Length == 0) 
+        { 
+            return new ValidationResponse(false, "File doesn't exist.");
+        }
+        foreach (string fileExtension in _arrayOfFileExtension)
+        {
+            if (file.ContentType.Contains(fileExtension))
+            {
+                //if the image is the correct type, it will be uploaded, since it fulfills the other criterias
+                //upload image to db
+                return new ValidationResponse(true ,"Uploaded image successfully.");
+            }
+        }
+        //if the code reaches this point, then the file type is none of the permitted file types, so an error is thrown
+        return new ValidationResponse(false, "File is not a valid image.");
+        
+    }
+
+    
+    
+    /// <summary>
+    /// Downloads a BlobClient to a memory stream.
+    /// Afterward it gets converted to a JSON file in the form of a string
+    /// </summary>
+    /// <param name="blobClient">BlobClient</param>
+    /// <returns>JSON file in the form of a string</returns>
+    public async Task<string> convertToJSONString(BlobClient blobClient)
+    {
+        using var memoryStream = new MemoryStream();
+        await blobClient.DownloadToAsync(memoryStream);
+        return System.Text.Encoding.UTF8.GetString(memoryStream.ToArray());//JSON file as string
+    }
+    
+    
+    
+    /// <summary>
+    ///Below functionality of cross-adding the image to the assigned datasets
+    ///is overriden by the hard-code creation of datasets in Api, Program.cs
+    ///
+    /// The idea for future reference is that when uploading an image, the user
+    ///inputs the names (ids) of datasets, and we can use below code (without
+    ///hard-coded ids) to cross-add the images to the relevant datasets.
+    /// </summary>
+    /// <param name="thisImage"></param>
+    private async Task AddImagesToDatasets(ImageModel thisImage)
+    {
+        var neededDataset = _DbContext.Datasets.Select(Dataset => Dataset)
+            .Where(Dataset =>
+                Dataset.Id == 1 || Dataset.Id == 2); //TODO dont do this - this is hardcoded for testing
+
+        foreach (Dataset dataset in neededDataset)
+
+        {
+            dataset.ImageIds.Add(thisImage.Id); //adds images to the datasets
+            await _DbContext.SaveChangesAsync();
+        }
+    }
+    
+    
+
+    private async Task UploadAsJSON(int counter, string jsonString)
+    {
+        
+        var byteContent = System.Text.Encoding.UTF8.GetBytes(jsonString); //JSON string becomes byte array
+        BlobClient thisImageBlobClient = _containerClient.GetBlobClient($"{counter}.json");
+
+        var blobHeaders = new BlobHttpHeaders
+        {
+            ContentType = "application/json"
+        };
+
+        // Trigger the upload function to push the data to blob
+        await thisImageBlobClient.UploadAsync(new MemoryStream(byteContent),
+            blobHeaders); //uploaded as byte array
+        //Should it be uploaded as a string instead? So far all endpoints retrieve the JSON files as strings?
+    }
+    
+    
+    
+    public async Task UploadingImage(IFormFile image, int counter, string category)
+    {
+            //fileExtension will always be a proper fileExtension because of the ValidateImage method
+            using (MemoryStream ms = new MemoryStream())
+            {
+                await image.OpenReadStream().CopyToAsync(ms);
+                var thisImage =
+                    new ImageModel() //TODO: dont do this - the title, description and datasetsId are hardcoded
+                    {
+                        Id = counter,
+                        Title = "idk",
+                        Description = "description",
+                        ImageData = ms.ToArray(),
+                        Category = category,
+                        DatasetsIds = new List<int>() { 1, 2 },
+                    };
+               
+                await AddImagesToDatasets(thisImage);
+                string jsonString = System.Text.Json.JsonSerializer.Serialize(thisImage); //objects becomes JSON string
+                await UploadAsJSON(counter, jsonString);
+
+            }
+    }
+
+    
+    
+    public void UploadImageError(ValidationResponse response)
+    {
+        Console.WriteLine("rejecting image");
+        Console.WriteLine(response.Message);
+    }
+
+    
+    
+    public async Task<ImageData> GetImageForFiltering(BlobContainerClient containerClient, BlobItem blobItem)
+    {
+        var blobClient = containerClient.GetBlobClient(blobItem.Name);
+        string jsonString = await convertToJSONString(blobClient);
+        var imageObject = System.Text.Json.JsonSerializer.Deserialize<ImageModel>(jsonString);//deserialize so it becomes imageModel
+        if (imageObject == null)
+        {
+            throw new Exception("img object is null");
+        }
+
+        return new(imageObject, jsonString);
+    }
+
+    
+    
+    public async Task<HashSet<string>> Filter(string category)
+    {
+        var listOfFiles = _containerClient.GetBlobsAsync().AsPages();//all data inside of blobContainer
+              
+        HashSet<string> collection = new HashSet<string>();
+        await foreach (Page<BlobItem> blobPage in listOfFiles)
+        {
+            foreach (BlobItem blobItem in blobPage.Values)//every image found
+            {
+                //goes through all images and check for the category
+                var imageData = await GetImageForFiltering(_containerClient, blobItem);
+                if (imageData.Image.Category == category)
+                {
+                    collection.Add(imageData.JSONString);
+                }
+            }
+
+        }
+        return collection;
+
+    }
+    
+    
+
+    public async Task<DatasetModel> GetDataset(string dataset)
+    {
+        var datasets = _DbContext.Datasets
+            .Select(u => new DatasetModel()
+            {
+                Id = u.Id,
+                ImageIds = u.ImageIds,
+                Category = u.Category,
+                AnnotatedBy= u.AnnotatedBy,
+                ReviewedBy = u.ReviewedBy
+            }).Where(DatasetModel => DatasetModel.Id == Int32.Parse(dataset));
+        //there is only one dataset with a certain Id, so no point of taking more
+        var datasetModel = await datasets.FirstOrDefaultAsync();
+
+        if (datasetModel == null)
+        {
+            throw new Exception("No dataset found");
+        }
+
+        return datasetModel;
+    }
+
+    
+    
+    public async Task<GetImageResult> GetImage(string imageId)
+    {
+        var cts = new CancellationTokenSource(5000);
+        //enters images
+        BlobClient blobClient = _containerClient.GetBlobClient(imageId + ".json");
+        if (!blobClient.Exists(cts.Token).ToString()
+                .Contains("404")) //checks if the blobClient is empty/couldn't find the image of that format
+        {
+            
+            return new GetImageResult(true, await convertToJSONString(blobClient));
+        }
+        return new GetImageResult(false, "");
+    }
+    
+    
+
+    public async Task<DatasetModel[]> GetAllDatasets()
+    {
+        //Datasets from DBContext are transformed to DatasetModels
+        var datasets = await _DbContext.Datasets
+            .Select(u => new DatasetModel()
+            {
+                Id = u.Id,
+                ImageIds = u.ImageIds,
+                Category = u.Category,
+                AnnotatedBy= u.AnnotatedBy,
+                ReviewedBy = u.ReviewedBy
+            })
+            .ToListAsync();
+                
+        //DatasetModel list is converted to Array for sending to Blazor front-end
+        return datasets.ToArray();
+    }
+    
+    
+
+    public async Task<bool> DeleteImage(string imageId)
+    {
+        var cts = new CancellationTokenSource(5000);
+        
+        BlobClient blobClient = _containerClient.GetBlobClient(imageId + ".json");
+        if (!blobClient.Exists(cts.Token).ToString().Contains("404"))
+        {
+            /*A snapshot is a read-only version of a blob that's taken at a point in time.
+            As of right now, we do not make snapshots of blobs, but it is still possible to manually create.*/
+            await blobClient.DeleteAsync(snapshotsOption: DeleteSnapshotsOption.IncludeSnapshots);
+            Console.WriteLine("image deleted successfully");
+            return true;
+        }
+
+        return false;
+    }
+    
+}
\ No newline at end of file
diff --git a/Annotations.Blazor.Tests/E2E/RBACwithAnnotationsPlaywrightUserTests.cs b/Annotations.Blazor.Tests/E2E/RBACwithAnnotationsPlaywrightUserTests.cs
index 599a3d2b..17804319 100644
--- a/Annotations.Blazor.Tests/E2E/RBACwithAnnotationsPlaywrightUserTests.cs
+++ b/Annotations.Blazor.Tests/E2E/RBACwithAnnotationsPlaywrightUserTests.cs
@@ -25,5 +25,9 @@ public async Task AccessNotDeniedAnnotateOnImage()
         await Page.GetByRole(AriaRole.Paragraph).Filter(new() { HasText = "Type" }).Locator("div").Nth(1).ClickAsync();
         await Page.GetByRole(AriaRole.Option, new() { Name = "Artery" }).ClickAsync();
         await Page.GetByRole(AriaRole.Button, new() { Name = "Save" }).ClickAsync();
+        
+        //TODO Fix test when annotate on image page has been updated
+        //await Expect(Page.GetByRole(AriaRole.Paragraph)).ToContainTextAsync("Artery 2mm");
+        await Expect(Page.GetByText("Blood Vessel Identification")).ToBeVisibleAsync();
     }
 }


Reviews:
user: login: RonoITU
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T18:28:31Z
user: login: RonoITU
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T08:30:15Z
user: login: RonoITU
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T08:30:25Z
user: login: RonoITU
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T17:31:23Z
user: login: RonoITU
body: I am now satisfied with this part of the refactor. The next task will be to document the code. 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-03T15:28:02Z
user: login: tomat-suppe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-04T14:44:58Z
user: login: tomat-suppe
body: Regarding: "For the endpoint that gets a single image, the endpoint functionality has been changed a bit. This was done to reuse some of the code for "images/datasets/{datasets}". The change was that instead of returning a IResult as a file, a string is returned."

I am trying to read through RetrieveImageHandler and GetImage. I am having difficulties completely understanding them. Do we only send json data over retrieval now? How does this work with the front-end, which assumes an alt text is sent over the Get request? Or have I completely misunderstood these classes? What exactly does the front end retrieve on a get request for an image?
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-04T14:53:44Z
user: login: nalm2812
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-04T14:58:45Z
user: login: tomat-suppe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-04T15:46:16Z
user: login: tomat-suppe
body: With my comments, I am happy to merge this :) I have left a comment of a wish I have for when you guys get to the Documentation SBI. Other than that, nice to see more readable code-structure!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-04T15:48:28Z

Review comments:
diff_hunk: @@ -0,0 +1,164 @@
+using Annotations.API.Services;
+using Annotations.Core.Models;
+using Microsoft.AspNetCore.Http.Extensions;
+using Annotations.Core.Entities;
+using Azure;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.Extensions.Azure;
+using SQLitePCL;
+
+
+namespace Annotations.API.Groups;
+
+public static class ImageEndpoints
+{
+    private static int _counter = 0;//change this - it gets reset every time the program resets
+    
+ 
+    public static void MapEndpoints(RouteGroupBuilder pathBuilder)
+    {
+        pathBuilder.RequireAuthorization().DisableAntiforgery();
+        
+        pathBuilder.MapPost("/upload", UploadImageHandler);
+        
+        pathBuilder.MapGet("/{imageId}", RetrieveImageHandler);
+
+        pathBuilder.MapDelete("/{imageId}", DeleteImageHandler);
+
+        pathBuilder.MapGet("/filter/{category}", FilterImagesHandler);
+
+        pathBuilder.MapGet("/datasets", RetrieveDatasetHandler);
+
+        pathBuilder.MapGet("/datasets/{dataset}", RetrieveImagesFromDatasetHandler);
+        
+        pathBuilder.MapGet("/exception",
+            () =>
+            {
+                throw new InvalidOperationException(
+                    "Exception has been raised in the API. Look for further details in the log.");
+            });
+
+        pathBuilder.MapGet("/APITest", () =>
+        {
+            return new string[] { "1", "2", "3", "Dette er en prøve" };
+        });
+    }
+
+    
+    //This is the upload endpoint where the image first gets validated,
+    //and then gets uploaded into your local Azurite BlobStorage as a JSON file
+
+    public static async Task<IResult> UploadImageHandler(IFormFile image, string category, AnnotationsDbContext context, 
+         [FromServices] IImageService _imageService)
+    {
+        ValidationResponse response = _imageService.ValidateImage(image);
+        if (!response.Success)
+        {
+            _imageService.UploadImageError(response);
+            return Results.StatusCode(422);
+            //TODO: how should the end user see this?
+        } 
+        await _imageService.UploadingImage(image,_counter, category);
+        _counter++;
+        return Results.StatusCode(200);
+    }
+
+    public static async Task<IResult> DeleteImageHandler(string imageId, [FromServices] IAzureClientFactory<BlobServiceClient> clientFactory, 
+        [FromServices] IImageService _imageService)
+    {
+        var cts = new CancellationTokenSource(5000);
path: Annotations.API/Groups/ImageEndpoints.cs
user: login: RonoITU
body: Could you make it so this token is created within the service, as it calls the blob storage?
created_at: 2025-04-30T18:16:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,164 @@
+using Annotations.API.Services;
+using Annotations.Core.Models;
+using Microsoft.AspNetCore.Http.Extensions;
+using Annotations.Core.Entities;
+using Azure;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.Extensions.Azure;
+using SQLitePCL;
+
+
+namespace Annotations.API.Groups;
+
+public static class ImageEndpoints
+{
+    private static int _counter = 0;//change this - it gets reset every time the program resets
+    
+ 
+    public static void MapEndpoints(RouteGroupBuilder pathBuilder)
+    {
+        pathBuilder.RequireAuthorization().DisableAntiforgery();
+        
+        pathBuilder.MapPost("/upload", UploadImageHandler);
+        
+        pathBuilder.MapGet("/{imageId}", RetrieveImageHandler);
+
+        pathBuilder.MapDelete("/{imageId}", DeleteImageHandler);
+
+        pathBuilder.MapGet("/filter/{category}", FilterImagesHandler);
+
+        pathBuilder.MapGet("/datasets", RetrieveDatasetHandler);
+
+        pathBuilder.MapGet("/datasets/{dataset}", RetrieveImagesFromDatasetHandler);
+        
+        pathBuilder.MapGet("/exception",
+            () =>
+            {
+                throw new InvalidOperationException(
+                    "Exception has been raised in the API. Look for further details in the log.");
+            });
+
+        pathBuilder.MapGet("/APITest", () =>
+        {
+            return new string[] { "1", "2", "3", "Dette er en prøve" };
+        });
+    }
+
+    
+    //This is the upload endpoint where the image first gets validated,
+    //and then gets uploaded into your local Azurite BlobStorage as a JSON file
+
+    public static async Task<IResult> UploadImageHandler(IFormFile image, string category, AnnotationsDbContext context, 
path: Annotations.API/Groups/ImageEndpoints.cs
user: login: RonoITU
body: In general with `IResult`: That can be used for a lot of basic types of returns. However, if I need to return i.e. a response body with a 400 code, I build my response using `HttpContext`. 
created_at: 2025-04-30T18:17:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,164 @@
+using Annotations.API.Services;
+using Annotations.Core.Models;
+using Microsoft.AspNetCore.Http.Extensions;
+using Annotations.Core.Entities;
+using Azure;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.Extensions.Azure;
+using SQLitePCL;
+
+
+namespace Annotations.API.Groups;
+
+public static class ImageEndpoints
+{
+    private static int _counter = 0;//change this - it gets reset every time the program resets
+    
+ 
+    public static void MapEndpoints(RouteGroupBuilder pathBuilder)
+    {
+        pathBuilder.RequireAuthorization().DisableAntiforgery();
+        
+        pathBuilder.MapPost("/upload", UploadImageHandler);
+        
+        pathBuilder.MapGet("/{imageId}", RetrieveImageHandler);
+
+        pathBuilder.MapDelete("/{imageId}", DeleteImageHandler);
+
+        pathBuilder.MapGet("/filter/{category}", FilterImagesHandler);
+
+        pathBuilder.MapGet("/datasets", RetrieveDatasetHandler);
+
+        pathBuilder.MapGet("/datasets/{dataset}", RetrieveImagesFromDatasetHandler);
+        
+        pathBuilder.MapGet("/exception",
+            () =>
+            {
+                throw new InvalidOperationException(
+                    "Exception has been raised in the API. Look for further details in the log.");
+            });
+
+        pathBuilder.MapGet("/APITest", () =>
+        {
+            return new string[] { "1", "2", "3", "Dette er en prøve" };
+        });
+    }
+
+    
+    //This is the upload endpoint where the image first gets validated,
+    //and then gets uploaded into your local Azurite BlobStorage as a JSON file
+
+    public static async Task<IResult> UploadImageHandler(IFormFile image, string category, AnnotationsDbContext context, 
+         [FromServices] IImageService _imageService)
+    {
+        ValidationResponse response = _imageService.ValidateImage(image);
+        if (!response.Success)
+        {
+            _imageService.UploadImageError(response);
+            return Results.StatusCode(422);
+            //TODO: how should the end user see this?
+        } 
+        await _imageService.UploadingImage(image,_counter, category);
+        _counter++;
+        return Results.StatusCode(200);
+    }
+
+    public static async Task<IResult> DeleteImageHandler(string imageId, [FromServices] IAzureClientFactory<BlobServiceClient> clientFactory, 
+        [FromServices] IImageService _imageService)
+    {
+        var cts = new CancellationTokenSource(5000);
+
+        bool deleting = await _imageService.DeleteImage(imageId, cts);
+        if (deleting)
+        {
+            return Results.StatusCode(204);
+        }
+            
+        Console.WriteLine("Cannot delete image because it doesn't exist");
+        return Results.StatusCode(404);
+    }
+    
+    public static async Task<string> RetrieveImageHandler([FromRoute] string imageId, [FromServices] IAzureClientFactory<BlobServiceClient> clientFactory, 
+        [FromServices] IImageService _imageService)
+    {
+        //insert password restrictions here 🐿️
+        var cts = new CancellationTokenSource(5000);
+        var containerClient = _imageService.createContainer();
path: Annotations.API/Groups/ImageEndpoints.cs
user: login: RonoITU
body: Could the function to obtain `containerClient` be called directly within `ImageService`?
created_at: 2025-04-30T18:19:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,164 @@
+using Annotations.API.Services;
+using Annotations.Core.Models;
+using Microsoft.AspNetCore.Http.Extensions;
+using Annotations.Core.Entities;
+using Azure;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.Extensions.Azure;
+using SQLitePCL;
+
+
+namespace Annotations.API.Groups;
+
+public static class ImageEndpoints
+{
+    private static int _counter = 0;//change this - it gets reset every time the program resets
+    
+ 
+    public static void MapEndpoints(RouteGroupBuilder pathBuilder)
+    {
+        pathBuilder.RequireAuthorization().DisableAntiforgery();
+        
+        pathBuilder.MapPost("/upload", UploadImageHandler);
+        
+        pathBuilder.MapGet("/{imageId}", RetrieveImageHandler);
+
+        pathBuilder.MapDelete("/{imageId}", DeleteImageHandler);
+
+        pathBuilder.MapGet("/filter/{category}", FilterImagesHandler);
+
+        pathBuilder.MapGet("/datasets", RetrieveDatasetHandler);
+
+        pathBuilder.MapGet("/datasets/{dataset}", RetrieveImagesFromDatasetHandler);
+        
+        pathBuilder.MapGet("/exception",
+            () =>
+            {
+                throw new InvalidOperationException(
+                    "Exception has been raised in the API. Look for further details in the log.");
+            });
+
+        pathBuilder.MapGet("/APITest", () =>
+        {
+            return new string[] { "1", "2", "3", "Dette er en prøve" };
+        });
+    }
+
+    
+    //This is the upload endpoint where the image first gets validated,
+    //and then gets uploaded into your local Azurite BlobStorage as a JSON file
+
+    public static async Task<IResult> UploadImageHandler(IFormFile image, string category, AnnotationsDbContext context, 
+         [FromServices] IImageService _imageService)
+    {
+        ValidationResponse response = _imageService.ValidateImage(image);
+        if (!response.Success)
+        {
+            _imageService.UploadImageError(response);
+            return Results.StatusCode(422);
+            //TODO: how should the end user see this?
+        } 
+        await _imageService.UploadingImage(image,_counter, category);
+        _counter++;
+        return Results.StatusCode(200);
+    }
+
+    public static async Task<IResult> DeleteImageHandler(string imageId, [FromServices] IAzureClientFactory<BlobServiceClient> clientFactory, 
+        [FromServices] IImageService _imageService)
+    {
+        var cts = new CancellationTokenSource(5000);
+
+        bool deleting = await _imageService.DeleteImage(imageId, cts);
+        if (deleting)
+        {
+            return Results.StatusCode(204);
+        }
+            
+        Console.WriteLine("Cannot delete image because it doesn't exist");
+        return Results.StatusCode(404);
+    }
+    
+    public static async Task<string> RetrieveImageHandler([FromRoute] string imageId, [FromServices] IAzureClientFactory<BlobServiceClient> clientFactory, 
+        [FromServices] IImageService _imageService)
+    {
+        //insert password restrictions here 🐿️
+        var cts = new CancellationTokenSource(5000);
+        var containerClient = _imageService.createContainer();
+
+        var getImageResult = await _imageService.GetImage(imageId, cts,containerClient);
+
+        if (getImageResult.Success)
+        {
+            return getImageResult.image; 
+        }
+        
+        //will only reach here if it cannot find an image with the id of the correct file type,
+        //or else the request will terminate inside the for-loop
+        Console.WriteLine("Cannot retrieve image because it doesn't exist");
+        return "Cannot retrieve image because it doesn't exist";
+    }
+    
+    public static async Task<string[]> FilterImagesHandler(string category, [FromServices] IAzureClientFactory<BlobServiceClient> clientFactory, 
+        [FromServices] IImageService _imageService)
+    {
+        var cts = new CancellationTokenSource(5000);
+
+        HashSet<string> collection = await _imageService.Filter(category);
+
+        if (collection.Count() == 0)
+        {
+            Console.WriteLine("No pictures have this category");
+            //TODO return proper error code
+            //return Results.StatusCode(404);
+        }
+        
+        return collection.ToArray();//returns array of the JSON files as strings
+
+    }
+    
+    public static async Task<DatasetModel[]> RetrieveDatasetHandler(AnnotationsDbContext context, [FromServices] IImageService _imageService)
+    {
+        var cts = new CancellationTokenSource(5000);
+
+        
+        return await _imageService.GetAllDatasets();
+
+    }
+    
+    public static async Task<string[]> RetrieveImagesFromDatasetHandler(string dataset, AnnotationsDbContext context, 
+        [FromServices] IAzureClientFactory<BlobServiceClient> clientFactory, [FromServices] IImageService _imageService)
+    {
path: Annotations.API/Groups/ImageEndpoints.cs
user: login: RonoITU
body: This looks like the only way to download the list of images from within a dataset. From what I read, this downloads all of the image data to do so. So for later: Could this be done in a way more compatible with lazy loading?
created_at: 2025-04-30T18:24:32Z
author_association: CONTRIBUTOR
path: Annotations.API/Services/ImageService.cs
user: login: RonoITU
body: I would really like to see some consistency with spaces and line breaks here. Don't shy away from adding 4 lines between each function. There may be tools available in the IDE to enforce code consistency. 
created_at: 2025-04-30T18:28:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,164 @@
+using Annotations.API.Services;
+using Annotations.Core.Models;
+using Microsoft.AspNetCore.Http.Extensions;
+using Annotations.Core.Entities;
+using Azure;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.Extensions.Azure;
+using SQLitePCL;
+
+
+namespace Annotations.API.Groups;
+
+public static class ImageEndpoints
+{
+    private static int _counter = 0;//change this - it gets reset every time the program resets
+    
+ 
+    public static void MapEndpoints(RouteGroupBuilder pathBuilder)
+    {
+        pathBuilder.RequireAuthorization().DisableAntiforgery();
+        
+        pathBuilder.MapPost("/upload", UploadImageHandler);
+        
+        pathBuilder.MapGet("/{imageId}", RetrieveImageHandler);
+
+        pathBuilder.MapDelete("/{imageId}", DeleteImageHandler);
+
+        pathBuilder.MapGet("/filter/{category}", FilterImagesHandler);
+
+        pathBuilder.MapGet("/datasets", RetrieveDatasetHandler);
+
+        pathBuilder.MapGet("/datasets/{dataset}", RetrieveImagesFromDatasetHandler);
+        
+        pathBuilder.MapGet("/exception",
+            () =>
+            {
+                throw new InvalidOperationException(
+                    "Exception has been raised in the API. Look for further details in the log.");
+            });
+
+        pathBuilder.MapGet("/APITest", () =>
+        {
+            return new string[] { "1", "2", "3", "Dette er en prøve" };
+        });
+    }
+
+    
+    //This is the upload endpoint where the image first gets validated,
+    //and then gets uploaded into your local Azurite BlobStorage as a JSON file
+
+    public static async Task<IResult> UploadImageHandler(IFormFile image, string category, AnnotationsDbContext context, 
path: Annotations.API/Groups/ImageEndpoints.cs
user: login: RonoITU
body: For later use.
created_at: 2025-05-01T08:30:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,164 @@
+using Annotations.API.Services;
+using Annotations.Core.Models;
+using Microsoft.AspNetCore.Http.Extensions;
+using Annotations.Core.Entities;
+using Azure;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.Extensions.Azure;
+using SQLitePCL;
+
+
+namespace Annotations.API.Groups;
+
+public static class ImageEndpoints
+{
+    private static int _counter = 0;//change this - it gets reset every time the program resets
+    
+ 
+    public static void MapEndpoints(RouteGroupBuilder pathBuilder)
+    {
+        pathBuilder.RequireAuthorization().DisableAntiforgery();
+        
+        pathBuilder.MapPost("/upload", UploadImageHandler);
+        
+        pathBuilder.MapGet("/{imageId}", RetrieveImageHandler);
+
+        pathBuilder.MapDelete("/{imageId}", DeleteImageHandler);
+
+        pathBuilder.MapGet("/filter/{category}", FilterImagesHandler);
+
+        pathBuilder.MapGet("/datasets", RetrieveDatasetHandler);
+
+        pathBuilder.MapGet("/datasets/{dataset}", RetrieveImagesFromDatasetHandler);
+        
+        pathBuilder.MapGet("/exception",
+            () =>
+            {
+                throw new InvalidOperationException(
+                    "Exception has been raised in the API. Look for further details in the log.");
+            });
+
+        pathBuilder.MapGet("/APITest", () =>
+        {
+            return new string[] { "1", "2", "3", "Dette er en prøve" };
+        });
+    }
+
+    
+    //This is the upload endpoint where the image first gets validated,
+    //and then gets uploaded into your local Azurite BlobStorage as a JSON file
+
+    public static async Task<IResult> UploadImageHandler(IFormFile image, string category, AnnotationsDbContext context, 
+         [FromServices] IImageService _imageService)
+    {
+        ValidationResponse response = _imageService.ValidateImage(image);
+        if (!response.Success)
+        {
+            _imageService.UploadImageError(response);
+            return Results.StatusCode(422);
+            //TODO: how should the end user see this?
+        } 
+        await _imageService.UploadingImage(image,_counter, category);
+        _counter++;
+        return Results.StatusCode(200);
+    }
+
+    public static async Task<IResult> DeleteImageHandler(string imageId, [FromServices] IAzureClientFactory<BlobServiceClient> clientFactory, 
+        [FromServices] IImageService _imageService)
+    {
+        var cts = new CancellationTokenSource(5000);
+
+        bool deleting = await _imageService.DeleteImage(imageId, cts);
+        if (deleting)
+        {
+            return Results.StatusCode(204);
+        }
+            
+        Console.WriteLine("Cannot delete image because it doesn't exist");
+        return Results.StatusCode(404);
+    }
+    
+    public static async Task<string> RetrieveImageHandler([FromRoute] string imageId, [FromServices] IAzureClientFactory<BlobServiceClient> clientFactory, 
+        [FromServices] IImageService _imageService)
+    {
+        //insert password restrictions here 🐿️
+        var cts = new CancellationTokenSource(5000);
+        var containerClient = _imageService.createContainer();
+
+        var getImageResult = await _imageService.GetImage(imageId, cts,containerClient);
+
+        if (getImageResult.Success)
+        {
+            return getImageResult.image; 
+        }
+        
+        //will only reach here if it cannot find an image with the id of the correct file type,
+        //or else the request will terminate inside the for-loop
+        Console.WriteLine("Cannot retrieve image because it doesn't exist");
+        return "Cannot retrieve image because it doesn't exist";
+    }
+    
+    public static async Task<string[]> FilterImagesHandler(string category, [FromServices] IAzureClientFactory<BlobServiceClient> clientFactory, 
+        [FromServices] IImageService _imageService)
+    {
+        var cts = new CancellationTokenSource(5000);
+
+        HashSet<string> collection = await _imageService.Filter(category);
+
+        if (collection.Count() == 0)
+        {
+            Console.WriteLine("No pictures have this category");
+            //TODO return proper error code
+            //return Results.StatusCode(404);
+        }
+        
+        return collection.ToArray();//returns array of the JSON files as strings
+
+    }
+    
+    public static async Task<DatasetModel[]> RetrieveDatasetHandler(AnnotationsDbContext context, [FromServices] IImageService _imageService)
+    {
+        var cts = new CancellationTokenSource(5000);
+
+        
+        return await _imageService.GetAllDatasets();
+
+    }
+    
+    public static async Task<string[]> RetrieveImagesFromDatasetHandler(string dataset, AnnotationsDbContext context, 
+        [FromServices] IAzureClientFactory<BlobServiceClient> clientFactory, [FromServices] IImageService _imageService)
+    {
path: Annotations.API/Groups/ImageEndpoints.cs
user: login: RonoITU
body: For later use.
created_at: 2025-05-01T08:30:25Z
author_association: CONTRIBUTOR
path: Annotations.API/Services/ImageService.cs
user: login: RonoITU
body: > I would really like to see some consistency with spaces and line breaks here. Don't shy away from adding 4 lines between each function. There may be tools available in the IDE to enforce code consistency.

Hi. I am doing a new review. I need this point from my previous review to be resolved. Please make room of 3 lines between functions, and between classes. Record types can remain grouped. 
created_at: 2025-05-02T17:24:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,267 @@
+using Annotations.Core.Entities;
+using Annotations.Core.Models;
+using Azure;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Azure;
+
+namespace Annotations.API.Services;
+
+public record ValidationResponse(bool Success, string Message);
+public record ImageData(ImageModel Image, string JSONString);
+
+public record GetImageResult(bool Success, string image);
+
+
+public interface IImageService
+
+{
+    ValidationResponse ValidateImage(IFormFile file);
+    Task UploadingImage(IFormFile image, int counter, string category);
+    void UploadImageError(ValidationResponse response);
+    Task<HashSet<string>> Filter(string category);
+    Task<DatasetModel> GetDataset(string dataset);
+    Task<GetImageResult> GetImage(string imageId);
+    Task<DatasetModel[]> GetAllDatasets();
+    Task<bool> DeleteImage(string imageId);
+
path: Annotations.API/Services/ImageService.cs
user: login: RonoITU
body: Please remove these excess lines from the interface declaration.
created_at: 2025-05-02T17:25:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,267 @@
+using Annotations.Core.Entities;
+using Annotations.Core.Models;
+using Azure;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Azure;
+
+namespace Annotations.API.Services;
+
+public record ValidationResponse(bool Success, string Message);
+public record ImageData(ImageModel Image, string JSONString);
+
+public record GetImageResult(bool Success, string image);
+
+
+public interface IImageService
+
path: Annotations.API/Services/ImageService.cs
user: login: RonoITU
body: Also remove this empty line, for consistency. 
created_at: 2025-05-02T17:25:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,267 @@
+using Annotations.Core.Entities;
+using Annotations.Core.Models;
+using Azure;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Azure;
+
+namespace Annotations.API.Services;
+
+public record ValidationResponse(bool Success, string Message);
+public record ImageData(ImageModel Image, string JSONString);
+
+public record GetImageResult(bool Success, string image);
+
+
+public interface IImageService
+
+{
+    ValidationResponse ValidateImage(IFormFile file);
+    Task UploadingImage(IFormFile image, int counter, string category);
+    void UploadImageError(ValidationResponse response);
+    Task<HashSet<string>> Filter(string category);
+    Task<DatasetModel> GetDataset(string dataset);
+    Task<GetImageResult> GetImage(string imageId);
+    Task<DatasetModel[]> GetAllDatasets();
+    Task<bool> DeleteImage(string imageId);
+
+
+
+
+}
+
+public class ImageService: IImageService
path: Annotations.API/Services/ImageService.cs
user: login: RonoITU
body: Please for consistency: `public class ImageService : IImageService`
created_at: 2025-05-02T17:26:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,267 @@
+using Annotations.Core.Entities;
+using Annotations.Core.Models;
+using Azure;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Azure;
+
+namespace Annotations.API.Services;
+
+public record ValidationResponse(bool Success, string Message);
+public record ImageData(ImageModel Image, string JSONString);
+
+public record GetImageResult(bool Success, string image);
+
+
+public interface IImageService
+
+{
+    ValidationResponse ValidateImage(IFormFile file);
+    Task UploadingImage(IFormFile image, int counter, string category);
+    void UploadImageError(ValidationResponse response);
+    Task<HashSet<string>> Filter(string category);
+    Task<DatasetModel> GetDataset(string dataset);
+    Task<GetImageResult> GetImage(string imageId);
+    Task<DatasetModel[]> GetAllDatasets();
+    Task<bool> DeleteImage(string imageId);
+
+
+
+
+}
+
+public class ImageService: IImageService
+{
+    private static string[] _arrayOfFileExtension = {"png", "jpg", "jpeg"};
+    private readonly IAzureClientFactory<BlobServiceClient> _clientFactory;
+    private readonly AnnotationsDbContext _DbContext;
+    private readonly BlobContainerClient _containerClient;
+
+    public ImageService(IAzureClientFactory<BlobServiceClient> clientFactory , AnnotationsDbContext context)
path: Annotations.API/Services/ImageService.cs
user: login: RonoITU
body: In constructor declaration, please edit for consistency:
`public ImageService(IAzureClientFactory<BlobServiceClient> clientFactory, AnnotationsDbContext context)`
created_at: 2025-05-02T17:27:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,267 @@
+using Annotations.Core.Entities;
+using Annotations.Core.Models;
+using Azure;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Azure;
+
+namespace Annotations.API.Services;
+
+public record ValidationResponse(bool Success, string Message);
+public record ImageData(ImageModel Image, string JSONString);
+
+public record GetImageResult(bool Success, string image);
+
+
+public interface IImageService
+
+{
+    ValidationResponse ValidateImage(IFormFile file);
+    Task UploadingImage(IFormFile image, int counter, string category);
+    void UploadImageError(ValidationResponse response);
+    Task<HashSet<string>> Filter(string category);
+    Task<DatasetModel> GetDataset(string dataset);
+    Task<GetImageResult> GetImage(string imageId);
+    Task<DatasetModel[]> GetAllDatasets();
+    Task<bool> DeleteImage(string imageId);
+
+
+
+
+}
+
+public class ImageService: IImageService
+{
+    private static string[] _arrayOfFileExtension = {"png", "jpg", "jpeg"};
+    private readonly IAzureClientFactory<BlobServiceClient> _clientFactory;
+    private readonly AnnotationsDbContext _DbContext;
+    private readonly BlobContainerClient _containerClient;
+
+    public ImageService(IAzureClientFactory<BlobServiceClient> clientFactory , AnnotationsDbContext context)
+    {
+        _clientFactory = clientFactory;
+        _DbContext = context;
+        var blobServiceClient = _clientFactory.CreateClient("Default");
+        _containerClient = blobServiceClient.GetBlobContainerClient("images");
+    }
+    /// <summary>
+    /// Helping method that validates an image based on type, size, and also checks if it even contains anything
+    /// Images can be JPEG, PNG and JPG, and everything else gets rejected
+    /// </summary>
+    /// <param name="file">The file to be validated</param>
+    /// <returns>ValidationResponse - a record that contains a boolean of whether the image is validated, and a message that describes either what went wrong, or that it was successful</returns>
+    public ValidationResponse ValidateImage(IFormFile file)
+    {
+        
+        if (file.Length > 50 * 1024 * 1024) {//50MB
+            return new ValidationResponse(false, "File is too large.");
+        }
+        if (file.Length == 0) 
+        { 
+            return new ValidationResponse(false, "File doesn't exist.");
+        }
+        foreach (string fileExtension in _arrayOfFileExtension)
+        {
+            if (file.ContentType.Contains(fileExtension))
+            {
+                //if the image is the correct type, it will be uploaded, since it fulfills the other criterias
+                //upload image to db
+                return new ValidationResponse(true ,"Uploaded image successfully.");
+            }
+        }
+        //if the code reaches this point, then the file type is none of the permitted file types, so an error is thrown
+        return new ValidationResponse(false, "File is not a valid image.");
+        
+    }
+
+    /// <summary>
+    /// Downloads a BlobClient to a memory stream.
+    /// Afterward it gets converted to a JSON file in the form of a string
+    /// </summary>
+    /// <param name="blobClient">BlobClient</param>
+    /// <returns>JSON file in the form of a string</returns>
+    public async Task<string> convertToJSONString(BlobClient blobClient)
+    {
+        using var memoryStream = new MemoryStream();
+        await blobClient.DownloadToAsync(memoryStream);
+        return System.Text.Encoding.UTF8.GetString(memoryStream.ToArray());//JSON file as string
+    }
+    
+    /// <summary>
+    ///Below functionality of cross-adding the image to the assigned datasets
+    ///is overriden by the hard-code creation of datasets in Api, Program.cs
+    ///
+    /// The idea for future reference is that when uploading an image, the user
+    ///inputs the names (ids) of datasets, and we can use below code (without
+    ///hard-coded ids) to cross-add the images to the relevant datasets.
+    /// </summary>
+    /// <param name="thisImage"></param>
+    private async Task AddImagesToDatasets(ImageModel thisImage)
+    {
+        var neededDataset = _DbContext.Datasets.Select(Dataset => Dataset)
+            .Where(Dataset =>
+                Dataset.Id == 1 || Dataset.Id == 2); //TODO dont do this - this is hardcoded for testing
+
+        foreach (Dataset dataset in neededDataset)
+
+        {
+            dataset.ImageIds.Add(thisImage.Id); //adds images to the datasets
+            await _DbContext.SaveChangesAsync();
+        }
+    }
+
+    private async Task UploadAsJSON(int counter, string jsonString)
+    {
+        
+        var byteContent = System.Text.Encoding.UTF8.GetBytes(jsonString); //JSON string becomes byte array
+        BlobClient thisImageBlobClient = _containerClient.GetBlobClient($"{counter}.json");
+
+        var blobHeaders = new BlobHttpHeaders
+        {
+            ContentType = "application/json"
+        };
+
+        // Trigger the upload function to push the data to blob
+        await thisImageBlobClient.UploadAsync(new MemoryStream(byteContent),
+            blobHeaders); //uploaded as byte array
+        //Should it be uploaded as a string instead? So far all endpoints retrieve the JSON files as strings?
+    }
path: Annotations.API/Services/ImageService.cs
user: login: RonoITU
body: Please make consistent space between functions. I prefer to see 3 blank lines space. 
created_at: 2025-05-02T17:31:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,291 @@
+using Annotations.Core.Entities;
+using Annotations.Core.Models;
+using Azure;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Azure;
+
+namespace Annotations.API.Services;
+
+public record ValidationResponse(bool Success, string Message);
+public record ImageData(ImageModel Image, string JSONString);
+
+public record GetImageResult(bool Success, string image);
+
+
+public interface IImageService
+{
+    ValidationResponse ValidateImage(IFormFile file);
+    Task UploadingImage(IFormFile image, int counter, string category);
+    void UploadImageError(ValidationResponse response);
+    Task<HashSet<string>> Filter(string category);
+    Task<DatasetModel> GetDataset(string dataset);
+    Task<GetImageResult> GetImage(string imageId);
+    Task<DatasetModel[]> GetAllDatasets();
+    Task<bool> DeleteImage(string imageId);
+}
+
+
+
+public class ImageService: IImageService
+{
+    private static string[] _arrayOfFileExtension = {"png", "jpg", "jpeg"};
+    private readonly IAzureClientFactory<BlobServiceClient> _clientFactory;
+    private readonly AnnotationsDbContext _DbContext;
+    private readonly BlobContainerClient _containerClient;
+
+    
+    
+    public ImageService(IAzureClientFactory<BlobServiceClient> clientFactory , AnnotationsDbContext context)
+    {
+        _clientFactory = clientFactory;
+        _DbContext = context;
+        var blobServiceClient = _clientFactory.CreateClient("Default");
+        _containerClient = blobServiceClient.GetBlobContainerClient("images");
+    }
+    
+    
+    
+    /// <summary>
+    /// Helping method that validates an image based on type, size, and also checks if it even contains anything
+    /// Images can be JPEG, PNG and JPG, and everything else gets rejected
+    /// </summary>
+    /// <param name="file">The file to be validated</param>
+    /// <returns>ValidationResponse - a record that contains a boolean of whether the image is validated, and a message that describes either what went wrong, or that it was successful</returns>
+    public ValidationResponse ValidateImage(IFormFile file)
+    {
+        
+        if (file.Length > 50 * 1024 * 1024) {//50MB
+            return new ValidationResponse(false, "File is too large.");
+        }
+        if (file.Length == 0) 
+        { 
+            return new ValidationResponse(false, "File doesn't exist.");
+        }
+        foreach (string fileExtension in _arrayOfFileExtension)
+        {
+            if (file.ContentType.Contains(fileExtension))
+            {
+                //if the image is the correct type, it will be uploaded, since it fulfills the other criterias
+                //upload image to db
+                return new ValidationResponse(true ,"Uploaded image successfully.");
+            }
+        }
+        //if the code reaches this point, then the file type is none of the permitted file types, so an error is thrown
+        return new ValidationResponse(false, "File is not a valid image.");
+        
+    }
+
+    
+    
+    /// <summary>
+    /// Downloads a BlobClient to a memory stream.
+    /// Afterward it gets converted to a JSON file in the form of a string
+    /// </summary>
+    /// <param name="blobClient">BlobClient</param>
+    /// <returns>JSON file in the form of a string</returns>
+    public async Task<string> convertToJSONString(BlobClient blobClient)
+    {
+        using var memoryStream = new MemoryStream();
+        await blobClient.DownloadToAsync(memoryStream);
+        return System.Text.Encoding.UTF8.GetString(memoryStream.ToArray());//JSON file as string
+    }
+    
+    
+    
+    /// <summary>
+    ///Below functionality of cross-adding the image to the assigned datasets
+    ///is overriden by the hard-code creation of datasets in Api, Program.cs
+    ///
+    /// The idea for future reference is that when uploading an image, the user
+    ///inputs the names (ids) of datasets, and we can use below code (without
+    ///hard-coded ids) to cross-add the images to the relevant datasets.
+    /// </summary>
+    /// <param name="thisImage"></param>
+    private async Task AddImagesToDatasets(ImageModel thisImage)
+    {
+        var neededDataset = _DbContext.Datasets.Select(Dataset => Dataset)
+            .Where(Dataset =>
+                Dataset.Id == 1 || Dataset.Id == 2); //TODO dont do this - this is hardcoded for testing
+
+        foreach (Dataset dataset in neededDataset)
+
+        {
+            dataset.ImageIds.Add(thisImage.Id); //adds images to the datasets
+            await _DbContext.SaveChangesAsync();
+        }
+    }
+    
+    
+
+    private async Task UploadAsJSON(int counter, string jsonString)
+    {
+        
+        var byteContent = System.Text.Encoding.UTF8.GetBytes(jsonString); //JSON string becomes byte array
+        BlobClient thisImageBlobClient = _containerClient.GetBlobClient($"{counter}.json");
+
+        var blobHeaders = new BlobHttpHeaders
+        {
+            ContentType = "application/json"
+        };
+
+        // Trigger the upload function to push the data to blob
+        await thisImageBlobClient.UploadAsync(new MemoryStream(byteContent),
+            blobHeaders); //uploaded as byte array
+        //Should it be uploaded as a string instead? So far all endpoints retrieve the JSON files as strings?
+    }
+    
+    
+    
+    public async Task UploadingImage(IFormFile image, int counter, string category)
+    {
+            //fileExtension will always be a proper fileExtension because of the ValidateImage method
+            using (MemoryStream ms = new MemoryStream())
+            {
+                await image.OpenReadStream().CopyToAsync(ms);
+                var thisImage =
+                    new ImageModel() //TODO: dont do this - the title, description and datasetsId are hardcoded
+                    {
+                        Id = counter,
+                        Title = "idk",
+                        Description = "description",
+                        ImageData = ms.ToArray(),
+                        Category = category,
+                        DatasetsIds = new List<int>() { 1, 2 },
+                    };
+               
+                await AddImagesToDatasets(thisImage);
+                string jsonString = System.Text.Json.JsonSerializer.Serialize(thisImage); //objects becomes JSON string
+                await UploadAsJSON(counter, jsonString);
+
+            }
+    }
+
+    
+    
+    public void UploadImageError(ValidationResponse response)
path: Annotations.API/Services/ImageService.cs
user: login: tomat-suppe
body: Is this a new addition? And do we use it anywhere? You don't have to change it according to my comment, but I'd like to know the thought process behind it and for what purpose it was coded c:

Harping a bit on old stuff, I've said about WriteLines, but you can choose to disregard, if you want:
I'm unsure about the usability of WriteLines. The end-user won't be able to see them, and I'm 99.99% sure it doesn't show up in Swagger either. The idea of having an warning to throw up, when the upload goes wrong, is good, but I would make it as something that reacts with the front end and throws up a message (since I heavily assume we do not want exception to be raised). This is for course _not_ a part of this SBI, and not something I expect you to make.

created_at: 2025-05-04T14:44:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,291 @@
+using Annotations.Core.Entities;
+using Annotations.Core.Models;
+using Azure;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Azure;
+
+namespace Annotations.API.Services;
+
+public record ValidationResponse(bool Success, string Message);
+public record ImageData(ImageModel Image, string JSONString);
+
+public record GetImageResult(bool Success, string image);
+
+
+public interface IImageService
+{
+    ValidationResponse ValidateImage(IFormFile file);
+    Task UploadingImage(IFormFile image, int counter, string category);
+    void UploadImageError(ValidationResponse response);
+    Task<HashSet<string>> Filter(string category);
+    Task<DatasetModel> GetDataset(string dataset);
+    Task<GetImageResult> GetImage(string imageId);
+    Task<DatasetModel[]> GetAllDatasets();
+    Task<bool> DeleteImage(string imageId);
+}
+
+
+
+public class ImageService: IImageService
+{
+    private static string[] _arrayOfFileExtension = {"png", "jpg", "jpeg"};
+    private readonly IAzureClientFactory<BlobServiceClient> _clientFactory;
+    private readonly AnnotationsDbContext _DbContext;
+    private readonly BlobContainerClient _containerClient;
+
+    
+    
+    public ImageService(IAzureClientFactory<BlobServiceClient> clientFactory , AnnotationsDbContext context)
+    {
+        _clientFactory = clientFactory;
+        _DbContext = context;
+        var blobServiceClient = _clientFactory.CreateClient("Default");
+        _containerClient = blobServiceClient.GetBlobContainerClient("images");
+    }
+    
+    
+    
+    /// <summary>
+    /// Helping method that validates an image based on type, size, and also checks if it even contains anything
+    /// Images can be JPEG, PNG and JPG, and everything else gets rejected
+    /// </summary>
+    /// <param name="file">The file to be validated</param>
+    /// <returns>ValidationResponse - a record that contains a boolean of whether the image is validated, and a message that describes either what went wrong, or that it was successful</returns>
+    public ValidationResponse ValidateImage(IFormFile file)
+    {
+        
+        if (file.Length > 50 * 1024 * 1024) {//50MB
+            return new ValidationResponse(false, "File is too large.");
+        }
+        if (file.Length == 0) 
+        { 
+            return new ValidationResponse(false, "File doesn't exist.");
+        }
+        foreach (string fileExtension in _arrayOfFileExtension)
+        {
+            if (file.ContentType.Contains(fileExtension))
+            {
+                //if the image is the correct type, it will be uploaded, since it fulfills the other criterias
+                //upload image to db
+                return new ValidationResponse(true ,"Uploaded image successfully.");
+            }
+        }
+        //if the code reaches this point, then the file type is none of the permitted file types, so an error is thrown
+        return new ValidationResponse(false, "File is not a valid image.");
+        
+    }
+
+    
+    
+    /// <summary>
+    /// Downloads a BlobClient to a memory stream.
+    /// Afterward it gets converted to a JSON file in the form of a string
+    /// </summary>
+    /// <param name="blobClient">BlobClient</param>
+    /// <returns>JSON file in the form of a string</returns>
+    public async Task<string> convertToJSONString(BlobClient blobClient)
+    {
+        using var memoryStream = new MemoryStream();
+        await blobClient.DownloadToAsync(memoryStream);
+        return System.Text.Encoding.UTF8.GetString(memoryStream.ToArray());//JSON file as string
+    }
+    
+    
+    
+    /// <summary>
+    ///Below functionality of cross-adding the image to the assigned datasets
+    ///is overriden by the hard-code creation of datasets in Api, Program.cs
+    ///
+    /// The idea for future reference is that when uploading an image, the user
+    ///inputs the names (ids) of datasets, and we can use below code (without
+    ///hard-coded ids) to cross-add the images to the relevant datasets.
+    /// </summary>
+    /// <param name="thisImage"></param>
+    private async Task AddImagesToDatasets(ImageModel thisImage)
+    {
+        var neededDataset = _DbContext.Datasets.Select(Dataset => Dataset)
+            .Where(Dataset =>
+                Dataset.Id == 1 || Dataset.Id == 2); //TODO dont do this - this is hardcoded for testing
+
+        foreach (Dataset dataset in neededDataset)
+
+        {
+            dataset.ImageIds.Add(thisImage.Id); //adds images to the datasets
+            await _DbContext.SaveChangesAsync();
+        }
+    }
+    
+    
+
+    private async Task UploadAsJSON(int counter, string jsonString)
+    {
+        
+        var byteContent = System.Text.Encoding.UTF8.GetBytes(jsonString); //JSON string becomes byte array
+        BlobClient thisImageBlobClient = _containerClient.GetBlobClient($"{counter}.json");
+
+        var blobHeaders = new BlobHttpHeaders
+        {
+            ContentType = "application/json"
+        };
+
+        // Trigger the upload function to push the data to blob
+        await thisImageBlobClient.UploadAsync(new MemoryStream(byteContent),
+            blobHeaders); //uploaded as byte array
+        //Should it be uploaded as a string instead? So far all endpoints retrieve the JSON files as strings?
+    }
+    
+    
+    
+    public async Task UploadingImage(IFormFile image, int counter, string category)
+    {
+            //fileExtension will always be a proper fileExtension because of the ValidateImage method
+            using (MemoryStream ms = new MemoryStream())
+            {
+                await image.OpenReadStream().CopyToAsync(ms);
+                var thisImage =
+                    new ImageModel() //TODO: dont do this - the title, description and datasetsId are hardcoded
+                    {
+                        Id = counter,
+                        Title = "idk",
+                        Description = "description",
+                        ImageData = ms.ToArray(),
+                        Category = category,
+                        DatasetsIds = new List<int>() { 1, 2 },
+                    };
+               
+                await AddImagesToDatasets(thisImage);
+                string jsonString = System.Text.Json.JsonSerializer.Serialize(thisImage); //objects becomes JSON string
+                await UploadAsJSON(counter, jsonString);
+
+            }
+    }
+
+    
+    
+    public void UploadImageError(ValidationResponse response)
path: Annotations.API/Services/ImageService.cs
user: login: nalm2812
body: The writeLines bit is old stuff that was mainly used to make it easier to test. It is correct they dont show up on Swagger, only your local terminal. The motivation with this method was for future use so that if more error-handling was added to the backend, there was an appropriate place the error could be handled. I hope this answers your concerns. I'm also fine with removing these WriteLines if you would prefer that
created_at: 2025-05-04T14:58:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,291 @@
+using Annotations.Core.Entities;
+using Annotations.Core.Models;
+using Azure;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Azure;
+
+namespace Annotations.API.Services;
+
+public record ValidationResponse(bool Success, string Message);
+public record ImageData(ImageModel Image, string JSONString);
+
+public record GetImageResult(bool Success, string image);
+
+
+public interface IImageService
+{
+    ValidationResponse ValidateImage(IFormFile file);
+    Task UploadingImage(IFormFile image, int counter, string category);
+    void UploadImageError(ValidationResponse response);
+    Task<HashSet<string>> Filter(string category);
+    Task<DatasetModel> GetDataset(string dataset);
+    Task<GetImageResult> GetImage(string imageId);
+    Task<DatasetModel[]> GetAllDatasets();
+    Task<bool> DeleteImage(string imageId);
+}
+
+
+
+public class ImageService: IImageService
+{
+    private static string[] _arrayOfFileExtension = {"png", "jpg", "jpeg"};
+    private readonly IAzureClientFactory<BlobServiceClient> _clientFactory;
+    private readonly AnnotationsDbContext _DbContext;
+    private readonly BlobContainerClient _containerClient;
+
+    
+    
+    public ImageService(IAzureClientFactory<BlobServiceClient> clientFactory , AnnotationsDbContext context)
+    {
+        _clientFactory = clientFactory;
+        _DbContext = context;
+        var blobServiceClient = _clientFactory.CreateClient("Default");
+        _containerClient = blobServiceClient.GetBlobContainerClient("images");
+    }
+    
+    
+    
+    /// <summary>
+    /// Helping method that validates an image based on type, size, and also checks if it even contains anything
+    /// Images can be JPEG, PNG and JPG, and everything else gets rejected
+    /// </summary>
+    /// <param name="file">The file to be validated</param>
+    /// <returns>ValidationResponse - a record that contains a boolean of whether the image is validated, and a message that describes either what went wrong, or that it was successful</returns>
+    public ValidationResponse ValidateImage(IFormFile file)
+    {
+        
+        if (file.Length > 50 * 1024 * 1024) {//50MB
+            return new ValidationResponse(false, "File is too large.");
+        }
+        if (file.Length == 0) 
+        { 
+            return new ValidationResponse(false, "File doesn't exist.");
+        }
+        foreach (string fileExtension in _arrayOfFileExtension)
+        {
+            if (file.ContentType.Contains(fileExtension))
+            {
+                //if the image is the correct type, it will be uploaded, since it fulfills the other criterias
+                //upload image to db
+                return new ValidationResponse(true ,"Uploaded image successfully.");
+            }
+        }
+        //if the code reaches this point, then the file type is none of the permitted file types, so an error is thrown
+        return new ValidationResponse(false, "File is not a valid image.");
+        
+    }
+
+    
+    
+    /// <summary>
+    /// Downloads a BlobClient to a memory stream.
+    /// Afterward it gets converted to a JSON file in the form of a string
+    /// </summary>
+    /// <param name="blobClient">BlobClient</param>
+    /// <returns>JSON file in the form of a string</returns>
+    public async Task<string> convertToJSONString(BlobClient blobClient)
+    {
+        using var memoryStream = new MemoryStream();
+        await blobClient.DownloadToAsync(memoryStream);
+        return System.Text.Encoding.UTF8.GetString(memoryStream.ToArray());//JSON file as string
+    }
+    
+    
+    
+    /// <summary>
+    ///Below functionality of cross-adding the image to the assigned datasets
+    ///is overriden by the hard-code creation of datasets in Api, Program.cs
+    ///
+    /// The idea for future reference is that when uploading an image, the user
+    ///inputs the names (ids) of datasets, and we can use below code (without
+    ///hard-coded ids) to cross-add the images to the relevant datasets.
+    /// </summary>
+    /// <param name="thisImage"></param>
+    private async Task AddImagesToDatasets(ImageModel thisImage)
+    {
+        var neededDataset = _DbContext.Datasets.Select(Dataset => Dataset)
+            .Where(Dataset =>
+                Dataset.Id == 1 || Dataset.Id == 2); //TODO dont do this - this is hardcoded for testing
+
+        foreach (Dataset dataset in neededDataset)
+
+        {
+            dataset.ImageIds.Add(thisImage.Id); //adds images to the datasets
+            await _DbContext.SaveChangesAsync();
+        }
+    }
+    
+    
+
+    private async Task UploadAsJSON(int counter, string jsonString)
+    {
+        
+        var byteContent = System.Text.Encoding.UTF8.GetBytes(jsonString); //JSON string becomes byte array
+        BlobClient thisImageBlobClient = _containerClient.GetBlobClient($"{counter}.json");
+
+        var blobHeaders = new BlobHttpHeaders
+        {
+            ContentType = "application/json"
+        };
+
+        // Trigger the upload function to push the data to blob
+        await thisImageBlobClient.UploadAsync(new MemoryStream(byteContent),
+            blobHeaders); //uploaded as byte array
+        //Should it be uploaded as a string instead? So far all endpoints retrieve the JSON files as strings?
+    }
+    
+    
+    
+    public async Task UploadingImage(IFormFile image, int counter, string category)
+    {
+            //fileExtension will always be a proper fileExtension because of the ValidateImage method
+            using (MemoryStream ms = new MemoryStream())
+            {
+                await image.OpenReadStream().CopyToAsync(ms);
+                var thisImage =
+                    new ImageModel() //TODO: dont do this - the title, description and datasetsId are hardcoded
+                    {
+                        Id = counter,
+                        Title = "idk",
+                        Description = "description",
+                        ImageData = ms.ToArray(),
+                        Category = category,
+                        DatasetsIds = new List<int>() { 1, 2 },
+                    };
+               
+                await AddImagesToDatasets(thisImage);
+                string jsonString = System.Text.Json.JsonSerializer.Serialize(thisImage); //objects becomes JSON string
+                await UploadAsJSON(counter, jsonString);
+
+            }
+    }
+
+    
+    
+    public void UploadImageError(ValidationResponse response)
path: Annotations.API/Services/ImageService.cs
user: login: tomat-suppe
body: I do see the advantage of already having a method for this, so the future error handling has 'a place to be implemented'. Maybe I can request that when you guys get to documentation SBI, you could describe with a method documentation, that this is the purpose of having it there :)
created_at: 2025-05-04T15:46:16Z
author_association: CONTRIBUTOR

PR comments:
user: login: tomat-suppe
created_at: 2025-05-04T14:58:39Z
body: From the SBI: "Follow an injectable service structure, where needed for code documentation."
Can someone elaborate for me on what an injectable service structure is, so I can review correctly? My internet search does not yield things that seem relevant here
author_association: CONTRIBUTOR
user: login: nalm2812
created_at: 2025-05-04T15:08:53Z
body: > Regarding: "For the endpoint that gets a single image, the endpoint functionality has been changed a bit. This was done to reuse some of the code for "images/datasets/{datasets}". The change was that instead of returning a IResult as a file, a string is returned."
> 
> I am trying to read through RetrieveImageHandler and GetImage. I am having difficulties completely understanding them. Do we only send json data over retrieval now? How does this work with the front-end, which assumes an alt text is sent over the Get request? Or have I completely misunderstood these classes? What exactly does the front end retrieve on a get request for an image?

The front end still receives a json file (which is in the form of a string in the backend) which also includes the alt-text and any other metadata. It basically receives a string which is treated as a json file. Beforehand, if you look on swagger, the return type was a json file, and now it is text. This was actually not something I fully understood until you asked me, so thank you for that.
 Beforehand, it was in the form of a IResult which also included a status code. If you fail at retrieving an image, now it technically returns a code 200 instead of 404, but instead it'll return a string with an error message. Currently, in the frontend it'll just crash without displaying any message (which I think was also what happened beforehand). This is obviously not ideal. I hope this answers your questions, and feel free to ask further questions :)

author_association: CONTRIBUTOR
user: login: nalm2812
created_at: 2025-05-04T15:10:01Z
body: > From the SBI: "Follow an injectable service structure, where needed for code documentation." Can someone elaborate for me on what an injectable service structure is, so I can review correctly? My internet search does not yield things that seem relevant here

This is what we have done with the service class, where from Program.cs we have "injected" a singleton of the service so that we always use the same functions. It is the same as what we did with DbContext and what we did with repositories/service in Chirp
author_association: CONTRIBUTOR
user: login: tomat-suppe
created_at: 2025-05-04T15:47:09Z
body: > > Regarding: "For the endpoint that gets a single image, the endpoint functionality has been changed a bit. This was done to reuse some of the code for "images/datasets/{datasets}". The change was that instead of returning a IResult as a file, a string is returned."
> > I am trying to read through RetrieveImageHandler and GetImage. I am having difficulties completely understanding them. Do we only send json data over retrieval now? How does this work with the front-end, which assumes an alt text is sent over the Get request? Or have I completely misunderstood these classes? What exactly does the front end retrieve on a get request for an image?
> 
> The front end still receives a json file (which is in the form of a string in the backend) which also includes the alt-text and any other metadata. It basically receives a string which is treated as a json file. Beforehand, if you look on swagger, the return type was a json file, and now it is text. This was actually not something I fully understood until you asked me, so thank you for that. Beforehand, it was in the form of a IResult which also included a status code. If you fail at retrieving an image, now it technically returns a code 200 instead of 404, but instead it'll return a string with an error message. Currently, in the frontend it'll just crash without displaying any message (which I think was also what happened beforehand). This is obviously not ideal. I hope this answers your questions, and feel free to ask further questions :)

This clears it up. So simply a matter of which 'form' the json file takes, it sounds like :)
author_association: CONTRIBUTOR
user: login: tomat-suppe
created_at: 2025-05-04T15:47:29Z
body: > > From the SBI: "Follow an injectable service structure, where needed for code documentation." Can someone elaborate for me on what an injectable service structure is, so I can review correctly? My internet search does not yield things that seem relevant here
> 
> This is what we have done with the service class, where from Program.cs we have "injected" a singleton of the service so that we always use the same functions. It is the same as what we did with DbContext and what we did with repositories/service in Chirp

Thank you, this also cleared up my question!
author_association: CONTRIBUTOR