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

Помните, у Урсулы Ле Гуин в «Волшебнике Земноморья»: «Никогда не спрашивайте человека о его имени. Никогда не называйте своего». К сожалению, 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 выделенные строки:

dc05cd10c1164887b5bc0b80aa3e2cc6.jpg

Кроме этого, необходимо будет добавить в проект ссылку на 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 (@»(? \w+?\.)*(? {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 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> 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 Удачных вам переименований!

© Habrahabr.ru