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.

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
);

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.

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
    };
}

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