7.5: Adding PhotoService

Adding DTOs

Inside of the .Models/Photo folder, add a new class called PhotoForCreation.cs. This will represent the data we will send from our client to our backend.

public class PhotoForCreation
{
    public string Url { get; set; }
    public IFormFile File { get; set; }
    public string Description { get; set; }
    public DateTime DateAdded { get; set; }
    public string PublicId { get; set; }

    public PhotoForCreation()
    {
        DateAdded = DateTime.Now;
    }
}

The IFormFile property is used to hold a file sent via HTTP. Read more about it in the docs if you'd like..

We'll also set the DateAdded property in the constructor to set the creation date to whenever this is instantiated.

Next, In the same folder, add another new class called PhotoForReturn.cs. This will represent the data we will return after creation.

public class PhotoForReturn
{
    public int Id { get; set; }
    public string Url { get; set; }
    public string Description { get; set; }
    public DateTime DateAdded { get; set; }
    public bool IsMain { get; set; }
    public string PublicId { get; set; }
}

Adding IPhotoService and PhotoService

First, add a package reference to CloudinaryDotNet in EFConnect.Services.csproj:

<ItemGroup>
    <PackageReference Include="CloudinaryDotNet" Version="1.2.0"/>
</ItemGroup>

Next, we'll add a IPhotoService to our .Contracts folder with two methods : AddPhotoForUser() and SaveAll():

public interface IPhotoService
{
    Task<PhotoForReturn> AddPhotoForUser(int userId, PhotoForCreation photoDto);
    Task<bool> SaveAll();
}

In the .Services folder, add a new class called PhotoService.cs

In the constructor, we'll inject our UserService and Cloudinary settings into the Controller.

We'll also initialize a new Account (from the CloudinaryDotNet namespace) and give it the values from our configuration.

We'll then initialize a new Cloudinary object and pass it our account. The Cloudinary object is what we'll use to access the methods to upload photos.

There is a lot going on in this constructor, and if you'd like to understand this better - you might consult the Cloudinary documentation - it's very thorough!

public class PhotoService : IPhotoService
{
    private readonly IUserService _userService;
    private readonly IOptions<CloudinarySettings> _cloudinaryConfig;
    private Cloudinary _cloudinary;
    private readonly EFConnectContext _context;

    public PhotoService(IUserService userService,
        IOptions<CloudinarySettings> cloudinaryConfig,
        EFConnectContext context)
    {
        _context = context;
        _userService = userService;
        _cloudinaryConfig = cloudinaryConfig;

        Account account = new Account(
            _cloudinaryConfig.Value.CloudName,
            _cloudinaryConfig.Value.ApiKey,
            _cloudinaryConfig.Value.ApiSecret
        );

        _cloudinary = new Cloudinary(account);
    }
}

Now, we'll implement our AddPhotoForUser() method:

public async Task<PhotoForReturn> AddPhotoForUser(int userId, PhotoForCreation photoDto)
{
    var user = await _context                                   //  1.
                .Users
                .Where(u => u.Id == userId)
                .FirstOrDefaultAsync();

    var file = photoDto.File;                                   //  2.

    var uploadResult = new ImageUploadResult();                 //  3.

    if (file.Length > 0)                                        //  4.
    {
        using (var stream = file.OpenReadStream())
        {
            var uploadParams = new ImageUploadParams()
            {
                File = new FileDescription(file.Name, stream)
                Transformation = new Transformation()           //  *
                                .Width(500).Height(500)
                                .Crop("fill")
                                .Gravity("face")
            };

            uploadResult = _cloudinary.Upload(uploadParams);    //  5.
        }
    }

    photoDto.Url = uploadResult.Uri.ToString();                 //  4. (cont'd)
    photoDto.PublicId = uploadResult.PublicId;                  //  4. (cont'd)

    var photo = new Photo                                       //  6.
    {
        Url = photoDto.Url,
        Description = "",
        DateAdded = photoDto.DateAdded,
        PublicId = photoDto.PublicId,
        User = user
    };

    if (!photo.User.Photos.Any(m => m.IsMain))                  //  7.
        photo.IsMain = true;

    user.Photos.Add(photo);
    await SaveAll();                                            //  8.

    return new PhotoForReturn                                   //  9.
    {
        Id = photo.Id,
        Url = photo.Url,
        Description = photo.Description,
        DateAdded = photo.DateAdded,
        IsMain = photo.IsMain,
        PublicId = photo.PublicId,
    };
}

Whew! There's a lot going on here. We'll try to break it down into 6 steps.

  1. Grabbing the user from the database

  2. Creating a file variable holding the path information on the uploaded photo (our IFormFile property)

  3. Creating an instance of ImageUploadResult - holds properties for the uploaded image

  4. Setting the properties on our photoDto from the ImageUploadResult

  5. Calling the Cloudinary Upload() method with our File as our parameters

  6. Creating a new photo from our DTO

  7. Checking if the user already has a photo set to main. If not we set the uploaded photo to their main photo

  8. Saving our photo to the database

  9. Returning a PhotoForReturn - constructed after saving the photo to the database so we know the ID.

Again, Cloudinary's docs are your best source if you would like to understand what's going on better.

One pretty awesome thing to point out is the part indicated with a * above. When we upload an image to Cloudinary - we're telling it to crop it to 500px x 500px. Ok. That's a bit interesting. But, we're also using Cloudinary's algorithm to try to identify the face in a photo and crop it where the face is centered! That's pretty cool for one line of code we have to write.

Next, we'll add the SaveAll() method (it will be the same as in the UserService):

public async Task<bool> SaveAll()
{
    return await _context.SaveChangesAsync() > 0;
}

Registering PhotoService

Finally, we need to register our PhotoService in the ConfigureServices() method in our Startup.cs file in our .API project:

services.AddScoped<IAuthService, AuthService>();
services.AddScoped<IUserService, UserService>();
services.AddScoped<IPhotoService, PhotoService>();      //  <--- Added

Last updated