Business Objects for Paging

This entry is part 3 of 7 in the series Ultimate MVC Paging

The .NET framework contains lots of ways to pass around data generically now. Tuples, Dynamic Type, Anonymous types, ExpandoObject, etc. But my favorite is a standard style DTO using Generic collections that I can use for business objects for paging.

For paging in an N-Tier, Repository Pattern environment, I use two different Data Transfer Objects (DTO). The first, is an object used to transfer the basic data from the data layer that contains the collection of return objects or DataRows. The second is a more detailed and flexible object that contains all the properties to satisfy 90% of my paging needs.

DataLayer PageResults

Since my service layer already knows the intimate details of my paging request, when I receive back the response from the data layer, I really only need two things:

  1. The collection I asked for
  2. The total number of items that fit my filters

I’ve always felt that if the API was made public, that less is more.

Yes, I could use the same object that I use in the service layer that has more properties, but I’ve always felt that if the API was made public, that less is more. The data layer return object is one that I call PagedResults, it’s structured like:

using System;
using System.Collections.Generic;

namespace YTG.Models
{

    /// <summary>
    /// Yasgar Technology Group, Inc.
    /// http://www.ytgi.com
    /// </summary>
    /// <typeparam name="TEntity"></typeparam>
    public class PagedResult<TEntity> : IPagedResult<TEntity>
    {

        /// <summary>
        /// Primary constructor
        /// </summary>
        /// <param name="items"></param>
        /// <param name="totalCount"></param>
        public PagedResult(List<TEntity> items, int totalCount)
        {
            Items = items;
            TotalCount = totalCount;
        }

        private Lazy<List<TEntity>> m_items = new Lazy<List<TEntity>>(() => new List<TEntity>(), true);

        /// <summary>
        /// IEnumerable list of items returned
        /// Yasgar Technology Group, Inc. - www.ytgi.com
        /// </summary>
        public List<TEntity> Items
        {
            get { return m_items.Value; }
            set
            {
                m_items = new Lazy<List<TEntity>>(() => value, true);
            }
    }

        /// <summary>
        /// Total count of records from query
        /// Yasgar Technology Group, Inc. - www.ytgi.com
        /// </summary>
        public int TotalCount { get; set; } = 0;

    }
}

You can read a little more about this object here:

Your data layer will pull back a dataset or IQueryable resultset. You can stuff that into the PagedResult with the following code:

return new PagedResult<Models.LuCategory>(_entityrows.ToList(), _totalCount);

I usually do this in a service layer before my web service front end, so something like:

  1. Pull back a DataSet or Entity IQueryable collection
  2. Return that to the service layer
  3. Loop through and map to a DTO and stuff into the Paging DTO

A ha! I know what you’re thinking, I would just stuff the DataSet or Entity collection in there and send it back, save some time. Right? I don’t recommend that for one main reason; you’re tying your UI to the structure of your database or ORM. I don’t want to digress in this post as to why this is not desirable, but I promise to discuss it in another post soon.

Hate writing redundant mapping code? Read this post: Stuffing DataSets into your objects.

I return this object from my system REST API or WCF web service. In my service layer in the web site, I combine the results into my UI based DTO that has the rest of the properties needed for paging, called SearchModel. It is IMPERATIVE that this DTO be based on the Interface in the project. The SearchModel object looks like this:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;

namespace YTG.MVC.Lookups.Models
{

    /// <summary>
    /// View Model for Searching and holding results
    /// </summary>
    public class SearchModel<TEntity> : ISearchModel
    {

        /// <summary>
        /// Default constructor
        /// </summary>
        public SearchModel() { }


        public SearchModel(int totalItems, int? page, int pageSize = 10)
        {
            TotalItems = totalItems;
            CurrentPage = page ?? 1;
            PageSize = pageSize;
        }

        private int m_TotalItems = 0;
        private int m_CurrentPage = 1;
        private int m_PageSize = 0;
        private Lazy<List<TEntity>> m_SortedResults = new Lazy<List<TEntity>>(() => new List<TEntity>(), true);

        /// <summary>
        /// Gets or set the total number of items to be paged
        /// Yasgar Technology Group, Inc. - www.ytgi.com
        /// </summary>
        public int TotalItems
        {
            get
            { return m_TotalItems; }
            set
            {
                m_TotalItems = value;
                if (m_PageSize > 0)
                { TotalPages = (m_TotalItems + (m_PageSize - 1)) / m_PageSize; }
            }
        }

        /// <summary>
        /// Gets or sets the current page being displayed
        /// Automatically populates the values of other properties
        /// Yasgar Technology Group, Inc. - www.ytgi.com
        /// </summary>
        public int CurrentPage
        {
            get
            { return m_CurrentPage; }
            set
            {
                m_CurrentPage = value < 1 ? 1 : value;
                StartPage = m_CurrentPage - 5;
                EndPage = m_CurrentPage + 4;

                if (StartPage <= 0)
                {
                    EndPage -= (StartPage - 1);
                    StartPage = 1;
                }

                if (EndPage > TotalPages)
                {
                    EndPage = TotalPages;
                    if (EndPage > 10)
                    {
                        StartPage = EndPage - 9;
                    }
                }
            }
        }

        /// <summary>
        /// Gets or sets the page size currently set
        /// Yasgar Technology Group, Inc. - www.ytgi.com
        /// </summary>
        public int PageSize
        {
            get
            { return m_PageSize; }
            set
            {
                m_PageSize = value;
                if (m_TotalItems > 0)
                { TotalPages = (m_TotalItems + (m_PageSize - 1)) / m_PageSize; }
                // To get EndPage set correctly if this property is set last
                CurrentPage = m_CurrentPage;
            }
        }

        /// <summary>
        /// Gets or set the total number of pages based on the TotalItems and PageSize
        /// Yasgar Technology Group, Inc. - www.ytgi.com
        /// </summary>
        public int TotalPages { get; set; } = 0;

        /// <summary>
        /// Gets or sets the start page for the pager display
        /// Yasgar Technology Group, Inc. - www.ytgi.com
        /// </summary>
        public int StartPage { get; set; } = 0;

        /// <summary>
        /// Gets or sets the end page for the pager display
        /// Yasgar Technology Group, Inc. - www.ytgi.com
        /// </summary>
        public int EndPage { get; set; } = 0;

        /// <summary>
        /// Gets or sets the search filter for the paged items
        /// Yasgar Technology Group, Inc. - www.ytgi.com
        /// </summary>
        public string SearchFilter { get; set; } = string.Empty;

        /// <summary>
        /// Gets or sets the search term for the paged items
        /// Yasgar Technology Group, Inc. - www.ytgi.com
        /// </summary>
        public string SearchTerm { get; set; } = string.Empty;

        /// <summary>
        /// Gets or sets the sort expression for the paged items
        /// Yasgar Technology Group, Inc. - www.ytgi.com
        /// </summary>
        public string SortColumn { get; set; } = string.Empty;

        /// <summary>
        /// Gets or sets the previous sort expression for the paged items when posting back to controller
        /// Yasgar Technology Group, Inc. - www.ytgi.com
        /// </summary>
        public string PrevSortColumn { get; set; } = string.Empty;

        /// <summary>
        /// Gets or sets the count of records in the current page
        /// Yasgar Technology Group, Inc. - www.ytgi.com
        /// </summary>
        public int RecordCount { get; set; } = 0;

        /// <summary>
        /// Gets or sets the sort order, default is asc
        /// Yasgar Technology Group, Inc. - www.ytgi.com
        /// </summary>
        public bool SortDescending { get; set; } = false;

        /// <summary>
        /// Gets or sets whether the paged items only contain active records
        /// Yasgar Technology Group, Inc. - www.ytgi.com
        /// </summary>
        [Display(Name = "Active Only")]
        public bool ActiveOnly { get; set; } = true;

        /// <summary>
        /// Gets or sets the Reference Id, to be used for a parent object
        /// Yasgar Technology Group, Inc. - www.ytgi.com
        /// </summary>
        public long RefId { get; set; } = 0;

        /// <summary>
        /// Gets or sets the Reference Unique Id, to be used for a parent object
        /// Yasgar Technology Group, Inc. - www.ytgi.com
        /// </summary>
        public Guid RefUniqueId { get; set; } = Guid.Empty;

        /// <summary>
        /// Gets or sets the List of pre-sorted results
        /// Yasgar Technology Group, Inc. - www.ytgi.com
        /// </summary>
        public List<TEntity> SortedResults
        {
            get { return m_SortedResults.Value; }
            set
            {
                m_SortedResults = new Lazy<List<TEntity>>(() => value, true);
            }
        }


        /// <summary>
        /// Gets or sets the controller name for this Model
        /// Needed for Pagination partial views
        /// Yasgar Technology Group, Inc. - www.ytgi.com
        /// </summary>
        public string controllerName { get; set; } = string.Empty;

        /// <summary>
        /// Gets or sets the action name for this model
        /// Needed for Pagination partial views
        /// Yasgar Technology Group, Inc. - www.ytgi.com
        /// </summary>
        public string actionName { get; set; } = string.Empty;

    }
}

I use the simpler PagedResults to return the data collection and the total count from my repository. Then push this data, combined with whatever filter options were sent in back to the View.

You can take a look at how these objects are used in my sample app on GitHub:

https://github.com/YTGI/YTG-MVC-Lookups

Stuffing DataSets into your objects

One thing that most developers loath is writing mapping classes. One common need is stuffing datasets into your objects from a return of your SQL Client code. Yes, I know that there are utilities that are out there like AutoMapper, but here’s something to consider.

The power of many of these utilities are their ability to map properties that have the same name, but different case without any code. This is definitely a time saver. I would highly recommend their use in the service layer of your UI if you’re mapping one object, or a small collection of objects like a paged response of 25 or 50 objects. But don’t use these tools if you are processing batch size collections. Why you ask?

Think about it, how would you find out if there was a property in an object with a particular name regardless of case? Reflection of course, but Reflection has a serious processing cost. So using it for a small collection of objects being displayed to a user is one thing, but when you’re processing thousands, or millions of objects, there is nothing that beats declarative code for speed.

I want to make it clear, this method has lots of advantages, as well as disadvantages, there’s never a panacea…

Advantages

  1. One standard place that maps ALL your return values to your DTO
  2. No maintenance of numerous areas mapping data. Think of the scenario where a PK or FK is changed from int to long…
  3. The entire team ALWAYS knows where code is mapped, they don’t have to hunt through multiple service layers and mapping classes
  4. Your object can consume DataSets from multiple stored procedures seamlessly and still populate, even if the field names are different.

Disadvantages

  1. This is only recommended if you’re getting back DataSets, as otherwise, you’d be putting a dependency for an EF entity in your DTO. (Don’t do this, as then you have to reference your Repository in your UI project, which is, how do doctors say it, contra-recommended)
  2. If you’re filling your object from multiple stored procedures, it will hide the fact that a particular column your expecting is absent, which is a code smell.

With the aforementioned caveats, let’s show the concept. The reason I only recommend this for DataSets is that it’s part of the .NET framework. I vehemently recommend that DTO’s don’t have any references to objects that don’t exist either 1) In the .NET framework, or 2) In your common utilities libraries.

Here’s an example of the constructors you would have in such an object:

        /// <summary>
        /// Default constructor
        /// </summary>
        public SalesProduct() { }

        /// <summary>
        /// Constructor with a DataSet to populate
        /// </summary>
        /// <param name="p_DataRow"></param>
        public SalesProduct(DataRow p_DataRow)
        {
            Populate(p_DataRow);
        }

Remember that when you code a constructor, like the second one, you stop the ability to instantiate your object without a parameter. So, in most cases, you want to declare an empty “Default” constructor as well. Remember, if you don’t have an empty constructor, you won’t be able to deserialize the object from a web service call…

Next step is to have the Populate(DataSet) method in your object like this example:

        /// <summary>
        /// Populate this object from a DataRow
        /// www.ytgi.com
        /// </summary>
        /// <param name="p_DataRow"></param>
        private void Populate(DataRow p_DataRow)
        {

            if (p_DataRow.Table.Columns.Contains("ProductID"))
            { this.ProductId = p_DataRow.IsNull("ProductID") ? 0 : p_DataRow.Field<int>("ProductID"); }

            if (p_DataRow.Table.Columns.Contains("ProductName"))
            { this.ProductName = p_DataRow.IsNull("ProductName") ? string.Empty : p_DataRow.Field<string>("ProductName"); }
            if (p_DataRow.Table.Columns.Contains("Description"))
            { this.ProductName = p_DataRow.IsNull("Description") ? string.Empty : p_DataRow.Field<string>("Description"); }


            if (p_DataRow.Table.Columns.Contains("UnitPrice"))
            { this.UnitPrice = p_DataRow.IsNull("UnitPrice") ? 0m : p_DataRow.Field<decimal>("UnitPrice"); }

            if (p_DataRow.Table.Columns.Contains("OnlineOrderFlag"))
            { this.OnlineOrderFlag = p_DataRow.IsNull("OnlineOrderFlag") ? false : p_DataRow.Field<bool>("OnlineOrderFlag"); }

            if (p_DataRow.Table.Columns.Contains("ShipDate"))
            { this.ShipDate = p_DataRow.IsNull("ShipDate") ? DateTime.MinValue : p_DataRow.Field<DateTime>("ShipDate"); }

        }

Notice one thing in the above code that’s my favorite feature of using this method, the ProductName property is being populated by either the “ProductName” column or the “Description” column, depending on the stored procedure you’re calling.

I typically use this implementation when I return a DataSet from a stored procedure and load the objects like this:

List<Models.SalesProduct> _sales = new List<Models.SalesProduct>();

// Would normally do this in the ServiceLayer
if (_returnData != null)
{
   foreach (DataRow _row in _returnData.Tables[0].Rows)
   {
       _sales.Add(new Models.SalesProduct(_row));
   }
}

I hope this helps folks to think of different ways to consolidate code that doesn’t violate the patterns they like to use.

If you’re going through the Paging series, visit the next post to continue: Business Objects for Paging.

Business Object used for Search and Paging

When I’m implementing a paged query, I wanted a simple object that I could return a strongly typed list of values along with a total count from my query. The business object used for search and paging also has the return of a List<T> of objects along with a TotalCount property from my Repository. You’ll see this as the return object in many of my sample paging posts.

This is basically a DTO, it doesn’t have any specific functionality.

    /// <summary>
    /// Yasgar Technology Group, Inc.
    /// http://www.ytgi.com
    /// </summary>
    /// <typeparam name="TEntity"></typeparam>
    [DataContract(Name = "PagedResult", Namespace = "", IsReference = true)]
    public class PagedResult<TEntity> : IPagedResult<TEntity>
    {

        #region Constructors

        /// <summary>
        /// Primary constructor
        /// </summary>
        /// <param name="items"></param>
        /// <param name="totalCount"></param>
        public PagedResult(IEnumerable<TEntity> items, int totalCount)
        {
            m_items = items;
            m_totalCount = totalCount;
        }

        #endregion // Constructors

        #region Fields

        private IEnumerable<TEntity> m_items;
        private int m_totalCount = 0;

        #endregion // Fields

        #region Properties

        /// <summary>
        /// IEnumerable list of items returned
        /// </summary>
        [DataMember]
        public IEnumerable<TEntity> Items
        {
            get { return m_items; }
            protected set { m_items = value; }
        }

        /// <summary>
        /// Total count of records from query
        /// </summary>
        [DataMember]
        public int TotalCount
        {
            get { return m_totalCount; }
            protected set { m_totalCount = value; }
        }

        #endregion // Properties

    }

This data transfer object (DTO) is used to house the results from a paging query that has the minimum amount of information needed to return from the database. It does not need to have search criteria etc, as it is assumed that the calling method already has all that data. This will help you have a standard return object that not only has the collection of objects, but the total count that would have been returned had there not been any filters.

Here is an example of this object in use: