The good architecture is at the heart of any project. The developer is always looking for great architecture that reduces repetitive code and separates the Data Access and Business Logic. So we will be creating our generic repository using ASP.NET MVC and Entity Framework. If you don’t know about Entity Framework, please click here to get started with Entity Framework. So before getting started with repository pattern, firstly we have to understand what is the repository pattern? and why we want it?
Repository and Unit of Work Pattern
In simple words, repository means, the abstraction between your Data Access and Business Logic layers which will very useful for unit testing or test-driven development (TDD). By using repository, your system will be more loosely coupled.
In programming terms, we have to create one interface for each repository or entity. e.g. Student
entity has One interface (IStudentInterface
) in which all methods are declared like Insert
, Update
, Delete
, Get
and another is class (StudentRepository
) which is inherited from IStudentInterface
and StudentRepository
will implement all the methods which are declared in Interface. When we instantiate the repository in our controller, we will use the interface so that the controller will accept a reference to any object that implements the repository interface. When the controller runs under a web server, it receives a repository that works with the Entity Framework.
When the controller runs under a unit test class, it receives a repository that works with data stored in a way that you can easily manipulate for testing, such as an in-memory collection. So we can supply fake repository in unit testing.
If you want to see the complete implementation, please refer to the following link:
Disadvantage
Each time we have to create Repository for each entity, it results in redundant code.
Using the Code
Now we need only a data access class which accepts any entity and performs required operation, e.g. CRUD. After studying lots of articles, theories and sample code, I got one very good reference of Generic Repository Pattern.
My code is heavily based on the Huy Nguyen’s Blog. Please refer to the following links:
I have changed a bit of code and added to new class library project as well as I added some common function which required to each project. Now I can use this library in any project. Following is the folder structure.
Mayur.DAL
– Class library project with generic repository and common functions.
- Core – Folder
- GlobalCommonHelper.cs – An
abstract
class which contents all common function required to each project
- Repository – Folder
- IRepository.cs – Interface for generic repository
- Repository.cs – Generic repository class inherited from
IRepository
. All methods are implemented in it.
- IUnitOfWork.cs – Interface for unit of work class.
- UnitOfWork.cs – Implemented
SaveChanges()
methods for Entity Framework . The unit of work class is very important when we execute any transitional command to the database like Insert
, Update
, Delete
the entity framework not commit to database until Savechanges()
method calls.
Mayur.Web
– MVC project
- Controller – Folder
- HomeController.cs – Home controller with
Index
, Create
, Edit
, Delete ActionResult
- Core – Folder
- CommonHelper.cs – Inherited from Mayur.DAL.Core.GlobalCommonHelper.cs which contents all common methods related to MVC project
- Model – Folder
- Student.cs –
Student Model
representing studnet
table
- Views – Folder
- Index.chtml – Display All student html
- Create.chtml – Create new student html
- Edit.cshtml – Update student info html
- Delete.cshtml – Delete student info html
Lets understand briefly each file in Mayur.DAL:
Repository Folder: In this folder, all the Data Access logic is present. There are 4 files, 2 are Interfaces and 2 are classes which inherited from respective Interface.
1. IRepository Interface
public interface IRepository : IDisposable
{
IUnitOfWork UnitOfWork { get; }
TEntity GetByKey<TEntity>(object keyValue) where TEntity : class;
IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class;
IQueryable<TEntity> GetQuery<TEntity>
(Expression<Func<TEntity, bool>> predicate) where TEntity : class;
IEnumerable<TEntity> GetAll<TEntity>() where TEntity : class;
IEnumerable<TEntity> Get<TEntity,
TOrderBy>(Expression<Func<TEntity, TOrderBy>> orderBy, int pageIndex,
int pageSize, SortOrder sortOrder = SortOrder.Ascending) where TEntity : class;
IEnumerable<TEntity> Get<TEntity,
TOrderBy>(Expression<Func<TEntity, bool>> criteria,
Expression<Func<TEntity, TOrderBy>> orderBy, int pageIndex, int pageSize,
SortOrder sortOrder = SortOrder.Ascending) where TEntity : class;
TEntity Single<TEntity>(Expression<Func<TEntity,
bool>> criteria) where TEntity : class;
TEntity First<TEntity>(Expression<Func<TEntity,
bool>> predicate) where TEntity : class;
IEnumerable<TEntity> Find<TEntity>
(Expression<Func<TEntity, bool>> criteria) where TEntity : class;
TEntity FindOne<TEntity>(Expression<Func<TEntity,
bool>> criteria) where TEntity : class;
int Count<TEntity>() where TEntity : class;
int Count<TEntity>(Expression<Func<TEntity,
bool>> criteria) where TEntity : class;
void Add<TEntity>(TEntity entity) where TEntity : class;
void Attach<TEntity>(TEntity entity) where TEntity : class;
void Update<TEntity>(TEntity entity) where TEntity : class;
void Delete<TEntity>(TEntity entity) where TEntity : class;
void Delete<TEntity>(Expression<Func<TEntity,
bool>> criteria) where TEntity : class;
(ISpecification<TEntity> criteria) where TEntity : class;
}
2. Repository Class
public partial class Repository : IRepository, IDisposable
{
private bool bDisposed;
private DbContext context;
private IUnitOfWork unitOfWork;
#region Contructor Logic
public Repository()
{
}
public Repository(DbContext contextObj)
{
if (contextObj == null)
throw new ArgumentNullException("context");
this.context = contextObj;
}
public Repository(ObjectContext contextObj)
{
if (contextObj == null)
throw new ArgumentNullException("context");
context = new DbContext(contextObj, true);
}
public void Dispose()
{
Close();
}
#endregion
#region Properties
protected DbContext DbContext
{
get
{
if (context == null)
throw new ArgumentNullException("context");
return context;
}
}
public IUnitOfWork UnitOfWork
{
get
{
if (unitOfWork == null)
{
unitOfWork = new UnitOfWork(DbContext);
}
return unitOfWork;
}
}
#endregion
#region Data Display Methods
public TEntity GetByKey<TEntity>(object keyValue) where TEntity : class
{
EntityKey key = GetEntityKey<TEntity>(keyValue);
object originalItem;
if (((IObjectContextAdapter)DbContext).
ObjectContext.TryGetObjectByKey(key, out originalItem))
{
return (TEntity)originalItem;
}
return default(TEntity);
}
public IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class
{
string entityName = GetEntityName<TEntity>();
return ((IObjectContextAdapter)DbContext).
ObjectContext.CreateQuery<TEntity>(entityName);
}
public IQueryable<TEntity> GetQuery<TEntity>
(Expression<Func<TEntity, bool>> predicate) where TEntity : class
{
return GetQuery<TEntity>().Where(predicate);
}
public IEnumerable<TEntity> GetAll<TEntity>() where TEntity : class
{
return GetQuery<TEntity>().AsEnumerable();
}
public IEnumerable<TEntity> Get<TEntity, TOrderBy>
(Expression<Func<TEntity, TOrderBy>> orderBy, int pageIndex,
int pageSize, SortOrder sortOrder = SortOrder.Ascending) where TEntity : class
{
if (sortOrder == SortOrder.Ascending)
{
return GetQuery<TEntity>()
.OrderBy(orderBy)
.Skip((pageIndex - 1) * pageSize)
.Take(pageSize)
.AsEnumerable();
}
return
GetQuery<TEntity>()
.OrderByDescending(orderBy)
.Skip((pageIndex - 1) * pageSize)
.Take(pageSize)
.AsEnumerable();
}
public IEnumerable<TEntity> Get<TEntity,
TOrderBy>(Expression<Func<TEntity, bool>> criteria,
Expression<Func<TEntity, TOrderBy>> orderBy, int pageIndex, int pageSize,
SortOrder sortOrder = SortOrder.Ascending) where TEntity : class
{
if (sortOrder == SortOrder.Ascending)
{
return GetQuery(criteria).
OrderBy(orderBy).
Skip((pageIndex - 1) * pageSize).
Take(pageSize)
.AsEnumerable();
}
return
GetQuery(criteria)
.OrderByDescending(orderBy)
.Skip((pageIndex - 1) * pageSize)
.Take(pageSize)
.AsEnumerable();
}
public TEntity Single<TEntity>
(Expression<Func<TEntity, bool>> criteria) where TEntity : class
{
return GetQuery<TEntity>().Single<TEntity>(criteria);
}
public TEntity First<TEntity>
(Expression<Func<TEntity, bool>> predicate) where TEntity : class
{
return GetQuery<TEntity>().First(predicate);
}
public IEnumerable<TEntity> Find<TEntity>
(Expression<Func<TEntity, bool>> criteria) where TEntity : class
{
return GetQuery<TEntity>().Where(criteria);
}
public TEntity FindOne<TEntity>
(Expression<Func<TEntity, bool>> criteria) where TEntity : class
{
return GetQuery<TEntity>().Where(criteria).FirstOrDefault();
}
public int Count<TEntity>() where TEntity : class
{
return GetQuery<TEntity>().Count();
}
public int Count<TEntity>
(Expression<Func<TEntity, bool>> criteria) where TEntity : class
{
return GetQuery<TEntity>().Count(criteria);
}
#endregion
#region Data Transactional Methods
public void Add<TEntity>(TEntity entity) where TEntity : class
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
DbContext.Set<TEntity>().Add(entity);
}
public void Attach<TEntity>(TEntity entity) where TEntity : class
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
DbContext.Set<TEntity>().Attach(entity);
}
public void Update<TEntity>(TEntity entity) where TEntity : class
{
string fqen = GetEntityName<TEntity>();
object originalItem;
EntityKey key =
((IObjectContextAdapter)DbContext).ObjectContext.CreateEntityKey(fqen, entity);
if (((IObjectContextAdapter)DbContext).ObjectContext.TryGetObjectByKey
(key, out originalItem))
{
((IObjectContextAdapter)DbContext).ObjectContext.ApplyCurrentValues
(key.EntitySetName, entity);
}
}
public void Delete<TEntity>(TEntity entity) where TEntity : class
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
DbContext.Set<TEntity>().Remove(entity);
}
public void Delete<TEntity>(Expression<Func<TEntity,
bool>> criteria) where TEntity : class
{
IEnumerable<TEntity> records = Find(criteria);
foreach (TEntity record in records)
{
Delete(record);
}
}
#endregion
#region Internal Processing Private Methods
private EntityKey GetEntityKey<TEntity>(object keyValue) where TEntity : class
{
string entitySetName = GetEntityName<TEntity>();
ObjectSet<TEntity> objectSet =
((IObjectContextAdapter)DbContext).ObjectContext.CreateObjectSet<TEntity>();
string keyPropertyName = objectSet.EntitySet.ElementType.KeyMembers[0].ToString();
var entityKey = new EntityKey
(entitySetName, new[] { new EntityKeyMember(keyPropertyName, keyValue) });
return entityKey;
}
private string GetEntityName<TEntity>() where TEntity : class
{
string entitySetName = ((IObjectContextAdapter)DbContext).ObjectContext
.MetadataWorkspace
.GetEntityContainer(((IObjectContextAdapter)DbContext).
ObjectContext.DefaultContainerName,
DataSpace.CSpace)
.BaseEntitySets.Where(bes => bes.ElementType.Name == typeof(TEntity).Name).First().Name;
return string.Format("{0}.{1}",
((IObjectContextAdapter)DbContext).ObjectContext.DefaultContainerName,
entitySetName);
}
private string RemoveAccent(string txt)
{
byte[] bytes = System.Text.Encoding.GetEncoding("Cyrillic").GetBytes(txt);
return System.Text.Encoding.ASCII.GetString(bytes);
}
private bool IsValidTag(string tag, string tags)
{
string[] allowedTags = tags.Split(',');
if (tag.IndexOf("javascript") >= 0) return false;
if (tag.IndexOf("vbscript") >= 0) return false;
if (tag.IndexOf("onclick") >= 0) return false;
var endchars = new char[] { ' ', '>', '/', '\t' };
int pos = tag.IndexOfAny(endchars, 1);
if (pos > 0) tag = tag.Substring(0, pos);
if (tag[0] == '/') tag = tag.Substring(1);
foreach (string aTag in allowedTags)
{
if (tag == aTag) return true;
}
return false;
}
#endregion
#region Disposing Methods
protected void Dispose(bool bDisposing)
{
if (!bDisposed)
{
if (bDisposing)
{
if (null != context)
{
context.Dispose();
}
}
bDisposed = true;
}
}
public void Close()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
}
3. IUnitOfWork Interface
public interface IUnitOfWork : IDisposable
{
void SaveChanges();
}
4. UnitOfWork Class
internal class UnitOfWork : IUnitOfWork
{
private readonly DbContext _dbContext;
public UnitOfWork(DbContext context)
{
_dbContext = context;
}
public void SaveChanges()
{
((IObjectContextAdapter)_dbContext).ObjectContext.SaveChanges();
}
#region Implementation of IDisposable
private bool _disposed;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!disposing)
return;
if (_disposed)
return;
_disposed = true;
}
#endregion
}
Now in Mayur.DAL.Core folder, there is one abstract
class with all common functions required for each project. If you have any new functions which we required in each project, please suggest in the comments.
5. Mayur.DAL.Core.GlobalCommonHelper.cs Class
abstract public class GlobalCommonHelper
{
#region General Methods
public string GetSHA1HashData(string data)
{
SHA1 sha1 = SHA1.Create();
byte[] hashData = sha1.ComputeHash(Encoding.Default.GetBytes(data));
StringBuilder returnValue = new StringBuilder();
for (int i = 0; i < hashData.Length; i++)
{
returnValue.Append(hashData[i].ToString());
}
return returnValue.ToString();
}
public string GetSlugURLFromString(string phrase)
{
string str = RemoveAccent(phrase).ToLower();
str = Regex.Replace(str, @"[^a-z0-9\s-]", "");
str = Regex.Replace(str, @"\s+", " ").Trim();
str = str.Substring(0, str.Length <= 45 ? str.Length : 45).Trim();
str = Regex.Replace(str, @"\s", "-"); return str;
}
public void DeleteTargetFile(string path)
{
if (File.Exists(path))
{
File.SetAttributes(path, FileAttributes.Normal);
File.Delete(path);
}
}
public bool SendEmailToTarget(string toEmail, string subject, string body)
{
bool success = false;
try
{
SmtpClient SmtpServer = new SmtpClient();
MailMessage mail = new MailMessage();
SmtpServer.Credentials = new NetworkCredential(
Convert.ToString(ConfigurationManager.AppSettings["fromEmail"]),
Convert.ToString(ConfigurationManager.AppSettings["fromPassword"]));
SmtpServer.Host = Convert.ToString
(ConfigurationManager.AppSettings["hostName"]);
SmtpServer.Port = Convert.ToInt32
(ConfigurationManager.AppSettings["portNumber"]);
if (Convert.ToBoolean
(ConfigurationManager.AppSettings["isEnableSSL"]) == true)
SmtpServer.EnableSsl = true;
mail.From = new MailAddress(Convert.ToString
(ConfigurationManager.AppSettings["senderName"]));
string[] multiEmails = toEmail.Split(';');
foreach (string email in multiEmails)
{
mail.To.Add(email);
}
mail.Subject = subject;
mail.IsBodyHtml = true;
mail.Body = body;
SmtpServer.Send(mail);
mail.Dispose();
success = true;
}
catch (Exception)
{
success = false;
}
return success;
}
public bool SendEmailToTarget(string toEmail, string subject, string body, string attachmentPath)
{
bool success = false;
try
{
SmtpClient SmtpServer = new SmtpClient();
MailMessage mail = new MailMessage();
SmtpServer.Credentials = new NetworkCredential(
Convert.ToString(ConfigurationManager.AppSettings["fromEmail"]),
Convert.ToString(ConfigurationManager.AppSettings["fromPassword"]));
SmtpServer.Host = Convert.ToString
(ConfigurationManager.AppSettings["hostName"]);
SmtpServer.Port = Convert.ToInt32
(ConfigurationManager.AppSettings["portNumber"]);
if (Convert.ToBoolean(ConfigurationManager.AppSettings["isEnableSSL"]) == true)
SmtpServer.EnableSsl = true;
mail.From = new MailAddress(Convert.ToString
(ConfigurationManager.AppSettings["senderName"]));
string[] multiEmails = toEmail.Split(';');
foreach (string email in multiEmails)
{
mail.To.Add(email);
}
Attachment attachment;
attachment = new System.Net.Mail.Attachment(attachmentPath);
mail.Attachments.Add(attachment);
mail.Subject = subject;
mail.IsBodyHtml = true;
mail.Body = body;
SmtpServer.Send(mail);
mail.Dispose();
success = true;
}
catch (Exception)
{
success = false;
}
return success;
}
public string RemoveHtmlFromString(string text)
{
if (String.IsNullOrEmpty(text))
return string.Empty;
text = Regex.Replace(text, @"(>)(\r|\n)*(<)", "><");
text = Regex.Replace(text, "(<[^>]*>)([^<]*)", "$2");
text = Regex.Replace(text, "(&#x?[0-9]{2,4};|"|&|
|<|>|€|©|®|‰|‡|†|‹|
›|„|”|“|‚|’|‘|—|
–|‏|‎|‍|‌| | | |˜|
ˆ|Ÿ|š|Š)", "@");
return text;
}
/// <summary>
/// Verifies that a string is in valid e-mail format
/// </summary>
/// <param name="email">Email to verify</param>
/// <returns>true if the string is a valid e-mail address and false if it's not</returns>
public bool IsValidEmail(string email)
{
if (String.IsNullOrEmpty(email))
return false;
email = email.Trim();
var result = Regex.IsMatch(email, "^(?:[\\w\\!\\#\\$\\%\\&\\
'\\*\\+\\-\\/\\=\\?\\^\\`\\{\\|\\}\\~]+\\.)*[\\w\\!\\#\\$\\%\\&\\
'\\*\\+\\-\\/\\=\\?\\^\\`\\{\\|\\}\\~]+@(?:(?:(?:[a-zA-Z0-9]
(?:[a-zA-Z0-9\\-](?!\\.)){0,61}[a-zA-Z0-9]?\\.)+[a-zA-Z0-9]
(?:[a-zA-Z0-9\\-](?!$)){0,61}[a-zA-Z0-9]?)|(?:\\[(?:(?:[01]?\\d{1,2}|2[0-4]
\\d|25[0-5])\\.){3}(?:[01]?\\d{1,2}|2[0-4]\\d|25[0-5])\\]))$", RegexOptions.IgnoreCase);
return result;
}
/// <summary>
/// Returns Allowed HTML only.
/// </summary>
/// <param name="text">Text</param>
/// <returns>Allowed HTML</returns>
public string EnsureOnlyAllowedHtml(string text)
{
if (String.IsNullOrEmpty(text))
return string.Empty;
const string allowedTags = "br,hr,b,i,u,a,div,ol,ul,li,blockquote,img,span,p,em," +
"strong,font,pre,h1,h2,h3,h4,h5,h6,address,cite";
var m = Regex.Matches(text, "<.*?>", RegexOptions.IgnoreCase);
for (int i = m.Count - 1; i >= 0; i--)
{
string tag = text.Substring(m[i].Index + 1, m[i].Length - 1).Trim().ToLower();
if (!IsValidTag(tag, allowedTags))
{
text = text.Remove(m[i].Index, m[i].Length);
}
}
return text;
}
#endregion
#region Internal Processing Private Methods
private string RemoveAccent(string txt)
{
byte[] bytes = System.Text.Encoding.GetEncoding("Cyrillic").GetBytes(txt);
return System.Text.Encoding.ASCII.GetString(bytes);
}
private bool IsValidTag(string tag, string tags)
{
string[] allowedTags = tags.Split(',');
if (tag.IndexOf("javascript") >= 0) return false;
if (tag.IndexOf("vbscript") >= 0) return false;
if (tag.IndexOf("onclick") >= 0) return false;
var endchars = new char[] { ' ', '>', '/', '\t' };
int pos = tag.IndexOfAny(endchars, 1);
if (pos > 0) tag = tag.Substring(0, pos);
if (tag[0] == '/') tag = tag.Substring(1);
foreach (string aTag in allowedTags)
{
if (tag == aTag) return true;
}
return false;
}
#endregion
}
Now our Generic repository is ready with inbuilt common functions. Now we can see how to use them in controller. Suppose we have Students
model in which studentID
, name
, rollNo
columns. Following is the code for HomeController
.
- Before we proceed, we are using code first approach and following is our
DbContext
class:
public partial class MyFirstDbContext : DbContext
{
public MyFirstDbContext()
: base("name=MyFirstDbContext")
{
Database.SetInitializer<MyFirstDbContext>(null);
}
public virtual DbSet<Students> Students { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
}
}
- We have created an
abstract
class in out Data Access Layer which contains Global common function means almost in each project we required that function. The class is an abstract
class so we cannot create an instance of it . So we create new CommonHelper.cs Class in Our Web project which is inherited from Abstract
class and we can implement project-specific common functions here.
public class CommonHelper : GlobalCommonHelper
{
public int PageSize = 25;
}
- Now we can see how we use repository in our
HomeController
. The HomeController
is as follows:
public HomeController()
{
IRepository repository = new Repository(new MyFirstDbContex);
CommonHelper helper = new CommonHelper();
}
- Index.cshtml represents the view of all
Students
in table format. So ActionResult
is as follows:
public ActionResult Index()
{
return View(repository.GetAll<Students>());
}
- Create.cshtml creates new
Students
record means it inserts new record in db.
public ActionResult Create()
{
return View();
}
[HttpPost]
public ActionResult Create(Students studPost)
{
if(ModelState.IsValid)
{
repository.Add<Students>(studPost);
repository.UnitOfWork.SaveChanges();
}
}
- Edit.cshtml represents the edit view in which we can modify the records.
public ActionResult Edit(int id=0)
{
if(id==0)
return HttpNotFound();
Students stud = repository.FindOne<Students>(x=>x.studentID == id);
return View(stud);
}
[HttpPost]
public ActionResult Edit(Students studPost)
{
if(ModelState.IsValid)
{
repository.Update<Students>(studPost);
repository.UnitOfWork.SaveChanges();
}
}
- And the last one is
Delete
as follows:
[HttpPost]
public ActionResult Delete(int id=0)
{
if(id==0)
return HttpNotFound();
Students stud = repository.FindOne<Students>(x=>x.studentID == id);
if(stud == null)
return HttpNotFound();
repository.Delete<Students>(stud);
repository.UnitOfWork.SaveChanges();
}
I need more suggestions and improvements from you. Please comment.
Revisions
- 31/05/2015: Article published
- 30/04/2016: Article Updated fixed some constructor error as per the "Irene Troupansky" Suggestions.
CodeProject