npgsql / efcore.pg

Entity Framework Core provider for PostgreSQL
PostgreSQL License
1.53k stars 223 forks source link

Can't intercept via wrapping reader because NpgsqlModificationCommandBatch depends casts to NpgsqlDataReader, and it is sealed #1113

Open danranqian opened 4 years ago

danranqian commented 4 years ago

We are trying to customize DataReader behavior in EF Core 3.0 to force all returned DateTime values to be interpreted as in the UTC timezone.

We are getting this exception on saving changes.

Unable to cast object of type 'UtcDbDataReader' to type 'Npgsql.NpgsqlDataReader'.

   at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.Execute(IRelationalConnection connection)
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(DbContext _, ValueTuple`2 parameters)
   at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(IEnumerable`1 commandBatches, IRelationalConnection connection)
   at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChanges(IList`1 entries)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(IList`1 entriesToSave)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
   ... our code

We intercept ReaderExecuted command and return a customized DbDataReader. We are trying to do in EF Core 3.0 something similar to this answer from Stack Overflow. (I know this example is for EF 6 but I cannot find any EF Core examples.)

/// <summary>
/// Intercepts the GetDateTime method to specify it to be in UTC timezone
/// </summary>
public class UtcInterceptor : DbCommandInterceptor
{
    public override DbDataReader ReaderExecuted(DbCommand command,
                                                CommandExecutedEventData eventData,
                                                DbDataReader result)
    {
        var dbDataReader = base.ReaderExecuted(command, eventData, result);

        if (!(dbDataReader is UtcDbDataReader))
        {
            var utcDataReader = new UtcDbDataReader(dbDataReader);

            return utcDataReader;
        }
        return dbDataReader;
    }

    public class UtcDbDataReader : DbDataReader
    {
        private readonly DbDataReader _source;

        public UtcDbDataReader(DbDataReader source)
        {
            this._source = source;
        }

        public override string GetDataTypeName(int ordinal)
        {
            return _source.GetDataTypeName(ordinal);
        }

        public override DateTime GetDateTime(int ordinal)
        {
            return DateTime.SpecifyKind(_source.GetDateTime(ordinal), DateTimeKind.Utc);
        }

        public override decimal GetDecimal(int ordinal)
        {
            return _source.GetDecimal(ordinal);
        }

        public override double GetDouble(int ordinal)
        {
            return _source.GetDouble(ordinal);
        }

        public override Type GetFieldType(int ordinal)
        {
            return _source.GetFieldType(ordinal);
        }

        public override float GetFloat(int ordinal)
        {
            return _source.GetFloat(ordinal);
        }

        public override Guid GetGuid(int ordinal)
        {
            return _source.GetGuid(ordinal);
        }

        public override short GetInt16(int ordinal)
        {
            return _source.GetInt16(ordinal);
        }

        public override int GetInt32(int ordinal)
        {
            return _source.GetInt32(ordinal);
        }

        public override long GetInt64(int ordinal)
        {
            return _source.GetInt64(ordinal);
        }

        public override string GetName(int ordinal)
        {
            return _source.GetName(ordinal);
        }

        public override int GetOrdinal(string name)
        {
            return _source.GetOrdinal(name);
        }

        public override string GetString(int ordinal)
        {
            return _source.GetString(ordinal);
        }

        public override object GetValue(int ordinal)
        {                       
            return _source.GetValue(ordinal);
        }

        public override int GetValues(object[] values)
        {               
            return _source.GetValues(values);
        }

        public override bool IsDBNull(int ordinal)
        {
            return _source.IsDBNull(ordinal);                
        }

        public override int FieldCount
        {
            get { return _source.FieldCount; }
        }

        public override object this[int ordinal] => _source[ordinal];

        public override object this[string name] => _source[name];

        public override int RecordsAffected => _source.RecordsAffected;

        public override bool HasRows => _source.HasRows;

        public override bool IsClosed => _source.IsClosed;

        public override bool NextResult() => _source.NextResult();

        public override bool Read() => _source.Read();

        public override int Depth => _source.Depth;

        public override IEnumerator GetEnumerator()
        {
            return _source.GetEnumerator();          
        }

        public new void Dispose()
        {
            _source.Dispose();
        }

        public override bool GetBoolean(int ordinal)
        {
            return _source.GetBoolean(ordinal);
        }

        public override byte GetByte(int ordinal)
        {
            return _source.GetByte(ordinal);
        }

        public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length)
        {
            return _source.GetBytes(ordinal, dataOffset, buffer, bufferOffset, length);
        }

        public override char GetChar(int ordinal)
        {
            return _source.GetChar(ordinal);
        }

        public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length)
        {
            return _source.GetChars(ordinal, dataOffset, buffer, bufferOffset, length);
        }
    }
}

Are we doing something wrong here? Or is this something that Npgsql is not supporting at the moment?

nathanpjones commented 4 years ago

Having a similar problem here. Would just inherit from NpgsqlDataReader but it's marked as a sealed class.

roji commented 4 years ago

The issue is that EFCore.PG casts DbDataReader down to NpgsqlDataReader in order to get each individual's statement changed row count.

mesztam commented 2 years ago

We have the same issue, is there any known workaround maybe? Or is there any plan to fix this in the near future?

roji commented 2 years ago

For now, I'd advise doing other ways for manipulating the data returned from the reader - EF Core value converters can do this, as well as command interceptors.