Pull to refresh

Entity Framework и Правило имён

Reading time 10 min
Views 10K
Помните, у Урсулы Ле Гуин в «Волшебнике Земноморья»: «Никогда не спрашивайте человека о его имени. Никогда не называйте своего». К сожалению, Entity Framework «из коробки» совершенно не руководствуется этим замечательным правилом, и при генерации классов на основании схемы базы данных (стратегия Database First) именует классы, свойства классов и навигационные свойства именно так, как именуется соответствующая таблица в БД.

А что делать, если есть задача разработать новый проект к уже существующей базе данных, в которой таблицы именуются по некоторому шаблону, скажем, по шаблону t_tablename (напр. t_order_product). А в проекте принято совершенно другое соглашение об именах, и разработчики желают видеть «человеческие», с их точки зрения, имена (OrderProduct). Конечно, можно выкрутиться из ситуации, приняв соответствующее административное решение, однако иногда очень не хочется идти наперекор чувству прекрасного.

К тому же как нельзя кстати в Entity Framework появились шаблоны кодогенерации. Казалось бы: для того они и появились, чтобы разработчик мог управлять процессом создания классов на основе схемы БД. Однако одними шаблонами ограничиться не удалось, но об этом чуть ниже.

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

Имя таблицы порождает следующие имена:

  • Собственно имя класса (Entity)
  • Имя файла, где хранится класс (Entity.cs)
  • Наименование свойства для доступа к множеству классов из контекста базы данных (DbContext.Entities)
  • Имена навигационных свойств
  • Наименование фигур в визуальном дизайнере


Непосредственно шаблон T4 генерирует только имена файлов, а всё остальное он черпает из файла Model.edmx. В этом файле хранятся описание сущностей базы данных, концептуальная модель и сопоставление одного другому. Теоретически можно заставить шаблон генерировать измененные имена сущностей, однако это приведет к печальным последствиям. Поскольку для нового имени не будет задано соответствие в .edmx-файле, то любой запрос к БД окончится неудачей — QueryProvider, составляющий SQL-запрос, просто не обнаружит имени таблицы по имени класса. Чтобы в этом убедиться, достаточно автоматическим рефакторингом попробовать переименовать имя какого-нибудь класса. Отсюда следует простой вывод — требуется модифицировать сам .edmx-файл.

На деле эта задача не столько сложная, сколько муторная. Необходимо знание структуры .edmx и понимание того, что там чему соответствует. Кроме этого, чтобы всё было в лучшем виде, понадобится в некоторых случаях переводить имя во множественное число. С DbContext всё просто: берем и переводим. С навигационными свойствами сложнее: надо анализировать связи и определять, на какую сторону эта связь «приходит»: один или многие. Кроме .edmx-файла так же потребуется изменить файл .edmx.diagram — именно там хранится описание красивых табличек, которые можно увидеть в дизайнере модели.

Итогом решения описанной выше задачи стал класс EntityTransformer, который я и хочу представить вашему вниманию. Всё, что он делает — это загружает .edmx-файл, анализирует его содержимое, меняет значения соответствующих атрибутов и сохраняет измененный файл. Если вы захотите использовать его в своих целях, то всё, что вам нужно — это модифицировать метод Transform(string inputString, bool pluralize), который как раз и определяет правила переименования сущностей.

Запустить преобразование можно несколькими способами — например, создать внешнее консольное приложение. Однако, поскольку хотелось получить как можно «бесшовное» решение и таки задействовать шаблоны (уж коли они есть), то класс был оформлен в шаблон и подключен в основной шаблон кодогенерации. Само подключение осуществляется тривиально, достаточно добавить в проект файл EntityTransformer.ttinclude, скопировать в него приведенный ниже шаблон и добавить в файл Model.tt выделенные строки:



Кроме этого, необходимо будет добавить в проект ссылку на System.Data.Entity.Design — это необходимо для использования PluralizationService. Если вам не нужно преобразование в множественное число, то ссылку можно не добавлять, и исключить соответствующие строки кода из шаблона.

Для запуска шаблонов их придется открыть и сохранить, причем это надо будет проделать дважды: для Model.tt и для Model.Context.tt. Иначе либо в DbContext, либо в классах-сущностях останутся оригинальные имена (имена таблиц). К сожалению, мне не удалось найти способа автоматического запуска шаблонов.

Шаблон EntityTransformer.ttinclude
<#@ assembly name="System.Data.Entity.Design" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Data.Entity.Design.PluralizationServices" #>
<#@ import namespace="System.Globalization" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System.Xml.Linq" #>

<#+
	public class EntityTransformer
    {
        readonly PluralizationService _pluralizationService = PluralizationService.CreateService(CultureInfo.GetCultureInfo("en-US"));
        const string DESIGNER_NAMESPACE = "http://schemas.microsoft.com/ado/2009/11/edmx";

        private string Transform(string inputString, bool pluralize)
        {
            string result = string.Empty;

            const string PREFIX = "t_";
            Regex regex = new Regex(string.Format(@"(?<namespace>\w+?\.)*(?<prefix>{0})*(\w+)", PREFIX));

            var groups = regex.Match(inputString).Groups;
            string namespc = groups["namespace"].Value;
            string[] parts = groups[1].Value.Split(new[] {"_"}, StringSplitOptions.RemoveEmptyEntries);

            for (int i = 0; i < parts.Length; i++)
            {
                string addingPart = FirstCharToUpper(parts[i]);
                if (pluralize && i == parts.Length - 1)
                    addingPart = _pluralizationService.Pluralize(addingPart);

                result += addingPart;
            }

            result = namespc + result;
            return result;
        }

        private string Transform(string inputString)
        {
            string result = Transform(inputString, false);
            return result;
        }

        private void Transform(XAttribute attribute)
        {
            attribute.Value = Transform(attribute.Value);
        }

        private void Transform(XAttribute attribute, bool pluralize)
        {
            attribute.Value = Transform(attribute.Value, pluralize);
        }

        public void TransformEntities(string inputFile)
        {
            XDocument document = XDocument.Load(inputFile);

            const string SSDL_NAMESPACE = "http://schemas.microsoft.com/ado/2009/11/edm/ssdl";
            const string CSDL_NAMESPACE = "http://schemas.microsoft.com/ado/2009/11/edm";
            const string MSL_NAMESPACE = "http://schemas.microsoft.com/ado/2009/11/mapping/cs";
            
            XElement ssdl = document.Descendants(XName.Get("Schema", SSDL_NAMESPACE)).First();
            XElement csdl = document.Descendants(XName.Get("Schema", CSDL_NAMESPACE)).First();
            XElement msl = document.Descendants(XName.Get("Mapping", MSL_NAMESPACE)).First();
            XElement designerDiagram = document.Descendants(XName.Get("Designer", DESIGNER_NAMESPACE)).First();

            TransformCsdl(csdl, ssdl);
            TransformMsl(MSL_NAMESPACE, msl);
            TransformDesigner(DESIGNER_NAMESPACE, designerDiagram, inputFile);

            document.Save(inputFile);
        }

        private void TransformDesigner(string designerNamespace, XElement designerDiagram, string modelFilePath)
        {
            Action<XElement> transformDesigner = diagram =>
            {
                var shapes = diagram.Descendants(XName.Get("EntityTypeShape", designerNamespace));

                foreach (var item in shapes)
                    Transform(item.Attribute("EntityType"));
            };

            transformDesigner(designerDiagram);

            string diagramFilePath = string.Format("{0}.diagram", modelFilePath);

            if (File.Exists(diagramFilePath))
            {
                XDocument document = XDocument.Load(diagramFilePath);
                designerDiagram = document.Descendants(XName.Get("Designer", DESIGNER_NAMESPACE)).First();

                transformDesigner(designerDiagram);

                document.Save(diagramFilePath);
            }
        }

        private void TransformMsl(string mslNamespace, XElement msl)
        {
            var entityContainerMapping = msl.Element(XName.Get("EntityContainerMapping", mslNamespace));
            if (entityContainerMapping == null)
                throw new Exception("Element EntityContainerMapping not found.");

            foreach (var entitySetMapping in entityContainerMapping.Elements(XName.Get("EntitySetMapping", mslNamespace)))
            {
                Transform(entitySetMapping.Attribute("Name"), true);

                foreach (var entityTypeMapping in entitySetMapping.Elements(XName.Get("EntityTypeMapping", mslNamespace)))
                    Transform(entityTypeMapping.Attribute("TypeName"));
            }
        }

        private void TransformCsdl(XElement csdl, XElement ssdl)
        {
            string csdlNamespace = csdl.GetDefaultNamespace().NamespaceName;

            Func<XElement, string, IEnumerable<XElement>> getElements =
                (root, localName) => root.Elements(XName.Get(localName, csdlNamespace));

            var entityContainer = csdl.Element(XName.Get("EntityContainer", csdlNamespace));
            if (entityContainer == null)
                throw new Exception("Element EntityContainer not found.");

            foreach (var entitySet in getElements(entityContainer, "EntitySet"))
            {
                Transform(entitySet.Attribute("Name"), true);
                Transform(entitySet.Attribute("EntityType"));
            }

            foreach (var associationSet in getElements(entityContainer, "AssociationSet"))
                foreach (var end in getElements(associationSet, "End"))
                    Transform(end.Attribute("EntitySet"), true);

            foreach (var entityType in getElements(csdl, "EntityType"))
                Transform(entityType.Attribute("Name"));


            foreach (var association in getElements(csdl, "Association"))
                foreach (var end in  getElements(association, "End"))
                    Transform(end.Attribute("Type"));

            TransformNavigationProperties(csdl, ssdl);
        }

        private void TransformNavigationProperties(XElement csdl, XElement ssdl)
        {
            string ssdlNamespace = ssdl.GetDefaultNamespace().NamespaceName;
            string csdlNamespace = csdl.GetDefaultNamespace().NamespaceName;

            var associationSets = ssdl.Descendants(XName.Get("AssociationSet", ssdlNamespace));

            foreach (XElement associationSet in associationSets)
            {
                var association =
                    ssdl.Descendants(XName.Get("Association", ssdlNamespace))
                        .Single(a => a.Attribute("Name").Value == associationSet.Attribute("Name").Value);

                var roles = association.Elements().Where(e => e.Name.LocalName == "End");

                var manyRole = roles.FirstOrDefault(e => e.Attribute("Multiplicity").Value == "*");
                        
                var csdlAssotiationSet =
                    csdl.Descendants(XName.Get("AssociationSet", csdlNamespace))
                        .Single(e => e.Attribute("Name").Value == associationSet.Attribute("Name").Value);

                string associationName = csdlAssotiationSet.Attribute("Association").Value;

                var navigationProperties =
                    csdl.Descendants(XName.Get("NavigationProperty", csdlNamespace))
                        .Where(e => e.Attribute("Relationship").Value == associationName);

                foreach (XElement navigationProperty in navigationProperties)
                {
                    bool pluralize = manyRole != null &&
                                     navigationProperty.Attribute("ToRole").Value == manyRole.Attribute("Role").Value;

                    Transform(navigationProperty.Attribute("Name"), pluralize);
                }
            }
        }

        private static string FirstCharToUpper(string input)
        {
            if (String.IsNullOrEmpty(input))
                throw new ArgumentException("Empty string");

            return input.First().ToString().ToUpper() + input.Substring(1);
        }
    }
#>



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

Скрипт на создание тестовых таблиц
CREATE TABLE [dbo].[t_address](
	[AddressId] [int] IDENTITY(1,1) NOT NULL,
	[AddressName] [nvarchar](500) NOT NULL,
 CONSTRAINT [PK_t_address] PRIMARY KEY CLUSTERED 
(
	[AddressId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

CREATE TABLE [dbo].[t_customer](
	[CustomerId] [int] IDENTITY(1,1) NOT NULL,
	[CustomerName] [nvarchar](50) NOT NULL,
	[LocationAddressId] [int] NULL,
	[PostalAddressId] [int] NULL,
PRIMARY KEY CLUSTERED 
(
	[CustomerId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

CREATE TABLE [dbo].[t_customer_info](
	[CustomerId] [int] NOT NULL,
	[CustomerDescription] [nvarchar](50) NULL,
 CONSTRAINT [PK_t_customer_info] PRIMARY KEY CLUSTERED 
(
	[CustomerId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

CREATE TABLE [dbo].[t_order](
	[OrderId] [int] IDENTITY(1,1) NOT NULL,
	[CustomerId] [int] NOT NULL,
	[CreateDate]  AS (getdate()),
 CONSTRAINT [PK__t_Order__C3905BCFC0AF501C] PRIMARY KEY CLUSTERED 
(
	[OrderId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

CREATE TABLE [dbo].[t_order_product](
	[OrderId] [int] NOT NULL,
	[ProductId] [int] NOT NULL,
	[Count] [int] NOT NULL,
 CONSTRAINT [PK_t_order_product] PRIMARY KEY CLUSTERED 
(
	[OrderId] ASC,
	[ProductId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

CREATE TABLE [dbo].[t_product](
	[ProductId] [int] IDENTITY(1,1) NOT NULL,
	[ProductName] [nvarchar](100) NOT NULL,
	[ProductPrice] [decimal](10, 2) NOT NULL,
 CONSTRAINT [PK_t_product] PRIMARY KEY CLUSTERED 
(
	[ProductId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

CREATE TABLE [dbo].[t_test_person](
	[TestId] [int] IDENTITY(1,1) NOT NULL,
 CONSTRAINT [PK_t_test_person] PRIMARY KEY CLUSTERED 
(
	[TestId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

ALTER TABLE [dbo].[t_customer]  WITH CHECK ADD  CONSTRAINT [FK_t_customer_t_address] FOREIGN KEY([LocationAddressId])
REFERENCES [dbo].[t_address] ([AddressId])
GO

ALTER TABLE [dbo].[t_customer] CHECK CONSTRAINT [FK_t_customer_t_address]
GO

ALTER TABLE [dbo].[t_customer]  WITH CHECK ADD  CONSTRAINT [FK_t_customer_t_address1] FOREIGN KEY([PostalAddressId])
REFERENCES [dbo].[t_address] ([AddressId])
GO

ALTER TABLE [dbo].[t_customer] CHECK CONSTRAINT [FK_t_customer_t_address1]
GO

ALTER TABLE [dbo].[t_customer_info]  WITH CHECK ADD  CONSTRAINT [FK_t_customer_info_t_customer] FOREIGN KEY([CustomerId])
REFERENCES [dbo].[t_customer] ([CustomerId])
GO

ALTER TABLE [dbo].[t_customer_info] CHECK CONSTRAINT [FK_t_customer_info_t_customer]
GO

ALTER TABLE [dbo].[t_order]  WITH CHECK ADD  CONSTRAINT [FK_t_Order_To_t_Customer] FOREIGN KEY([CustomerId])
REFERENCES [dbo].[t_customer] ([CustomerId])
ON DELETE CASCADE
GO

ALTER TABLE [dbo].[t_order] CHECK CONSTRAINT [FK_t_Order_To_t_Customer]
GO

ALTER TABLE [dbo].[t_order_product]  WITH CHECK ADD  CONSTRAINT [FK_t_order_product_t_order] FOREIGN KEY([OrderId])
REFERENCES [dbo].[t_order] ([OrderId])
ON DELETE CASCADE
GO

ALTER TABLE [dbo].[t_order_product] CHECK CONSTRAINT [FK_t_order_product_t_order]
GO

ALTER TABLE [dbo].[t_order_product]  WITH CHECK ADD  CONSTRAINT [FK_t_order_product_t_product] FOREIGN KEY([ProductId])
REFERENCES [dbo].[t_product] ([ProductId])
ON DELETE CASCADE
GO

ALTER TABLE [dbo].[t_order_product] CHECK CONSTRAINT [FK_t_order_product_t_product]
GO



Удачных вам переименований!
Tags:
Hubs:
+12
Comments 5
Comments Comments 5

Articles