Shrinkr - Url Shrinking Service Developed with Entity Framework 4.0, Unity, ASP.NET MVC And jQuery (Part 1)

Creating a full blown url shrinking service was pocking around in my mind for quite some time(of course by using Twitter). Since I heard quite a few good things on Entity Framework 4.0, so I decided to start with it. The first thing I usually do when developing an application is creating the domain model. But to create the domain model, we first have to define the basic functionalities:

  • The system will only use Open ID for authentication.
  • User should be able to shrink url without logging in.
  • When shrinking url, user should be able to specify alias, if alias is not specified, the system will auto generate it.
  • Shrinked url will also have a associated webpage preview image.
  • The system will maintain statistic of shrinked url like number of visit, referrer domain, geographic data etc. (requires login)
  • The user should be able to reset shrinked url statistics. (requires login)
  • Should have a REST service for creating shrinked url which will work upon the daily limit that was previously set.
  • It should have nice web 2.0 style interface and should support adaptive rendering.

For the above functionalities, I come up with the following Domain Entities:

DomainObjects

As you can see most of entities are nothing but some getter/setter properties, please don’t think it as a anemic domain model, in fact the url shrinking service does not have the kind of behaviors that you can put into your entities. When creating the entities one important thing I did was making the properties virtual, so that Entity Framework can lazy load the associated objects (although it is not necessary for the intrinsic data types). For example, the following shows the codes of User and Alias:

User

namespace Shrinkr
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Diagnostics;
    using System.Linq;

    public class User : IEntity
    {
        private ApiSetting apiSetting;

        public User()
        {
            CreatedAt = SystemTime.Now();
            LastActivityAt = SystemTime.Now();
            Aliases = new List<Alias>();
        }

        public virtual long Id
        {
            get;
            set;
        }

        public virtual string Name
        {
            get;
            set;
        }

        public virtual string Email
        {
            get;
            set;
        }

        public virtual bool IsLockedOut
        {
            get;
            set;
        }

        public virtual DateTime CreatedAt
        {
            get;
            set;
        }

        public virtual DateTime LastActivityAt
        {
            get;
            set;
        }

        public Role Role
        {
            [DebuggerStepThrough]
            get
            {
                return (Role) InternalRole;
            }

            [DebuggerStepThrough]
            set
            {
                InternalRole = (int) value;
            }
        }

        [EditorBrowsable(EditorBrowsableState.Never)]
        public virtual int InternalRole
        {
            get;
            set;
        }

        public virtual IList<Alias> Aliases
        {
            get;
            private set;
        }

        public virtual ApiSetting ApiSetting
        {
            [DebuggerStepThrough]
            get
            {
                if (apiSetting == null)
                {
                    apiSetting = new ApiSetting();
                }

                return apiSetting;
            }

            [DebuggerStepThrough]
            set
            {
                Check.Argument.IsNotNull(value, "value");

                apiSetting = value;
            }
        }

        public virtual bool CanAccessApi
        {
            get
            {
                bool canAccess=  (ApiSetting != null) &&
                                 (ApiSetting.Allowed.GetValueOrDefault()) &&
                                 (ApiSetting.DailyLimit == ApiSetting.InfiniteLimit || ApiSetting.DailyLimit > 0);

                return canAccess;
            }
        }

        public virtual void AllowApiAccess(int dailyLimit)
        {
            if (dailyLimit != ApiSetting.InfiniteLimit)
            {
                Check.Argument.IsNotNegative(dailyLimit, "dailyLimit");
            }

            if (string.IsNullOrEmpty(ApiSetting.Key))
            {
                ApiSetting.Key = Guid.NewGuid().ToString().ToUpperInvariant();
            }

            ApiSetting.Allowed = true;
            ApiSetting.DailyLimit = dailyLimit;
        }

        public virtual bool HasExceedsDailyLimit()
        {
            DateTime lastOneDay = SystemTime.Now().AddDays(-1);

            bool exceeded = CanAccessApi &&
                            ((ApiSetting.DailyLimit != ApiSetting.InfiniteLimit) &&
                             (ApiSetting.DailyLimit <= Aliases.Count(alias => alias.CreatedAt > lastOneDay && alias.CreatedByApi)));

            return exceeded;
        }
    }
}

Alias:

namespace Shrinkr
{
    using System;
    using System.Collections.Generic;

    public class Alias : IEntity
    {
        public Alias()
        {
            Visits = new List<Visit>();
            CreatedAt = SystemTime.Now();
        }

        public virtual long Id
        {
            get;
            set;
        }

        public virtual string Name
        {
            get;
            set;
        }

        public virtual string IPAddress
        {
            get;
            set;
        }

        public virtual DateTime CreatedAt
        {
            get;
            set;
        }

        public virtual IList<Visit> Visits
        {
            get;
            private set;
        }

        public virtual User User
        {
            get;
            set;
        }

        public virtual ShortUrl ShortUrl
        {
            get;
            set;
        }
    }
}

Next, define the Repositories, in the above entities there are two aggregate root User and ShortUrl, so we will create repositories for those two:

Repositories:

Repositories

The last thing in the domain model is the Services. Please don’t confuse the Service with the Web Service or something else, here Service refers to some domain logic which does not belongs to entities or repositories, usually these services are called from the presentation layer in our case the ASP.NET MVC Controllers. In this application, we do have few things that directly does not belongs to the above entities or repositories, For example, shrinking url, ensuring unique alias etc etc.

Services:

Services

If you are wondering about the purpose of FindByUser and GetByAlias method of the above IShortUrlService as they already exits in IShortUrlRepository, let me tell you that returning Domain Entities directly in presentation layer is not a good practise, instead you should create some Data Transfer Objects AKA DTO for returning those. The above two methods should do those kind of mappings - flattering the object hierarchy, so that we can easily map it in the UI and do serialization when required. In this application we will have the following two dtos:


DTOs:

DataTransferObjects 

So far we have discussed about application domain model, In the next post, we will disscuss about the domain model mapping to database with Entity Framework 4.0.

Stay tuned!!!

Shout it

10 Comments

  • Quote "returning Domain Entities directly in presentation layer is not a good practice, instead you should create some Data Transfer Objects AKA DTO for returning those."
    Why it is not a good practice! as long as the entities itself is lightweight. As well as they are pure POCO!

    Not sure about the accuracy of being not a good practice! as I guess this will return to the purpose of the application and the design of the models.
    I would do that if I was using EF v1 because the entity itself is not POCO. And as you know and did KiGG you overcome this by passing the abstract interface to Presentation!!

    Still the design was good, has few things to tweak but not the hard need to to refactor the return of services to DTO instead of Model Interfaces!

    Would love to hear other opinions regarding this.

  • If your service tracks stats, you should provide a URL endpoint to reverse the URL without it counting against the stats (like bit.ly does).

  • @mosessaur Domain objects are optimized for performing domain logic and they should live in the domain model boundary on the other hand DTOs are created for transferring the data between layer/tier which means they do not have any behavior (no method only get/set property). It is very common that the DTO's are build from different Domain Objects and optimized for serialization which also reduce the multiple service layer call.

    I know this is not the best of the explanation, there are some excellent article available which I think you should also consult.

  • @John Sheehan: Did you mean a REST service that expand the compressed url?

  • @kazimanzurrashid Will I got your point! maybe not convinced but it is fine :o) I will do more studies about it to convince myself :o)

    Thank you Rashid.

  • hi rashid!

    excellent blog post, i'm really looking forward to the rest of this series! i've been considering using your articles as a model for building an app of my own (but a different app, so i actually have to think when i do each implementation step... ;) ).

    i would like to learn more about other view engines than the default one. is it possible for you to use a non-standard view engine for this project - so i can learn? =)

    keep up the good work!

  • Good stuff kazi....

  • @tomas : Sure I do have plan to include Spark along with the Default.

  • great, thanks! if i was looking forward to this before, i'm *really* looking forward to it now! ;)

  • Are you planning to publish the code of this series either on your blog or maybe on codeplex so that we all can really see all the classes/dependencies and learn a thing or two from you?

Comments have been disabled for this content.