Best way to handle complex entities (relational) with Generic CRUD functions

前提是你 提交于 2019-12-11 14:39:44

问题


I have tried using this generic functions to insert-update Entities but I always thought that maybe I am doing this totally wrong so therefore I would like to have your opinions/suggestions.

These are my Insert & Update functions:

 public static bool Insert<T>(T item) where T : class 
{
    using (ApplicationDbContext ctx = new ApplicationDbContext())
    {
        try
        {
            ctx.Set<T>().Add(item);
            ctx.SaveChanges();
            return true;
        }
        catch (Exception ex)
        {
           // ...
        }
    }
}

 public static bool Update<T>(T item) where T : class 
{
    using (ApplicationDbContext ctx = new ApplicationDbContext())
    {
        try
        {
            Type itemType = item.GetType();
            // switch statement to perform actions according which type we are working on

            ctx.SaveChanges();
            return true;
        }
        catch (Exception ex)
        {
           // ...
        }
    }
}

I have learned that i can use ctx.Entry(item).State = EntityState.Modified; and I have seen so many ways of inserting-updating entities that I am very curious on what is the easiest most manageable way of performing CRUD actions ?

I know about the repository pattern and so on but i don't have much experience with interfaces or I don't seem to fully understand whats used so I prefer not to use it till I fully get it.


回答1:


my approach for that is to use IRepository pattern to wrap CRUD and to make dependencies injection easier in my application, here an example on how i do it:

Define your contract like following: (i am simplifying the example and admitting that all your tables have an integer id -i mean it is not guid or string or whatever- )

public interface IGenericRepository<TEntity> where TEntity : class
{
    #region ReadOnlyRepository

    TEntity GetById(int id);
    ICollection<TEntity> GetAll();
    ICollection<TEntity> GetAll(params Expression<Func<TEntity, object>>[] includeProperties);
    ICollection<TEntity> Query(Expression<Func<TEntity, bool>> expression, params Expression<Func<TEntity, object>>[] includeProperties);
    PagedModel<TEntity> Query(Expression<Func<TEntity, bool>> expression, SortOptions sortOptions, PaginateOptions paginateOptions, params Expression<Func<TEntity, object>>[] includeProperties);
    int Max(Expression<Func<TEntity, int>> expression);

    #endregion



    #region PersistRepository

    bool Add(TEntity entity);
    bool AddRange(IEnumerable<TEntity> items);
    bool Update(TEntity entity);
    bool Delete(TEntity entity);
    bool DeleteById(int id);

    #endregion
}

and then the implementation:

public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class
    {
        #region Fields

        protected DbContext CurrentContext { get; private set; }
        protected DbSet<TEntity> EntitySet { get; private set; }

        #endregion

        #region Ctor

        public GenericRepository(DbContext context)
        {
            CurrentContext = context;
            EntitySet = CurrentContext.Set<TEntity>();
        }

        #endregion

        #region IReadOnlyRepository Implementation

        public virtual TEntity GetById(int id)
        {
            try
            {
                //use your logging method (log 4 net used here)
                DomainEventSource.Log.Info(string.Format("getting entity {0} with id {1}", typeof(TEntity).Name, id));

                return EntitySet.Find(id); //dbcontext manipulation
            }
            catch (Exception exception)
            {
                /// example of error handling
                DomainEventSource.Log.Error(exception.Message);
                var errors = new List<ExceptionDetail> { new ExceptionDetail { ErrorMessage = exception.Message } };
                throw new ServerException(errors);// this is specific error formatting class you can do somthing like that to fit your needs
            }
        }

        public virtual ICollection<TEntity> GetAll()
        {
            try
            {
                return EntitySet.ToList();
            }
            catch (Exception exception)
            {
                //... Do whatever you want
            }
        }

        public virtual ICollection<TEntity> GetAll(params Expression<Func<TEntity, object>>[] includeProperties)
        {
            try
            {
                var query = LoadProperties(includeProperties);

                return query.ToList();
            }
            catch (Exception exception)
            {
                //... Do whatever you want
            }

        }

        public virtual ICollection<TEntity> Query(Expression<Func<TEntity, bool>> expression, params Expression<Func<TEntity, object>>[] includeProperties)
        {
            try
            {
                var query = LoadProperties(includeProperties);

                return query.Where(expression).ToList();
            }
            catch (Exception exception)
            {
                //... Do whatever you want
            }
        }

        // returning paged results for example
        public PagedModel<TEntity> Query(Expression<Func<TEntity, bool>> expression,SortOptions sortOptions, PaginateOptions paginateOptions, params Expression<Func<TEntity, object>>[] includeProperties)
        {
            try
            {
                var query = EntitySet.AsQueryable().Where(expression);
                var count = query.Count();

                //Unfortunatly includes can't be covered with a UT and Mocked DbSets...
                if (includeProperties.Length != 0)
                    query = includeProperties.Aggregate(query, (current, prop) => current.Include(prop));

                if (paginateOptions == null || paginateOptions.PageSize <= 0 || paginateOptions.CurrentPage <= 0)
                    return new PagedModel<TEntity> // specific pagination model, you can define yours
                    {
                        Results = query.ToList(),
                        TotalNumberOfRecords = count
                    };

                if (sortOptions != null)
                    query = query.OrderByPropertyOrField(sortOptions.OrderByProperty, sortOptions.IsAscending);

                var skipAmount = paginateOptions.PageSize * (paginateOptions.CurrentPage - 1);
                query = query.Skip(skipAmount).Take(paginateOptions.PageSize);
                return new PagedModel<TEntity>
                {
                    Results = query.ToList(),
                    TotalNumberOfRecords = count,
                    CurrentPage = paginateOptions.CurrentPage,
                    TotalNumberOfPages = (count / paginateOptions.PageSize) + (count % paginateOptions.PageSize == 0 ? 0 : 1)
                };
            }
            catch (Exception exception)
            {
                var errors = new List<ExceptionDetail> { new ExceptionDetail { ErrorMessage = exception.Message } };
                throw new ServerException(errors);
            }
        }

        #endregion

        #region IPersistRepository Repository

        public bool Add(TEntity entity)
        {
            try
            {
                // you can do some extention methods here to set up creation date when inserting or createdBy etc...
                EntitySet.Add(entity);
                return true;
            }
            catch (Exception exception)
            {
                //DomainEventSource.Log.Failure(ex.Message);
                //or
                var errors = new List<ExceptionDetail> { new ExceptionDetail { ErrorMessage = exception.Message } };
                throw new ServerException(errors);
            }
        }

        public bool AddRange(IEnumerable<TEntity> items)
        {
            try
            {
                foreach (var entity in items)
                {
                    Add(entity);
                }
            }
            catch (Exception exception)
            {
                //DomainEventSource.Log.Failure(ex.Message);
                var errors = new List<ExceptionDetail> { new ExceptionDetail { ErrorMessage = exception.Message } };
                throw new ServerException(errors);
            }
            return true;
        }

        public bool Update(TEntity entity)
        {
            try
            {
                CurrentContext.Entry(entity).State = EntityState.Modified;
            }
            catch (Exception exception)
            {
                var errors = new List<ExceptionDetail> { new ExceptionDetail { ErrorMessage = exception.Message } };
                throw new ServerException(errors);
            }
            return true;

        }

        public bool Delete(TEntity entity)
        {
            try
            {
                if (CurrentContext.Entry(entity).State == EntityState.Detached)
                {
                    EntitySet.Attach(entity);
                }
                EntitySet.Remove(entity);
            }
            catch (Exception exception)
            {
                var errors = new List<ExceptionDetail> { new ExceptionDetail { ErrorMessage = exception.Message } };
                throw new ServerException(errors);
            }
            return true;
        }

        public bool DeleteById(TKey id)
        {
            var entityToDelete = GetById(id);

            return Delete(entityToDelete);
        }

        #endregion

        #region Loading dependancies Utilities
        private IQueryable<TEntity> LoadProperties(IEnumerable<Expression<Func<TEntity, object>>> includeProperties)
        {
            return includeProperties.Aggregate<Expression<Func<TEntity, object>>, IQueryable<TEntity>>(EntitySet, (current, includeProperty) => current.Include(includeProperty));
        }
        #endregion
    }

I am admitting that your model classes are already created and decorated. After this , you need to create your entityRepository like following : this is an example of managing entity called Ticket.cs

public class TicketRepository : GenericRepository<Ticket>, ITicketRepository
{
    // the EntityRepository classes are made in case you have some ticket specific methods that doesn't 
    //have to be in generic repository

    public TicketRepository(DbContext context)
        : base(context)
    {

    }

    // Add specific generic ticket methods here (not business methods-business methods will come later-)
}

After this comes the UnitOfWork class which allows us to unify entry to the database context and provides us an instance of repositories on demand using dependency injection

public class UnitOfwork : IUnitOfWork
{
    #region Fields

    protected DbContext CurrentContext { get; private set; }

    private ITicketRepository _tickets;

    #endregion

    #region ctor

    public UnitOfwork(DbContext context)
    {
        CurrentContext = context;
    }


    #endregion

    #region UnitOfWorkBaseImplementation




    public void Commit()
    {
        try
        {
            CurrentContext.SaveChanges();
        }
        catch (Exception e)
        {
           /// catch
        }

    }

    public void Rollback()
    {
        foreach (var entry in CurrentContext.ChangeTracker.Entries())
        {
            switch (entry.State)
            {
                case EntityState.Modified:
                case EntityState.Deleted:
                    entry.State = EntityState.Modified; //Revert changes made to deleted entity.
                    entry.State = EntityState.Unchanged;
                    break;
                case EntityState.Added:
                    entry.State = EntityState.Detached;
                    break;
                case EntityState.Detached:
                    break;
                case EntityState.Unchanged:
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }
    }

    #region complete RollBack()


    private void RejectScalarChanges()
    {
        foreach (var entry in CurrentContext.ChangeTracker.Entries())
        {
            switch (entry.State)
            {
                case EntityState.Modified:
                case EntityState.Deleted:
                    entry.State = EntityState.Modified; //Revert changes made to deleted entity.
                    entry.State = EntityState.Unchanged;
                    break;
                case EntityState.Added:
                    entry.State = EntityState.Detached;
                    break;
                case EntityState.Detached:
                    break;
                case EntityState.Unchanged:
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }
    }

    private void RejectNavigationChanges()
    {
        var objectContext = ((IObjectContextAdapter)this).ObjectContext;
        var deletedRelationships = objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted).Where(e => e.IsRelationship && !this.RelationshipContainsKeyEntry(e));
        var addedRelationships = objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Added).Where(e => e.IsRelationship);

        foreach (var relationship in addedRelationships)
            relationship.Delete();

        foreach (var relationship in deletedRelationships)
            relationship.ChangeState(EntityState.Unchanged);
    }

    private bool RelationshipContainsKeyEntry(System.Data.Entity.Core.Objects.ObjectStateEntry stateEntry)
    {
        //prevent exception: "Cannot change state of a relationship if one of the ends of the relationship is a KeyEntry"
        //I haven't been able to find the conditions under which this happens, but it sometimes does.
        var objectContext = ((IObjectContextAdapter)this).ObjectContext;
        var keys = new[] { stateEntry.OriginalValues[0], stateEntry.OriginalValues[1] };
        return keys.Any(key => objectContext.ObjectStateManager.GetObjectStateEntry(key).Entity == null);
    }

    #endregion

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

    #endregion

    #region properties



    public ITicketRepository Tickets
    {
        get { return _tickets ?? (_tickets = new TicketRepository(CurrentContext)); }
    }


    #endregion
}

Now for the last part we move to our business service layer and make a ServiceBase class which will be implemented by all business services

public class ServiceBase : IServiceBase
{
    private bool _disposed;

    #region IServiceBase Implementation

    [Dependency]
    public IUnitOfWork UnitOfWork { protected get; set; }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
            return;

        if (disposing)
        {
            var disposableUow = UnitOfWork as IDisposable;
            if (disposableUow != null)
                disposableUow.Dispose();
        }

        _disposed = true;
    }

    #endregion
}

and finally one example of business service class and how to use your CRUD and play with your business rules (i am using properties injection which is not the best to do so i suggest to change it and use constructor injection instead)

    public class TicketService : ServiceBase, ITicketService
    {
        #region fields

        private IUserService _userService;
        private IAuthorizationService _authorizationService;

        #endregion

        #region Properties

        [Dependency]
        public IAuthorizationService AuthorizationService
        {
            set { _authorizationService = value; }
        }

        [Dependency]
        public IUserService UserService
        {
            set { _userService = value; }
        }



        public List<ExceptionDetail> Errors { get; set; }

        #endregion

        #region Ctor

        public TicketService()
        {
            Errors = new List<ExceptionDetail>();
        }

        #endregion

        #region IServiceBase Implementation
        /// <summary>
        /// desc
        /// </summary>
        /// <returns>array of TicketAnomalie</returns>
        public ICollection<Ticket> GetAll()
        {
            return UnitOfWork.Tickets.GetAll();
        }

        /// <summary>
        /// desc
        /// </summary>
        /// <param name="id"></param>
        /// <returns>TicketAnomalie</returns>
        public Ticket GetTicketById(int id)
        {
            return UnitOfWork.Tickets.GetById(id);
        }

        /// <summary>
        /// description here
        /// </summary>
        /// <returns>Collection of Ticket</returns>
        public ICollection<Ticket> GetAllTicketsWithDependencies()
        {
            return UnitOfWork.Tickets.Query(tick => true, tick => tick.Anomalies);
        }

        /// <summary>
        /// description here
        /// </summary>
        /// <param name="id"></param>
        /// <returns>Ticket</returns>
        public Ticket GetTicketWithDependencies(int id)
        {
            return UnitOfWork.Tickets.Query(tick => tick.Id == id, tick => tick.Anomalies).SingleOrDefault();
        }

        /// <summary>
        /// Add new ticket to DB
        /// </summary>
        /// <param name="anomalieId"></param>
        /// <returns>Boolean</returns>
        public bool Add(int anomalieId)
        {
            var anomalie = UnitOfWork.Anomalies.Query(ano => ano.Id.Equals(anomalieId), ano => ano.Tickets).FirstOrDefault();
            var currentUser = WacContext.Current;
            var superv = _userService.GetSupervisorUserProfile();
            var sup = superv.FirstOrDefault();

            if (anomalie != null)
            {
                var anomalies = new List<Anomalie>();
                var anom = UnitOfWork.Anomalies.GetById(anomalieId);
                anomalies.Add(anom);

                if (anomalie.Tickets.Count == 0 && sup != null)
                {
                    var ticket = new Ticket
                    {
                        User = sup.Id,
                        CreatedBy = currentUser.GivenName,
                        Anomalies = anomalies,
                        Path = UnitOfWork.SearchCriterias.GetById(anom.ParcoursId),
                        ContactPoint = UnitOfWork.ContactPoints.GetById(anom.ContactPointId)
                    };
                    UnitOfWork.Tickets.Add(ticket);
                    UnitOfWork.Commit();
                }
            }
            else
            {
                Errors.Add(AnomaliesExceptions.AnoNullException);
            }
            if (Errors.Count != 0) throw new BusinessException(Errors);
            return true;
        }


        public bool Update(Ticket ticket)
        {
            if (ticket == null)
            {
                Errors.Add(AnomaliesExceptions.AnoNullException);
            }
            else
            if (!Exists(ticket.Id))
            {
                Errors.Add(AnomaliesExceptions.AnoToUpdateNotExistException);
            }
            if (Errors.Count != 0) throw new BusinessException(Errors);
            UnitOfWork.Tickets.Update(ticket);
            UnitOfWork.Commit();
            return true;
        }



        public bool Exists(int ticketId)
        {
            var operationDbEntity =
                UnitOfWork.Tickets.Query(t => t.Id.Equals(ticketId)).ToList();
            return operationDbEntity.Count != 0;
        }

        #endregion

        #region Business Implementation


       //play with your buiness :)

        #endregion
}

Finally, i suggest that you redo this using asynchronous methods (async await since it allows a better management of service pools in the web server)

Note that this is my own way of managing my CRUD with EF and Unity. you can find a lot of other implementations that can inspire you.

Hope this helps,



来源:https://stackoverflow.com/questions/53314580/best-way-to-handle-complex-entities-relational-with-generic-crud-functions

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!