Generic Repository Entity Framework Part I

Generic mengacau pada teknik menulis kode untuk sebuah class tanpa menentukan tipe datanya. Dengan menggunakan Generic class kita tidak perlu menulis implementasi repository untuk semua class yang ada. Intinya adalah memaksimalkan code reuse.

C# juga menyediakan feature pada generic untuk membatasi tipe apa saja yang bisa menjadi parameter class, sebagai contoh parameter T pada interface IGenericRepository dibatasi pada tipe class saja.

Pertama kita akan buat interface, ya usahakan selalu membuat abstraksi sebelum membuat implementasi class untuk memudahkan unit test dan mengurangi tingkat depency antar class.

public interface IGenericRepository where TEntity : class
        {
            IEnumerable Get(Expression<Func<TEntity, bool>> filter = null, Func<IQueryable, System.Linq.IOrderedQueryable> orderBy = null, string includeProperties = "");

            TEntity GetById(object id);

            TEntity GetById(params object[] keyValues);

            void Insert(TEntity entity);

            void Update(TEntity entityToUpdate);

            void InsertOrUpdate(TEntity entity);

            void Delete(object id);

            void Delete(TEntity entityToDelete);

            bool Exists(TEntity entity);

            long Count(Expression<Func<TEntity, bool>> filter = null);
        }

Selanjutnya kita buat class konkrit untuk implentasi dari IGenericRepository ini. Kita menggunakan Entity Framework. Jika kita menggunakan provider lain buat implementasi yang berbeda.

public class EFGenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class
     {
         internal BookStoreEntities Context;
         internal DbSet<TEntity> Entities;

         public EFGenericRepository(BookStoreEntities dbContext)
         {
             Context = dbContext;
             Entities = dbContext.Set<TEntity>();
         }

         /// <summary>
         /// Gets the specified filter.
         /// </summary>
         /// <param name="filter">The filter.</param>
         /// <param name="orderBy">The order by.</param>
         /// <param name="includeProperties">The include properties.</param>
         /// <returns></returns>
         public IEnumerable<TEntity> Get(Expression<Func<TEntity, bool>> filter = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, string includeProperties = "")
         {
             IQueryable<TEntity> query = Entities;

             if (filter != null)
             {
                 query = query.Where(filter);
             }

             foreach (var includeProperty in includeProperties.Split
                 (new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
             {
                 query = query.Include(includeProperty);
             }

             if (orderBy != null)
             {
                 return orderBy(query).ToList();
             }

             return query.ToList();
         }

         /// <summary>
         /// Get entity the by identifier.
         /// </summary>
         /// <param name="id">The identifier.</param>
         /// <returns></returns>
         public TEntity GetById(object id)
         {
             return Entities.Find(id);
         }

         /// <summary>
         /// Get entity by identifiers.
         /// </summary>
         /// <param name="keyValues">The key values.</param>
         /// <returns></returns>
         public TEntity GetById(params object[] keyValues)
         {
             return Entities.Find(keyValues);
         }

         /// <summary>
         /// Inserts the specified entity.
         /// </summary>
         /// <param name="entity">The entity.</param>
         public void Insert(TEntity entity)
         {
             Entities.Add(entity);
         }

         /// <summary>
         /// Updates the specified entity.
         /// </summary>
         /// <param name="entityToUpdate">The entity to update.</param>
         public void Update(TEntity entityToUpdate)
         {
             Context.Entry(entityToUpdate).State = EntityState.Modified;
         }

         /// <summary>
         /// Inserts the entity if not exist or update the entity if exist.
         /// </summary>
         /// <param name="entity">The entity.</param>
         public void InsertOrUpdate(TEntity entity)
         {
             if (!Exists(entity))
                 Insert(entity);
             else
                 Update(entity);
         }

         /// <summary>
         /// Deletes entity by identifier.
         /// </summary>
         /// <param name="id">The identifier.</param>
         public void Delete(object id)
         {
             var entityToDelete = Entities.Find(id);
             Entities.Remove(entityToDelete);
         }

         /// <summary>
         /// Deletes the specified entity.
         /// </summary>
         /// <param name="entityToDelete">The entity to delete.</param>
         public void Delete(TEntity entityToDelete)
         {
             if (Context.Entry(entityToDelete).State == EntityState.Detached)
             {
                 Entities.Attach(entityToDelete);
             }
             Entities.Remove(entityToDelete);
         }

         /// <summary>
         /// Check if the entity exist.
         /// </summary>
         /// <param name="entity">The entity.</param>
         /// <returns></returns>
         public bool Exists(TEntity entity)
         {
             var objContext = ((IObjectContextAdapter)Context).ObjectContext;
             var objSet = objContext.CreateObjectSet<TEntity>();
             var entityKey = objContext.CreateEntityKey(objSet.EntitySet.Name, entity);

             Object foundEntity;
             var exists = objContext.TryGetObjectByKey(entityKey, out foundEntity);
             return (exists);
         }

         /// <summary>
         /// Counts the specified filter.
         /// </summary>
         /// <param name="filter">The filter.</param>
         /// <returns></returns>
         public long Count(Expression<Func<TEntity, bool>> filter = null)
         {
             IQueryable<TEntity> query = Entities;

             if (filter != null)
             {
                 query = query.Where(filter);
             }
             return query.Count();
         }
     }

 

Untuk jaminan proses transaksi database atomic (satu gagal, gagal semua). Kita akan membuat class UnitOfWork yang bertanggung jawab menangani proses transaksi di tulisan yang lain.

Func dan Expression C#

Perhatikan senarai kode program di bawah ini.

Expression<Func<TEntity, bool>> filter

Func adalah sebuah generic delegate – delegate adalah method yang di lewatkan sebagai parameter dalam  method yang lain. Func tersebut memiliki parameter TEntity dan nilai kembali bertipe bool.  Contoh:

func

Sedangkan Expression<Func> menghasilkan tree data struktur untuk lamba expression. Karena kembalian Func adalah bool, kita bisa gunakan expression tersebut sebagai Lamba Expression pada Linq .Where

exp

Clean Code – Function Part II

Kondisi

Sebuah fungsi bisa jadi memiliki banyak pengecekan kondisi di dalamnya. Kita bisa memastikan bahwa tiap kondisi dilakukan pada low-level class yaitu dengan menggunakan polymorphisme dan inheritance.

public abstract class Employee
        {
            public double BasicSalary;
            public abstract double CalculatePayment();
        }

Dari potongan kode program di atas, semakin banyak tipe customer dan semakin kompleks kalkulasi payment tiap tipe customer, akan semakin panjang kode program pada fungsi CalculatePay() sehingga melanggar prinsip bahwa sebuah fungsi haruslah kecil. Mari kita refactor menjadi beberapa implementasi pada low-level class seperti ini.

Kita bisa menggunakan interface ataupun class abstract. Selanjutnya kita buat 2 konkrit class yaitu class Sales dan Manager, masing-masing mengimplementasikan operasi CalculatePay() secara berbeda.

public class Sales : Employee
       {
           private const double COMMISION_PERCENTAGE = 0.2;

           public override double CalculatePayment()
           {
               return BasicSalary + (COMMISION_PERCENTAGE * BasicSalary);
           }
       }

       public class Manager : Employee
       {

           private const double BONUS_PERCENTAGE = 0.5;

           public override double CalculatePayment()
           {
               return BasicSalary + (BONUS_PERCENTAGE * BasicSalary);
           }
       }

Untuk menentukan class mana yang sesuai, kita bisa menggunakan Factory Pattern. Melalui Factory Pattern sebuah kelas mendelagasikan tanggung jawab pembuatan objectnya.

public class EmployeeFactory
        {
            public Employee MakeEmployee(EmployeeType employeeType)
            {
                switch (employeeType)
                {
                    case EmployeeType.Sales:
                        return new Sales();
                    case EmployeeType.Manager:
                        return new Manager();
                    default:
                        throw new Exception("Incorrect Employee");
                }
            }
        }

Bentuk akhir dari class PayrollService jadinya seperti ini.

public class PayrollService
        {
            private Employee _employee;
            public PayrollService(EmployeeType employeeType)
            {
                _employee = new EmployeeFactory().MakeEmployee(employeeType);
            }
            public double GetEmployeeSalary(double basicSalary)
            {
                _employee.BasicSalary = basicSalary;
                return _employee.CalculatePayment();
            }
        }

 

 

Clean Code – Function Part I

Sebuah fungsi  ideal adalah fungsi yang hanya memiliki 0 argument (cuman ini sulit untuk dihindari). Sebisa mungkin maksimum jumlah argument pada sebuah fungsi tidak lebih dari 3 argument.

employee.CalculateSalary();

Jika argumen sebuah fungsi lebih dari 2 buah, sebaiknya dibungkus dalam sebuah class. Ingat penamaan fungsi harus mengandung kata kerja (Verb) dan keyword.

Circle MakeCircle(double x, double y, double radius)

Argument x dan y lebih baik kita bungkus dalam satu class seperti ini:

Circle MakeCircle(Point center, double radius)

Sebuah fungsi haruslah kecil. Jika kita menulis sebuah fungsi yang sangat panjang kita bisa membaginya menjadi beberapa fungsi yang lebih kecil.  Fungsi yang kecil adalah fungsi yang terdiri dari kurang 150 karakter per barisnya dan  tidak lebih dari 20 baris.

Sebuah fungsi harusnya hanya focus melakukan satu hal saja dan hanya memiliki 1 high level abstraksi seperti contoh di bawah ini, High level abstraksinya adalah CreateTestPage()

public void CreateTestPage()
            {
                IncludeSetups();
                IncludeTestPageContent();
                IncludeTeardowns();
            }

Clean Code – Naming Part II

Lanjutan dari Clean Code – Naming Part I

Mudah dilafalkan

Gunakanlah penamaan yang mudah untuk dilafalkan sehingga memudahkan kita mendiskusikannya dengan teman kerja secara verbal. Contoh penamaan yang tidak baik seperti di bawah ini.

public class DtPrsn
        {
            public int DtKy { get; set; }
            public string FrstNm { get; set; }
            public string LstNm { get; set; }
            public DateTime DtoBrth { get; set; }
        }

Bandingkan jika diubah menjadi seperti ini.

public class Person
        {
            public int Id { get; set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }
            public DateTime DateOfBirth { get; set; }
        }

Mudah dicari

Jika kode program yang kita tulis sudah sangat banyak, tentulah akan semakin memakan waktu untuk mencari variable tertentu yang ingin kita modifikasi nilainya.

if (person.Gender == 0)

Jika kita menulisnya secara hardcode seperti di potongan kode di atas, maka pada saat kita ingin memodifikasi nilai Gender untuk Female menjadi 1 dan kondisi tersebut sudah dipanggil diberbagai tempat  sehingga akan sangat berisiko jika kita melakukan replace 0 menjadi 1. Karena bisa jadi beberapa enum menggunakan nilai 0 juga untuk membernya. Buatlah dalam bentuk enum atau konstanta.

if (person.Gender == (int)Gender.Female)

Penamaan Class and Method

Penamaan class haruslah mengandung kata benda (noun) seperti Customer, Account, hindari penamaan seperti Calculate, Processor. Sedangkan penamaan method harusnya mengandung kata kerja (verb)  dan Keyword seperti SaveCustomer, DeletePage, PostPayment.

Semakin pendek penamaan kita, selama itu jelas dan tidak membingungkan adalah semakin bagus. Tapi jangan sampai menimbulkan keambiguan. Contoh, Address adalah contoh yang tepat penamaan sebuah object (instance dari class) tapi bisa tidak begitu bagus sebagai nama class jika kita ingin membedakannya dengan MAC Address, Port Address. Nah, disini kita bisa memilih penamaan  menjadi PostalAddress.

Clean Code – Naming Part I

Programming is the art of telling another human what one wants the computer to do.

— Donald Knuth

Clean code diperkenalkan oleh Rober C. Martin dalam bukunya:  Clean Code: A Handbook of Agile Software Craftsmanship.  Clean code adalah bagaimana kode program yang kita tulis mudah dimengerti dan mudah dimodifikasi. Jika kita menulis dengan Clean Code akan memudahkan diri kita dan juga teman kerja kita di kemudian hari untuk melakukan perbaikan bug maupun modifikasi kode program.

Penamaan (Naming)

Jelas

Kita melakukan penamaan dihampir seluruh code program, entah itu menamakan variablel, fungsi, argument ataupun class. Penamaan yang benar menguras banyak waktu, tapi membuat waktu pembacaan kode program lebih efisien di kemudian hari.  Semakin banyak komentar  yang kita butuhkan untuk menjelaskan maksud penamaan kita, semakin tidak bagus penamaan kita tersebut.

int d; //elapsed time in days

Variable d pada senarai program di atas tidak mengungkapkan sesuatu yang begitu jelas bagi kita tanpa membaca komentar. Alangkah baiknya jika kita memberi nama yang lebih jelas seperti ini:

int elapsedTimeInDays;

Penamaan yang baik juga membantu kita dalam memahami dan memodifikasi kode program. Perhatikan senarai program dibawah ini.

public List GetThem(List Persons)
{
   var p = new List();
   foreach (var person in Persons)
   {
      if (person.Gender == 0)
        p.Add(person);
    }
    return p;
}

Senarai  di atas bisa kita refaktor menjadi kode program yang lebih mudah dimengerti dengan menjawab pertanyaan di bawah ini.

  1. Apa maksud dari fungsi ini?
  2. Nilai 0 pada kondisi Gender mengacu pada apa
public List GetFemaleInPersons(List Persons)
        {
            var femaleInPersons = new List();
            foreach (var person in Persons)
            {
                if (person.Gender == (int)Gender.Female)
                    females.Add(person);
            }
            return femaleInPersons;
        }

Seringkali kita memberikan penamaan argument (parameter yang dilewatkan pada sebuah fungsi) yang bisa membuat pengguna fungsinya harus membaca keseluruhan kode program untuk mengetahui argument sumber dan argument tujuan pada proses duplikasi larik.

public static void CopyChar(char[] a1, char[] a2)
        {
            for (int i = 0; i < a1.Length; i++)
            {
                a2[i] = a1[i];
            }
        }

Tentu lebih baik jika penamaan argument a1 diganti menjadi source dan a2 diganti menjadi destination.

public static void CopyChar(char[] source, char[] destination)
        {
            for (int i = 0; i < source.Length; i++)
            {
                destination[i] = source[i];
            }
        }

SiteMap dan User Role pada ASP.Net MVC

Pada ASP.Net MVC kita bisa menyembunyikan menu/node tertentu pada sitemap beradasarkan user role. Tambahkan roles pada existing sitemapnode kita seperti di bawah ini. Jika kita multiple role gunakan “,” sebagai pemisah.

<mvcSiteMapNode title="Vehicle Index" key="VehicleIndex" controller="Vehicle" action="Index" visibility="false" roles="Admin, Accountant"/>

kemudian buatlah Customer provider untuk mengoverride visibility dari sitemapnode tersebut.

public class SiteMapVisibilityCustomProvider : MvcSiteMapProvider.SiteMapNodeVisibilityProviderBase
    {
        private bool IsInRole(ISiteMapNode node)
        {
            if (HttpContext.Current == null)
               return true;
            if (HttpContext.Current.User == null)
                return true;

            if(node.Roles.Count == 0)
                return true;
            var IsInRole = false;
            foreach(var role in node.Roles)
            {
                IsInRole = HttpContext.Current.User.IsInRole(role);
                if (IsInRole)
                    break;

            }
            return IsInRole;
        }
        public override bool IsVisible(ISiteMapNode node, IDictionary<string, object> sourceMetadata)
        {
            if (node == null)
            {
                return true;
            }
            
            // Is a visibility attribute specified?
            object visibilityValue = null;
            node.Attributes.TryGetValue("visibility", out visibilityValue);
            if (visibilityValue != null)
            {
                bool nodeVisible;
                string visibility = visibilityValue.ToString();

                if (bool.TryParse(visibility, out nodeVisible))
                {
                    return nodeVisible;
                }
            }
            else
            {
                //check user role
                return IsInRole(node);
            }
            return true;
        }
    }

Pada web config set default visibility provider ke class tersebut, value = {className}, {assemblyName}

<add key="MvcSiteMapProvider_DefaultSiteMapNodeVisibiltyProvider" value="Astral.MVC.Helpers.SiteMapVisibilityCustomProvider, Astral.MVC"/>    

Sekarang, bagaimana agar mencegah user mengakses secara langsung alamat url dari page tersebut? kita buat custom authorization attribute yang akan melewatkan key dari sitemapnode dan medirect url ke halaman tertentu jika role user tidak memiliki akses.

[AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class SiteMapAuthorizeAttribute : AuthorizeAttribute
{
  public string Key { get; set; }

  public override void OnAuthorization(AuthorizationContext filterContext)
  {
    base.OnAuthorization(filterContext);
    ISiteMapNode siteMapNode = null;
    var siteMap = MvcSiteMapProvider.SiteMaps.Current;
    if (siteMap == null)
      return;
    siteMapNode = siteMap.FindSiteMapNodeFromKey(Key);
    if (siteMapNode == null)
      return;
    if (siteMapNode.Roles.Count == 0)
      return;

    var IsInRole = false;
    foreach (var role in siteMapNode.Roles)
    {
      IsInRole = filterContext.HttpContext.User.IsInRole(role);
      if (IsInRole)
        break;
     }
     if (!IsInRole)
     {
        HandleUnauthorizedRequest(filterContext);
     }

}

//Called when access is denied
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{

//User isn't logged in
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
  filterContext.Result = new RedirectToRouteResult(
   new RouteValueDictionary(new { controller = "Account", action = "Login" })
  );
}
//User is logged in but has no access
else
{
  filterContext.Result = new RedirectToRouteResult(
  new RouteValueDictionary(new { controller = "Home", action = "Index" })
  );
}
}

Cara penggunaan attribute ini di controller action seperti ini.

[SiteMapAuthorize(Key ="VehicleIndex")]
public ActionResult Index()
{
}

Atau dengan cara lebih praktis, dimanage pada BaseController seperti ini

private void CheckAuthorization(ActionExecutingContext filterContext, string currentPageUrl)
      {
          ISiteMapNode siteMapNode = null;
          var siteMap = MvcSiteMapProvider.SiteMaps.Current;
          if (siteMap == null)
              return;
          siteMapNode = siteMap.FindSiteMapNode(currentPageUrl);
          if (siteMapNode == null)
              return;
          if (siteMapNode.Roles.Count == 0)
              return;

          var IsInRole = false;
          foreach (var role in siteMapNode.Roles)
          {
              IsInRole = filterContext.HttpContext.User.IsInRole(role);
              if (IsInRole)
                  break;
          }
          if (!IsInRole)
          {
              filterContext.Result = new RedirectToRouteResult(
                       new RouteValueDictionary(new { controller = "User", action = "Unauthorized" })
               );
          }
      }

private string GetPageUrl(ActionExecutingContext filterContext)
        {
            string controllerName = filterContext.Controller.GetType().Name;
            controllerName = controllerName.Substring(0, controllerName.Length - 10);

            string actionName = filterContext.ActionDescriptor.ActionName;
            return string.Format("/{0}/{1}", controllerName, actionName);

        }

override OnActionExecuting pada BaseController seperti ini.

protected override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            base.OnActionExecuting(filterContext);
            var currentPageUrl = GetPageUrl(filterContext);
             CheckAuthorization(filterContext, currentPageUrl);
        }