Search code examples
c#asp.netlinqasp.net-coreselectlistitem

Populate SelectListItem from generic data type


I have the following code which returns results from a database table comprising of an Id field and a Name field, and transfers it to a list of SelectListItems (this populates a dropdown box in my view.)

var locationTypes = await APIHelper.GetAsync<List<LocationType>>(url);

var items = new List<SelectListItem>();

items.AddRange(locationTypes.Select(locationType =>
{
    var item = new SelectListItem();
    item.Value = locationType.LocationTypeId.ToString();
    item.Text = locationType.Name;
    return item;
}));

I am repeating this a lot throughout my application, substituting LocationType for various other things. The item.Value always gets the Id property of the data returned (the Id field is always in the format of {TableName}+"Id"), and the item.Text always gets ".Name" property.

How can I make this generic? I am trying to achieve something like this, although it is syntactically incorrect and may be the incorrect approach:

var myGenericObjects = await APIHelper.GetAsync<List<T>>(url)

var items = new List<SelectListItem>();

items.AddRange(myGenericObjects .Select(myGenericObjects =>
{
    var item = new SelectListItem();
    item.Value = myGenericObject.Field[0].ToString();
    item.Text = myGenericObject.Name;
    return item;
}));

Solution

  • You can create a custom extension for a generic list object, then, using reflection retrieve the values that you are wanting to map to the SelectListItem.Text and Name fields. Note I am using "nameof" in order to prevent any confusion or magic string representations of the properties to which I am trying to map.

    I did define a default value of "Name" to the namePropertyName parameter. Per your description it sounded like, by convention, most of your DTOs have the property "Name" in them. If that's not the case simply remove the default value that is defined.

    There are additional checks that could be made to this extension to prevent NullReference and ArgumentExceptions as well, but for simplicity of the example were left out. Example: Ensuring a value is provided in the idPropertyName and namePropertyName parameters and ensuring those property names exist on the provided generic object prior to conversion.

    public static class ListExtensions
    {
        public static List<SelectListItem> ToSelectList<T>(this List<T> list, string idPropertyName, string namePropertyName = "Name")
            where T : class, new()
        {
            List<SelectListItem> selectListItems = new List<SelectListItem>();
    
            list.ForEach(item =>
            {
                selectListItems.Add(new SelectListItem
                {
                    Text = item.GetType().GetProperty(namePropertyName).GetValue(item).ToString(),
                    Value = item.GetType().GetProperty(idPropertyName).GetValue(item).ToString()
                });
            });
    
            return selectListItems;
        }
    }
    

    Example Use:

    var testList = new List<TestDto>
    {
        new TestDto { Name = "Test0", TestId = 0 },
        new TestDto { Name = "Test1", TestId = 1 },
        new TestDto { Name = "Test2", TestId = 2 },
        new TestDto { Name = "Test3", TestId = 3 },
        new TestDto { Name = "Test4", TestId = 4 },
    };
    
    var selectList = testList.ToSelectList(nameof(TestDto.TestId), nameof(TestDto.Name));
    

    Here is the TestDto class for reference:

    public class TestDto
    {
        public int TestId { get; set; }
        public string Name { get; set; }
    }