0

I have a custom extension method "ToSelectList" and I am currently using it like so:

        var myList = await _myService.GetItems();
        var mySelectList = myList.ToSelectList(nameof(MyEntity.Id));

That's fine and works great, however, I tried to do this as one line:

        var myList = await _myService.GetItems().ToSelectList(nameof(MyEntity.Id));

But I get:

'Task<List<MyEntity>>' does not contain a definition for 'ToSelectList'
and no extension method 'ToSelectList' accepting a first argument of type
'Task<List<MyEntity>>' could be found (are you missing a using directive 
or an assembly reference?)

My ToSelectList method is as follows:

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace MyProject.Extensions
{
    public static class ListExtensions
    {
        public static List<SelectListItem> ToSelectList<T>(this List<T> list, string id, string text = "Name")
        {
            var selectListItems = new List<SelectListItem>();

            list.ForEach(item =>
            {
                selectListItems.Add(new SelectListItem
                {
                    Text = item.GetType().GetProperty(text).GetValue(item).ToString(),
                    Value = item.GetType().GetProperty(id).GetValue(item).ToString()
                });
            });

            return selectListItems;
        }
    }

I tried changing method signature to public async static Task<List<SelectListItem>> but no joy.

Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
egmfrs
  • 1,272
  • 3
  • 17
  • 31
  • 4
    `(await _myService.GetItems()).ToSelectList(nameof(MyEntity.Id))` – DavidG Jul 12 '18 at 16:54
  • 3
    While @DavidG's comment is correct, that's actually pretty bad. Don't get all data from the database to project only a small part. Make the service call return only the required data – Camilo Terevinto Jul 12 '18 at 16:56
  • `ToSelectList` could be replaced by a `Select(it=>new SelectListItem{Text="id",Value=>it.id});`. You don't need reflection for that. If you wanted the name/value pair for serialization, simply selecting the value would work. XML and Json serializers would discover the property name and construct the correct XML or JSON string. – Panagiotis Kanavos Jul 12 '18 at 17:03
  • @CamiloTerevinto why is this bad? The db table contains an id and a name column and a list of items i need to return to a dropdown list. I always need the entire contents of this table to populate my dropdown. – egmfrs Jul 12 '18 at 17:21
  • In that case that's not a problem, but since you didn't post your models this could have been a table with many columns – Camilo Terevinto Jul 12 '18 at 17:24
  • @PanagiotisKanavos Thanks for the suggestion. Text="id" needs to be Text=it.Name (what does it stand for? item?) – egmfrs Jul 12 '18 at 17:24
  • @CamiloTerevinto I see what you mean. I may add columns in future so I will keep this in mind. What would you name the GetItems method if it changed to only get Id and Name columns? GetItemNameValuePairs ? – egmfrs Jul 12 '18 at 17:28
  • @egmfrs `it` means `it`, the english word. It's just a parameter name. LINQ already does what you attempt. All LINQ methods accept Expressions that specify conditions, selections, properties etc. The same trick is used to implement generic property change handlers, as shown [in the third example here](https://stackoverflow.com/questions/7677854/notifypropertychanged-event-where-event-args-contain-the-old-value). – Panagiotis Kanavos Jul 13 '18 at 07:25
  • @egmfrs Quite often though you'll realize that making your code too generic or model-driven results in *more hardwiring* than just using what's available. Hence the question, why do you want this? – Panagiotis Kanavos Jul 13 '18 at 07:26
  • @PanagiotisKanavos to save me having to repeat the code to build a select list every time I want to populate a dropdown in my view model. I have about 5 tables, each containing a list of options for a form, essentially. The number of questions on the form may grow, and there will be more tables with more options. On the post action, if the model validation fails, the dropdowns need to be repopulated to the view model again. So the majority of my controller code seemed to be building and re-building select lists all over the place. – egmfrs Jul 13 '18 at 10:51
  • @egmfrs then your actual problem is a form that can display editors for any property in a DTO, eg an editable Card view. That's not as easy as it looks. After a while you'll find out that you *have* to be able to customize individual fields. And what about *validation*? Annotations? You may end up writing a *lot* more code with the *generic* solution than sending the entity as a `Model` to the view and writing the `EditorFor` statements. – Panagiotis Kanavos Jul 13 '18 at 11:11
  • @egmfrs You should probably look for a control or widget to do that before trying to roll your own. There are many (possibly too many) such *Javascript* controls and jQuery plugins, form builders for ASP.NET MVC etc. Commercial packages like those offered by Telerik, Devexpress etc. cost but can save you a *lot* of time, since they've already implemented the tricky parts – Panagiotis Kanavos Jul 13 '18 at 11:19

3 Answers3

1

The problem is that await applies to the entire line so you either need to wrap it in parentheses:

var myList = (await _myService.GetItems())
    .ToSelectList(nameof(MyEntity.Id));

Or split it onto 2 lines:

var items = await _myService.GetItems();
var myList = items.ToSelectList(nameof(MyEntity.Id));
DavidG
  • 113,891
  • 12
  • 217
  • 223
  • parentheses - thats what I needed, thanks. I'm not sure if the call to GetItems even needs to be async. It returns results of a query to the db. – egmfrs Jul 12 '18 at 17:18
  • 1
    If it's doing DB calls, then yes, that should be async. – DavidG Jul 12 '18 at 17:19
1

While @DavidG's answer is correct, I would suggest you to simplify your logic. You don't need any of that reflection code if you use the built-in Tuple<T1, T2>:

// let's assume this is in your service class
// you most likely want a better name
public Task<List<Tuple<int, string>>> GetItemsAsEnumAsync()
{
    return _context.SomeTable
        .Select(x => Tuple.Create(x.Id, x.Name))
        .ToListAsync();
}

Your ToSelectList would then just be:

public static List<SelectListItem> ToSelectList(this List<Tuple<int, string>> list)
{
    return list
        .Select(x => new SelectListItem
        {
            Value = x.Item1.ToString(),
            Text = x.Item2
        })
        .ToList();
}

This assumes that your service is not in the same project as the ASP.NET Core application and hence cannot return the List<SelectListItem> itself. You could make ToSelectList more generic by using ToSelectList<T1, T2>(this List<KeyValuePair<T1, T2>> list) if you find it useful.

Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
  • That's very much appreciated. Which part of my code is using reflection? The NameOf() parts? The fields for the select list are not fixed. My db conventions are SomeTableId rather than just Id. And Name is not always Name. How would I call it if I use the T1, T2 version? – egmfrs Jul 13 '18 at 09:34
  • @egmfrs `.GetType().GetProperty(text).GetValue(item)`, that's all reflection. You would still just use `List> x = ...; var selectListItems = x.ToSelectList()` – Camilo Terevinto Jul 13 '18 at 11:02
  • I am also getting 'IEnumerable>' does not contain a definition for 'ToListAsync' – egmfrs Jul 13 '18 at 11:22
  • @egmfrs Where exactly are you getting that? – Camilo Terevinto Jul 13 '18 at 11:26
  • `.Select(x => Tuple.Create(x.Id, x.Name)) .ToListAsync();` I only have .ToList() available. – egmfrs Jul 13 '18 at 11:47
  • @egmfrs You are either missing a `using Microsoft.EntityFrameworkCore;` or you are not using `Select` on an `IQueryable` – Camilo Terevinto Jul 13 '18 at 12:02
  • Sorted. I'm having to call it like this though, which is a bit a mouthful: `var mySelectList= (await _myService.GetItemsAsEnum()).ToSelectList>>();` instead of just being able to do `var mySelectList= (await _myService.GetItemsAsEnum()).ToSelectList();` – egmfrs Jul 13 '18 at 15:18
-5

You can either go for:

var myList = _myService.GetItems().Result.ToSelectList(nameof(MyEntity.Id));

but this will just block the thread and you will execute code non async

or change result of _myService.GetItems() to List<SelectListItem>.

Anyway what is the point of this async if you immediately need the result?

Rozrzutnik
  • 11
  • 1
  • 2
  • 1
    `async` means you won't block waiting for a response and won't waste a thread to wait for an IO/network call that doesn't use the CPU. It *allows* you to immediatelly use the result. – Panagiotis Kanavos Jul 12 '18 at 17:02
  • 1
    `.Result` though will block the caller and not just waste a thread, it will start by spinwaiting thus wasting CPU, before suspending the thread. That's not a bug, blocking is *not* supposed to take so long that the thread needs to be suspended – Panagiotis Kanavos Jul 12 '18 at 17:06
  • I thought async was best because it calls to the database. I'm not entirely sure. Is it useless? – egmfrs Jul 12 '18 at 17:15
  • 2
    @egmfrs This answer is completely wrong in every sense, please disregard it – Camilo Terevinto Jul 12 '18 at 17:23