Помните, у Урсулы Ле Гуин в «Волшебнике Земноморья»: «Никогда не спрашивайте человека о его имени. Никогда не называйте своего». К сожалению, Entity Framework «из коробки» совершенно не руководствуется этим замечательным правилом, и при генерации классов на основании схемы базы данных (стратегия Database First) именует классы, свойства классов и навигационные свойства именно так, как именуется соответствующая таблица в БД.
А что делать, если есть задача разработать новый проект к уже существующей базе данных, в которой таблицы именуются по некоторому шаблону, скажем, по шаблону t_tablename (напр. t_order_product). А в проекте принято совершенно другое соглашение об именах, и разработчики желают видеть «человеческие», с их точки зрения, имена (OrderProduct). Конечно, можно выкрутиться из ситуации, приняв соответствующее административное решение, однако иногда очень не хочется идти наперекор чувству прекрасного.
К тому же как нельзя кстати в Entity Framework появились шаблоны кодогенерации. Казалось бы: для того они и появились, чтобы разработчик мог управлять процессом создания классов на основе схемы БД. Однако одними шаблонами ограничиться не удалось, но об этом чуть ниже.
Итак, стоит задача: добиться автоматического переименования сущностей при создании/обновлении модели данных. Для простоты допустим, что имена столбцов находятся в приемлемом формате, и будем оперировать только именами таблиц.
Имя таблицы порождает следующие имена:
Непосредственно шаблон T4 генерирует только имена файлов, а всё остальное он черпает из файла Model.edmx. В этом файле хранятся описание сущностей базы данных, концептуальная модель и сопоставление одного другому. Теоретически можно заставить шаблон генерировать измененные имена сущностей, однако это приведет к печальным последствиям. Поскольку для нового имени не будет задано соответствие в .edmx-файле, то любой запрос к БД окончится неудачей — QueryProvider, составляющий SQL-запрос, просто не обнаружит имени таблицы по имени класса. Чтобы в этом убедиться, достаточно автоматическим рефакторингом попробовать переименовать имя какого-нибудь класса. Отсюда следует простой вывод — требуется модифицировать сам .edmx-файл.
На деле эта задача не столько сложная, сколько муторная. Необходимо знание структуры .edmx и понимание того, что там чему соответствует. Кроме этого, чтобы всё было в лучшем виде, понадобится в некоторых случаях переводить имя во множественное число. С DbContext всё просто: берем и переводим. С навигационными свойствами сложнее: надо анализировать связи и определять, на какую сторону эта связь «приходит»: один или многие. Кроме .edmx-файла так же потребуется изменить файл .edmx.diagram — именно там хранится описание красивых табличек, которые можно увидеть в дизайнере модели.
Итогом решения описанной выше задачи стал класс EntityTransformer, который я и хочу представить вашему вниманию. Всё, что он делает — это загружает .edmx-файл, анализирует его содержимое, меняет значения соответствующих атрибутов и сохраняет измененный файл. Если вы захотите использовать его в своих целях, то всё, что вам нужно — это модифицировать метод
Запустить преобразование можно несколькими способами — например, создать внешнее консольное приложение. Однако, поскольку хотелось получить как можно «бесшовное» решение и таки задействовать шаблоны (уж коли они есть), то класс был оформлен в шаблон и подключен в основной шаблон кодогенерации. Само подключение осуществляется тривиально, достаточно добавить в проект файл EntityTransformer.ttinclude, скопировать в него приведенный ниже шаблон и добавить в файл Model.tt выделенные строки:
Кроме этого, необходимо будет добавить в проект ссылку на
Для запуска шаблонов их придется открыть и сохранить, причем это надо будет проделать дважды: для Model.tt и для Model.Context.tt. Иначе либо в DbContext, либо в классах-сущностях останутся оригинальные имена (имена таблиц). К сожалению, мне не удалось найти способа автоматического запуска шаблонов.
Для тестирования шаблона использовалась база данных, эмулирующая наиболее типовые ситуации: отношения один-ко-многим, один-к-одному, многие-ко-многим, несколько связей между двуми таблицами.
Удачных вам переименований!
А что делать, если есть задача разработать новый проект к уже существующей базе данных, в которой таблицы именуются по некоторому шаблону, скажем, по шаблону 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
Удачных вам переименований!