Bug 24452

Summary: yield statement used in foreach returns first record only
Product: [Mono] Class Libraries Reporter: eetasoft
Component: System.DataAssignee: Marek Safar <masafa>
Status: RESOLVED FIXED    
Severity: normal CC: masafa, mono-bugs+mono
Priority: ---    
Version: 3.2.x   
Target Milestone: Untriaged   
Hardware: PC   
OS: Linux   
Tags: Is this bug a regression?: ---
Last known good build:
Attachments: Visual Studio solution to reproduce the issue

Description eetasoft 2014-11-11 17:11:46 UTC
Yield statement in foreach loop returns only first record data on dynamic data.

Steps to reproduce:

Run attached MVC4 solution.

Observed output in browser:

A
A

Expected output:

A
B

Reported also in

https://github.com/npgsql/npgsql/issues/295
Comment 1 eetasoft 2014-11-11 17:15:44 UTC
Created attachment 8695 [details]
Visual Studio solution to reproduce the issue
Comment 2 eetasoft 2014-11-11 17:21:15 UTC
In Mono ASP .NET MVC4 application, WebGrid shows npgsql dynamic data last row value in every row.

In Mono code below and from attached solution returns

?column?
B
B

( in bug descrition it was wrong )

If running in windows it returns proper result:

?column?
A
B

Controller:

    public ActionResult Test()
    {
        var data = TestData();
        return View("ReportData", new ReportDataViewModel(data, ""));
    }

    IEnumerable<dynamic> TestData()
    {
        using (var connection = new NpgsqlConnection(ConnectionString()))
        {
            connection.Open();
            DbCommand command = (DbCommand)connection.CreateCommand();
            command.CommandText = "select 'A' union select 'B'";
            using (command)
            {
                IEnumerable<string> columnNames = null;
                using (DbDataReader reader = command.ExecuteReader())
                {
                    foreach (DbDataRecord record in reader)
                    {
                        if (columnNames == null)
                            columnNames = GetColumnNames(record);
                        yield return new CustomDynamicRecord(columnNames, record);
                    }
                }
            }
        }
    }

    static IEnumerable<string> GetColumnNames(DbDataRecord record)
    {
        for (int i = 0; i < record.FieldCount; i++)
        {
            yield return record.GetName(i);
        }
    }

    static string ConnectionString()
    {
        NpgsqlConnectionStringBuilder csb = new NpgsqlConnectionStringBuilder()
        {
            SearchPath = "public",
            Host = "localhost",
            Database = "eeva",
            UserName = "postgres",
            Password = "secret"
        };
        return csb.ConnectionString;
    }

CustomDynamicRecord class:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Dynamic;
using System.Globalization;
using System.Linq;
using WebMatrix.Data.Resources;

public sealed class CustomDynamicRecord : DynamicObject, ICustomTypeDescriptor
{
    public CustomDynamicRecord(IEnumerable<string> columnNames, IDataRecord record)
    {
        Columns = columnNames.ToList();
        Record = record;
    }

    public IList<string> Columns { get; private set; }

    private IDataRecord Record { get; set; }

    public object this[string name]
    {
        get
        {
            return GetValue(Record[name]);
        }
    }

    public object this[int index]
    {
        get
        {
            return GetValue(Record[index]);
        }
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        result = this[binder.Name];
        return true;
    }

    static object GetValue(object value)
    {
        if (DBNull.Value == value || value == null)
          return null;
       return value;
    }

    public override IEnumerable<string> GetDynamicMemberNames()
    {
        return Columns;
    }

    private void VerifyColumn(string name)
    {
        if (!Columns.Contains(name, StringComparer.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException(
                String.Format(CultureInfo.CurrentCulture,
                              "Invalid Column Name " + name));
        }
    }

    AttributeCollection ICustomTypeDescriptor.GetAttributes()
    {
        return AttributeCollection.Empty;
    }

    string ICustomTypeDescriptor.GetClassName()
    {
        return null;
    }

    string ICustomTypeDescriptor.GetComponentName()
    {
        return null;
    }

    TypeConverter ICustomTypeDescriptor.GetConverter()
    {
        return null;
    }

    EventDescriptor ICustomTypeDescriptor.GetDefaultEvent()
    {
        return null;
    }

    PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty()
    {
        return null;
    }

    object ICustomTypeDescriptor.GetEditor(Type editorBaseType)
    {
        return null;
    }

    EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes)
    {
        return EventDescriptorCollection.Empty;
    }

    EventDescriptorCollection ICustomTypeDescriptor.GetEvents()
    {
        return EventDescriptorCollection.Empty;
    }

    PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes)
    {
        return ((ICustomTypeDescriptor)this).GetProperties();
    }

    PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties()
    {
        var properties = from columnName in Columns
                         let columnIndex = Record.GetOrdinal(columnName)
                         let type = Record.GetFieldType(columnIndex)
                         select new DynamicPropertyDescriptor(columnName, type);

        return new PropertyDescriptorCollection(properties.ToArray(), readOnly: true);
    }

    object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd)
    {
        return this;
    }

    private class DynamicPropertyDescriptor : PropertyDescriptor
    {
        private static readonly Attribute[] _empty = new Attribute[0];
        private readonly Type _type;

        public DynamicPropertyDescriptor(string name, Type type)
            : base(name, _empty)
        {
            _type = type;
        }

        public override Type ComponentType
        {
            get { return typeof(EevaDynamicRecord); }
        }

        public override bool IsReadOnly
        {
            get { return true; }
        }

        public override Type PropertyType
        {
            get { return _type; }
        }

        public override bool CanResetValue(object component)
        {
            return false;
        }

        public override object GetValue(object component)
        {
            EevaDynamicRecord record = component as EevaDynamicRecord;
            // REVIEW: Should we throw if the wrong object was passed in?
            if (record != null)
            {
                return record[Name];
            }
            return null;
        }

        public override void ResetValue(object component)
        {
            throw new InvalidOperationException(
                String.Format(CultureInfo.CurrentCulture,
                              "DataResources.RecordIsReadOnly", Name));
        }

        public override void SetValue(object component, object value)
        {
            throw new InvalidOperationException(
                String.Format(CultureInfo.CurrentCulture,
                              "DataResources.RecordIsReadOnly", Name));
        }

        public override bool ShouldSerializeValue(object component)
        {
            return false;
        }
    }
}

View:

@inherits ViewBase<ViewModels.ReportDataViewModel>
@using System.Web.Helpers

@{ Layout = null;
 var gd = new WebGrid(source: Model.Rows, canSort: false , rowsPerPage:100 );
}

<!DOCTYPE html>
<html>
<head>
</head>
<body>
  @gd.GetHtml()
</body>
</html>
Model:

namespace ViewModels
{
    public class ReportDataViewModel : ViewModelBase
    {
        public IEnumerable<dynamic> Rows { get; set; }

        public ReportDataViewModel(IEnumerable<dynamic> rows)
        {
            Rows = rows;
        }
    }
}
Comment 3 Marek Safar 2014-11-18 08:18:38 UTC
Attached sample fails to build for me with

Controllers/ReportController.cs(24,59): error CS0030: Cannot convert type `Npgsql.NpgsqlCommand' to `System.Data.Common.DbCommand'
Comment 4 eetasoft 2014-11-18 13:55:52 UTC
Complete testcase is in 

http://wikisend.com/download/851310/yieldstatementinforeachreturnsfirstrecord.zip

link is valid for 90 days.
This is 11MB and bugziiila did'nt allow to attach it.
Fixing compiler erorr is easy, just change type to interface in few places.
Comment 5 Marek Safar 2014-11-20 11:00:54 UTC
I still cannot reproduce it. What url should I hit to see the error?
Comment 6 eetasoft 2014-11-20 11:05:20 UTC
Url is  Report/Test

This  is specified in solution settings. This is the only url in this solution. There is single controller Report which contains single method Test
Comment 7 Marek Safar 2014-11-21 04:29:55 UTC
Fixed in master and 3.12 branch