Типичный вывод набора данных в опрятном дизайне
Типичный вывод набора данных в опрятном дизайне

Во имя чего?

Недавно проходил очередное собеседование, попросили написать CRUD для данных из 5 таблиц с максимальной глубиной связей 3 (таблица 1->таблица 2->таблица 3).

В принципе, это по количеству работы аналог какого-нибудь форума (users, files, themes, groups, messages). Сделайте нам пожалуйста phphbb :). И слышать "Ой, а ваше решение плохо поддерживается" в ответ на труд более двух дней не хотелось.

Встроенный в VisualStudio генератор CRUD по EntityFramework тоже оказался почти бессилен.

Плюс, довольно известный генератор CRUD EasyData(ссылка) не поддерживает валидацию данных через атрибуты (по крайней мере на стороне сервера).

(Для тех, кто уже может создавать простые системы классов)

Часть 1 - Система типов

Сначала построим все классы для хранения результатов.

Информация о контексте из DbContext и связанные типы
Информация о контексте из DbContext и связанные типы
DbSetInfo.cs - Информация о контексте
    /// <summary>
    /// Информация о таблицах и их элементах 
    /// </summary>
    internal class DbSetInfo
    {
        /// <summary>
        /// Имя контекста БД
        /// </summary>
        public string Name;
        /// <summary>
        /// Список таблиц для данной БД
        /// </summary>
        public List<ModelInfo> Models = new List<ModelInfo>();
    }

ModelInfo.cs - Информация о таблице
 /// <summary>
    /// Информация о таблицах БД
    /// </summary>
    internal class ModelInfo
    {
        /// <summary>
        /// Имя класса таблицы
        /// </summary>
        public string ClassName;

        /// <summary>
        /// Имя таблицы
        /// </summary>
        public string TableName
        {
            get
            {
                if (ClassName.EndsWith("Model"))
                    return ClassName.Substring(0, ClassName.Length - 5);
                else
                    return ClassName;
            }
        }

        /// <summary>
        /// Список поле таблицы
        /// </summary>
        public List<ItemInfo> Items = new List<ItemInfo>();

        /// <summary>
        /// Список ссылок из таблицы
        /// </summary>
        public List<SingleRelativeElementInfo> RelativeElements = new List<SingleRelativeElementInfo>();
    }

ItemInfo.cs - Информация о полях таблицы
 /// <summary>
    /// Информация о поле в таблице
    /// </summary>
    internal class ItemInfo
    {
        /// <summary>
        /// Имя поля таблицы
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// Тип поля
        /// </summary>
        public eItemTypeInfo Type { get; set; }

        /// <summary>
        /// Список аттрибутов
        /// </summary>
        public List<ValidationAttributeInfo> ValidationAttributes { get; set; }
    }

eItemTypeInfo.cs - Типы данных в полях таблицы
    /// <summary>
    /// Список типов элементов в таблице
    /// </summary>
    internal enum eItemTypeInfo
    {
        //Строка
        @string,
        //GUID
        Guid
    }

ValidationAttributeInfo.cs - Информация об атрибутах полей (возможно я разделю атрибуты по типам на этапе рисования шаблонов)
    /// <summary>
    /// Информация об аттрибуте
    /// </summary>
    public class ValidationAttributeInfo
    {
        /// <summary>
        /// Обьект аттрибута
        /// </summary>
        public Attribute Attribute;
    }

RelativeElementInfo.cs - ссылка 1 к 1 на другую таблицу
    /// <summary>
    /// Ссылка на другую таблицу
    /// </summary>
    internal class SingleRelativeElementInfo
    {
        /// <summary>
        /// Имя поля
        /// </summary>
        public string PropertyName;

        /// <summary>
        /// Имя таблицы
        /// </summary>
        public string TableName
        {
            get
            {
                if (@Type.Name.EndsWith("Model"))
                    return @Type.Name.Substring(0, @Type.Name.Length - 5);
                else
                    return @Type.Name;
            }
        }

        /// <summary>
        /// Тип, описывающий таблицу, на которую ссылаются
        /// </summary>
        public Type @Type;
    }

Как видим, ничего интересного, у контекста есть таблицы, у таблицы есть поля, у полей есть атрибуты. Есть ссылочные поля (ссылка 1 к 1).

Часть 2 - Основная часть парсера

Типы собственно парсера, объекта с информацией о таблицах и классы Helper-ы
Типы собственно парсера, объекта с информацией о таблицах и классы Helper-ы

Естественно, данные которые мы получаем от Helper-а тоже заносятся в отдельный класс.

RawTableInfo.cs - Класс для хранения данных парсера
    /// <summary>
    /// "сырая" информация о таблице
    /// </summary>
    internal class RawTableInfo
    {
        /// <summary>
        /// Имя класса таблицы
        /// </summary>
        public string ClassName;
        /// <summary>
        /// Имя таблицы
        /// </summary>
        public string TableName;
        /// <summary>
        /// Имя свойства таблицы
        /// </summary>
        public string FieldName;
        /// <summary>
        /// Тип свойства таблицы
        /// </summary>
        public Type TableType;
        /// <summary>
        /// Тип модели данных таблицы
        /// </summary>
        public Type ModelType;
    }

Так же обычно помогают Extention-методы для object, которые получают почти любую информацию о типе (без дублирования typeof\GetType).

ObjectExtentions.cs - Методы расширения для объектов
    /// <summary>
    /// Получает данные о полях, свойствах, аттрибутах через Extention-методы для обьектов
    /// </summary>
    internal static class ObjectExtentions
    {
        public static List<PropertyInfo> GetProperties(this object obj)
        {
            var t = obj.GetType();

            return t.GetRuntimeProperties().ToList();
        }

        public static List<FieldInfo> GetFields(this object obj)
        {
            var t = obj.GetType();

            return t.GetRuntimeFields().ToList();
        }

        public static List<Attribute> GetAttributes(this object obj)
        {
            var t = obj.GetType();

            return t.GetCustomAttributes().ToList();
        }


        public static List<PropertyInfo> GetProperties(this Type obj)
        {
            return obj.GetRuntimeProperties().ToList();
        }

        public static List<FieldInfo> GetFields(this Type obj)
        {
            return obj.GetRuntimeFields().ToList();
        }

        public static List<Attribute> GetAttributes(this Type obj)
        {
            return obj.GetCustomAttributes().ToList();
        }
    }

Теперь перейдем к нашему основному классу, который получает данные о DbContext и возвращает класс DbSetInfo.

DbSetParser.cs - Парсер информации DbContext
internal class DbSetParser
    {
        /// <summary>
        /// Получает информацию о всех таблицах, их элементах и аттрибутах
        /// </summary>
        /// <param name="dbcontext">Обьект с требуемыми классами</param>
        /// <returns>Информация о всех таблицах и элементах</returns>
        public DbSetInfo ParseDefinition(Type dbcontextType)
        {

            //Проверям что dbcontextType имеет тип DbContext
            if (!typeof(DbContext).IsAssignableFrom(dbcontextType))
                throw new Exception("Неподдерживаемый тип описания БД");

            var tables = dbcontextType.GetTables();

            var tableReferences = tables.Select(item => item.ModelType).ToList();

            var res = new DbSetInfo();

            foreach (var table in tables)
            {
                ProcessTable(res, table, tableReferences);
            }

            return res;
        }


        /// <summary>
        /// Получает информацию о всех таблицах, их элементах и аттрибутах
        /// </summary>
        /// <param name="dbcontext">Обьект с требуемыми классами</param>
        /// <returns>Информация о всех таблицах и элементах</returns>
        public DbSetInfo ParseDefinition(DbContext dbcontext)
        {
            var tables = dbcontext.GetTables();

            var tableReferences = tables.Select(item => item.ModelType).ToList();

            var res = new DbSetInfo();

            foreach (var table in tables)
            {
                ProcessTable(res, table, tableReferences);
            }

            return res;
        }

        /// <summary>
        /// Получает информацию о всех полях таблицы (Имя, тип - для простых полей)
        /// Тип - для ссылок
        /// </summary>
        /// <param name="res">Обьект с результатами разбора</param>
        /// <param name="table">Поле со ссылкой на таблицу</param>
        /// <param name="tableReferences">Список типов с табличными значениями</param>
        private void ProcessTable(DbSetInfo res, RawTableInfo table, List<Type> tableReferences)
        {

            var model = new ModelInfo()
            {
                ClassName = table.ModelType.Name
            };

            processDataFields(model, table, tableReferences);

            process1to1References(model, table, tableReferences);

            res.Models.Add(model);
        }

        /// <summary>
        /// Добавляет информацию о ссылках 1 к 1 в таблице table
        /// </summary>
        /// <param name="model">Описание таблицы</param>
        /// <param name="table">Исходная таблица</param>
        /// <param name="tableReferences">Обьекты существующих таблиц</param>
        private void process1to1References(ModelInfo model, RawTableInfo table, List<Type> tableReferences)
        {
            var relFields = table.GetReferenceFields(tableReferences);

            model.RelativeElements = relFields.Select(item =>
            new SingleRelativeElementInfo()
            {
                PropertyName = item.Name,
                Type = item.PropertyType
            })
                .ToList();
        }

        /// <summary>
        /// Добавиляет информацию о полях данных в таблице table
        /// </summary>
        /// <param name="model">Описание таблицы</param>
        /// <param name="table">Исходная таблица</param>
        /// <param name="tableReferences">Обьекты существующих таблиц</param>
        private void processDataFields(ModelInfo model, RawTableInfo table, List<Type> tableReferences)
        {
            var dataFields = table.GetDataFields(tableReferences);

            model.Items = dataFields.Select(item =>
            new ItemInfo()
            {
                Name = item.Name,
                @Type = Enum.Parse<eItemTypeInfo>(item.PropertyType.Name, true),
                ValidationAttributes = item.GetCustomAttributes().Select(item2 => new ValidationAttributeInfo()
                {
                    Attribute = item2
                }).ToList()
            }
            )
                .ToList();
        }
    }

В основном (кроме вызова говорящих методов GetRuntimeFields() GetRuntimeProperties()) встречаются 3 проблемы:

  1. Как реализовать is/as

    Очень просто, использовать IsAssignableFrom(). Метод вернет true если аргумент можно привести к базовому типу.

    typeof(baseClassType).IsAssignableFrom(DerivedClassType)

  2. Как определить что тип унаследован от Generic

    Тоже довольно просто. Приводить все к GetGenericTypeDefinition(). Следующий код вернет true для всех объектов, унаследованных от DbSet<T>.

    item.PropertyType.GetGenericTypeDefinition() != typeof(DbSet<object>).GetGenericTypeDefinition()

  3. Как получить параметры Generic типов (string из List<string>)

    Специальная коллекция GenericTypeArguments со ссылками на типы параметров

    GenericTypeArguments[0] - В случае DbSet

Ну и немного про сам DbSetParser. В принципе, все написано в стиле SOLID и мне ничто не помешает добавить ссылки 1 к N либо новые типы данных. Я просто добавлю поля в ModelInfo и метод наподобие process1toNReferences не нарушая структуры кода.

Заключение

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

Атрибуты можно использовать в cshtml.

Во второй части сформулируем чем похожи все исходные коды на всех языках, построим модели данных и сделаем пару простых шаблонов (Controller/CrudViews (Index, Add, Edit, Delete).

Комментарии (40)