суббота, 30 августа 2008 г.

Расширения для System.Enum

Довольно странно, что языковые средства расширяются от версии к версии платформы, однако нововведения как-правило не затрагивают те части FCL, что знакомы нам с .NET 1.0. Речь пойдет о способах скрыть неуклюжие обращения к методам класса System.Enum, и добавить кое-какую функциональность. Определимся со списком целей:
  • метод object Enum.Parse(Type, string): приходится дважды указывать тип перечислителя (один раз аргументом метода, другой для приведения результата). Попытаемся подсахарить;
  • метод bool Enum.IsDefined(Type, object) так же требует указания типа перечислителя. В случае, когда тип перечислителя уже зашит в аргументе это лишнее. Например, когда требуется проверить принадлежность значения перечислителя к набору констант перечислителя без применения конструкции switch. Подсахарить;
  • проверка на наличие установленного флага выглядит слишком громоздко для использования в операторах ветвления:
    if(FileAccess.Read == (fileAccess & FileAccess.Read)) { ... }
    Еще хуже выглядит проверка на наличие сразу нескольких флагов. Определим метод, который с небольшим оверхедом по производительности скрасит конструкцию. (Наверняка, в Nemerle есть соответствующие макросы)
Для реализации задуманного потребуется два класса. Один - generic класс, в котором будут определены методы для работы с перечислителями а так же кое-какие статические поля, обеспечивающие работу этих методов:
public static class EnumExtensions<TEnum>
   where TEnum : struct, IComparable, IConvertible, IFormattable
{
}
Обратите внимание на ограничения параметра типа. Я постарался выбрать ограничения, максимально близкие к типу enum, просто для того, чтобы компилятор и intellisense не позволяли использовать этот класс для других типов. На самом деле ни одно из ограничений не понадобится для реализации требуемой функциональности. Они фиктивны. К сожалению, не удалось исключить создание типов аля EnumExtensions<int>. При создании таких типов можно добиться двух эффектов: исключения TypeInitializationException, либо ограниченной работоспособности. В данном конкретном случае предпочитаю избежать TypeInitializationException.

Второй класс нужен только для объявления extension-методов, т.к. они не могут быть определены в generic-классах:
public static class EnumExtensions
{
}
Начнем с метода Parse. В generic-классе определим следующий метод:
public static TEnum Parse(string value)
{
   return (TEnum)Enum.Parse(typeof(TEnum), value);
}
В классе для extension методов определим метод-обертку для вышеописанного:
public static TEnum Parse<TEnum>(this string value)
   where TEnum : struct, IComparable, IConvertible, IFormattable
{
   return EnumExtensions<TEnum>.Parse(value);
}
Обращаться к методу-расширению можно в следующей форме:
var mode = modeString.Parse<FileMode>();
Для реализации метода IsDefined(TEnum value) потребуется хранение набора констант, полученных с помощью метода Enum.GetValues(Type). Дело в том, что подглядев реализацию метода Enum.IsDefined(Type, object), я понял что меня не устраивает такое количество выполняемого кода для проверки значения на допустимость. Даже цена получения массива значений в Enum.GetValues(Type) слишком высока для обращения к этому методу более одного раза! Добавим в generic-класс следующий код:
private static readonly TEnum[] s_values = GetValues();

private static TEnum[] GetValues()
{
   return (typeof(TEnum).IsEnum)
       ? (TEnum[])Enum.GetValues(typeof(TEnum))
       : new TEnum[]{};
}

public static int IndexOf(TEnum value)
{
   return Array.IndexOf(s_values, value);
}

public static bool IsDefined(TEnum value)
{
   return IndexOf(value) > -1;
}
В порядке объявления: s_values - статическое поле для хранения массива величин типа TEnum. Инициализируется в объявлении. Статический метод GetValues проверяет, является ли тип TEnum перечислителем, и возвращает массив значений перечислителя, либо пустой массив, избегая исключения TypeInitializationException (оно непременно возникнет при возбуждении исключения ArgumentException при обращении к методу Enum.GetValues(Type) во время инициализации типа ExtensionMethods<TEnum>). Методы IndexOf и IsDefined тривиальны.

Отмечу только, что я не гарантирую их работоспособность при использовании типов, отличных от enum, которые смогли пролезть через набор ограничений Type-параметра (примитивные типы int, long,... и некоторые пользовательские типы). Уверен, что при желании читатель сможет самостоятельно реализовать достойное поведение этих методов для типов, отличных от enum. Думаю, что самым достойным здесь будет возбуждение исключения NotSupportedException.

Да, метод GetIndex(TEnum) получился в качестве бонуса, и я не вижу необходимости скрывать его. Может оказаться полезным. Добавим соответствующие extension-методы в класс EnumExtensions (не generic-класс):
public static int GetIndex<TEnum>(this TEnum value)
   where TEnum : struct, IComparable, IConvertible, IFormattable
{
   return EnumExtensions<TEnum>.IndexOf(value);
}

public static bool IsDefined<TEnum>(this TEnum value)
   where TEnum : struct, IComparable, IConvertible, IFormattable
{
   return EnumExtensions<TEnum>.IsDefined(value);
}
Вот код, проверяющий работу метода IsDefined и демонстрирующий обращение к нему через extension-метод:
Assert.IsTrue(FileMode.Open.IsDefined());

Assert.IsFalse(((FileMode)1000).IsDefined());
Получился весьма элегантный способ проверки значений на принадлежность к набору констант. Приступим к реализации метода bool HasFlags(TEnum value, TEnum flags). Не сложно реализовать такой метод с помощью обращения к методу ulong IConvertible.ToUInt64(IFormatProvider), однако производительность такого решения будет не на высоте (как минимум 4 операции boxing-а, 2 обращения к extern методам, 2 unboxing-а). Так же этот метод будет оперировать 64-х разрядными величинами, даже в тех случаях, когда перечислитель основан на более коротких типах.

Недавно пришло в голову, что динамически сгенерированный метод для соответствующего enum-типа может быть вполне приемлемым по производительности решением. Следующий метод определяет динамический метод DynamicMethod и делегирует генерацию кода переданному в качестве аргумента методу.
private static DynamicMethod BuildHasFlagsMethod(Type enumType, Action<ILGenerator> ilGenAction)
{
   var method = new DynamicMethod(
       "HasFlagMethod",
       typeof(bool),
       new Type[] { enumType, enumType },
       typeof(EnumExtensions).Module);

   var il = method.GetILGenerator();
   ilGenAction(il);
   return method;
}
Я не зря параметризовал этот метод параметром Type, в то время как в generic-классе известен тип TEnum. Для оптимизации работы JIT-компилятора и объема генерируемого им машинного кода следует выносить методы, не использующие явно типы generic аргументов, в не generic-классы. В рамках этого поста я буду располагать такие методы в generic-классе, но буду подразумевать, что читатель вынесет их во вспомогательный класс (если, конечно, статья окажется полезной для него, и он решит воспроизвести код).

Следующие два метода генерируют код реализации метода HasFlags. Первый генерирует хорошую реализацию, а второй - реализацию, которая выбрасывает исключение NotSupportedException. Вторая реализация пригодится для перечислителей без атрибута [Flags], либо для типов, не являющихся enum-ами.
static void EmitHasFlags(ILGenerator il)
{
   il.Emit(OpCodes.Ldarg_1);
   il.Emit(OpCodes.Ldarg_0);
   il.Emit(OpCodes.Ldarg_1);
   il.Emit(OpCodes.And);
   il.Emit(OpCodes.Ceq);
   il.Emit(OpCodes.Ret);
}

static void EmitThrowException(ILGenerator il)
{
   var ctor = typeof(NotSupportedException).GetConstructor(new Type[] { });
   il.Emit(OpCodes.Newobj, ctor);
   il.Emit(OpCodes.Throw);
}
Метод EmitHasFlags записывает два аргумента в стек для выполнения операций логического умножения и сравнения результата со втрорым аргументом, выполняет операции умножения и сравнения, затем возвращает результат. Операция логического умножения может быть выполнена только над примитивными типами, потому необходимо гарантировать обращение к этому методу генерации только при корректном generic-аргументе.
private static readonly Func<TEnum, TEnum, bool> s_hasFlagsMethod;

public static bool IsFlags { get; private set; }

static EnumExtensions()
{
   IsFlags = HasFlagsAttribute(typeof(TEnum));

   Action<ILGenerator> ilGenAction;
   if(IsFlags)
       ilGenAction = EmitHasFlags;
   else
       ilGenAction = EmitThrowException;

   var method = BuildHasFlagsMethod(typeof(TEnum), ilGenAction);

   s_hasFlagsMethod = (Func<TEnum, TEnum, bool>)method.CreateDelegate(
       typeof(Func<TEnum, TEnum, bool>));
}

internal static bool HasFlagsAttribute(Type enumType)
{
   return enumType.GetCustomAttributes(typeof(FlagsAttribute), false).Length > 0;
}

public static bool HasFlags(TEnum value, TEnum flags)
{
   return s_hasFlagsMethod(value, flags);
}
В порядке объявления: s_hasFlagsMethod - делегат для сгенерированного метода; IsFlags - свойство, указывающее на наличие атрибута [Flags] у типа TEnum. Это еще один полезный бонус, который можно оставить опубликованным; далее - статический конструктор.

Остановимся на нем подробнее: Первым делом он инициирует свойство IsFlags значением, возвращаенным методом HasFlagsAttribute(Type). Затем выбирается метод для генерации кода. При значении свойства IsFlags, равном true, можно гарантировать, что TEnum - enum тип, т.к. компилятор не даст применить атрибут [Flags] к любому другому типу, кроме enum. Однако, варьируя условие выбора метода генерации IL кода, можно добиться генерации рабочего метода для целых типов TEnum, при необходимости. Я, правда, такой необходимости не вижу. Для целых методов нет нужды обращаться к динамически сгенерированному коду, потому как можно определить методы обычным образом.

Наконец, реализация метода HasFlags(TEnum, TEnum), которая делегирует динамически сгенерированному методу. Объявление соответствующего extension-метода:
public static bool HasFlags<TEnum>(this TEnum value, TEnum flags)
   where TEnum : struct, IComparable, IConvertible, IFormattable
{
   return EnumExtensions<TEnum>.HasFlags(value, flags);
}
Использование этого метода может быть например таким:
if(fileAccess.HasFlags(FileAccess.Read)) { ... }
Предлагаю сравнить этот кусок кода с аналогичным выше. Думаю, что этот информативнее. Однако, не следует использовать этот метод в критичном по производительности коде.

P.S. Код для проверки флагов в типе int может выглядеть так:
public static bool HasFlags(this int value, int flags)
{
   return flags == (value & flags);
}

...
if(myIntValue.HasFlags(0x80)) { ... }
P.P.S Пробовал сгенерировать код с помощью Expression. Вот что вышло:
var valueParam = Expression.Parameter(typeof(TEnum), "value");
var flagsParam = Expression.Parameter(typeof(TEnum), "flags");
Expression body = Expression.Equal(
    flagsParam,
    Expression.And(valueParam, flagsParam));
В последней строке получил исключение: System.InvalidOperationException: The binary operator And is not defined for the types 'System.IO.FileAccess' and 'System.IO.FileAccess'.. Получается, что il.Emit(OpCodes.And); можно выполнить над Enum-ом, а Expression.And - нет. Был искренне удивлен.

пятница, 15 августа 2008 г.

Короткий синтаксис ADO .NET (часть IV)

Короткий синтаксис ADO .NET (часть I)
Короткий синтаксис ADO .NET (часть II)
Короткий синтаксис ADO .NET (часть III)

В предыдущих частях было описано все что необходимо для обращения к БД за один вызов, кроме разве что передачи параметров. В ADO .NET уже проделана вся работа по унифицированному способу задания параметров. Осталось лишь воспользоваться им.

Задумка в том, чтобы объявлять параметры к команде в том же обращении (ExecuteReader, ExecuteScalar либо ExecuteNonQuery) через запятые. Для каждого параметра достаточно передать только 2 значения - имя параметра и значение параметра. А тип параметра будет определяться типом значения. Определим следующий класс:
public class Parameter
{
    public string ParameterName { get; private set; }

    public DbType DbType { get; private set; }

    public object Value { get; private set; }

    private Parameter(string name, DbType type, object value)
    {
        ParameterName = name;
        DbType = type;
        Value = value;
    }

    public void ApplyParameter(IDbCommand command)
    {
        IDbDataParameter parameter = command.CreateParameter();
        parameter.ParameterName = ParameterName;
        parameter.DbType = DbType;

        if (ReferenceEquals(Value, null))
        {
            parameter.Value = DBNull.Value;
        }
        else
        {
            parameter.Value = Value;
        }

        command.Parameters.Add(parameter);
    }

    ...
}
Метод ApplyParameter создает унифицированным способом экземпляр параметра, передает ему хранящиеся в экземпляре имя, унифицированный тип и значение параметра, добавляет созданный параметр в коллекцию параметров команды. Обратите внимание, что конструктор класса Parameter объявлен с private модификатором видимости. Это потому, что я параноидально опасаюсь создания экземпляров с типом параметра не согласованным со значением параметра. Определение типов параметра возьмет на себя серия производящих методов.

Метод ConvertToObject<T>(T? value) конвертирует переданные nullable значения в object тип. В сочетании с методом ApplyParameter они удачно проецируют nullable типы на значения параметров.
internal static object ConvertToObject<T>(T? value)
    where T : struct
{
    if (value.HasValue)
    {
        return value.Value;
    }
    else
    {
        return null;
    }
}

public static Parameter Make(string name, string value)
{
    return new Parameter(name, DbType.String, value);
}

public static Parameter Make(string name, Guid? value)
{
    return new Parameter(name, DbType.Guid, ConvertToObject(value));
}

public static Parameter Make(string name, int? value)
{
    return new Parameter(name, DbType.Int32, ConvertToObject(value));
}

public static Parameter Make(string name, bool? value)
{
    return new Parameter(name, DbType.Boolean, ConvertToObject(value));
}

public static Parameter Make(string name, double? value)
{
    return new Parameter(name, DbType.Double, ConvertToObject(value));
}

public static Parameter Make(string name, DateTime? value)
{
    return new Parameter(name, DbType.DateTime, ConvertToObject(value));
}

public static Parameter Make(string name, decimal? value)
{
    return new Parameter(name, DbType.Decimal, ConvertToObject(value));
}
Таким образом, обращение
Make("@Value", (int?)null)
сформирует параметр целого типа со значением DbNull.Value. Почему производящие методы, а не перегруженные конструкторы? Просто в некоторых случаях могут потребоваться делегаты (например Func<string, int?, Parameter>), а делегат можно создать только для метода. Для добавления списка параметров к методам Execute*** воспользуемся ключевым словом params.

Вот обновленный интерфейс IDbDriver:
public interface IDbDriver
{
    IDbConnection MakeConnection();

    IDbCommand MakeCommand(IDbConnection connection, string commandText, params Parameter[] parameters);
    IDbCommand MakeCommand(string text, params Parameter[] parameters);

    int ExecuteNonQuery(string commandText, params Parameter[] parameters);
    int ExecuteNonQuery(IDbConnection connection, string commandText, params Parameter[] parameters);

    void ExecuteReader(Action<IDataReader> action, string commandText, params Parameter[] parameters);
    void ExecuteReader(IDbConnection connection, Action<IDataReader> action, string commandText, params Parameter[] parameters);
}
Не составит сложности модифицировать методы Execute*** и протянуть параметр parameters до метода MakeCommand. Вот его новая реализация:
public IDbCommand MakeCommand(IDbConnection connection, string commandText, params Parameter[] parameters)
{
    if (connection == null)
    {
        throw new ArgumentNullException("connection");
    }
    var result = connection.CreateCommand();

    result.CommandText = commandText;
    foreach (Parameter parameter in parameters)
    {
        parameter.ApplyParameter(result);
    }
    return result;
}
Осталось привести пример обращения к БД для выполнения команды с параметром:
[TestMethod]
public void ExecuteReaderRealTest()
{
    var dbDriver = new DbDriver(
        "Data Source=localhost;Initial Catalog=Northwind;Integrated Security=True",
        SqlClientFactory.Instance);

    DataTable table = new DataTable();
    dbDriver.ExecuteReader(
         table.Load,
         "SELECT * FROM [Order Details] WHERE UnitPrice > @Price",
         Parameter.Make("@Price", 20));

    Assert.IsTrue(table.Rows.Count > 0);
}
И в заключение содержательной части пример с выполнением более чем одной команды в рамках одного соединения:
static void LoadTable(DataTable table, IDbCommand command)
{
    using (var reader = command.ExecuteReader())
    {
        table.Load(reader);
    }
}

[TestMethod]
public void MultiCommandTest()
{
    var dbDriver = new DbDriver(
        "Data Source=localhost;Initial Catalog=Northwind;Integrated Security=True",
        SqlClientFactory.Instance);

    var dataSet = new DataSet();
    var orderDetails = dataSet.Tables.Add("Order Details");
    var orders = dataSet.Tables.Add("Orders");

    int orderId = 10248;

    using (var connection = dbDriver.MakeConnection())
    {
        var orderIdParameter = Parameter.Make("@OrderId", orderId);
      
        Func<string, IDbCommand> makeCommand =
            x => dbDriver.MakeCommand(connection, x, orderIdParameter);
                    
        using (var selectOrders = makeCommand("SELECT * FROM Orders WHERE OrderId = @OrderId"))
        using (var selectDetails = makeCommand("SELECT * FROM [Order Details] WHERE OrderId = @OrderId"))
        using (connection.CreateSession())
        {
            LoadTable(orders, selectOrders);
            LoadTable(orderDetails, selectDetails);
        }
    }

    Assert.IsTrue(orderDetails.Rows.Count > 0);
}
Обратите внимание, для короткой записи создания команды я использовал анонимный метод, который использует один экземпляр класса Parameter для формирования всех команд. При чтении DataSet наборов данных довольно часто используется одинаковый набор параметров при обращении к БД за разными таблицами. На этом и сыграл. Запись получилась компактной и наглядной. В первом посте я поведал об истории моего собеседования.

Надеюсь, что теперь вы меня поймете, что используя такую обвязку к ADO .NET не мудрено забыть кое-что из последовательности действий, описываемых в MSDN... Применяя такую обвязку к ADO, будьте внимательны на собеседованиях ;)

Содержательная часть серии постов об короткой нотации ADO .NET закончилась. Осталось только поудивляться тому, что разработчики Microsoft все необходимое сделали сами, как то инвариантные относительно движка БД способы создания соединений, команд, параметров команд, и даже предусмотрели инвариантные типы параметров. Однако, по каким-то причинам Microsoft просто не довела эту кухню до этого логического конца. Почему-то их логическим концом стал безумный кодогенератор дизайнера адаптеров данных, который генерирует дикое количество нечитаемого кода, завязанного на тип БД. Если разработчик работает с SqlServer, то дизайнер генерирует соединения SqlConnection, команды SqlCommand, параметры SqlParameter и совершенно не ясно, как модифицировать этот код для работы с другим движком БД, либо с несколькими. К тому же, вся документация ADO .NET кишит примерами, завязанными на конкретные движки БД. Скромно замечу, что преподнесенный мной код обвязки абсолютно инвариантен к типу БД. Но заслуга в этом разработчиков Microsoft.

Короткий синтаксис ADO .NET (часть III)

Короткий синтаксис ADO .NET (часть I)
Короткий синтаксис ADO .NET (часть II)
Короткий синтаксис ADO .NET (часть IV)

Приступим к созданию соединений и команд. Я для этого предпочитаю использовать некий класс DbDriver, параметризованный ConnectionString-ом и соответствующим DbProviderFactory классом. DbDriver создает соединения сразу же инициированные ConnectionString-ом, и команды, инициированные соединением. Следующий интерфейс как раз описывает эту сущность:
public interface IDbDriver
{
    IDbConnection MakeConnection();

    IDbCommand MakeCommand(IDbConnection connection, string commandText);
    IDbCommand MakeCommand(string text);

    int ExecuteNonQuery(string commandText);
    int ExecuteNonQuery(IDbConnection connection, string commandText);

    void ExecuteReader(Action<IDataReader> action, string commandText);
    void ExecuteReader(IDbConnection connection, Action<IDataReader> action, string commandText);
}
Вообще, рекомендуется в перегружаемых методах опциональный параметр писать в конце списка параметров. Забегая вперед, скажу, что это не окончательная сигнатура методов создания и выполнения команд. Перегруженные методы MakeCommand нужны для двух вариантов создания команд. Один вариант (принимающий IDbConnection) нужен для создания команд в рамках указанного соединения, когда потребуется выполнение более одной команды, другой вариант (без соединения) - когда нам требуется выполнить лишь одну команду в соединении.

Методы выполнения команд (ExecuteNonQuery и ExecuteReader) перегружены со следующей целью. Один вариант принимает соединение, создает и инициирует команду, открывает открывает соединение (если не было открыто), выполняет команду, закрывает (если было закрыто), уничтожает команду и возвращает результат. Второй вариант - создает соединение, вызывает первый вариант, после чего уничтожает соединение.
public class DbDriver : IDbDriver
{
    public DbDriver(string connectionString, DbProviderFactory providerFactory)
    {
        if (providerFactory == null)
        {
            throw new ArgumentNullException("providerFactory");
        }
        ProviderFactory = providerFactory;
        ConnectionString = connectionString;
    }

    public string ConnectionString { get; private set; }

    public DbProviderFactory ProviderFactory { get; private set; }

    public IDbConnection MakeConnection()
    {
        IDbConnection result = ProviderFactory.CreateConnection();
        result.ConnectionString = ConnectionString;
        return result;
    }

    public IDbCommand MakeCommand(IDbConnection connection, string commandText)
    {
        if (connection == null)
        {
            throw new ArgumentNullException("connection");
        }
        var result = connection.CreateCommand();
        result.CommandText = commandText;
        return result;
    }

    public IDbCommand MakeCommand(string commandText)
    {
        return MakeCommand(MakeConnection(), commandText);
    }
    ....
}
Выше приведена реализация конструктора класса и производящих методов. Надеюсь, что комментарии излишни. Далее реализация методов, выполняющих команды:
public int ExecuteNonQuery(string commandText)
{
    using (var connection = MakeConnection())
    {
        return ExecuteNonQuery(connection, commandText);
    }
}

public int ExecuteNonQuery(IDbConnection connection, string commandText)
{
    if (connection == null)
    {
        throw new ArgumentNullException("connection");
    }

    using (var command = MakeCommand(connection, commandText))
    using (connection.CreateSession())
    {
        return command.ExecuteNonQuery();
    }
}

public void ExecuteReader(IDbConnection connection, Action<IDataReader> action, string commandText)
{
    if (connection == null)
    {
        throw new ArgumentNullException("connection");
    }

    using (var command = MakeCommand(connection, commandText))
    using (connection.CreateSession())
    using (var reader = command.ExecuteReader())
    {
        action(reader);
    }
}

public void ExecuteReader(Action<IDataReader> action, string commandText)
{
    using (var connection = MakeConnection())
    {
        ExecuteReader(connection, action, commandText);
    }
}
Методы Execute*** делают попытку открыть соединение в самый последний момент перед обращением к команде и закрывают соединение, если оно было закрыто перед вызовом, сразу после выполнения команды. Повторюсь, гибкость метода CreateSession позволяет одними методами работать как с первоначально закрытым соединением с БД, так и с уже октрытым. ExecuteReader принимает Action<IDataReader>. Это связано с тем, что IDataReader должен быть освобожден до закрытия соединения. Если метод ExecuteReader будет закрывать соединение, т.е. если соединение было закрыто перед вызовом, то к тому моменту требуется прочитать все что надо из IDataReader-а. Метод ExecuteScalar я даже не стал включать в пример, т.к. он работает полностью аналогично методу ExecuteReader.

Unit тесты приводить не буду. Но один интеграционный, пожалуй приведу. Он довольно показателен по части того, что достигнуто:
[TestMethod]
public void ExecuteReaderRealTest()
{
    var dbDriver = new DbDriver(
        "Data Source=localhost;Initial Catalog=Northwind;Integrated Security=True",
        SqlClientFactory.Instance);

    DataTable table = new DataTable();

    dbDriver.ExecuteReader(
        reader => table.Load(reader),
        "SELECT OrderId FROM [Order Details]");

    Assert.IsTrue(table.Rows.Count > 0);
}
Код, что отвечает непосредственно за обращение к БД занимает всего 3 строчки. В рамках обращения к одному методу выполняются действия, перечисленные в конце первой части этой серии постов.

Почти все! Не хватает только чего-то для работы с параметрами команд. А это в следующем посте...

четверг, 14 августа 2008 г.

Короткий синтаксис ADO .NET (часть II)

Короткий синтаксис ADO .NET (часть I)
Короткий синтаксис ADO .NET (часть III)
Короткий синтаксис ADO .NET (часть IV)

В предыдущей части я привел перечень действий, необходимых для выполнения запроса и корректного овсобождения ресурсов. Начнем с открытия и закрытия соединения. В пределе хочется иметь сущность, которая бы:
  1. анализировала состояние указанного соединения, и открывала бы его, если оно не открыто;
  2. гарантированно закрывала бы соединение после использования, в случае если соединение было первоначально закрыто;
  3. обращение к такой конструкции не занимало бы много места.
Отличный повод воспользоваться конструкцией using и интерфейсом IDisposable чтобы избежать конструкция try/finally. Вот код требуемой сущности:
public class ConnectionSession : IDisposable
{
   public ConnectionSession(IDbConnection dbConnection)
   {
       if (dbConnection == null)
       {
           throw new ArgumentNullException("dbConnection");
       }
       DbConnection = dbConnection;

       WasOpened = ConnectionState.Open == (DbConnection.State & ConnectionState.Open);

       if (!WasOpened)
       {
           DbConnection.Open();
       }
   }

   public IDbConnection DbConnection { get; private set; }
   public bool WasOpened { get; private set; }

   public void Dispose()
   {
       if (!WasOpened)
       {
           DbConnection.Close();
       }
   }
}
Прежде чем начну комментировать код, хочу заметить, что в связи с грядущим переходом на C# 3.0, я начинаю пробовать на вкус новый синтаксис. Однако, все или почти все можно повторить на C# 2.0.

Как я уже сказал, интерфейс IDisposable реализован чисто для поддержания короткого синтаксиса using. Сей объект не нуждается в финализации и DbConnection сам будет освобождать необходимые ресурсы при сборке мусора. Быть может когда-нибудь я уделю связке using/IDisposable больше внимания.

Почему я использовал IDbConnection интерфейс, вместо базового класса DbConnection? Очень просто. При тестировании я могу подменить объект без средств тестирования, использующих инструментацию сборок. Для генерации mock объектов и заглушек (stub) я использую Rhino Mocks. Это первая библиотека мокогенераторов, которую я использую, и очень доволен ей. Пока не было поводов переходить на другие. А с новым синтаксисом (для .NET 3.5) я просто обожаю Rhino (это не реклама! Rhino Mocks - бесплатная библиотека и мне за упоминание о ней никто не платит).

В постах я буду приводить некоторые тесты, но далеко не все, которые пишу.
[TestMethod]
   public void UsingConstructionWithOpenedConnectionTest()
   {
       var connection = MockRepository.GenerateStub<IDbConnection>();

       connection.Stub(x => x.State).Return(ConnectionState.Closed);

       using (new ConnectionSession(connection))
       {
           connection.AssertWasCalled(x => x.Open());
           connection.AssertWasNotCalled(x => x.Close());
       }

       connection.AssertWasCalled(x => x.Close());
   }

   [TestMethod]
   public void UsingConstructionWithClosedConnectionTest()
   {
       var connection = MockRepository.GenerateStub<IDbConnection>();

       connection.Stub(x => x.State).Return(ConnectionState.Open);

       using (new ConnectionSession(connection))
       {
           connection.AssertWasNotCalled(x => x.Open());
       }

       connection.AssertWasNotCalled(x => x.Close());
   }
Несмотря на то, что в каждом тесте рекомендуется проверять не более одного условия, я напичкал их множеством условий, потому как я тестировал поведение ConnectionSession в рамках конструкции using. Это уже даже не unit тест, а некий интеграционный. Unit тесты класса ConnectionSession я опустил. Их больше, чем хотелось бы вставлять в пост. Оговорки в условиях к ConnectionSession (вначале поста) нужны для безопасного использования соединения в области действия нескольких вложенных блоков using для ConnectionSession объектов. В рамках C# 3.0 будет удобен следующй extension метод:
public static class ConnectionSessionExtensions
{
    public static ConnectionSession CreateSession(this IDbConnection connection)
    {
        return new ConnectionSession(connection);
    }
}
это позволит писать код в стиле
using (connection.CreateSession())
{
    return command.ExecuteNonQuery();
}
Довольно емкий смысл вложен в одну строчку:
если соединение не открыто, то открыть его, если было закрыто, то закрыть после выхода из блока.
Уже этот код позволит многим сделать обращения к ADO (и не только) более лаконичными. Но на этом не остановимся, продолжение следует.

вторник, 12 августа 2008 г.

Короткий синтаксис ADO .NET (часть I)

Короткий синтаксис ADO .NET (часть II)
Короткий синтаксис ADO .NET (часть III)
Короткий синтаксис ADO .NET (часть IV)

Да, есть такая технология, пока. Можно даже сказать была, или почти была. Но ADO .NET еще поживет какое-то время. Не хотелось бы обсуждать недостатки этого динозавра в сравнении с LINQ to SQL или Entity Framework, либо ORM подходами. Это не тема данного поста.

Здесь лишь примем факт, что ADO .NET еще пока актуально, хоть и сильно сдает позиции. На этой технологии уже много что написано, и еще пишется, и кое-что будет написано. Хочу обратить внимание, что несмотря на повсеместное засилье ADO в среде .NET разработчиков (до последнего времени), пользоваться ADO в чистом виде довольно неудобно, громоздко. Многие вероятно видали код, сгенерированный дизайнером для DbAdapter-ов, DbCommand и их параметров... А про кол-во манипуляций, необходимых для выполнения запроса и закрытия всех щелей за собой для очистки совести, есть целая история.

История о том, как я проходил собеседование в одну контору на позицию удаленного разработчика. Надо заметить, что вакансия была с заманчивой почасовой оплатой, соразмерной ставке freelancer-а. Интервьюера в большей степени интересовали навыки работы с WinForms и ADO .NET. На вопросы о WinForms я довольно резво ответил (как мне показалось), но с ADO - поплавал: Меня попросили набросать код для чтения каких-либо данных из таблицы БД в DataTable прямо в ICQ. Я что-то быстро набросал и отправил набросок. На что последовал ответ: "Ваш код работать не будет!". Это меня довольно сильно смутило. Я смотрел на этот код и не мог понять, что же в нем не так. Когда я его вставил в Visual Studio и попробовал выполнить, меня озарило: я забыл открыть DbConnection. О чем и поведал интервьюеру.

Заверения о том, что я обычно тестирую свой код и то что такие ляпы не выходят из под моего пера в продакшн, не подействовали на собеседника. Но и сразу отказ я не получил. Мне было позволено задать пару наводящих вопросов и подождать до того, как со мной свяжутся. За пару наводящих вопросов выяснилось, что работа, мягко говоря, не фантан: имеется 250 (или около того) набросков форм, кои надо выполнить в WinForms и сваять для них запросы к 250-ти таблицам. Никакого творчества, инициативы, рефакторинга, тестирования не подразумевалось. Нужно было выдать bug free код как можно быстрее и сделать это как можно ближе к способам, описываемым в MSDN (M$ way).

Понятно, что заклинание использования ADO должно было отскакивать от кандидата как "Отче наш" от послушника в монастыре! Я рад, что мне не предложили эту работу. Я застрелился бы наверное на 10-ой форме. Однако, оправдывает мою забывчивость лишь то, что я не один год работал со своим заклинанием, которое в работе с ADO было на порядок удобнее, чем то что предлагал MSDN. А сейчас мои заклы (заклинания) стали еще удобнее. Именно о том, как может быть удобно с ADO .NET, будет мой пост (может пару или тройку, как попрет).

Попробую перечислить действия, необходимые для выполнения запроса и уборки за собой в общем случае для типа команды ExecuteReader:
  1. Создать DbConnection. Вообще его можно создать обратившись к конструктору, например, SqlConnection. Но в .NET 2.0 появился более правильный путь (через DbProviderFactories, что требует дополнительных манипуляций);
  2. Указать ConnectionString;
  3. Создать DbCommand. Мы можем создать ее через конструктор, либо попросить это сделать connection;
  4. Указать текст команды;
  5. Создать и заполнить свойства необходимых параметров. Тобишь имена, типы, значения;
  6. Добавить параметры в коллекцию параметров команды;
  7. Открыть соединение (именно это я забыл сделать на собеседовании);
  8. Выполнить команду и получить объект DbDataReader;
  9. Вчитать данные из ридера;
  10. Закрыть и уничтожить ридер (можно просто уничтожить);
  11. [Вообще на предыдущем этапе многие и бросают и все работает, но я доведу до конца.] Уничтожить команду. Вопрос, конечно философский, но раз команда реализует IDisposable, то уничтожить ее дело чести для меня, не вдаваясь в подробности реализации. А подробности бывают разные. Конечно, если мы хотим сделать reuse этой команды, то уничтожать ее не надо пока;
  12. Закрыть соединение (процедура крайне желательная, если не последует другая команда сразу за выполнением очередной);
  13. Уничтожить соединение.
Использование DbDataAdapter-а лишь расширит этот перечень. Ничего не забыл? :) Сосредотачиваясь на таком объеме инструкций легко забыть, для чего идешь в БД... Шутка! Но не без доли правды. Так вот, есть трюки, позволяющие сделать это все в рамках одного вызова большого метода. И этот вызов будет сосредоточен в одном месте, охвачен одним взглядом, а не размазан по коду дизайнера подальше от глаз программиста.

На авторство приемов я не претендую. Вполне допускаю, что они были известны до меня, и может быть где-то опубликованы. Но я не сталкивался ни с чем подобным до сих пор. Пока перекур. Блог свежий, пока народ подтянется (если подтянется вообще), все задуманное будет описано.

Трюк с generic-ами.

Сегодня покажу как можно одной строчкой прикрутить к классу некоторую функциональность с учетом типа класса. К сожалению, данный трюк имеет ограничения, но все же я им часто пользуюсь.

Приведу случай из реальной жизни. В системе, в разработке которой я принимаю участие, требуется довольно много разных данных сохранять в xml. Поначалу мы много использовали xml сериализацию. Требовалось уметь сериализовывать и десериализовывать 5 или 6 типов графов. Единственное, что было общее у этих графов - необходимость сериализации в файл, в строку. Вполне естевственно, что после появления второй иерархии объектов для сериализации, возникло желание использовать общий код для сериализации и десериализации графов. Приходила в голову идея использовать внешнюю утилиту, однако использование внешней утилиты показалось не очень красивым (требовалась передача типа корня сериализации во внешний метод):
Foo foo = XmlUtils.DeserializeFromFile(typeof(Foo), "Foo.xml");
или
Foo foo = XmlUtils.DeserializeFromFile<Foo>("Foo.xml");
а душе хотелось полета:
Foo foo = Foo.DeserializeFromFile("Foo.xml");
Таким образом, требуется определить статический метод, который бы для типа Foo возвращал результат типа Foo, а для типа Bar возвращал бы результат типа Bar. Есть средство. Определим хитрый базовый класс:
public class XmlSerializationRoot<TRoot>
   where TRoot : XmlSerializationRoot<TRoot>
{
   public static TRoot DeserializeFrom(TextReader reader)
   {
       if (reader == null)
           throw new ArgumentNullException("reader");

       return (TRoot)CreateSerializer().Deserialize(reader);
   }

   public static TRoot DeserializeFrom(Stream stream)
   {
       if (stream == null)
           throw new ArgumentNullException("stream");

       using (StreamReader reader = new StreamReader(stream))
           return DeserializeFrom(reader);
   }

   public static TRoot DeserializeFromString(string xml)
   {
       using (TextReader reader = new StringReader(xml))
           return DeserializeFrom(reader);
   }

   public static TRoot DeserializeFromFile(string fileName)
   {
       using (TextReader reader = File.OpenText(fileName))
           return DeserializeFrom(reader);
   }

   private static XmlSerializer CreateSerializer()
   {
       return new XmlSerializer(typeof(TRoot));
   }

   public void SerializeTo(TextWriter writer)
   {
       if (writer == null)
           throw new ArgumentNullException("writer");

       CreateSerializer().Serialize(writer, this);
   }

   public string SerializeToString()
   {
       StringBuilder builder = new StringBuilder();
       using (StringWriter writer = new StringWriter(builder))
           this.SerializeTo(writer);

       return builder.ToString();
   }

   public void SerializeToFile(string fileName)
   {
       using (StreamWriter writer = File.CreateText(fileName))
           this.SerializeTo(writer);
   }

   public void SerializeTo(Stream stream)
   {
       using (StreamWriter writer = new StreamWriter(stream))
           this.SerializeTo(writer);
   }
}
Особый интерес представляют статические методы десериализации. Методы сериализации были внесены до кучи, чтобы пример был полным. Вообще говоря, класс XmlSerializationRoot не будет являться базовым классом для классов с возможностью сериализации. Базовым классом для них будет XmlSerializationRoot<T>. Т.е. при следующем объявлении класса Foo
class Foo : XmlSerializationRoot<Foo>
{
}
мы получим ситуацию, где класс Foo наследует определенные у класса XmlSerializationRoot<Foo> методы. Теперь можно пользоваться этими методами через идентификатор типа Foo:
Foo foo = Foo.DeserializeFromFile("foo.xml");
В качестве завершающего штриха предлагаю выделить интерфейс
public interface IXmlSerializable
{
   void SerializeTo(TextWriter writer);
   void SerializeToFile(string fileName);
   void SerializeTo(Stream stream);
   string SerializeToString();
}
и поддержать его классом XmlSerializationRoot. Теперь мы сможем вызывать методы сериализации не зная типа сохраняемого объекта. Теперь буду писать гадости про этот подход.
  1. Данный подход навязывает ограничение наследования на разрабатываемые классы. При необходимости наследования классов от другого класса, использование усложняется, однако, можно объявить собственные методы и делегировать их вспомогательному классу. Например:
    class Foo : SomeBaseClass // наследуемся от чего-то другого
    {
       public static Foo DeserializeFromFile(string fileName)
       {
           return XmlSerializationRoot<Foo>.DeserializeFromFile(fileName);
       }
    }
    В данном случае потребуется убрать constraint у класса XmlSerializationRoot. В принципе, он объявлен формально для контроля за способом использования функциональности, и не требуется для компиляции методов класса.
  2. Resharper ругается на использование методов базового класса через идентификатор производного типа. Можно подкрутить его настройки, вставить управляющий комментарий, либо просто игнорировать данные сообщения.
  3. Пользуясь этим подходом мы не можем прочитать что-то из файла, а потом разобраться, что это было. Но в этом виноват не только сей подход, а так же устройство XML сериализации в FCL.
  4. Если требуется объявить только статические методы, то мы не сможем объявить базовый класс с модификатором static. Если объявим, то не сможем унаследовать от него.
Хоть область применения данного подхода не широка, но иногда это то что надо. Например, при необходимости подсчета экземпляров классов разного типа.

P.S. Хочу обратить внимание, что в данном посте не шла речь о корректности данного подхода к сериализации в архитектурном плане. Это лишь пример издевательства над языком. Однако, данный код отлично работал и был удобно используемым, пока мы не перешли на более тяжелый (но более гибкий) способ сериализации через XmlDocument.

Вывод типов (Type Inference) в С# 2.0

Довольно часто использую вывод типов чисто в декларативных целях. Представим ситуацию, когда требуется объявить массив пар (KeyValuePair<string, int>). Ситуация не частая, но на ее примере я покажу пару незатейливых приемов.
KeyValuePair<string, int>[] pairs = new KeyValuePair<string, int>[] {
  new KeyValuePair<string, int>("A", 0),
  new KeyValuePair<string, int>("B", 1)
};
Код довольно простой, но выглядит громоздко. Попробуем что-нибудь сделать с объявлением элементов массива: Введем вспомогательный метод MakePair:
static KeyValuePair<K, V> MakePair<K, V>(K key, V value)
{
  return new KeyValuePair<K, V>(key, value);
}
С учетом введенного метода, объявление массива можно переписать следующим образом:
KeyValuePair<string, int>[] pairs = new KeyValuePair<string, int>[] {
  MakePair("A", 0),
  MakePair("B", 1)
};
Следующий метод позволит переложить ответственность за создание экземпляра массива на компилятор:
public static T[] MakeArray<T>(params T[] items)
{
return items;
}
Таким образом, вышеописанный пример превращается в прилично читаемый код:
KeyValuePair<string, int>[] pairs = MakeArray(
  MakePair("A", 0),
  MakePair("B", 1));
Код приведенных методов настолько прост, что может быть расположен прямо по месту использования. Однако, я предпочитаю объявлять такие вещи в классе Utils, либо Tools (как шарахнет сверху).

понедельник, 11 августа 2008 г.

Подсветка синтаксиса

Первое, с чем столкнулся при попытке вставить пост с примерами - сложности с подсветкой синтаксиса и форматированием кода примеров. Однако, довольно быстро нашел статью, как все побороть. Спасибо проекту SyntaxHighlighter, thanks to Neil Kilbride (статья здесь). Будет время, напишу о процессе прикручивания этого проекта к блогу.

[+/-] скрытый текст

Инструкция по вставке скрытого текста

О блоге

Решил публиковать разные приемчики, которые я использую при программировании. Последнее время я в основном программирую под платформу .NET в среде MSVS на языке C#. Посмотрим, что из этого получится. Будет очень приятно, если это кому-то пригодится, кроме меня.