Пространство имён Rsdn.Framework.Data

Автор: Игорь Ткачёв
The RSDN Group
Опубликовано: 01.07.2003
Версия текста: 1.2

Введение
Класс DbManager
Инициализация и создание экземпляра объекта
Параметры запроса
Методы Execute
Отображение данных
MapFieldAttribute
MapValueAttribute
Отображение вложенных классов
Класс Map
Заключение
Добавления и исправления версии 1.1
Добавления и исправления версии 1.2
Абстрактные классы
MapTypeAttribute
MapIgnoreAttribute
MapDescriptor
IMapObjectFactory
MapFieldAttribute
Nullable values
Guid
Xml схемы отображения
MapXmlAttribute
Добавления и исправления версии 1.3
SetCommand, SetSpCommand
Update
MapTypeAttribute
ISupportInitialize
IMapSettable
Известные проблемы

rfd.rar - 54 k
rfd_dev.rar - 213 k


Введение

Rsdn.Framework.Data является пространством имён, содержащим набор классов, представляющих собой высокоуровневую обёртку над ADO.NET. Казалось бы, ADO.NET сама по себе штука достаточно высокоуровневая и зачем над ней ещё городить какой-то огород? Всё это так, но как это часто бывает, в борьбе добра со злом обычно, увы, побеждает лень.

Рассмотрим в качестве примера функцию, которая возвращает список объектов, содержащих информацию о категории товаров: ID категории, имя и число товаров, соответствующее данной категории (здесь и далее мы будем использовать базу данных Northwind из поставки MS SQL Server). Вот как это может выглядеть с использованием ADO.NET:

public class CategoryInfo
{
    public int    CategoryID;
    public string CategoryName;
    public int    Count;
}

ArrayList GetCategories(int min)
{
    string connectionString =
        "Server=.;Database=Northwind;Integrated Security=SSPI";
    string commandText = @"
        SELECT 
            p.CategoryID,
            c.CategoryName,
            Count(p.CategoryID) Count
        FROM Products p
            INNER JOIN Categories c ON c.CategoryID = p.CategoryID
        GROUP BY p.CategoryID, c.CategoryName
        HAVING Count(p.CategoryID) >= @min
        ORDER BY c.CategoryName";

    using (SqlConnection con = new SqlConnection(connectionString))
    {
        con.Open();

        using (SqlCommand cmd = new SqlCommand(commandText, con))
        {
            cmd.Parameters.Add("@min", min);

            using (SqlDataReader rd = cmd.ExecuteReader())
            {
                ArrayList al = new ArrayList();

                while (rd.Read())
                {
                    CategoryInfo ci = new CategoryInfo();

                    ci.CategoryID   = Convert.ToInt32 (rd["CategoryID"]);
                    ci.CategoryName = Convert.ToString(rd["CategoryName"]);
                    ci.Count        = Convert.ToInt32 (rd["Count"]);

                    al.Add(ci);
                }

                return al;
            }
        }
    }
}

А теперь тоже самое в исполнении Rsdn.Framework.Data:

public class CategoryInfo
{
    public int    CategoryID;
    public string CategoryName;
    public int    Count;
}

ArrayList GetCategories(int min)
{
    using (DbManager db = new DbManager())
    {
        return db
            .SetCommand(@"
                SELECT 
                    p.CategoryID,
                    c.CategoryName,
                    Count(p.CategoryID) Count
                FROM Products p
                    INNER JOIN Categories c ON c.CategoryID = p.CategoryID
                GROUP BY p.CategoryID, c.CategoryName
                HAVING Count(p.CategoryID) >= @min
                ORDER BY c.CategoryName",
                db.Parameter("@min", min)
            .ExecuteList(typeof(CategoryInfo));
    }
}

Не трудно заметить, что последний вариант заметно короче. Фактически все, что у нас осталось – это текст самого запроса. Класс DbManager самостоятельно осуществляет всю работу по созданию списка объектов и отображению (mapping) полей рекордсета на заданную структуру.

Естественно функциональность данного класса этим не ограничивается. Но, давайте обо всём по порядку.

Класс DbManager

Инициализация и создание экземпляра объекта

Класс DbManager является основным классом рассматриваемого пространства имён и в единственном лице представляет собой замену всем основным объектам ADO.NET.

Для создания экземпляра данного класса служат следующие конструкторы, которые инициализируют объект и открывают соединение с базой данных:

public DbManager();

public DbManager(
    string configurationString
);

public DbManager(
    IDbConnection connection
);

public DbManager(
    IDbConnection connection,
    string        configurationString
);

ConfigurationString в данном случае это не строка соединения с базой данных (Connection String), а только ключ, по которому строка соединения будет читаться из конфигурационного файла. Правила задания строки соединения с БД в конфигурационном файле следующие:

<appSettings>
  <add key="ConnectionString.configurationString" value="...">
</appSettings>

Если configurationString не задана, то используется следующее правило:

<appSettings>
  <add key="ConnectionString" value="...">
</appSettings>

Таким образом, мы можем одновременно работать с различными конфигурациями, например, “Production”, “Development”, “QA” и т.п. Секция appSettings может находиться как в app.config или web.config так и machine.config файле.

Если же вам не хочется возиться с конфигурационными файлами, то для задания строки соединения можно воспользоваться методом AddConnectionString:

DbManager.AddConnectionString("MyConfig", connectionString);

using (DbManager db = new DbManager("MyConfig"))
{
    // ...
}

или

DbManager.AddConnectionString(connectionString);

using (DbManager db = new DbManager())
{
    // ...
}

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

Отличительной особенностью класса DbManager является то, что он работает исключительно с интерфейсами пространства имён System.Data и вполне может использоваться для работы с различными провайдерами данных. На данный момент поддерживается работа с Data Provider for SQL Server, Data Provider for Oracle, Data Provider for OLE DB и Data Provider for ODBC. Выбор провайдера осуществляется также с помощью строки конфигурации. Для этого достаточно добавить к ней один из следующих постфиксов: “.OleDb”, “.Odbc”, “.Oracle”, “.Sql”. Если постфикс не задан, то по умолчанию выбирается провайдер для SQL Server. В дополнение к существующим провайдерам совсем несложно подключить любой другой. Следующий пример демонстрирует подключение Borland Data Providers for .NET (BDP.NET):

using System;
using System.Data;
using System.Data.Common;

using Borland.Data.Provider;

using Rsdn.Framework.Data;
using Rsdn.Framework.Data.DataProvider;

namespace Example
{
    public class BdpDataProvider: IDataProvider
    {
        IDbConnection IDataProvider.CreateConnectionObject()
        {
            return new BdpConnection();
        }

        DbDataAdapter IDataProvider.CreateDataAdapterObject()
        {
            return new BdpDataAdapter();
        }

        void IDataProvider.DeriveParameters(IDbCommand command)
        {
            BdpCommandBuilder.DeriveParameters((BdpCommand)command);
        }

        Type IDataProvider.ConnectionType
        {
            get
            {
                return typeof(BdpConnection);
            }
        }

        string IDataProvider.Name
        {
            get
            {
                return "Bdp";
            }
        }
    }

    class Test
    {
        static void Main()
        {
            DbManager.AddDataProvider(new BdpDataProvider());
            DbManager.AddConnectionString(".bdp",
                "assembly=Borland.Data.Mssql,Version=1.1.0.0,” +
                ”Culture=neutral,PublicKeyToken=91d62ebb5b0d1b1b;" +
                "vendorclient=sqloledb.dll;osauthentication=True;" +
                "database=Northwind;hostname=localhost;provider=MSSQL");

            using (DbManager db = new DbManager())
            {
                int count = (int)db
                    .SetCommand("SELECT Count(*) FROM Categories")
                    .ExecuteScalar();

                Console.WriteLine(count);
            }
        }
    }
}

Параметры запроса

Большинство используемых запросов требуют тот или иной набор параметров для своего выполнения. В приведённом выше примере таким параметром является @min - минимальное количество типов товаров для заданной категории. Зачастую, среднеленивый программист предпочитает использовать в подобных случаях обычную конкатенацию строк, т.е. что-то наподобие следующего:

void Test(int id)
{
    string commandText = @"
        SELECT CategoryName
        FROM   Categories
        WHERE  CategoryID = " + id;

    // ...
}

К сожалению, при всей своей простоте, такой стиль плохо читаем, часто ведёт к непредсказуемым ошибкам и долгим мучениям с подбором формата, если в качестве параметра, например, используется дата. Более того, если наш параметр имеет строковый тип, то применение такого подхода в Web-приложениях может сделать их весьма уязвимыми для хакерских атак. Поэтому, отложим шутки в сторону и серьёзно займёмся рассмотрением возможностей, предоставляемых классом DbManager для работы с параметрами.

Для создания параметров служит следующий набор методов:

public IDbDataParameter Parameter(
    string parameterName,
    object value
);

public IDbDataParameter InputParameter(
    string parameterName,
    object value
);

Создаёт входной (ParameterDirection.Input) параметр с именем parameterName и значением value.

public IDbDataParameter NullParameter(
    string parameterName,
    object value
);

Делает тоже, что и предыдущие методы и в дополнение проверяет значение value. Если оно представляет собой null, пустую строку, значение даты DateTime.MinValue или 0 для целых типов, то вместо заданного значения подставляется DBNull.Value.

public IDbDataParameter OutputParameter(
    string parameterName,
    object value
);

Создаёт выходной (ParameterDirection.Output) параметр.

public IDbDataParameter InputOutputParameter(
    string parameterName,
    object value
);

Создаёт параметр, работающий как входной и выходной (ParameterDirection.InputOutput).

public IDbDataParameter ReturnValue(
    string parameterName
);

Создаёт параметр-возвращаемое значение (ParameterDirection.ReturnValue).

public IDbDataParameter Parameter(
    ParameterDirection parameterDirection,
    string parameterName,
    object value
);

Создаёт параметр с заданными значениями.

Создание выходных параметров и возвращаемое значение используются для работы с сохранёнными процедурами. Входной параметр можно использовать для построения любых запросов.

Для чтения выходных параметров после выполнения запроса служит следующий метод:

public IDbDataParameter Parameter(
    string parameterName
);

Каждая версия метода Execute… имеет в своём составе метод, принимающий в качестве последнего аргумента список параметров запроса. Например, для ExecuteNonQuery одна из таких функций имеет следующий вид:

public int ExecuteNonQuery(
    string commandText,
    params IDbDataParameter[] commandParameters
);

Таким образом, список параметров задаётся простым перечислением через запятую:

void InsertRegion(int id, string description)
{
    using (DbManager db = new DbManager())
    {
        db
            .SetCommand(@"
                INSERT INTO Region (
                    RegionID,
                    RegionDescription
                ) VALUES (
                    @id,
                    @desc
                )",
                db.Parameter("@id",   id),
                db.Parameter("@desc", description))
            .ExecuteNonQuery();
    }
}

Для создания списка параметров из бизнес объектов существует метод CreateParameters, который принимает в качестве аргумента объект DataRow или любой бизнес-объект. Допустим, у нас имеется класс Region, содержащий информацию о регионе. В этом случае мы могли бы переписать предыдущий пример следующим образом:

public class Region
{
    public int    ID;
    public string Description;
}

void InsertRegion(Region region)
{
    using (DbManager db = new DbManager())
    {
        db
            .SetCommand(@"
                INSERT INTO Region (
                    RegionID,
                    RegionDescription
                ) VALUES (
                    @ID,
                    @Description
                )",
                db.CreateParameters(region)).
            .ExecuteNonQuery();
    }
}

Для передачи параметров сохранённой процедуре можно воспользоваться ещё одним способом, не требующим явного указания имён параметров:

DataSet SalesByCategory(string categoryName, string ordYear)
{
    using (DbManager db = new DbManager())
    {
        return db
            .SetSpCommand("SalesByCategory", categoryName, ordYear)
            .ExecuteDataSet();
    }
}

В данном случае важен лишь порядок следования аргументов процедуры. Данная функция самостоятельно строит список параметров исходя из списка параметров сохранённой процедуры.

Для анализа возвращаемого значения и выходных параметров можно воспользоваться следующим методом:

public IDbDataParameter Parameter(
    string parameterName
);

Например, в приведённом выше примере возвращаемое значение сохранённой процедуры можно проверить следующим образом:

DataSet SalesByCategory(string categoryName, string ordYear)
{
    using (DbManager db = new DbManager())
    {
        DataSet dataSet = db
            .SetSpCommand("SalesByCategory", categoryName, ordYear)
            .ExecuteSpDataSet();

        int returnValue = (int)db.Parameter("@RETURN_VALUE").Value;

        if (returnValue != 0)
        {
            throw new Exception(
                string.Format("Return value is '{0}'", returnValue));
        }

        return dataSet;
    }
}

Последней возможностью работы с параметрами, которую нам осталось рассмотреть, является использование функции подготовки запроса Prepare, которая может быть полезной при выполнении одного и того же запроса несколько раз. Фактически в данном случае вызов метода Execute… разбивается на две части: первая - вызов Prepare с заданием типа, текста и параметров запроса, вторая - вызов соответствующего метода Execute… для выполнения запроса определённое число раз. Следующий пример демонстрирует данную возможность.

void InsertRegionList(Region[] regionList)
{
    using (DbManager db = new DbManager())
    {
        db
            .SetCommand (@"
                INSERT INTO Region (
                    RegionID,
                    RegionDescription
                ) VALUES (
                    @ID,
                    @Description
                )",
                db.Parameter("@ID",          regionList[0].ID),
                db.Parameter("@Description", regionList[0].Description))
            .Prepare();

        foreach (Region r in regionList)
        {
            db.Parameter("@ID").Value          = r.ID;
            db.Parameter("@Description").Value = r.Description;

            db.ExecuteNonQuery();
        }
    }
}

Либо мы можем упростить его следующим образом для бизнес объектов...

void InsertRegionList(Region[] regionList)
{
    using (DbManager db = new DbManager())
    {
        db
            .SetCommand(@"
                INSERT INTO Region (
                    RegionID,
                    RegionDescription
                ) VALUES (
                    @ID,
                    @Description
                )",
                db.CreateParameters(regionList[0]))
            .Prepare();

        foreach (Region r in regionList)
        {
            db.AssignParameterValues(r);
            db.ExecuteNonQuery();
        }
    }
}

и класса DataRow

static void InsertRegionTable(DataTable dataTable)
{
    using (DbManager db = new DbManager())
    {
        db
            .SetCommand(@"
                INSERT INTO Region (
                    RegionID,
                    RegionDescription
                ) VALUES (
                    @ID,
                    @Description
                )",
                db.CreateParameters(dataTable.Rows[0]))
            .Prepare();

        foreach (DataRow dr in dataTable.Rows)
            db.AssignParameterValues(dr).ExecuteNonQuery();
    }
}

Методы Execute

В свою очередь предыдущие примеры тоже можно упростить. Класс DbManager уже содержит реализацию методов для выполнения подобных групповых операций:

public int Execute(IList list);

public int Execute(DataTable table);

public int Execute(DataSet dataSet);

public int Execute(DataSet dataSet, string tableName);

Эти методы могут использоваться для выполнения операций добавления и модификации группы записей в базе данных.

Также класс DbManager предоставляет аналогичные ADO.NET методы Execute, доступные при использовании интерфейса IDbCommand, и добавляет свои расширения для классов DataSet, DataTable, бизнес объектов и списков.

ExecuteNonQuery

Метод ExecuteNonQuery возвращает число записей обработанных запросом.

public int ExecuteNonQuery();

ExecuteScalar

Данный метод отличается от предыдущего только тем, что вместо числа обработанных запросом записей возвращает значение первой колонки первой записи из набора данных, полученного в результате запроса.

public object ExecuteScalar();

ExecuteReader

Метод ExecuteReader возвращает IDataReader интерфейс.

public IDataReader ExecuteReader();

Для полученного объекта нужно не забыть вызвать его метод Dispose или использовать ключевое слово using.

using (DbManager   db = new DbManager())
using (IDataReader dr = db.SetCommand(...).ExecuteReader())
{
    while (dr.Read())
    {
        // ...
    }
}

ExecuteDataSet

Данная версия метода предназначена для чтения данных в DataSet, для которого в ADO.NET используется один из наследников объекта DbDataAdapter.

public DataSet ExecuteDataSet();

public DataSet ExecuteDataSet(DataSet dataSet);

public DataSet ExecuteDataSet(string tableName);

public DataSet ExecuteDataSet(DataSet dataSet, string tableName);

public DataSet ExecuteDataSet(
    DataSet dataSet,
    int     startRecord,
    int     maxRecords,
    string  tableName);

ExecuteDataTable

Чтение данных в отдельную таблицу также представлено соответствующими методами в Rsdn.Framework.Data:

public DataTable ExecuteDataTable();

public DataTable ExecuteDataTable(DataTable dataTable);

ExecuteBizEntity

Этот метод предназначен для чтения одной записи из набора данных в бизнес-объект.

public object ExecuteBizEntity(object entity);

public object ExecuteBizEntity(Type type);

Если в результирующем наборе данных не будет обнаружено ни одной записи, то данный метод сгенерирует исключение RsdnDataRecordNotFoundException.

ExecuteList

Метод ExecuteList позволяет читать все записи набора данных в коллекцию.

public ArrayList ExecuteList(Type type);

public IList     ExecuteList(IList list, Type type);

На этом описание класса DbManager можно завершить и перейти к рассмотрению принципов работы последних двух методов.

Отображение данных

ADO.NET поддерживает два способа чтения данных из источника: прямое чтение из объекта класса DataReader, либо с помощью класса DataAdapter в экземпляр класса DataSet, который по сути представляет собой единственный вариант бизнес сущностей, предлагаемых и культивируемых Microsoft.

Оставим сегодня в покое достоинства и преимущества класса DataSet, и лишь заметим, что часто бывает необходимо уметь читать данные непосредственно в бизнес объекты приложения. При этом иногда нужно выполнять некоторые действия по отображению данных, например, из строковых значений в перечислители (enumerators) или замене значений NULL на нечто более удобоваримое. Как вы заметили из нашего самого первого примера, класс DbManager великодушно предоставляет нам такие возможности.

Рассмотрим ещё один пример, который возвращает все записи из таблицы Categories:

public class Category
{
    public int    CategoryID;
    public string CategoryName;
    public string Description;
}

static ArrayList GetAllCategories()
{
    using (DbManager db = new DbManager())
    {
        return db
            .SetCommand(@" 
                SELECT
                    CategoryID,
                    CategoryName,
                    Description
                FROM Categories")
            .ExecuteList(typeof(Category));
    }
}

Метод ExecuteList создаёт экземпляр класса Category для каждой записи в таблице, затем осуществляет отображение данных на поля объекта и добавляет его в список. Для отображения колонок таблицы на поля и свойства нашего объекта используется механизм Reflection, единственным недостатком которого является некоторая нерасторопность. Для решения этой проблемы применён ещё один механизм .NET – генерация исполняемого кода во время выполнения программы (System.Reflection.Emit namespace), что позволяет максимально увеличить производительность и свести использование Reflection только для начальной инициализации.

Ко всему прочему, метод ExecuteList берёт на себя работу по замене значений NULL на string.Empty для строковых переменных, в результате вам не придётся каждый раз проверять ваши строки на null. Также, по умолчанию, все концевые пробелы строковые переменные усекаются.

Если имена полей нашего класса и таблицы базы данных не совпадают, то мы можем воспользоваться двумя способами. Первый – использование стандартной возможности SQL сервера по замене имён колонок их псевдонимами:

public class Category
{
    public int    ID;
    public string CategoryName;
    public string Description;
}

static ArrayList GetAllCategories()
{
    using (DbManager db = new DbManager())
    {
        return db
            .SetCommand(@"
                SELECT
                    CategoryID as ID,
                    CategoryName,
                    Description
                FROM Categories")
            .ExecuteList(typeof(Category));
    }
}

Второй – применение специального атрибута MapFieldAttribute.

MapFieldAttribute

Данный атрибут отвечает за именование отображаемого поля нашего бизнес объекта, позволяет определить поля, для которых разрешено нулевое значение и управляет усечением концевых пробелов для строковых переменных. Ниже приведены свойства данного атрибута с кратким описанием.

СвойствоОписаниеЗначение по умолчанию
NameЗаменяет отображаемое имя поля/свойства на заданное.null, используется оригинальное имя поля
IsNullabletrue, если поле может содержать нулевое значение. При сохранении поля из объекта в базу данных вместо нулевого значения такого поля подставляется NULL.false
IsTrimmabletrue, если концевые пробелы для строкового поля должны быть усечены.True

Для свойства IsNullable конвертация в NULL производится для следующих типов и их значений:

ТипЗначение
StringLength == 0
DateTimeDateTime.MinValue
Int160
Int320
Int640

Таким образом, предыдущий пример можно переписать следующим образом с применением рассматриваемого атрибута:

public class Category
{
    [MapField(Name = "CategoryID")]
    public int    ID;

    public string CategoryName;

    [MapField(IsNullable = true)]
    public string Description;
}

static ArrayList GetAllCategories()
{
    using (DbManager db = new DbManager())
    {
        return db
            .SetCommand(@"
                SELECT
                    CategoryID,
                    CategoryName,
                    Description
                FROM Categories")
            .ExecuteList(typeof(Category));
    }
}

MapValueAttribute

Ещё одной возможностью, предоставляемой пространством имён Rsdn.Framework.Data, является атрибут MapValueAtribute, который позволяет гибко управлять значениями отображаемых полей. С помощью данного атрибута можно переотобразить, например, строковые значения на числа или перечислители и наоборот, задать значение по умолчанию и значения NULL.

Следующий пример демонстрирует преобразование целого значения в Boolean:

public class Sample
{
    [MapValue(true,  1)]
    [MapValue(false, 0)]
    public bool Value;
}

Либо же более осмысленный вариант, в случае, если логические значения хранятся в вашей базе данных в виде литералов:

public class Sample
{
    [MapValue(true,  "Y")]
    [MapValue(false, "N")]
    public bool Value;
}

Таким же способом можно воспользоваться для отображения данных на перечислители. Кроме того, данный атрибут можно применить к самому перечислителю, задав, таким образом, отображение для него во всех объектах, в которых он будет использоваться:

[MapValue(Status.Active,   "A")]
[MapValue(Status.Inactive, "I")]
[MapValue(Status.Pending,  "P")]
[MapNullValue(Status.Unknown)]
[MapDefaultValue(Status.Unknown)]
public enum Status
{
    Unknown,
    Active,
    Inactive,
    Pending
}

Данный пример демонстрирует ещё две версии атрибута. Предпоследний вариант используется для задания значения, если в базе данных поле-источник представлено как NULL. Последний вариант задаёт значение в случае, если исходное значение не соответствует ни одному из вышеперечисленных.

Отображение вложенных классов

Для отображения данных на поля, которые сами являются классами можно воспользоваться следующим способом:

public class RecordHeader
{
    public int    ID;
    public string Name;
}

public class Category
{
    public RecordHeader Header = new RecordHeader();
    public string       Description;
}

static ArrayList GetAllCategories()
{
    using (DbManager db = new DbManager())
    {
        return db
            .SetCommand(@"
                SELECT
                    CategoryID   as [Header.ID],
                    CategoryName as [Header.Name],
                    Description
                FROM Categories")
            .ExecuteList(typeof(Category));
    }
}

Здесь следует обратить внимание, прежде всего, на два момента. Во-первых, поле Header должно быть явно проинициализировано приведённым выше способом или в конструкторе без параметров, который используется для создания экземпляра объекта Category. Инициализацией таких полей библиотека не занимается и нулевое значение данного поля будет просто проигнорировано. Во-вторых, для разделения имени вложенного класса и его полей используется специальное соглашение об именовании – такие имена должны разделяться точкой. Ограничения на число вложений не существует.

Класс Map

Для всех перечисленных манипуляций рассматриваемый нами метод ExecuteList пользуется услугами класса Map, который может иметь и вполне самостоятельное применение. Ниже приведён полный список методов класса Map.

ToValue

public static object ToValue(object sourceValue, Type type);

С помощью данного метода можно получить отображаемое на него значение для заданного типа. Например, следующий код:

object o = Map.ToValue("P", typeof(Status));
Console.WriteLine(o);

напечатает соответствующее название значения перечислителя

Pending

FromValue

public static object FromValue(object sourceValue);

Этот метод может применяться для обратной задачи – получения значения, которое отображается на заданное значение указанного типа. Следующий код:

object o = Map.FromValue(Status.Pending);
Console.WriteLine(o);

напечатает строку

P

IsNull

public static bool IsNull(object value);

Возвращает true, если заданное значение рассматривается библиотекой как нулевое. Список таких значений мы рассматривали при описании атрибута MapFieldAttribute.

ToObject

Данный метод предназначен для отображения одного объекта на другой и имеет несколько версий.

public static object ToObject(object source, object dest);

Отображает объект source на dest и возвращает последний. В качестве исходного и целевого объекта могут выступать как обычные бизнес объекты, так и экземпляр класса DataRow или DataTable. В последнем случае будет использоваться первая запись таблицы.

public static object ToObject(object source, Type type);

Создаёт объект заданного типа type и отображает на него source.

public static object ToObject(
    DataRow dataRow,
    DataRowVersion version,
    object dest);

public static object ToObject(
    DataRow dataRow,
    DataRowVersion version,
    Type type);

Специальные версии для класса DataRow, позволяющие задавать также и версию отображаемых записей.

ToList

Данное семейство методов применяется для отображения одного списка записей на другой.

public static IList ToList(
    DataTable sourceTable,
    IList     list,
    Type      type);

Отображает таблицу sourceTable на список объектов list заданного типа type.

public static IList ToList(
    DataTable      sourceTable,
    DataRowVersion version,
    IList          list,
    Type           type);

Специальная версия предыдущего метода, позволяющая задавать версию записей исходной таблицы.

public static ArrayList ToList(
    DataTable dataTable,
    Type      type);

Создаёт экземпляр класса ArrayList и отображает на него исходную таблицу.

public static ArrayList ToList(
    DataTable      dataTable,
    DataRowVersion version,
    Type           type  );

Специальная версия предыдущего метода.

public static DataTable ToList(
    IList list);

Создаёт экземпляр объекта DataTable и отображает на него заданный список объектов.

public static DataTable ToList(
    IList list,
    DataTable dataTable);

Отображает список объектов на заданную таблицу.

Заключение

Данная библиотека используется продолжительное время на нашем сайте и показала себя достаточно удобным и лёгким в использовании средством. Надеюсь, вам она так же не доставит особых хлопот, и её применение будет для вас простым делом.

Дополнительную справочную информацию, а так же множество примеров вы можете найти в файле-справке, приложенному к дистрибутиву.

Добавления и исправления версии 1.1

  1. Добавлен метод DbManager.ExecuteDictionary, позволяющий читать записи набора данных в хеш-таблицу или любой другой объект, поддерживающий интерфейс IDictionary.
public Hashtable   ExecuteDictionary(Type type);

public IDictionary ExecuteDictionary(IDictionary dic, Type type);
  1. Добавлен метод Map.ToDictionary, отображающий данный в / из списка объектов в хеш-таблицу или другой объект, реализующий интерфейс IDictionary.
  2. Добавлены версии методов Map.ToList и Map.ToDictionary, позволяющие отображать данные в / из объекта IDataReader.
  3. Добавлены атрибуты MapNullValueAttribute и MapDefaultValueAttribute.
  4. Атрибут MapValueAttribute теперь может применяться к значению перечислителя, что является единственной возможностью его задания в Managed C++.
public enum Status
{
    [MapValue("A")] Active,
    [MapValue("I")] Inactive,
    [MapValue("P")] Pending
}
  1. Добавлены классы исключений RsdnDbManagerException и RsdnMapException. Все исключения, возникающие в процессе работы библиотеки, включая исключения используемых классов, заворачиваются в данные классы и возвращаются как вложенные.
  2. Исправлен баг, не позволявший помещать объекты класса DbManager на форму.

Добавления и исправления версии 1.2

Как видно из приведённого списка основные добавления в данной версии коснулись отображения данных. Класс MapData переименован и перенесён в пространство имён Rsdn.Framework.Data.Mapping. Так же переименованы некоторые методы данного класса. Старые имена объявлены obsolete и будут удалены в следующей версии библиотеки.

Абстрактные классы

Поддержка абстрактных классов позволяет добиться практически декларативного объявления бизнес объектов и полностью опустить реализацию свойств. Далее приведено типичное объявление бизнес объекта:

public class MyBizEntity
{
    private int _id;
    public  int  ID
    {
        get { return _id;  }
        set { _id = value; }
    }

    private string _description;
    public  string  Description
    {
        get { return _description;  }
        set { _description = value; }
    }
}

Абстрактное объявление выглядит следующим образом:

public abstract class MyBizEntity
{
    public abstract int    ID          { get; }
    public abstract string Description { get; set; }
}

Такой способ несколько короче оригинального, к тому же, он позволяет задавать read-only свойства класса.

MapTypeAttribute

Если одно из абстрактных свойств должно реализовываться отличным от возвращаемого типа классом, то в данном случае может пригодиться атрибут MapTypeAttribute. Допустим, мы имеем внутреннюю реализацию свойства при помощи следующего класса:

public class MyString
{
    private string _value;
    public  string  Value
    {
        get { return _value;  }
        set { _value = value; }
    }
}

Обычное объявление свойства может быть следующим:

public class MyBizEntity
{
    private MyString _description;
    public  string    Description
    {
        get { return _description.Value;  }
        set { _description.Value = value; }
    }
}

Абстрактное объявление свойства:

public abstract class MyBizEntity
{
    [MapType(typeof(MyString))]
    public abstract string Description { get; set; }
}

В данном случае маппер подразумевает у используемого типа наличие свойства Value с типом, совпадающим с типом возвращаемого значения данного свойства.

MapIgnoreAttribute

Данный атрибут позволяет исключать поля из процесса отображения.

MapDescriptor

Класс MapDescriptor является описателем отображаемого типа и предоставляет методы для создания объектов и доступа к их свойствам и полям. Например, создать экземпляр класса MyBizEntity из предыдущего примера и задать значение для его read-only свойства ID можно следующим образом:

MapDescriptor desc   = MapDescriptor.GetDescriptor(typeof(MyBizEntity));
MyBizEntity   entity = desc.CreateInstance() as MyBizEntity;

Desc["ID"].SetValue(entity, 1);

На самом деле такой способ установления значения является шпионским и не предназначен для повседневного использования. Но, тем не менее, может быть полезен в качестве “чёрного входа” для написания всевозможных фреймворков.

IMapObjectFactory

Данный интерфейс позволяет полностью контролировать процесс создания бизнес объектов.

public MyBizEntityManager : IMapObjectFactory
{
    private MapDescriptor _descriptor;

    public MyBizEntityManager()
    {
        _descriptor = MapDescriptor.getDescriptor(typeof(MyBizEntity));
        _descriptor.ObjectFactory = this;
    }

    object IMapObjectFactory.CreateInstance(
        IMapDataSource dataSource,
        object         sourceObject,
        object[]       parameters,
        ref bool       stopMapping)
    {
        return _descriptor.CreateInstance(
            dataSource, sourceObject, parameters, stopMapping);
    }
}

Как видно из примера объект может быть создан путём обращения к описателю типа. Но это совсем не обязательно.

MapFieldAttribute

Данный атрибут теперь может применяться к классу и позволяет задавать дополнительное именование полей источника и приёмника, которое невозможно осуществить обычным способом.

public class BaseBizEntity
{
    public int ID;
}

[MapField(SourceName="OrderID", TargetName="ID")]
public class Order : BaseBizEntity
{
    public int Number;
}

[MapField("CustomerID", "ID")]
[MapField("OrderID",    "Order.ID")]
public class Customer : BaseBizEntity
{
    public string Name;
    public Order  Order = new Order();
}

Nullable values

Теперь маппер понимает типы, поддерживающие работу со значениями, которые могут иметь нулевое значение. Не путать с MapNullValueAttribute. К таким типам относятся типы пространства имён System.Data.SqlTypes и их аналоги.

public abstract class MyBizEntity
{
    public abstract SqlString Description { get; set; }
}

Для таких типов маппер подразумевает наличие свойств Value и IsNull.

Guid

Добавлена поддержка типа Guid.

Xml схемы отображения

В общем случае xml схема отображения выглядит следующим образом:

<?xml version="1.0" encoding="utf-8" ?>
<mapping xmlns="http://www.rsdn.ru/mapping.xsd">
    <type name="NamespaceName.TypeName">
        <field name="field1" source="field1" trimmable="true" />
        <field name="field2">
            <value target="3" source="2" source_type="System.Int32" />
        </field>
        <field name="field3" nullable="true">
            <value target="3" source="2" source_type="System.Int32" />
            <null_value target="111"/>
        </field>
        <field name="field4" ignore="true" />
    </type>
    <value_type name="NamespaceName.ValueTypeName ">
        <value target="Active"   source="A" source_type="System.String" />
        <value target="Inactive" source="I" source_type="System.String" />
        <value target="Pending"  source="P" source_type="System.String" />
        <null_value target="Unknown"/>
    </value_type>
</mapping>

Секция type определяет схему отображения для конкретного типа. Секция value_type может быть использована для задания отображения перечислителей.

Зарегистрировать xml схему можно с помощью одной из версий метода SetMappingSchema. Метод SetMappingSchema(string) производит поиск файла с заданным именем на диске, и если такой не найден, то в ресурсах сборки, которая вызвала данный метод.

Если у типа уже заданы атрибуты отображения, то xml схема имеет высший приоритет. Например:

<?xml version="1.0" encoding="utf-8" ?>
<mapping xmlns="http://www.rsdn.ru/mapping.xsd">
    <type name="NamespaceName.Dest">
        <field name="Field1">
            <null_value target="-1" />
        </field>
    </type>
</mapping>

public class Src
{
    public object Field1 = null;
    public object Field2 = null;
}

public class Dest
{
    [MapNullValue(-2)]
    public int Field1;

    [MapNullValue(-3)]
    public int Field2;
}

void Test()
{
    MapDescriptor.SetMappingSchema("Map.xml");

    Src  s = new Src();
    Dest d = Map.ToObject(s, typeof(Dest)) as Dest;

    // d.Field1 == -1
    // d.Field2 == -3
}

При выполнении данного теста значение поля Field1 будет взято из xml схемы, значение Field2 из атрибута.

MapXmlAttribute

Данный атрибут позволяет задать конкретные имена xml файла и строки поиска типа в нём. Например:

<?xml version="1.0" encoding="utf-8" ?>
<mapping xmlns="http://www.rsdn.ru/mapping.xsd">
    <type name="MyBizEntity">
        ...
    </type>
</mapping>

[MapXml("MyMapping.xml", "MyBizEntity")]
public class MyBizEntity
{
    // ...
}

Или, если xml схема задаётся с помощью SetMappingSchema:

[MapXml("MyBizEntity")]
public class MyBizEntity
{
    // ...
}

Добавления и исправления версии 1.3

SetCommand, SetSpCommand

Основные добавления данной версии коснулись класса DbManager. Из методов Execute... вынесены параметры, отвечающие за формирование выполняемой команды. Так, если вызов метода ExecuteScalar ранее выглядел следующим образом

using (DbManager db = new DbManager())
{
    return (int)db.ExecuteScalar("SELECT Count(*) FROM Categories");
}
using (DbManager db = new DbManager())
{
    return (int)db
         .SetCommand("SELECT Count(*) FROM Categories")
         .ExecuteScalar();
}

Старые методы объявлены obsolete и будут удалены в следующей версии библиотеки.

Update

Метод Update представляет собой обёртку для вызова метода DbDataAdapter.Update. Для задания команд вставки обновления и удаления используются соответствующие методы SetInsertCommand, SetUpdateCommand, SetDeleteCommand.

using (DbManager db = new DbManager())
{
    db
        .SetInsertCommand("INSERT statement")
        .SetUpdateCommand("UPDATE statement")
        .SetDeleteCommand("DELETE statement")
        .Update(dataSet);
}

MapTypeAttribute

Данный атрибут обзавёлся набором параметров, которые могут быть переданы конструктору класса, имплементирующего абстрактное свойство. Например:

public abstract class MyBizEntity : BizEntityBase
{
    [MapType(typeof(RequiredString), "Description", 20)]
    public abstract string Description { get; set; }
} 

См. полный пример использования данного атрибута в документации.

ISupportInitialize

Если класс, участвующий в маппинге реализует интерфейс ISupportInitialize, то маппер вызовет метод BeginInit перед началом отображения и EndInit перед его завершением.

public class InitializedEntity : ISupportInitialize
{
    private bool _isBeingMapped;

    void ISupportInitialize.BeginInit()
    {
        _isBeingMapped = true;
    }

    void ISupportInitialize.EndInit()
    {
        _isBeingMapped = false;
    }
}

IMapSettable

Данный интерфейс имеет всего лишь один метод – SetField. В процессе отображения поля источника передаются в этот метод одно за другим. Если объект обрабатывает поле самостоятельно, то он должен вернуть true. В противном случае маппер попытается произвести отображение самостоятельно. Следующий пример демонстрирует применение данного интерфейса для реализации свойства ID базового класса бизнес объектов:

public abstract class BizEntityBase : IMapSettable
{
    private Guid _id;
    public  Guid  ID { get { return _id; } }

    bool IMapSettable.SetField(string fieldName, object value)
    {
        if (string.Compare(fieldName, GetType().Name + "ID") == 0)
        {
            _id = (Guid)value;

            return true;
        }

        return false;
    }
}

Известные проблемы

Существует несколько проблем использования отображения, связанные с ограничениями абстрактных классов и динамических классов и сборок, созданных с помощью System.Reflection.Emit.

В процессе работы маппер создаёт наследуемый от абстрактного класса класс, который реализует указанные абстрактные свойства. Вполне естественно, что полное имя этого класса отличается от оригинального. Имя сгенерированного класса строится по следующим правилам:

NamespaceName.AbstractClassName.MappingExtension.AbstractClassName

Таким образом, метод GetType().Name для класса MyBizEntity и его наследника выдаст одинаковые имена, а метод GetType().FullName различные. Также не будет работать прямое сравнение типов:

MyBizEntity entity = 
    MapDescriptor.GetDescriptor(typeof(MyBizEntity)).CreateInstanceEx();

if (entity.GetType() == typeof(MyBizEntity))  // false
{
}

if (entity is MyBizEntity)  // true
{
}

Проблема динамических сборок заключается в следующем. Так как такие сборки создаются в памяти, то они не реализуют некоторые свойства и при обращении к ним генерируют исключение NotImplementedException. Данную проблему можно обойти с помощью явной проверки типа библиотеки.

foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies())
{
    if (a is System.Reflection.Emit.AssemblyBuilder)
        continue;

    Console.WriteLine(a.CodeBase);
}

Спасибо всем принявшим участие в обсуждение библиотеки.

Отдельное спасибо Илье Рыженкову и Андрею Касьянову за детальное review библиотеки и ценные замечания и предложения.


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.