Search code examples
c#genericsmultiple-inheritancemixinscastle-dynamicproxy

How can I set up a generic paging object that lives on top of my generic collection object?


I have been working on removing a lot of code duplication from my application, specifically around my models. Several models also have a collection variant that is an IEnumerable of the model type. Previously all of the collection variants were individual implementations but I was able to combine the majority of their code down into a ModelCollection base class.

Now on top of these collection models are additional models with paging values, that all have the exact same properties and so I would like to also collapse these into a base class. The problem I'm running into is that .NET does not support multiple inheritance and because each of the ModelCollection implementations still need to be explicitly implemented, as most have some special logic, a simple Generic chain won't solve the problem either.

The ModelCollection Base class:

public abstract class ModelCollection<TModel> : IEnumerable<T>, IModel where TModel : IPersistableModel
{
    protected readonly List<TModel> Models;

    protected ModelCollection()
    {
        Models = new List<TModel>();
    }

    protected ModelCollection(params TModel[] models)
        : this((IEnumerable<TModel>)models)
    {
    }

    protected ModelCollection(IEnumerable<TModel> models)
        : this()
    {
        models.ForEach(Models.Add);
    }

    public virtual int Count
    {
        get { return Models.Count; }
    }

    public virtual IEnumerator<TModel> GetEnumerator()
    {
        return Models.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public virtual void Add(TModel model)
    {
        Models.Add(model);
    }

    public virtual void Accept(Breadcrumb breadcrumb, IModelVisitor visitor)
    {
        foreach (var model in this)
        {
            model.Accept(breadcrumb.Attach("Item"), visitor);
        }
        visitor.Visit(breadcrumb, this);
    }

    public bool IsSynchronized { get; set; }
}

A sample of a PagedCollection:

public class PagedNoteCollection : NoteCollection
{
    public PagedNoteCollection(params Note[] notes)
        : base(notes)
    {
    }
    public int CurrentPage { get; set; }
    public int TotalPages { get; set; }
    public int TotalNotesCount { get; set; }
}

The IModel interface for reference:

public interface IModel
{
    void Accept(Breadcrumb breadcrumb, IModelVisitor visitor);
}

Solution

  • This problem can be solved using Castle.DynamicProxy and its Mixins functionality.

    First the ModelCollection base class will need to have an interface, and in this specific example since the implementation is only a combination of the IModel and IEnumerable interfaces it need only inherit those interfaces:

    public interface IModelCollection<T> : IEnumerable<T>, IModel
    {
    }
    

    Second you will need an interface and one implementation to represent the paging fields:

    public interface IPagingDetails
    {
        int CurrentPage { get; set; }
        int TotalPages { get; set; }
        int TotalCount { get; set; }
        bool HasPreviousPage { get; }
        int PreviousPage { get; }
        bool HasNextPage { get; }
        int NextPage { get; }
    }
    
    public class PagingDetails : IPagingDetails
    {
        public int CurrentPage { get; set; }
        public int TotalPages { get; set; }
        public int TotalCount { get; set; }
        public bool HasPreviousPage
        {
            get { return CurrentPage > 1; }
        }
    
        public int PreviousPage
        {
            get { return CurrentPage - 1; }
        }
    
        public bool HasNextPage
        {
            get { return CurrentPage < TotalPages; }
        }
    
        public int NextPage
        {
            get { return CurrentPage + 1;}
        }
    }
    

    Next, in order to simplify accessing the various parts once the proxy has been generated, (specifically to remove the requirement to cast the proxy between the ModelCollection and PagingDetails interfaces) we need an empty interface to combine the paging and collections interfaces:

    public interface IPagedCollection<T> : IPagingDetails, IModelCollection<T>
    {}
    

    Finally we need to create the proxy with the paging details as a mixin:

    public class PagedCollectionFactory
    {
        public static IPagedCollection<TModel> GetAsPagedCollection<TModel, TCollection>(int currentPage, int totalPages, int totalCount, TCollection page)
            where TCollection : IModelCollection<TModel>
            where TModel : IPersistableModel
        {
            var generator = new ProxyGenerator();
            var options = new ProxyGenerationOptions();
            options.AddMixinInstance(new PagingDetails { CurrentPage = currentPage, TotalPages = totalPages, TotalCount = totalCount });
            return (IPagedCollection<TModel>)generator.CreateClassProxyWithTarget(typeof(TCollection), new[] { typeof(IPagedCollection<TModel>) }, page, options);
        }
    }
    

    And now we can use the proxy (this is taken from the unit test I wrote to ensure it worked):

    var collection = new NoteCollection(
                new Note {Text = "note 1"},
                new Note {Text = "note 2"},
                new Note {Text = "note 3"},
                new Note {Text = "note 4"});
    
    var pagedCollection = PagedCollectionFactory.GetAsPagedCollection<Note,NoteCollection>(1, 1, 1, collection);
    
    Assert.That(pagedCollection.CurrentPage, Is.EqualTo(1));
    Assert.That(pagedCollection.ToList().Count, Is.EqualTo(4));