Repository pattern as abstraction level for Data Access Layer or just helpful nuget package

Working with data | создано: 20.12.2017 | опубликовано: 21.12.2017 | обновлено: 13.01.2024 | просмотров: 4502 | всего комментариев: 1

How do I access a database without much effort? How do I apply a repository and a working template unit? This article describes Calabonga.EntityFramework assembly, which will largely could be simplify some aspects. You can use this publication as a guide for this build.

"Correct" or "Good"

Usually, before embarking on the implementation of the business logic of any application, site, program, you must perform a lot of routine work. And when using the "Correct" approach, the complexity of implementation is multiplied. There are two ways to program, regardless of the platform: the first is "correct", the second is "good". The first method involves the use of different infrastructure patterns, design patterns, SOLID and DRY approaches, abstractions, OOP, and other code development laws and rules. The second way is usually called the "spaghetti code". What is Calabonga.EntityFramework? This is a nuget package that contains abstractions for the implementation of the "Repository level", which is described in the post "Application architecture: conceptual layers and arrangements for their use". In this post I'll going to talk about the Calabonga.EntityFramework library. The principles and rules of its use with examples and explanations.

The "Correct" approach

When working with a database through ORM (in my case it's EntityFramework), I usually have to create the same standard methods of entity management: creation, editing, deleting and reading (CRUD). And only after that add more specific methods that are necessary to implement the business processes of the application. Creating basic methods for each of the entities is very tiring. And besides basic methods, often, if not always, you have to create mapping mechanisms one entity to another (Model -> ViewModel). You can also add the functionality of getting a paging collection (PagedList). As well as some logging functionality. In general, before you engage directly programming business processes, you have to do a lot of work routine. To minimize this routine I have been created a Calabonga.EntityFramework library. The nuget package contains two main classes of Readblerepositorybase and Writablerepositorybase. Inheriting from the first class gives a Repository that can only read the data, and from the second, respectively, a full CRUD for a particular entity. 

Before you start, you must have a Model entity and two ViewModels for update entity and create new entity: CreateViewModel and UpdateViewModel. In my example, the following entities will be: Person, PersonViewModel, PersonCreateViewModel, PersonUpdateViewModel.

In some situations, additional ViewModel are not required, and you can send the model itself as a ViewModel for Creating or updating entity. But this practice was confirmed only when used "simple" applications. It is very difficult (or impossible) to manage entities without UpdateViewModel and CreateViewModel, especially, if you creating big application with more complex business processes.

At the end of this article I'll add a demo project of the console application. But you can use Calabonga.EntityFramework on the ASP.NET MVC or on WPF and other platforms. Let's take a look at what the Calabonga.EntityFramework is. And how to use it in real application.

ReadableRepositoryBase

An abstract class ReadableRepositoryBase implementing an interface IReadableRepositoryBase:

/// <summary>
    /// Generic service abstraction for read only operation with entity
    /// </summary>
    /// <typeparam name="TModel"></typeparam>
    /// <typeparam name="TQueryParams"></typeparam>
    public interface IReadableRepository<TModel, in TQueryParams>
: IDisposable where TQueryParams : IPagedListQueryParams
    {
        /// <summary>
        /// The role name for user with full access (without filtering data) rights
        /// </summary>
        bool IsFullAccess { get; set; }

        /// <summary>
        /// ApplicationDbContext for current app
        /// </summary>
        IEntityFrameworkContext AppContext { get; }

        /// <summary>
        /// Returns paged colletion by pageIndex
        /// </summary>
        /// <param name="queryParams"></param>
        /// <param name="orderPredecat"></param>
        /// <param name="sortDirection"></param>
        /// <param name="includeProperties"></param>
        /// <returns></returns>
        OperationResult<PagedList<TResult>> GetPagedResult<TResult, TSortType>(TQueryParams queryParams,
 Expression<Func<TModel, TSortType>> orderPredecat, SortDirection sortDirection,
params Expression<Func<TModel, object>>[] includeProperties);

        /// <summary>
        /// Returns paged colletion by pageIndex
        /// </summary>
        /// <param name="index">page index</param>
        /// <param name="size"></param>
        /// <param name="orderPredecat">order by</param>
        /// <param name="sortDirection"></param>
        /// <param name="search"></param>
        /// <returns></returns>
        OperationResult<PagedList<TResult>> GetPagedResult<TResult, TSortType>(int index, int size,
 Expression<Func<TModel, TSortType>> orderPredecat, SortDirection sortDirection,
string search = "");

        /// <summary>
        /// Returns request result with Model of the entity this service
        /// </summary>
        /// <param name="id"></param>
        /// <param name="includeProperties"></param>
        /// <returns></returns>
        OperationResult<TModel> GetById(Guid id, params Expression<Func<TModel, object>>[] includeProperties);


        /// <summary>
        /// Filter data by <see cref="TQueryParams"/>
        /// </summary>
        /// <param name="queryParams"></param>
        /// <param name="includeProperties"></param>
        /// <returns></returns>
        IQueryable<TModel> FilterData(TQueryParams queryParams, params Expression<Func<TModel,
object>>[] includeProperties);
    }

I want to note that the result of the GetPagedResult method can be both Model and ViewModel. By overloading the FilterData method, you can apply your own filters. You can use a special property IsFullAccess to exclude from pipeline ApplyRowLevelSecurity. That's means that the base method All() will not check permissions.

Here it is necessary to make an important remark. When working with EntityFramework, I usually disable Lazy Loading because I prefer to control the process of loading objects through navigation properties. So, in this assembly, it is possible to connect additional navigation properties using a set of lambda expressions. Such an option is available for the All(), Update(), Created(), GetEditById() methods.

WritableRepositoryBase

An abstract class WritableRepositoryBase implementing an interface IWritableRepositoryBase:

/// <summary>
/// Generic service abstraction for writable view model operation with entity
/// </summary>
/// <typeparam name="TModel"></typeparam>
/// <typeparam name="TUpdateModel"></typeparam>
/// <typeparam name="TCreateModel"></typeparam>
/// <typeparam name="TQueryParams"></typeparam>
public interface IWritableRepository<TModel, TUpdateModel, TCreateModel, in TQueryParams>
    : IReadableRepository<TModel, TQueryParams>
    where TModel : class, IEntityId
    where TUpdateModel : class, IEntityId
    where TCreateModel : class
    where TQueryParams : IPagedListQueryParams
{
    /// <summary>
    /// Returns OpertaionResult with just added entity wrapped to ViewModel
    /// </summary>
    /// <param name="model">CreateViewModel with data for new object</param>
    /// <param name="beforeAddExecuted"></param>
    /// <param name="afterSaveChanges"></param>
    /// <returns></returns>
    OperationResult<TModel> Add(TCreateModel model, Expression<Action<TModel, TCreateModel>> beforeAddExecuted,
Expression<Action<TModel, TCreateModel>> afterSaveChanges);

    /// <summary>
    /// Returns OperationResult with flag about action success
    /// </summary>
    /// <param name="model">ViewModel with date for update entity</param>
    /// <param name="beforeUpdateExecuted"></param>
    /// <param name="afterSaveChanges"></param>
    /// <returns></returns>
    OperationResult<TModel> Update(TUpdateModel model, Expression<Action<TModel, TUpdateModel>> beforeUpdateExecuted = null,
Expression<Action<TModel, TUpdateModel>> afterSaveChanges = null);

    /// <summary>
    /// Retuns viewModel for editing view for entity
    /// </summary>
    /// <param name="id"></param>
    /// <param name="includeProperties"></param>
    /// <returns></returns>
    OperationResult<TUpdateModel> GetEditById(Guid id, params Expression<Func<TModel,
object>>[] includeProperties);

    /// <summary>
    /// Returns Delete operation result
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    OperationResult<TModel> Delete(Guid id);
}

The name of the methods must be clear. The only thing worth mentioning is the ГетЕдитвид(). A returned result is a ViewModel to edit the entity, that is, a UpdateViewModel prepared for entity edititng. 

By the way, that all methods in ReadableRepositoryBase and WritableRepositoryBase are declared as virtual, and therefore can be redefined at your want.

OperationResult and PagedList

You have probably noticed that the results of all operations is a special class OperationResult. This is an ordinary wrapper for the convenience of processing the results of the method (query) invocation. The key features of this wrapper are the presence of a service message and a field for storing an exception information when performing an operation. This approach ensures that the user (developer) always has an adequate response from the server. Adequate, so without Exception with the presence of additional information. It also uses PagedListLite to work with paging requests. Examples you can see later in this article.

Features of Calabonga. EntityFramework

In the library, the mechanism for tracking the results of the last execution of the SaveChanges command is implemented. To do this, you can always view the SaveChangesResult from IEntityFrameworkContext (about this interface below in the text) after its execution. There is also a nested InnerException processing for more convenient representation of information about InnerException.

The whole library is built on abstractions. if the implementation is not suitable from the base classes, than you can write your own implementation. The first abstraction is the IEntityFrameworkContext interface, which is a DbContext (or IdentityDbContext) representation for the inner data manipulations. Through this interface you can access to the database. By the way, if you want to use another than DbContext from Microsoft, your can create own implementation, for example, based on NHibernate.

Next abstraction IEntityFrameworkLogService. I think it should be clear from the name that this interface can conduct logging of interactions with the database.

One more important abstraction of IEntityFrameworkMapper. This is tool to map one entity to another. You can choice a mapper, for example: AutoMapper, ExpressMapper, or other one. You just need to select one of the existing and "wrap" in this interface. The IEntityFrameworkMapper allows you to select from the database the list of entities (or single item) and project to another entity if you need it. So to speak, this is "Model to ViewModel mapping".

One more important point. Your entities must implement the IEntityId interface, or they must inherit from the EntityId class. Because through this abstraction is built work with DbContext and in particular sampling by identifiers. By the way, IEntityId have only one property:

public Guid Id { get; set; }

Yep, the identifier should be a type of Guid.

Samples

For example, I created a demo project that shows the necessary inheritance and how to use them. In this example, you will see the code. So, the first is a Person model:

/// <summary>
/// Entity for testing Calabonga.EntityFramework
/// </summary>
public class Person : IEntityId, IHaveName
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public Guid Id { get; set; }
    public string Name { get; set; }
}

The Person class implements the IEntityId interface, but you can use EntityId  as base class for your enity. For Person class I have three additional Viewmodels classes described above:

public class PersonViewModel : IHaveName
{
    public Guid Id { get; set; }
    public string Name { get; set; }
}

This class is designed to fetch a list (paging). Person models are projected in PersonViewModel. But I can also use another type for mapping in the GetPagedList <T> method. For example, if you specify a Person type, I will get a set of person objects without mapping. The following class of CreateViewModel:

public class CreateViewModel
{
    public string Name { get; set; }
}

This ViewModel is designed to create a new record in the database. It is the one that is sent to the Create () method. Next is UpdateViewModel. This class is used to update the Person entity.

public class UpdateViewModel : IEntityId
{
    public string Name { get; set; }
    public Guid Id { get; set; }
}

Next, IEntityFrameworkLogService:

public class Logger : IEntityFrameworkLogService
{
    public void LogInfo(string message)
    {
        return;
    }

    public void LogError(Exception exception)
    {
        return;
    }
}

As you can see from the code, in the demo example I'm not going to write anything to the event log, but you could use this interface to get event log messages with exceptions or "record {ID} saved" or "update" messages. For exsample, "Unable to record {ID} because no record was found".

IEntityFrameworkMapper is the next candidate for inheritance. Let me remind you that this is just an interface, which can be implemented in different ways, as already existing frameworks such as AutoMapper or ExpressMapper, and personally written code projection. For example, I used ExpressMapper:

public class Mapper : IEntityFrameworkMapper
{
    private IMappingServiceProvider _mapper;

    public Mapper()
    {
        if (_mapper == null)
        {
            _mapper = ExpressMapper.Mapper.Instance;
            RegisterMaps();
        }
    }

    public void RegisterMaps()
    {
        _mapper.Register<Person, PersonViewModel>();
        _mapper.Register<UpdateViewModel, Person>();
        _mapper.RegisterCustom<PagedList<Person>, PagedList<PersonViewModel>, PagedListResolver>();
    }

    public TDestionation Map<TSource, TDestionation>(TSource source)
    {
        return _mapper.Map<TSource, TDestionation>(source);
    }

    public void Map<TDestionation, TSource>(TDestionation model, TSource item)
        where TDestionation : class, IEntityId
        where TSource : class, IEntityId
    {
        _mapper.Map(typeof(TDestionation), typeof(TSource), model, item);
    }
}

Configuration and registration of each of the selected third-party frameworks is usually described on the manufacturer's website.

The most important interface for implementing IEntityFrameworkContext. This is the primary "data carrier" for Calabonga.EntityFramework. I implemented it in the following way:

public class ApplicationDbContext : DbContext, IEntityFrameworkContext
{
    public ApplicationDbContext() : base("DefaultConnection")
    {
        Configuration.AutoDetectChangesEnabled = true;
        Configuration.LazyLoadingEnabled = false;
        Configuration.ProxyCreationEnabled = false;
        LastSaveChangesResult = new SaveChangesResult();
    }
    public SaveChangesResult LastSaveChangesResult { get; }

    public DbSet<Person> People { get; set; }

    public override int SaveChanges()
    {
        try
        {
            var createdSourceInfo = ChangeTracker.Entries().Where(e => e.State == EntityState.Added);
            var modifiedSourceInfo = ChangeTracker.Entries().Where(e => e.State == EntityState.Modified);

            foreach (var entry in createdSourceInfo)
            {
                // do some staff
                // ...

                // Add system message
                LastSaveChangesResult.AddMessage($"ChangeTracker has new entities: {entry.Entity.GetType()}");
            }

            foreach (var entry in modifiedSourceInfo)
            {
                // do some staff
                // ...

                LastSaveChangesResult.AddMessage($"ChangeTracker has modified entities: {entry.Entity.GetType()}");
            }

            return base.SaveChanges();
        }
        catch (DbUpdateException exception)
        {
            LastSaveChangesResult.Exception = exception;
            return 0;
        }
    }
}

Please see how the Lastsavechangesresult is used. I think this is a very useful feature.

Program.cs

So, we have reached the most important class of the our console application. I will comment on the lines of the main method, because you can always see the details in the demo project. The project is set to Autofac (dependency Injection container), and therefore the Main method is to create the container: first.

var builder = new ContainerBuilder();
builder.RegisterType<ApplicationDbContext>().As<IEntityFrameworkContext>();
builder.RegisterType<Mapper>().As<IEntityFrameworkMapper>();
builder.RegisterType<Logger>().As<IEntityFrameworkLogService>();
builder.RegisterType<PeopleRepostiory>().AsSelf();
var container = builder.Build();

Register the necessary interfaces in the container and from the implementation. Next, we get the instance from DI our repository and ApplicationDbContext:

var repostiory = container.Resolve<PeopleRepostiory>();
var context = container.Resolve<IEntityFrameworkContext>();
if (!context.Database.Exists())
{
    //Add items
    AddItems(repostiory);
}

If the database is not created then create it and add a number of records through the repository. Next, you display all the records in the console page:

// Getting pagedList
Print(repostiory, ((ApplicationDbContext)context).People.Count());

After that creating a new record:

// Add demo
AddNewPerson(repostiory);

And finally, make updates to some of the newly created entries:

// Update demo
UpdatePerson(repostiory);

Deleting an entity works on the same principle.

Advanced samples

I want to show examples from my real projects. Here is the ActionResult of getting CategoryViewModel used in this blog in the admin panel.

public ActionResult Index(int? id)
{
    var qp = new PagedListQueryParams
    {
        PageIndex = id ?? 1
    };
    var pagedResult = _categoryRepository.GetPagedResult<CategoryViewModel, DateTime>(
        qp,
        x => x.CreatedAt,
        SortDirection.Ascending,
        x=>x.Posts);
    if (pagedResult.Ok)
    {
        return View(pagedResult.Result);
    }
    return View();
}

The following example calls GetPagedList for the Post entity from its repository already with multiple connected navigation properties (Include). In this example, three dependent entities are already used:

var posts = _postRepository.GetPagedResult<PostViewModel, DateTime>(
    queryParams,
    x => x.CreatedAt,
    SortDirection.Descending,
    x => x.Category,
    x => x.Tags,
    x => x.Comments);
return View(posts);

The following example updates Post with the Update method. Notice the BeforeUpdateExecuted method. There are overloads in the Update method that allow you to include two additional methods in the process. The first is executed immediately after the Mapping operation has been performed, that is, the properties from the PostViewModel have updated the values of the Post entity, but no persistence has been made. The second method from the overload is executed after the write to the database, that is, after calling the SaveChanges method.

[HttpPost]
[ValidateAntiForgeryToken]
[ValidateInput(false)]
public ActionResult Edit(PostUpdateViewModel model, string returnUrl = null)
{
    if (ModelState.IsValid)
    {
        var update = _postRepository.Update(model, (m, i) => BeforeUpdateExecuted(m, i));
        if (update.Ok)
        {
            if (!string.IsNullOrEmpty(returnUrl))
            {
                return Redirect(returnUrl);
            }
            return RedirectToRoute("Post", new { title = model.UniqueId });
        }

        if (update.Error != null)
        {
            ViewData["Error"] = ExceptionHelper.GetMessages(update.Error);
        }
    }
    FillCategories();
    return View(model);
}

private void BeforeUpdateExecuted(Post post, PostUpdateViewModel postUpdateViewModel)
{
    _tagService.ProcessTags(postUpdateViewModel.TagsAsString, post);
}

Next, the override for ApplyRowLevelSecurity, which using IsFullAccess property:

protected override IQueryable<Post> ApplyRowLevelSecurity(IQueryable<Post> items)
{
    return IsFullAccess
        ? base.ApplyRowLevelSecurity(items)
        : items.Where(x => x.State.HasFlag(PostState.Published));
}

Storing comment to database:

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
[ValidateInput(false)]
public ActionResult Add(CommentCreateViewModel model, string returnUrl)
{
    var recaptchaHelper = this.GetRecaptchaVerificationHelper();
    if (string.IsNullOrEmpty(recaptchaHelper.Response))
    {
        ModelState.AddModelError("", "Captcha is required");
        return Redirect(returnUrl);
    }
    if (ModelState.IsValid)
    {
        var operationResult = _commentRepository.Add(model,
            (m, i) => ProcessCommentsBeforeSave(m, i),
            (m, i) => ProcessCommentsAfterSave(m, i));
        if (!operationResult.Ok) return Redirect(returnUrl);
        TempData["ReturnUrl"] = returnUrl;
        return RedirectToAction("ThanksForComment", new { operationResult.Result.Id });
    }
    return Redirect(returnUrl);
}

private void ProcessCommentsBeforeSave(Comment comment, CommentCreateViewModel create)
{
    // Do some staff
    // Is user admin validation
}

private void ProcessCommentsAfterSave(Comment comment, CommentCreateViewModel create)
{
    // Do some staff
    // Send notification to administrator
}

And another one exsample. First is the code, than comments:

/// <summary>
/// Get top view vount for posts
/// </summary>
/// <param name="daysCount"></param>
/// <param name="categoryName"></param>
/// <param name="tag"></param>
/// <param name="totalItems"></param>
/// <returns></returns>
public OperationResult<List<PostTopViewModel>> GetTop(int daysCount, string categoryName, string tag, int totalItems = 6)
{
    var statisticPeriod = new StatisticPeriod(new TimeSpan(daysCount, 0, 0, 0));
    var all = All(x => x.Category);
    var allStats = ((IApplicationDbContext)AppContext).Statistics;

    var statistics = allStats.Where(x => DbFunctions.TruncateTime(x.UpdatedAt) >= statisticPeriod.DateStart.Date
        && DbFunctions.TruncateTime(x.UpdatedAt) <= statisticPeriod.DateEnd.Date);

    if (!string.IsNullOrEmpty(categoryName) && categoryName.ToLower() != "all")
    {
        all = all.Where(x => x.Category.UniqueId.Equals(categoryName));
    }
    var hasTag = !string.IsNullOrEmpty(tag);
    if (hasTag && all.Any())
    {
        all = all.Where(x => x.Tags.Any(t => t.Name == tag));
    }

    // Some manipulations with selected items
    // and as result itemsViewModels

    return OperationResult.CreateResult(itemsViewModels);
}

In the 12th row of the All() method, which is available in the context of all repositories inherited from ReadableRepositoryBase or WritableRepositoryBase. are connected via lambda as a Category, i.e. Incude (x => x. Category). In the 13th line, AppContext is cast to the IApplicationDbContext type, and then I get all the entity types that have been registered in IApplicationDbContext.

Conclution

In conclusion I would like to note that Calabonga.EntityFramework exists in the version for the .net Standard 2.0, that is, you can use it in ASP.NET Core projects together with EntityFrameworkCore. I want to make the library as useful as possible, that is why I accepts any additions, comments, suggestions and even criticism. As a last resort, you can write through the website feedback form.

Links

Calabonga.EntityFramework - nuget-пакет for .NET Framework

Calabonga.EntityFrameworkCore - nuget-пакет for .NET Standard (Core)

The console application (demo) you can find on GitHub

Комментарии к статье (1)

There may be noticeably a bundle to learn about this. I assume you made certain good factors in options also.