Source code
C# Delegates Generics

How to Map Types using Delegates in Generic Collections

Welcome to today’s post.

In today’s post I will be exploring how to use generic collections to help us map arrays from one type to another.

There are a handful of useful extension methods within the generic collections library that are useful for mapping, filtering, and sorting collections.

In one of my previous posts, I showed how to use the AutoMapper library to help us to map from one source DTO class to another destination DTO class. With AutoMapper we converted DTO classes for single instance objects to another single instance object. In this post, we will see how to map from a collection of one DTO class, into another collection of a different DTO class.

In the first section, I will show how to use the ConvertAll() extension method to perform mapping between collections.

Using the ConvertAll() Generic Extension Method

First I will look at the ConvertAll() generic extension method which is used to convert an array which has objects of one type to an array of a different type. The definition is below:

public static TOutput[] ConvertAll<TInput,TOutput> (
    TInput[] array, 
    Converter<TInput,TOutput> converter
);

In the definition, TInput and TOutput corresponds to the DTO classes for the source and destination objects.

Essentially it takes two parameters, a source array to be converted, and a conversion delegate, which is a comparison method that converts each item of type TInput into an object of type TOutput.

The conversion method is a conversion delegate with definition:

public delegate TOutput Converter<in TInput,out TOutput>(TInput input);

The resulting array of items contains objects of type TOutput.

In the next section, I will cover a manual approach to mapping between collections, then show how to use the cleaner approach with ConvertAll().

Improving on the Manual Mapping Approach between Collections

In this section, I will show how to map between collections using a manual approach, which is familiar to an approach that uses the ConvertAll() extension method.

Suppose we have arrays of different types we would like to convert. The first array contains classes with items derived from the class shown:

public class BookViewModel
{
    public int ID { get; set; }
    
    [Required]
    public string Title { get; set; }
    
    [Required]
    public string Author { get; set; }
    public int YearPublished { get; set; }
    public string Genre { get; set; }
    public string Edition { get; set; }
    public string ISBN { get; set; }
    
    [Required]
    public string Location { get; set; }
    public DateTime DateCreated { get; set; }
    public DateTime DateUpdated { get; set; }
	…
}

The destination array contains classes derived from classes as shown:

public class BookStatusViewModel: BookViewModel
{
    public string Status { get; set; }
    public DateTime DateLoaned { get; set; }
    public DateTime DateDue { get; set; }
    public DateTime DateReturn { get; set; }
    public string Borrower { get; set; }
    public bool OnShelf { get; set; }
	…
}

An example of an array conversion that does not utilise any mapping utilities is shown below:

public async Task<List<BookLoan.Models.BookStatusViewModel>> 
  OnLoanReport(string currentuser = null)
{
    List<BookLoan.Models.BookStatusViewModel> loanstats = 
        new List<Models.BookStatusViewModel>();

    var books = await _bookService.GetBooks();

    foreach (Models.BookViewModel book in books)
    {
        BookLoan.Models.BookStatusViewModel bsvm = await 
            _loanService.GetBookLoanStatus(book.ID);
        if (((currentuser != null) && (bsvm.Borrower == currentuser))
          || (currentuser == null))
        {
            loanstats.Add(new Models.BookStatusViewModel()
            {
                ID = book.ID,
                Author = book.Author,
                Title = book.Title,
                Genre = book.Genre,
                ISBN = book.ISBN,
                Edition = book.Edition,
                Location = book.Location,
                YearPublished = book.YearPublished,
                OnShelf = bsvm.OnShelf,
                DateLoaned = bsvm.DateLoaned,
                DateReturn = bsvm.DateReturn,
                DateDue = bsvm.DateDue,
                Status = bsvm.Status,
                Borrower = bsvm.Borrower
            });
        }
    }
    return loanstats;
}

As we can see, the pattern here is to use a foreach on the list or array and within the loop block we populate the destination list or array after a condition is satisfied. We can also achieve the same result by conditionally filtering out the source collection then use a utility like AutoMapper to convert the array.

Below is the same outcome using the ConvertAll extension method with the class types that require conversion:

public async Task<List<BookLoan.Models.BookStatusViewModel>> OnLoanReport(
  string currentuser = null)
{
    List<BookStatusViewModel> loanstats; 

    var books = await _bookService.GetBooks();

    BookViewModel[] arrBooks =  books.ToArray(); 

    BookStatusViewModel[] arrBookLoanStatus = Array.ConvertAll(arrBooks, 
        new Converter<BookViewModel, BookStatusViewModel>(BookToBookLoanStatus));

    loanstats = Array.FindAll(arrBookLoanStatus, c =>
      ((currentuser != null) && (c.Borrower == currentuser)) || (currentuser == null)
    ).ToList<BookStatusViewModel>();

    return loanstats;
}

The conversion delegate method input is an object of type BookViewModel and returns an object of type BookStatusViewModel.

private BookStatusViewModel BookToBookLoanStatus(BookViewModel input)
{
    BookLoan.Models.BookStatusViewModel bookStatus = 
        _loanService.GetBookLoanStatus(input.ID).GetAwaiter().GetResult();

    return new BookStatusViewModel()
    {
        ID = input.ID,
        Author = input.Author,
        Title = input.Title,
        Genre = input.Genre,
        ISBN = input.ISBN,
        Edition = input.Edition,
        Location = input.Location,
        YearPublished = input.YearPublished,
        OnShelf = bookStatus.OnShelf,
        DateLoaned = bookStatus.DateLoaned,
        DateReturn = bookStatus.DateReturn,
        DateDue = bookStatus.DateDue,
        Status = bookStatus.Status,
        Borrower = bookStatus.Borrower
    };
}

In the next section, I will show how to improve the conversion and the output of the collection.

Refactoring the Conversion and Improving the Output

With some refactoring we can combine the ConvertAll and FindAll extension method calls to:

public async Task<List<BookLoan.Models.BookStatusViewModel>> OnLoanReport(
  string currentuser = null)
{
    List<BookStatusViewModel> loanstats; 

    var books = await _bookService.GetBooks();

    loanstats =
        Array.FindAll(
            Array.ConvertAll(books.ToArray(),
            new Converter<BookViewModel, BookStatusViewModel>(BookToBookLoanStatus)
        ),
        c => ((currentuser != null) && (c.Borrower == currentuser)) || 
            (currentuser == null)
    )
    .ToList<BookStatusViewModel>();

    return loanstats;
}

We can also apply the Sort() extension method to the result of the FindAll() method to sort the array before returning it. The Sort() generic method has the following definition:

public static void Sort<T> (T[] array, Comparison<T> comparison);

Which takes an input array of type T and a comparison delegate method, which has the following definition:

public delegate int Comparison<in T>(T x, T y);

The comparison delegate takes a type T and returns the result of the comparison. The result depends on the comparison of the object x of type T with the object y of type T.  The table below summarizes the results:

ConditionResult
x<y-1
x=y0
x>y1

Below is the same method with sorting applied after the result of the array conversion. The sort delegate in this case is an anonymous lambda function:

public async Task<List<BookLoan.Models.BookStatusViewModel>> OnLoanReport(
  string currentuser = null)
{
    List<BookStatusViewModel> loanstats; 

    var books = await _bookService.GetBooks();

    BookStatusViewModel[] arrLoanStatus =
        Array.FindAll(
            Array.ConvertAll(books.ToArray(),
                new Converter<BookViewModel, BookStatusViewModel>(
                    BookToBookLoanStatus)
                ),
                c => ((currentuser != null) && (c.Borrower == currentuser)) || 
                      (currentuser == null));

    Array.Sort<BookStatusViewModel>(arrLoanStatus,
        (a, b) => {
            if (a.Borrower == null)
            {
                if (b.Borrower == null)
                    return 0;
                else
                    return -1;
            }
            else
            {
                if (b.Borrower == null)
                    return 1;
                else
                    return string.Compare(a.Borrower, b.Borrower);
            }
        }
    );

    loanstats = arrLoanStatus.ToList<BookStatusViewModel>();

    return loanstats;
}

As we can see, we have simplified the conversion of arrays from one type to another, including provision of filtering and sorting. For structures that are bound to data contexts I would recommend using a mapping tool like AutoMapper in the conversion delegate in conjunction with list filtering and sorting.

That’s all for today’s post.

I hope you found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial