Click here to Skip to main content
15,942,156 members
Articles / Programming Languages / C#

Building Entity Framework Generic Repository 2 Connected

Rate me:
Please Sign up or sign in to vote.
4.79/5 (7 votes)
18 Dec 2017CPOL6 min read 15.2K   247   18   2
Continue with the Entity Framework Generic Repository, in this case Connected for stateful apps.

Introduction

A few weeks ago, we looked at a first article about of Disconnected Repository, in this article, let’s complete with other piece off the puzzle, the connected Generic Repository. In this new Repository type, a very important new actor appears, is none other than ObservableCollection<T>, this will be an inseparable friend for Connected Generic Repository.

Index

Entity Framework Generic Repositories Connected

Entity Framework generic repository connected is used in state process as WPF, Silverlight, Windows Forms, console app, etc.

This repository working with group changes, and it has fixed to ItemsControls. This generic repository type works with direct DataGrid modifications.

Its main characteristics are:

  • Should receive the DbContext from dependency injection
  • Should implement IDisposable interface for releasing unmanaged resources
  • Should have a DbContext protected property. This property will be alive during generic repository life and will only die in the Dispose method.
  • The DbContext property will hear all repository changes.
  • It doesn’t have methods (add, remove and update), because these actions are performed through the Local DbSet property. Local is ObservableCollection<TEntity> (INotifyCollectionChanged) and it usually will be linked directly to (ListBox, ListView, DataGrid, etc.).
  • The repository connected has a SaveChanged method for send all changes to database.

It has a SaveChanged method.

Image 1

We must consider Connected Repository use, because it has much impact on connection database consumption. This process consumes a connection for each user and screen loaded.

The ObservableCollection<T> is the key for the Repository, it is the intermediary between user/machine interactions and DbSet/DbContext. The ObservableCollection<T> receives the data from database through queries and listen the insert/delete changes through your event CollectionChanged and the modified by the event INotifiedPropertyChanged of the model.

Set<TEntity> DbContext method

Set<TEntity> is the same as in the Disconnected Repository is very important, but in this case, our Connected Repository saves its reference in a protected field, because it has to be available in all repository live.

For more information, you read the DbSet<T> section in Disconnected Repository.

Example Classes

This is the example classes:

C#
public partial class MyDBEntities : DbContext
{
    public MyDBEntities()
        : base("name=MyDBEntities")
    {
    }
 
    public virtual DbSet<City> Cities { get; set; }
    public virtual DbSet<FootballClub> FootballClubs { get; set; }
 
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<City>()
            .Property(e => e.Name)
            .IsUnicode(false);
 
        modelBuilder.Entity<FootballClub>()
            .Property(e => e.Name)
            .IsUnicode(false);
 
        modelBuilder.Entity<FootballClub>()
            .Property(e => e.Members)
            .HasPrecision(18, 0);
    }
}

public partial class City
{
    public int Id { get; set; }
 
    [Required]
    [StringLength(50)]
    public string Name { get; set; }
 
    [Column(TypeName = "numeric")]
    public decimal? People { get; set; }
 
    [Column(TypeName = "numeric")]
    public decimal? Surface { get; set; }
 
    public ICollection<FootballClub> FootballClubs { get; set; }
}

public partial class FootballClub
{
    public int Id { get; set; }
 
    public int CityId { get; set; }
 
    [Required]
    [StringLength(50)]
    public string Name { get; set; }
 
    [Column(TypeName = "numeric")]
    public decimal Members { get; set; }
 
    [Required]
    [StringLength(50)]
    public string Stadium { get; set; }
 
    [Column(TypeName = "date")]
    public DateTime? FundationDate { get; set; }
 
    public string Logo { get; set; }
}

Building Entity Framework Generic Repositories Disconnected

In the first step, we will create the generic ConGenericRepository class:

C#
public class ConGenericRepository<TEntity> : IDisposable where TEntity : class
{
    protected internal readonly DbContext _dbContext;

    protected internal readonly DbSet<TEntity> _dbSet;

    public ConGenericRepository(DbContext dbContext)
    {
        if (dbContext == null) throw new ArgumentNullException(nameof(dbContext), 
                               $"The parameter dbContext can not be null");

        _dbContext = dbContext;
        _dbSet     = _dbContext.Set<TEntity>();
    }

    public void Dispose()
    {
        if (_dbContext != null) _dbContext.Dispose();
    }
}

To start, we will inject the DbContext object, will consult the DbSet and will save in a dbset field.

The class should implement IDisposible interface for releasing the unmanage resources.

The class has a generic constraint from reference types.

Some methods are very similar that Disconnected in description, but are different in implementation.

Let’s go to build all methods.

All / AllAsync

The All/AllAsync methods return the All table data.

C#
public ObservableCollection<TEntity> All()
{
    _dbSet.Load();

    var result = _dbSet.Local;

    return result;
}

public Task<ObservableCollection<TEntity>> AllAsync()
{
    return Task.Run(() =>
    {
        return All();
    });
}

In use:

C#
[TestMethod]
public void All_OK()
{
    ObservableCollection<FootballClub> result = instance.All();

    Assert.IsNotNull(result);
    Assert.IsTrue(result.Count > 0);
}

The method All/Async loads the complete table in the Local (ObservableCollection) property and returns the collection. The local property continuously listens to the changes.

Find / FindAsync

The Find/FindAsync methods, is very similar to All/AllAsync methods, but Find/FindAsync searches a simple row for PK. The PK can be simple or complex. Return one row always.

C#
public TEntity Find(params object[] pks)
{
    if (pks == null) throw new ArgumentNullException(nameof(pks), 
                     $"The parameter pks can not be null");

    var result = _dbSet.Find(pks);

    return result;
}

public Task<TEntity> FindAsync(object[] pks)
{
    return  _dbSet.FindAsync(pks);
}

The param pks behavior is identical to Disconnected, view this section of Disconnected article for more information.

In use:

C#
[TestMethod]
public void Find_OK()
{
    object[] pks = new object[] { 1 };

    FootballClub result = instance.Find(pks);

    Assert.AreEqual(result.Id, 1);
}

GetData / GetDataAsync

Like Find/FindAsync, the methods GetData/GetDataAsync are very similar than All/AllAsync unlike, GetData has an Expression<Func<TEntity,bool>> parameter for filter the query and the Find/FindAsync return only one item and GetData/GetDataAsync returns a collection ever although the collection has one item.

C#
public ObservableCollection<TEntity> GetData(Expression<Func<TEntity, bool>> filter)
{
    if (filter == null) throw new ArgumentNullException(nameof(filter), 
                        $"The parameter filter can not be null");

    _dbSet.Where(filter).Load();

    var filterFunc = filter.Compile();

    var result =  new ObservableCollection<TEntity>(_dbSet.Local.Where(filterFunc));

    RelinkObservableCollection(result);

    return result;
}

public Task<ObservableCollection<TEntity>> 
       GetDataAsync(Expression<Func<TEntity, bool>> filter)
{
    return Task.Run(() =>
    {
        return GetData(filter);
    });
}

Note that we have used a private method RelinkObservableCollection:

C#
private void RelinkObservableCollection(ObservableCollection<TEntity> result)
{
    result.CollectionChanged += (sender, e) =>
    {
        switch (e.Action)
        {
            case System.Collections.Specialized.NotifyCollectionChangedAction.Add:
                _dbSet.Add((TEntity)e.NewItems[0]);
                break;
            case System.Collections.Specialized.NotifyCollectionChangedAction.Remove:
                _dbSet.Remove((TEntity)e.OldItems[0]);
                break;
            default:
                break;
        }
    };
}

This method is necessary because we should return a part of DbSet property Local information only. For it, we create a new ObservableCollection with the filter data and in this moment, the Local property unlink it. The RelinkObservableCollection relinked the ObservableCollection changes with the DbSet.

In use:

C#
[TestMethod]
public void GetData_OK()
{
    Expression<Func<FootballClub, bool>> filter = a => a.Name == "Real Madrid C. F.";

    ObservableCollection<FootballClub> result = instance.GetData(filter);

    Assert.IsNotNull(result);
    Assert.IsTrue(result.Count == 1);
}

SaveChanges / SaveChangesAsync

The method SaveChanges/SaveChangesAsync preserves the ObservableCollection Local property in database.

C#
public int SaveChanges()
{
    var result = _dbContext.SaveChanges();

    return result;
}

public Task<int> SaveChangesAsync()
{
    return _dbContext.SaveChangesAsync();
}

In action:

C#
[TestMethod]
public void SaveChanges_OK()
{
    ObservableCollection<FootballClub> data = instance.All();

    data.Add(new FootballClub
        {
            CityId = 1,
            Name = "New Team",
            Members = 0,
            Stadium = "New Stadium",
            FundationDate = DateTime.Today
        });

    int result = instance.SaveChanges();
    int expected = 1;

    RemovedInsertRecords();

    Assert.AreEqual(expected, result);
}

HasChanges / HasChangesAsync

The method HasChanges/HasChangesAsync verifies if the DbSet property has been modified. In WPF applications, this is very practical in conjunction of Commands for enabled or disabled save buttons.

C#
public bool HasChanges()
{
    var result = _dbContext.ChangeTracker.Entries<TEntity>()
                    .Any(a => a.State == EntityState.Added 
                            || a.State == EntityState.Deleted 
                            || a.State == EntityState.Modified);
 
    return result;
}
 
public Task<bool> HasChangesAsync()
{
    return Task.Run(() =>
    {
        return HasChanges();
    });
}

The method verified the ChangeTracker property search rows in state: Added, Deleted or Modified.

In action:

C#
[TestMethod]
public void HasChanges_OK()
{
    ObservableCollection<FootballClub> data = instance.All();

    data.Add(new FootballClub
    {
        CityId = 1,
        Name = "New Team",
        Members = 0,
        Stadium = "New Stadium",
        FundationDate = DateTime.Today
    });

    bool result = instance.HasChanges();


    Assert.IsTrue(result);
}

Extracting the Interface

Once this has been done, we will extract the Interface.

Image 2

Result:

C#
public interface IConGenericRepository<TEntity> : IDisposable where TEntity : class
{
    ObservableCollection<TEntity> All();
    Task<ObservableCollection<TEntity>> AllAsync();
    ObservableCollection<TEntity> GetData(Expression<Func<TEntity, bool>> filter);
    Task<ObservableCollection<TEntity>> GetDataAsync(Expression<Func<TEntity, bool>> filter);
    TEntity Find(params object[] pks);
    Task<TEntity> FindAsync(object[] pks);
    int SaveChanges();
    Task<int> SaveChangesAsync();
    bool HasChanges();
    Task<bool> HasChangesAsync();
}

We have translated the IDisposable implements to this interface.

WPF Example

The example making is the same to for Disconnected Repository, so that you can see it in the Disconnected article.

Thinking in our Connected Generic Repository, MainViewModel is the most important class. In our example, we interact directly with the datagrid for three actions:

  1. Insert - For insert new row, we will fill the last empty datagridrow.
  2. Update - For update rows, we will click the datagridcell for entry in edit mode, and we will modify data.
  3. Delete - For delete rows, we will select the datagridrow and press the ‘supr’ key.

Image 3

In Action:

Image 4

In the following, we will show the classes (ViewModels) where we use the Connected Generic Repository in the WPF project.

C#
public class MainViewModel : ViewModelBase, IDisposable
{
    private readonly IConGenericRepository<FootballClub> _repository;
 
 
    public ObservableCollection<FootballClub> Data { get; set; }
 
 
    private FootballClub _selectedItem;
    public FootballClub SelectedItem
    {
        get { return _selectedItem; }
        set { Set(nameof(SelectedItem), ref _selectedItem, value); }
    } 
 
    public MainViewModel(IConGenericRepository<FootballClub> repository)
    {
        _repository = repository;
 
        Data = _repository.All();
    }
 
    public void Dispose()
    {
        _repository.Dispose();
    } 
 
    public RelayCommand SaveCommand => new RelayCommand(SaveExecute, SaveCanExecute);
 
    private bool SaveCanExecute()
    {
        var result = _repository.HasChanges();
 
        return result;
    }
 
    private void SaveExecute()
    {
        Action callback = () =>
        {
            var changes = _repository.SaveChanges();
 
            Messenger.Default.Send(new PopupMessage
            ($"It has been realized {changes} change(s) in Database." ));
 
            CollectionViewSource.GetDefaultView(Data).Refresh();
        };
 
        Messenger.Default.Send(new PopupMessage
        ("Do you want to make changes in DataBase ?", callback));
    } 
}

The class MainViewModel receives injected a IConGenericRepository<FootballClub> interface. In its constructor, feed your namesake inject field and fill the ObservableCollection uses All generic repository method. This class has a RelayCommand with name SaveCommand, this command uses two methods, Execute and CanExecute. For CanExecute method, we will use the HasChanges repository method, this will provide enabled or disabled the Save button and we will us to assure save without changes. The SaveExecuted method sends message to view for show messagebox, and it waits for the messagebox answer to save data in database for SaveChanges repository method.

Extending ConGenericRepository

The connected world can be confused. In the previous example, we could make serveral changes in the datagrid and as we were going to save the changes, we knew nothing of which rows are inserted or which rows are updated or deleted. For this reason, we will create a new datagrid column with this information. This column will be the row state.

Image 5

In the Enity Framework model class, we will create a new NotMapped property:

C#
private string _state;
[NotMapped]
public string State
{
    get { return _state; }
    set
    {
        if (_state != value)
        {
            _state = value;
 
            OnPropertyChanged();
        } 
    }
}

This property will contain the all property general state. In Entity Framework initial version, all generates entities class had this property.

We will add the new specific generic repository connected, FutballClubConRepository:

C#
public class FootballClubConRepository : 
ConGenericRepository<FootballClub>, IFootballClubConRepository
{
    public FootballClubConRepository(DbContext dbContext) : base(dbContext) { }
 
 
 
    public string GetState(FootballClub entity)
    {
        var stateEntity = _dbContext.Entry(entity).State;
 
        return stateEntity.ToString();
    } 
}

FutballClubConRepository should be inherits ConGenericRepository<TEntity> and implements a constructor base. Add the GetState method for advice the Entity Framework internal state.

We will update MainViewModel:

C#
public class MainViewModel : ViewModelBase, IDisposable
{
    private readonly IFootballClubConRepository _repository;
 
    //private readonly IConGenericRepository<FootballClub> _repository;
    public ObservableCollection<FootballClub> Data { get; set; } 
 
    //public MainViewModel(IConGenericRepository<FootballClub> repository)
    public MainViewModel(IFootballClubConRepository repository)
    {
        _repository = repository;
 
        Data = _repository.All();
 
        ListenerChangeState(Data, _repository);
    }
 
    private void ListenerChangeState(ObservableCollection<FootballClub> data, 
                                     IFootballClubConRepository repository)
    {
        data.ToList().ForEach(a => ChangeStateRegister(a, repository));
 
        data.CollectionChanged += (sender, e) =>
        {
            if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
            {
                var entity = e.NewItems[0] as FootballClub;
 
                entity.State = "Added";
            }
        };
    }
 
    private void ChangeStateRegister
    (FootballClub entity, IFootballClubConRepository repository)
    {
        entity.PropertyChanged += (sender, e) =>
        {
            if (e.PropertyName != "State")
            {
                entity.State = repository.GetState(entity);
            }
        };
    }
 
    public void Dispose()
    {
        _repository.Dispose();
    } 
 
    public RelayCommand SaveCommand => new RelayCommand(SaveExecute, SaveCanExecute);
 
    private bool SaveCanExecute()
    {
        var result = _repository.HasChanges();
 
        return result;
    }
 
    private void SaveExecute()
    {
        Action callback = () =>
        {
            var changes = _repository.SaveChanges();
 
            Messenger.Default.Send(new PopupMessage
            ($"It has been realized {changes} change(s) in Database." ));
 
            CollectionViewSource.GetDefaultView(Data).Refresh();
 
            ResetDataStates(Data);
        };
 
        Messenger.Default.Send(new PopupMessage
        ("Has you make the changes in DataBase ?", callback));
    }
 
    private void ResetDataStates(ObservableCollection<FootballClub> data)
    {
        data.ToList().ForEach(a => a.State = null);
    }
}

We have added two private methods for register changes ListenerChangedState, that register the insert changes and ChangeStateRegister that register the modified changes.

Ultimately, we will review the class converter:

C#
public class StateConverter : IMultiValueConverter
{ 
    public ImageBrush _imgInsert;
    public ImageBrush _imgUpdate; 
 
    public StateConverter()
    {
        _imgInsert = Application.Current.FindResource("Inserted") as ImageBrush;
        _imgUpdate = Application.Current.FindResource("Edited") as ImageBrush;
    } 
 
    public object Convert(object[] values, Type targetType, 
                          object parameter, CultureInfo culture)
    {
        if (values[0] == null) return null;
 
        var valueStr = values[0].ToString();
 
        switch (valueStr)
        {
            case "Added"   : return _imgInsert;
            case "Modified": return _imgUpdate;
        }
 
        return null;
    }
 
    public object[] ConvertBack(object value, Type[] targetTypes, 
                                object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

This class transforms the state description for the image description.

In action:

Image 6

Test Project

The test project still having the same structure that the previous article Generic Repository Disconnected. We add a new WPF project BuildingEFGRepository.WPF_Con with the new example.

You will the change connectionstring of project BuildingEFGRepository.WPF_Con too.

History

  • 18th December, 2017: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Cecabank
Spain Spain
MVP C# Corner 2017

MAP Microsoft Active Professional 2014

MCPD - Designing and Developing Windows Applications .NET Framework 4
MCTS - Windows Applications Development .NET Framework 4
MCTS - Accessing Data Development .NET Framework 4
MCTS - WCF Development .NET Framework 4

Comments and Discussions

 
QuestionUse the framework's async methods instead of calling your sync methods asynchronously Pin
Tohid Azizi25-Dec-17 23:00
professionalTohid Azizi25-Dec-17 23:00 
AnswerRe: Use the framework's async methods instead of calling your sync methods asynchronously Pin
Juan Francisco Morales Larios26-Dec-17 23:20
Juan Francisco Morales Larios26-Dec-17 23:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.