2
\$\begingroup\$

I have to create model which will handle various types of images in my app.

INTRO

Plan is to have one Image type which will be universal and then extended with specific types of image, for example UserUploadedImage which will have reference to User who uploaded image, privacy level etc.

Currently I have Image type implemented. The idea behind implementation was to create entity which will be behaviour rich, follow SOLID and other OO principles (such as TellDontAsk, etc.).

Also, I wanted to create entity as POCO class which would be independent of any frameworks and persistence logic.

General structure

  • Entity - Entity class, inherited by all other entities
    • Has generic Id and implements IEquitable
  • Image - Entity
  • ImageMemento - implementation of memento pattern
    • I got inspiration for this in book PPPDDD ImageMemento is basically used both as support for undo mechanism and mapping between "real" entity class and its database persisted version (PeristedImage)
  • PeristedImage - handles persistence of Image entity
    • I am using Entity Framework ORM to handle persistence, PersistedImage is object which will be actually stored in database
  • Project also implements UoW and Repository patterns and I will present code for ImageRepository

CODE

One more note about Image. It has to support "Image Variants" - for example if someone uploads 500 x 500 .png image, some of the variants would be: 100 x1 00, 200 x 200 .jpg etc. (different sizes, and types of same image). Original image has to hold reference to all variants.

Image

/// <summary>
///     Purpose of image is name suggest is handling of images
///     <para>
///         Image variants are also supported
///         Image variant is essentially same image but different width, height and url
///     </para>
/// </summary>
/// <seealso cref="Guid" />
/// <autogeneratedoc />
public class Image : Entity<Guid>
{
    private static readonly HashSet<string> SupportedImageExtensions = new HashSet<string>
    {
        "jpg",
        "jpeg",
        "png"
    };

    /// <summary>
    ///     Initializes a new instance of the <see cref="Image" /> class.
    /// </summary>
    /// <param name="name">Image name.</param>
    /// <param name="extension">Image extension.</param>
    /// <param name="width">Image width.</param>
    /// <param name="height">Image height.</param>
    /// <param name="url">Image URL.</param>
    /// <autogeneratedoc />
    public Image(string name, string extension, int width, int height, string url)
    {
        //clans up the data
        name = name.Trim();
        extension = extension.Trim();
        url = url.Trim();

        //Forces the invariants
        //Invariants are attributes or business rules that should never change
        ForceInvariants(name, extension, width, height, url);

        Id = Guid.NewGuid();

        Name = name;
        Extension = extension.ToLowerInvariant();
        Width = width;
        Height = height;
        Url = url.ToLowerInvariant();
        CreatedOn = DateTime.UtcNow;


        Variants = new List<Image>();
    }

    /// <summary>
    ///     Initializes a new instance of the <see cref="Image" /> class from its memento.
    /// </summary>
    /// <param name="memento">The image memento.</param>
    /// <exception cref="ArgumentNullException">memento - Image memento cannot be null</exception>
    /// <autogeneratedoc />
    private Image(ImageMemento memento)
    {
        if (memento == null)
            throw new ArgumentNullException(nameof(memento), "Image memento cannot be null");
        //just in case
        ForceInvariants(memento.Name, memento.Extension, memento.Width, memento.Height, memento.Url);

        if (memento.Id == Guid.Empty)
            throw new ArgumentException("Id cannot be empty", nameof(memento.Id));

        Id = memento.Id;
        Name = memento.Name;
        Extension = memento.Extension;
        Width = memento.Width;
        Height = memento.Height;
        Url = memento.Url;

        CreatedOn = memento.CreatedOn;

        Variants = memento.Variants;
    }


    /// <summary>
    ///     Gets or sets the name of the Image.
    ///     <para>
    ///     </para>
    /// </summary>
    /// <value>
    ///     The name.
    /// </value>
    /// <autogeneratedoc />
    private string Name { get; }

    /// <summary>
    ///     Gets or sets the extension of the image.
    ///     <para>
    ///         Some of supported extensions are: jpg, png and jpeg
    ///     </para>
    /// </summary>
    /// <value>
    ///     The extension.
    /// </value>
    /// <autogeneratedoc />
    private string Extension { get; }

    /// <summary>
    ///     Gets or sets the width of the image in pixels.
    /// </summary>
    /// <value>
    ///     The width.
    /// </value>
    /// <autogeneratedoc />
    private int Width { get; }

    /// <summary>
    ///     Gets or sets the height if the image in pixels.
    /// </summary>
    /// <value>
    ///     The height.
    /// </value>
    /// <autogeneratedoc />
    private int Height { get; }

    /// <summary>
    ///     Gets or sets the URL where image is actually located.
    /// </summary>
    /// <value>
    ///     The URL.
    /// </value>
    /// <autogeneratedoc />
    private string Url { get; }

    /// <summary>
    /// When was image firstly created
    /// </summary>
    /// <value>
    /// Creation date
    /// </value>
    /// <autogeneratedoc />
    private DateTime CreatedOn { get; }

    /// <summary>
    ///     Variants are list of images which are actually same as this image, but with different width, height & URLs
    /// </summary>
    /// <value>
    ///     The variants.
    /// </value>
    /// <autogeneratedoc />
    private ICollection<Image> Variants { get; }


    /// <summary>
    ///     Creates image memento.
    /// </summary>
    /// <returns>Image memento</returns>
    /// <autogeneratedoc />
    public ImageMemento GetMemento()
    {
        return new ImageMemento(Id, Name, Extension, Width, Height, Url, CreatedOn, Variants);
    }

    /// <summary>
    ///     Reverts image to its previous state from the memento
    /// </summary>
    /// <param name="memento">The memento.</param>
    /// <returns>Image reverted from memento</returns>
    /// <autogeneratedoc />
    public static Image CreateFrom(ImageMemento memento)
    {
        return new Image(memento);
    }

    /// <summary>
    ///     Creates the variant of the image.
    /// </summary>
    /// <param name="image">The image variant.</param>
    /// <exception cref="ArgumentNullException">image - Image variant cannot be null</exception>
    /// <autogeneratedoc />
    public void AddVariant(Image image)
    {
        if (image == null)
        {
            throw new ArgumentNullException(nameof(image), "Image variant cannot be null");
        }

        Variants.Add(image);
    }

    /// <summary>
    /// Gets the number of variants.
    /// </summary>
    /// <returns>The number of variants</returns>
    /// <autogeneratedoc />
    public int GetNumberOfVariants()
    {
        return Variants.Count;
    }

    /// <summary>
    ///     Makes sure that business requirements are respected
    /// </summary>
    /// <remarks>
    ///     More info: https://en.wikipedia.org/w/index.php?title=Class_invariant
    /// </remarks>
    /// <param name="name">Image name</param>
    /// <param name="extension">Image extension.</param>
    /// <param name="width">Image width.</param>
    /// <param name="height">Image height.</param>
    /// <param name="url">Image URL.</param>
    /// <exception cref="ArgumentNullException">
    ///     name - Image name cannot be empty
    ///     or
    ///     extension - Image extension cannot be empty
    ///     or
    ///     url - Image url cannot be empty
    /// </exception>
    /// <exception cref="ArgumentException">
    ///     Image name cannot be longer than 50 chars - name
    ///     or
    ///     Image extension cannot be longer than 5 chars - extension
    ///     or
    ///     Image extension not supported
    ///     or
    ///     Provided url is not valid - url
    /// </exception>
    /// <exception cref="ArgumentOutOfRangeException">
    ///     width - Image width cannot be less than 1px
    ///     or
    ///     width - Width is too large it cannot be over 4000px
    ///     or
    ///     height - Image height cannot be less than 1px
    ///     or
    ///     width - Height is too large it cannot be over 4096px
    /// </exception>
    /// <autogeneratedoc />
    private void ForceInvariants(string name, string extension, int width, int height, string url)
    {
        if (string.IsNullOrEmpty(name))
            throw new ArgumentNullException(nameof(name), "Image name cannot be empty");

        if (name.Length > 50)
            throw new ArgumentException("Image name cannot be longer than 50 chars", nameof(name));

        if (string.IsNullOrEmpty(extension))
            throw new ArgumentNullException(nameof(extension), "Image extension cannot be empty");

        if (extension.Length > 5)
            throw new ArgumentException("Image extension cannot be longer than 5 chars", nameof(extension));

        if (!SupportedImageExtensions.Contains(extension))
            throw new ArgumentException("Image extension not supported");

        if (width <= 0)
            throw new ArgumentOutOfRangeException(nameof(width), "Image width cannot be less than 1px");

        //note this is 4k (4096 x 2160) resolution
        if (width > 4096)
            throw new ArgumentOutOfRangeException(nameof(width), "Width is too large it cannot be over 4000px");

        if (height <= 0)
            throw new ArgumentOutOfRangeException(nameof(height), "Image height cannot be less than 1px");

        //note this is even more than 4k (4096 x 2160) resolution
        if (height > 4096)
            throw new ArgumentOutOfRangeException(nameof(width), "Height is too large it cannot be over 4096px");

        if (string.IsNullOrEmpty(url))
            throw new ArgumentNullException(nameof(url), "Image url cannot be empty");

        //we have to check whether provided url is actually valid url
        //according to SO post this is the best way to do it
        //more info: https://stackoverflow.com/a/7581824/1284262
        var urlIsValid = Uri.TryCreate(url, UriKind.Absolute, out var uriResult)
                         && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
        if (!urlIsValid)
            throw new ArgumentException("Provided url is not valid", nameof(url));
    }
}

ImageMemento

/// <summary>
///     This is Memento for the Image class
/// </summary>
/// <remarks>
///     <para>
///         Memento pattern was suggested in book PPP of DDD
///     </para>
///     <para>
///         For more info about memento got o this link:
///         http://www.dofactory.com/net/memento-design-pattern
///     </para>
/// </remarks>
/// <autogeneratedoc />
public class ImageMemento
{
    /// <summary>
    ///     Initializes a new instance of the <see cref="ImageMemento" /> class.
    /// </summary>
    /// <autogeneratedoc />
    public ImageMemento()
    {
        Variants = new List<Image>();
    }

    /// <summary>
    ///     Initializes a new instance of the <see cref="ImageMemento" /> class.
    /// </summary>
    /// <param name="id">The identifier.</param>
    /// <param name="name">The name.</param>
    /// <param name="extension">The extension.</param>
    /// <param name="width">The width.</param>
    /// <param name="height">The height.</param>
    /// <param name="url">The URL.</param>
    /// <param name="variants">The variants.</param>
    /// <autogeneratedoc />
    public ImageMemento(Guid id, string name, string extension, int width, int height, string url, DateTime createdOn,
        ICollection<Image> variants)
    {
        Id = id;
        Name = name;
        Extension = extension;
        Width = width;
        Height = height;
        Url = url;
        CreatedOn = createdOn;
        Variants = variants;
    }

    /// <summary>
    ///     Gets or sets the identifier.
    /// </summary>
    /// <value>
    ///     The identifier.
    /// </value>
    /// <autogeneratedoc />
    public Guid Id { get; }

    /// <summary>
    ///     Gets or sets the name of the Image.
    ///     <para>
    ///     </para>
    /// </summary>
    /// <value>
    ///     The name.
    /// </value>
    /// <autogeneratedoc />
    public string Name { get; }

    /// <summary>
    ///     Gets or sets the extension of the image.
    ///     <para>
    ///         Supported extensions are: jpg, png and jpeg
    ///     </para>
    /// </summary>
    /// <value>
    ///     The extension.
    /// </value>
    /// <autogeneratedoc />
    public string Extension { get; }

    /// <summary>
    ///     Gets or sets the width of the image in pixels.
    /// </summary>
    /// <value>
    ///     The width.
    /// </value>
    /// <autogeneratedoc />
    public int Width { get; }

    /// <summary>
    ///     Gets or sets the height if the image in pixels.
    /// </summary>
    /// <value>
    ///     The height.
    /// </value>
    /// <autogeneratedoc />
    public int Height { get; }

    /// <summary>
    ///     Gets or sets the URL where image is actually located.
    /// </summary>
    /// <value>
    ///     The URL.
    /// </value>
    /// <autogeneratedoc />
    public string Url { get; }

    /// <summary>
    ///     Variants are list of images which are actually same as this image, but with different width, height & URLs
    /// </summary>
    /// <value>
    ///     The variants.
    /// </value>
    /// <autogeneratedoc />
    public ICollection<Image> Variants { get; }
    /// <summary>
    /// When was image firstly created
    /// </summary>
    /// <value>
    /// Creation date
    /// </value>
    /// <autogeneratedoc />
    public DateTime CreatedOn { get;  }

}

PersistedImage

/// <summary>
/// Purpose of this class is to map actual <see cref="Image"/> entity class to this model which will be persisted to database
/// </summary>
/// <autogeneratedoc />
public class PersistedImage 
{
    public PersistedImage()
    {

    }

    public PersistedImage(Guid id, string name, string extension, int width, int height, string url, DateTime createdOn)
    {
        Id = id;
        Name = name;
        Extension = extension;
        Width = width;
        Height = height;
        Url = url;
        CreatedOn = createdOn;
        ParentImage = null;
    }

    public PersistedImage(Guid id, string name, string extension, int width, int height, string url,DateTime createdOn,
        PersistedImage parent) : this(id, name, extension, width, height, url, createdOn)
    {
        ParentImageId = parent.Id;
        ParentImage = parent;
    }



    /// <summary>
    ///     Gets or sets the identifier.
    /// </summary>
    /// <value>
    ///     The identifier.
    /// </value>
    /// <autogeneratedoc />
    public Guid Id { get; set; }

    /// <summary>
    ///     Gets or sets the name of the Image.
    ///     <para>
    ///     </para>
    /// </summary>
    /// <value>
    ///     The name.
    /// </value>
    /// <autogeneratedoc />
    public string Name { get; set; }

    /// <summary>
    ///     Gets or sets the extension of the image.
    ///     <para>
    ///         Supported extensions are: jpg, png and jpeg
    ///     </para>
    /// </summary>
    /// <value>
    ///     The extension.
    /// </value>
    /// <autogeneratedoc />
    public string Extension { get; set; }

    /// <summary>
    ///     Gets or sets the width of the image in pixels.
    /// </summary>
    /// <value>
    ///     The width.
    /// </value>
    /// <autogeneratedoc />
    public int Width { get; set; }

    /// <summary>
    ///     Gets or sets the height if the image in pixels.
    /// </summary>
    /// <value>
    ///     The height.
    /// </value>
    /// <autogeneratedoc />
    public int Height { get; set; }

    /// <summary>
    ///     Gets or sets the URL where image is actually located.
    /// </summary>
    /// <value>
    ///     The URL.
    /// </value>
    /// <autogeneratedoc />
    public string Url { get; set; }

    /// <summary>
    /// When was image firstly created
    /// </summary>
    /// <value>
    /// Creation date
    /// </value>
    /// <autogeneratedoc />
    public DateTime CreatedOn { get; set; } 

    public Guid? ParentImageId { get; set; }
    /// <summary>
    /// Gets or sets the parent image.
    /// <para>
    /// Parent image is image from which this image was created as variant
    /// If null, this is original image
    /// </para>
    /// </summary>
    /// <value>
    /// The parent image.
    /// </value>
    /// <autogeneratedoc />
    public PersistedImage ParentImage { get; set; }


    /// <summary>
    ///     Maps the specified memento to persisted image.
    /// </summary>
    /// <param name="memento">The memento.</param>
    /// <returns></returns>
    /// <autogeneratedoc />
    public static PersistedImage Map(ImageMemento memento)
    {
        return new PersistedImage(memento.Id, memento.Name, memento.Extension, memento.Width, memento.Height,
            memento.Url, memento.CreatedOn);
    }

    /// <summary>
    ///     Maps the specified memento to persisted image.
    /// </summary>
    /// <param name="memento">The memento.</param>
    /// <param name="parentImage">The parent image.</param>
    /// <returns></returns>
    /// <autogeneratedoc />
    public static PersistedImage Map(ImageMemento memento, PersistedImage parentImage)
    {
        return new PersistedImage(memento.Id, memento.Name, memento.Extension, memento.Width, memento.Height,
            memento.Url, memento.CreatedOn, parentImage);
    }

    /// <summary>
    ///     Maps the specified image to image memento.
    /// </summary>
    /// <param name="image">The image.</param>
    /// <param name="images">The images.</param>
    /// <returns></returns>
    /// <autogeneratedoc />
    public static ImageMemento Map(PersistedImage image, ICollection<Image> images)
    {
        return new ImageMemento(image.Id, image.Name, image.Extension, image.Width, image.Height, image.Url,
            image.CreatedOn, images);
    }
}

ImageRepository

/// <inheritdoc />
/// <summary>
///     Repository for dealing with various images
/// </summary>
/// <seealso cref="!:Master.Domain.DataAccessLayer.Repository.Generic.MasterRepository{Master.Domain.Model.Image.PersistedImage, System.Guid}" />
/// <seealso cref="T:Master.Domain.DataAccessLayer.Repository.Image.IImageRepository" />
/// <autogeneratedoc />
public class ImageRepository : MasterRepository<PersistedImage, Guid>, IImageRepository
{
    /// <inheritdoc />
    /// <summary>
    ///     Initializes a new instance of the <see cref="T:Master.Domain.DataAccessLayer.Repository.Image.ImageRepository" />
    ///     class.
    /// </summary>
    /// <param name="unitOfWork">The unit of work.</param>
    /// <autogeneratedoc />
    public ImageRepository(IUnitOfWork unitOfWork) : base(unitOfWork)
    {
    }

    /// <inheritdoc />
    /// <summary>
    ///     Adds the image to repository.
    /// </summary>
    /// <param name="image">The image to add.</param>
    /// <exception cref="T:System.ArgumentNullException">image - Image cannot be null</exception>
    /// <autogeneratedoc />
    public void Add(Model.Image.Image image)
    {
        if (image == null)
            throw new ArgumentNullException(nameof(image), @"Image cannot be null");
        var persistedImage = PersistedImage.Map(image.GetMemento());
        DbSet.Add(persistedImage);
    }

    /// <inheritdoc />
    /// <summary>
    ///     Adds the image to repository with all the variants
    /// </summary>
    /// <param name="image">The image.</param>
    /// <exception cref="T:System.ArgumentNullException">image - Image cannot be null</exception>
    /// <autogeneratedoc />
    public void AddWithVariants(Model.Image.Image image)
    {
        if (image == null)
            throw new ArgumentNullException(nameof(image), @"Image cannot be null");

        //just in case check if there are actually any variants, if not just image as it is
        if (image.GetNumberOfVariants() < 1)
            Add(image);

        var imgMemento = image.GetMemento();
        var imageToPersist = PersistedImage.Map(imgMemento);

        DbSet.Add(imageToPersist);

        foreach (var variant in imgMemento.Variants)
        {
            var variantMemento = variant.GetMemento();
            DbSet.Add(PersistedImage.Map(variantMemento, imageToPersist));
        }
    }

    /// <inheritdoc />
    /// <summary>
    ///     Adds the variant of the image.
    /// </summary>
    /// <param name="image">The image to which variant is added.</param>
    /// <param name="imageVariant">Image variant</param>
    /// <exception cref="T:System.ArgumentNullException">image - Image cannot be null</exception>
    /// <autogeneratedoc />
    public void AddVariant(Model.Image.Image image, Model.Image.Image imageVariant)
    {
        if (image == null)
            throw new ArgumentNullException(nameof(image), @"Image cannot be null");

        //firstly we add variant to the list of variants in image object
        image.AddVariant(imageVariant);
        //then we get persistence model
        var parentImagePersisted = PersistedImage.Map(image.GetMemento());
        //then we get persistence model of the variant
        var imageVariantToPersist = PersistedImage.Map(imageVariant.GetMemento(), parentImagePersisted);
        //finally we actually persist it
        DbSet.Add(imageVariantToPersist);
    }

    /// <inheritdoc />
    /// <summary>
    ///     Gets image the by id without variants
    /// </summary>
    /// <param name="id">The identifier.</param>
    /// <returns>
    ///     The image
    /// </returns>
    /// <autogeneratedoc />
    public async Task<Model.Image.Image> GetByIdWithoutVariantsAsync(Guid id)
    {
        //gets image from db
        //but it is in its persistence model
        var persistedImage = await GetAsnyc(id);

        //so we have to map it to actual image
        return persistedImage == null
            ? null
            : Model.Image.Image.CreateFrom(PersistedImage.Map(persistedImage, new List<Model.Image.Image>()));
    }

    /// <inheritdoc />
    /// <summary>
    ///     Gets the image with all variants included.
    /// </summary>
    /// <param name="id">The identifier.</param>
    /// <returns>
    ///     Image with all the variants
    /// </returns>
    /// <autogeneratedoc />
    public async Task<Model.Image.Image> GetByIdAsync(Guid id)
    {
        var rootPersistedImage = await DbSet.FirstOrDefaultAsync(r => r.Id == id);

        if (rootPersistedImage == null)
            return null;

        var variants = (await DbSet.Where(r => r.ParentImageId == rootPersistedImage.Id).ToListAsync())
            .Select(r => Model.Image.Image.CreateFrom(PersistedImage.Map(r, new List<Model.Image.Image>()))).ToList();
        var rootImageMemento = PersistedImage.Map(rootPersistedImage, variants);

        return Model.Image.Image.CreateFrom(rootImageMemento);
    }

    /// <inheritdoc />
    /// <summary>
    ///     Finds the invariants which satisfy given condition.
    /// </summary>
    /// <param name="image"></param>
    /// <param name="condition">The condition.</param>
    /// <returns>List of image variants which satisfy condition</returns>
    /// <autogeneratedoc />
    public async Task<List<Model.Image.Image>> FindVariantsAsync(Model.Image.Image image,
        Expression<Func<PersistedImage, bool>> condition)
    {
        var persistedVariants = await DbSet.Where(r => r.ParentImageId == image.Id).Where(condition).ToListAsync();
        return persistedVariants.Select(r => Model.Image.Image.CreateFrom(PersistedImage.Map(r, new List<Model.Image.Image>())))
            .ToList();
    }
}
\$\endgroup\$
  • \$\begingroup\$ What is the scope of DbSet and what s its life span? \$\endgroup\$ – Gert Arnold Sep 20 '17 at 18:30
  • \$\begingroup\$ @GertArnold thanks for your comment! DbSet is exposed trough Unit of Work. It goes like this: UoW is injected in Repos and Services. In UoW DbContextFactory (which is singleton factory) is injected. UoW in constructor calls DbContextFactory which creates new DbContext instance. Both DbContext and UoW live during one request. This is ensured by Ninject: kernel.Bind<IUnitOfWork>().To<UnitOfWork>().InRequestScope(); \$\endgroup\$ – hyperN Sep 21 '17 at 4:31
  • \$\begingroup\$ @GertArnold I have also posted my UoW & Repo implementations here on code review before, you can find more details here \$\endgroup\$ – hyperN Sep 21 '17 at 4:32
  • \$\begingroup\$ Hi I like your code but there are a couple of things that I dont get. Could you post an example of how to perform "undo"? Do you really need to separate between image and persisted image or I just getting confuse because of the names? \$\endgroup\$ – ogomrub Oct 9 '17 at 11:27
  • \$\begingroup\$ Hey, I am not actually performing undo, I haven't implemented this functionality yet, as I don't find it necessary at the moment. I decided to completely separate Image and persisted image mainly because of Entity Framework, I wanted to have my entities "clean" from ORM stuff. For example Image has list of variants, but PersistedImage has pointer to ParentImage - that is how I decided to design "Varaint" handling with EF. Up to my knowledge I believe this separation wouldn't be necessary with NHibernate because it seems to be much better ORM, but I haven't had any experience with it. \$\endgroup\$ – hyperN Oct 9 '17 at 18:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.