Compare commits

...
2 Commits
36 changed files with 1027 additions and 288 deletions
+62
View File
@@ -0,0 +1,62 @@
namespace Pf2eDataScrapper.App;
/// <summary>
/// Общий аттрибут обработчика.
/// </summary>
/// <param name="name">Название обработчика.</param>
/// <param name="dependencies">Названия обработчиков, которые должны быть выполнены до запуска этого обработчика.</param>
[AttributeUsage(AttributeTargets.Class)]
public class ProcessAttribute(string name, ICollection<string> dependencies) : Attribute
{
/// <summary>
/// Название обработчика.
/// </summary>
public string Name { get; init; } = name;
/// <summary>
/// Коллекция названий обработчиков, которые должны быть выполнены до этого оброботчика.
/// </summary>
public ICollection<string> Dependencies { get; init; } = dependencies;
public ProcessAttribute(string name)
: this(name, []) { }
}
/// <summary>
/// Аттрибут обработчика для уточнения группы.
/// </summary>
/// <param group="group">Группа обработчиков, к которому относится обработчик.</param>
[AttributeUsage(AttributeTargets.Class)]
internal class ProcessorGroupAttribute(ProcessorGroupEnum group) : Attribute
{
/// <summary>
/// Название обработчика, к которому относится обработчик.
/// </summary>
public ProcessorGroupEnum Group { get; init; } = group;
}
/// <summary>
/// Аттрибут обработчика для уточнения списка зависимостей.
/// </summary>
/// <param name="dependencies">Список названий зависимостей.</param>
[AttributeUsage(AttributeTargets.Class)]
internal class ProcessorDependenciesAttribute(ICollection<string> dependencies) : Attribute
{
/// <summary>
/// Коллекция названий обработчиков, которые должны быть выполнены до этого оброботчика.
/// </summary>
public ICollection<string> Dependencies { get; init; } = dependencies;
}
/// <summary>
/// Аттрибут обработчика для внутреннего типа элемента.
/// </summary>
/// <param name="innerType">Внутренний типа элемента.</param>
[AttributeUsage(AttributeTargets.Class)]
internal class ProcessorInnerTypeAttribute(string innerType) : Attribute
{
/// <summary>
/// Коллекция названий обработчиков, которые должны быть выполнены до этого оброботчика.
/// </summary>
public string InnerType { get; init; } = innerType;
}
+229
View File
@@ -0,0 +1,229 @@
using System.ComponentModel;
using System.Reflection;
using Pf2eDataScrapper.Processors;
namespace Pf2eDataScrapper.App;
/// <summary>
/// Класс с коллекцией обработчиков.
/// </summary>
internal class ProcessorCollection
{
/// <summary>
/// Коллекция всех доступных обработчиков.
/// </summary>
public ICollection<ProcessorCollectionItem> AllItems { get; } = [];
/// <summary>
/// Коллекция обработчиков, которые будут обрабатываться.
/// Для изменения необходимо использовать <see cref="FilterProcessors"/>.
/// </summary>
public List<ProcessorCollectionItem> Items { get; private set; } = [];
/// <summary>
/// Конструктор класса <see cref="ProcessorCollection"/>.
/// </summary>
public ProcessorCollection()
{
Assembly assembly = Assembly.GetExecutingAssembly();
foreach (Type type in assembly.GetTypes())
{
if (type.GetCustomAttributes(typeof(ProcessAttribute), true).Length <= 0)
{
continue;
}
AllItems.Add(CreateItem(type));
}
CheckDependencies();
FilterProcessors([]);
}
/// <summary>
/// Создание элемента коллекции обработчиков.
/// </summary>
/// <param name="type">Тип обработчика.</param>
/// <returns></returns>
private ProcessorCollectionItem CreateItem(Type type)
{
if (!type.IsSubclassOf(typeof(IProcessor)))
{
throw new(
$"'ProcessAttribute' attribute should only be applied to children of 'IProcessor' class. '{type.FullName}' is not."
);
}
ProcessAttribute processAttribute = type.GetCustomAttribute<ProcessAttribute>()!;
DescriptionAttribute? descriptionAttribute =
type.GetCustomAttribute<DescriptionAttribute>();
ProcessorGroupAttribute? groupAttribute =
type.GetCustomAttribute<ProcessorGroupAttribute>();
ProcessorDependenciesAttribute? dependenciesAttribute =
type.GetCustomAttribute<ProcessorDependenciesAttribute>();
ProcessorInnerTypeAttribute? innerTypeAttribute =
type.GetCustomAttribute<ProcessorInnerTypeAttribute>();
bool isCollection = innerTypeAttribute is not null;
ConstructorInfo? constructor = isCollection
? type.GetConstructor([])
: type.GetConstructor([typeof(ProcessorInput)])!;
if (constructor is null)
{
throw new($"Не удаётся определить конструктор класса {type}");
}
return new()
{
Name = processAttribute.Name,
Type = type,
Description = descriptionAttribute?.Description ?? processAttribute.Name,
Group = groupAttribute?.Group ?? ProcessorGroupEnum.General,
Dependencies = dependenciesAttribute?.Dependencies ?? [],
IsCollectionProcessor = isCollection,
InnerType = innerTypeAttribute?.InnerType,
Constructor = constructor,
};
}
/// <summary>
/// Проверка, что все зависимости всех обработчиков могут быть удовлетворены.
/// </summary>
private void CheckDependencies()
{
foreach (ProcessorCollectionItem item in Items)
{
foreach (string dependency in item.Dependencies)
{
if (Items.All(i => i.Name != dependency))
{
throw new($"Dependency '{dependency}' for processor '{item.Name}' not found.");
}
}
}
}
/// <summary>
/// Сортировать обработчики в соответсвии со всеми зависимостями.
/// </summary>
public void SortItems()
{
Dictionary<string, int> priorities = [];
while (priorities.Count < Items.Count)
{
foreach (ProcessorCollectionItem item in Items)
{
if (item.Priority > 0)
{
continue;
}
IEnumerable<int> dependencyPriorities = item.Dependencies.Select(d =>
priorities.GetValueOrDefault(d, -1)
);
if (dependencyPriorities.Any(p => p == -1))
{
continue;
}
item.Priority = dependencyPriorities.Any() ? dependencyPriorities.Max() + 1 : 1;
priorities.Add(item.Name, item.Priority);
}
}
Items.Sort((a, b) => a.Priority - b.Priority);
}
/// <summary>
/// Ограничить список используемых обработчиков по названию.
/// Автоматически включает все обработчики, от которых зависят указанные в аргументе обработчики.
/// </summary>
/// <param name="processorNames">Коллекция названиий требуемых обработчиков.</param>
public void FilterProcessors(ICollection<string> processorNames)
{
if (processorNames.Count == 0)
{
Items = [.. AllItems];
SortItems();
return;
}
Dictionary<string, bool> nameFlags = processorNames.ToDictionary(name => name, _ => false);
while (nameFlags.Values.Any(v => !v))
{
foreach (
KeyValuePair<string, bool> pair in nameFlags.Where(pair => !pair.Value).ToList()
)
{
ProcessorCollectionItem item = AllItems.First(item => item.Name == pair.Key);
foreach (string dependency in item.Dependencies)
{
nameFlags.TryAdd(dependency, false);
}
nameFlags[pair.Key] = true;
}
}
Items = AllItems.Where(item => nameFlags.ContainsKey(item.Name)).ToList();
}
}
/// <summary>
/// Элемент коллекции обработчиков.
/// </summary>
internal class ProcessorCollectionItem
{
/// <summary>
/// Название обработчика.
/// </summary>
public required string Name { get; init; }
/// <summary>
/// Тип класса обработчика.
/// </summary>
public required Type Type { get; init; }
/// <summary>
/// Описание обработчика.
/// </summary>
public string Description { get; init; } = "";
/// <summary>
/// Группа обработчика.
/// </summary>
public required ProcessorGroupEnum Group { get; init; }
/// <summary>
/// Коллекция названий обработчиков, после которых должен выполняться обработчик.
/// </summary>
public ICollection<string> Dependencies { get; init; } = [];
/// <summary>
/// Приоритет выполнения обработчика.
/// </summary>
public int Priority { get; set; } = -1;
/// <summary>
/// Флаг, указывающий, обрабатывает ли обработчик коллекцию элементов.
/// Внутренний тип элементов указывается в <see cref="InnerType"/>.
/// </summary>
public required bool IsCollectionProcessor { get; init; }
/// <summary>
/// Флаг, указывающий внутренний тип коллекции обрабатываемых элементов.
/// </summary>
public string? InnerType { get; init; }
/// <summary>
/// Конструктор обработчика.
/// </summary>
public required ConstructorInfo Constructor { get; init; }
}
@@ -0,0 +1,21 @@
using System.ComponentModel;
namespace Pf2eDataScrapper.App;
public enum ProcessorGroupEnum
{
[Description("Общие процессоры")]
General,
[Description("Архивы Нефиса: Подготовка")]
AonprdPreparation,
[Description("Архивы Нефиса: Основные сущности")]
AonprdEntities,
[Description("Архивы Нефиса: Подклассы")]
AonprdSubclasses,
[Description("Foundry EN: Подготовка")]
FoundryEnPreparation,
}
+80
View File
@@ -0,0 +1,80 @@
using System.ComponentModel;
using dotenv.net;
using dotenv.net.Utilities;
namespace Pf2eDataScrapper;
/// <summary>
/// Настройки программы.
/// </summary>
internal class ApplicationSettings
{
/// <summary>
/// Конструктор класса <see cref="ApplicationSettings"/>.
/// </summary>
public ApplicationSettings()
{
DotEnv.Load();
}
#region Настройки из .env
/// <summary>
/// Путь к папке с входными данными.
/// </summary>
[Browsable(true)]
[ReadOnly(true)]
[DisplayName("Путь к папке с входными данными.")]
[Description("Редактирование через .env-файл.")]
public string DataDirPath { get; } = EnvReader.GetStringValue("DATA_DIR");
/// <summary>
/// Папка в входными данными.
/// </summary>
[Browsable(false)]
public DirectoryInfo DataDir => new(DataDirPath);
/// <summary>
/// Путь к папке для сохранения результатов.
/// </summary>
[Browsable(true)]
[ReadOnly(true)]
[DisplayName("Путь к папке для сохранения результатов.")]
[Description("Редактирование через .env-файл.")]
public string OutputDirPath { get; } = EnvReader.GetStringValue("OUTPUT_DIR");
/// <summary>
/// Папка для сохранения результатов.
/// </summary>
[Browsable(false)]
public string OutputDir => new(OutputDirPath);
/// <summary>
/// Строка подключения к базе данных.
/// </summary>
[Browsable(true)]
[ReadOnly(true)]
[DisplayName("Строка подключения к базе данных.")]
[Description("Редактирование через .env-файл.")]
public string DatabaseConnection { get; } = EnvReader.GetStringValue("DATABASE_CONNECTION");
#endregion
#region Основные настройки
/// <summary>
/// Флаг, указывающий, что все старые записи необходимо удалить.
/// </summary>
[Browsable(true)]
[DisplayName("Удаление всех записей перед запуском.")]
[Description("Редактирование через .env-файл.")]
public bool TruncateAllTables { get; set; } = true;
/// <summary>
/// Список обработчиков, которые должны быть выполнены.
/// Пустой список означает, что должны быть выполнены все обработчики.
/// </summary>
public ICollection<string> ProcessorNames { get; } = new List<string>();
#endregion
}
+166
View File
@@ -0,0 +1,166 @@
namespace Pf2eDataScrapper.Forms;
partial class StartForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
MainSplitContainer = new SplitContainer();
PropertiesGroupBox = new GroupBox();
SettingsPropertyGrid = new PropertyGrid();
StartButton = new Button();
ProcessorsGroupBox = new GroupBox();
DeselectAllButton = new Button();
SelectAllButton = new Button();
ProcessorsListView = new ListView();
((System.ComponentModel.ISupportInitialize)MainSplitContainer).BeginInit();
MainSplitContainer.Panel1.SuspendLayout();
MainSplitContainer.Panel2.SuspendLayout();
MainSplitContainer.SuspendLayout();
PropertiesGroupBox.SuspendLayout();
ProcessorsGroupBox.SuspendLayout();
SuspendLayout();
//
// MainSplitContainer
//
MainSplitContainer.Dock = DockStyle.Fill;
MainSplitContainer.Location = new Point(0, 0);
MainSplitContainer.Name = "MainSplitContainer";
//
// MainSplitContainer.Panel1
//
MainSplitContainer.Panel1.Controls.Add(PropertiesGroupBox);
MainSplitContainer.Panel1MinSize = 300;
//
// MainSplitContainer.Panel2
//
MainSplitContainer.Panel2.Controls.Add(ProcessorsGroupBox);
MainSplitContainer.Panel2.Controls.Add(StartButton);
MainSplitContainer.Panel2MinSize = 300;
MainSplitContainer.Size = new Size(931, 677);
MainSplitContainer.SplitterDistance = 300;
MainSplitContainer.TabIndex = 0;
//
// PropertiesGroupBox
//
PropertiesGroupBox.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
PropertiesGroupBox.Controls.Add(SettingsPropertyGrid);
PropertiesGroupBox.Location = new Point(3, 3);
PropertiesGroupBox.Name = "PropertiesGroupBox";
PropertiesGroupBox.Size = new Size(294, 671);
PropertiesGroupBox.TabIndex = 0;
PropertiesGroupBox.TabStop = false;
PropertiesGroupBox.Text = "Свойства";
//
// SettingsPropertyGrid
//
SettingsPropertyGrid.BackColor = SystemColors.Control;
SettingsPropertyGrid.Dock = DockStyle.Fill;
SettingsPropertyGrid.Location = new Point(3, 19);
SettingsPropertyGrid.Name = "SettingsPropertyGrid";
SettingsPropertyGrid.Size = new Size(288, 649);
SettingsPropertyGrid.TabIndex = 0;
//
// StartButton
//
StartButton.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
StartButton.Font = new Font("Segoe UI", 18F, FontStyle.Bold);
StartButton.Location = new Point(3, 611);
StartButton.Name = "StartButton";
StartButton.Size = new Size(621, 63);
StartButton.TabIndex = 0;
StartButton.Text = "Запуск";
StartButton.UseVisualStyleBackColor = true;
//
// ProcessorsGroupBox
//
ProcessorsGroupBox.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
ProcessorsGroupBox.Controls.Add(ProcessorsListView);
ProcessorsGroupBox.Controls.Add(SelectAllButton);
ProcessorsGroupBox.Controls.Add(DeselectAllButton);
ProcessorsGroupBox.Location = new Point(3, 3);
ProcessorsGroupBox.Name = "ProcessorsGroupBox";
ProcessorsGroupBox.Size = new Size(621, 602);
ProcessorsGroupBox.TabIndex = 1;
ProcessorsGroupBox.TabStop = false;
ProcessorsGroupBox.Text = "Обработчики";
//
// DeselectAllButton
//
DeselectAllButton.Location = new Point(6, 19);
DeselectAllButton.Name = "DeselectAllButton";
DeselectAllButton.Size = new Size(137, 23);
DeselectAllButton.TabIndex = 0;
DeselectAllButton.Text = "Снять выделение всех";
DeselectAllButton.UseVisualStyleBackColor = true;
//
// SelectAllButton
//
SelectAllButton.Location = new Point(149, 19);
SelectAllButton.Name = "SelectAllButton";
SelectAllButton.Size = new Size(137, 23);
SelectAllButton.TabIndex = 1;
SelectAllButton.Text = "Выделить все";
SelectAllButton.UseVisualStyleBackColor = true;
//
// ProcessorsListView
//
ProcessorsListView.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
ProcessorsListView.CheckBoxes = true;
ProcessorsListView.Location = new Point(6, 48);
ProcessorsListView.Name = "ProcessorsListView";
ProcessorsListView.Size = new Size(606, 548);
ProcessorsListView.TabIndex = 2;
ProcessorsListView.UseCompatibleStateImageBehavior = false;
//
// StartForm
//
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(931, 677);
Controls.Add(MainSplitContainer);
Name = "StartForm";
Text = "Настройки импорта данных";
MainSplitContainer.Panel1.ResumeLayout(false);
MainSplitContainer.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)MainSplitContainer).EndInit();
MainSplitContainer.ResumeLayout(false);
PropertiesGroupBox.ResumeLayout(false);
ProcessorsGroupBox.ResumeLayout(false);
ResumeLayout(false);
}
#endregion
private SplitContainer MainSplitContainer;
private GroupBox PropertiesGroupBox;
private PropertyGrid SettingsPropertyGrid;
private Button StartButton;
private GroupBox ProcessorsGroupBox;
private Button SelectAllButton;
private Button DeselectAllButton;
private ListView ProcessorsListView;
}
+9
View File
@@ -0,0 +1,9 @@
namespace Pf2eDataScrapper.Forms;
public partial class StartForm : Form
{
public StartForm()
{
InitializeComponent();
}
}
+120
View File
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
+58
View File
@@ -0,0 +1,58 @@
using Microsoft.Extensions.DependencyInjection;
using Pf2eDataScrapper.App;
using Pf2eDataScrapper.Forms;
using Pf2eModel;
namespace Pf2eDataScrapper;
internal sealed class ImportApplication : ApplicationContext
{
public ImportApplication()
{
InitializeServices();
CreateStartForm();
}
#region "Внедрение зависимостей (DI)"
public IServiceCollection Services = new ServiceCollection();
private void InitializeServices()
{
Services.AddSingleton(this);
Services.AddSingleton(Settings);
Services.AddSingleton<ProcessorCollection>();
Services.AddDbContext<Pf2eDbContext>();
}
#endregion
#region "Настройки"
public ApplicationSettings Settings = new();
#endregion
#region "Формы"
private StartForm? _startForm;
#endregion
#region "Жизненный цикл"
private void CreateStartForm()
{
_startForm = new() { StartPosition = FormStartPosition.CenterParent };
_startForm.Show();
}
private void StartProcess()
{
_startForm!.Close();
}
#endregion
}
@@ -2,6 +2,8 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium; using Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium;
using Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder.Processors; using Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder.Processors;
using Pf2eDataScrapper.Processors;
using Pf2eModel;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder;
@@ -11,11 +13,6 @@ namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder;
/// <param name="loader"></param> /// <param name="loader"></param>
internal class AonprdDataBuilder(AonprdLoader loader) internal class AonprdDataBuilder(AonprdLoader loader)
{ {
/// <summary>
/// Класс работы с базой данных.
/// </summary>
public readonly Pf2eDatabase Database = Pf2eDatabase.Instance;
/// <summary> /// <summary>
/// Основной класс для работы с данными Архива Нефиса. /// Основной класс для работы с данными Архива Нефиса.
/// </summary> /// </summary>
@@ -38,7 +35,10 @@ internal class AonprdDataBuilder(AonprdLoader loader)
{ {
Console.WriteLine("Truncating all old data..."); Console.WriteLine("Truncating all old data...");
Database.ExecuteCommand("SELECT f_truncate_tables();"); using Pf2eDbContext context = new();
string cmd = context.Entities.Truncate() + "\n" + context.TruncateAll();
Console.WriteLine(cmd);
Console.WriteLine("Truncation completed!"); Console.WriteLine("Truncation completed!");
} }
@@ -1,43 +1,22 @@
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder; using Pf2eDataScrapper.App.Attributes;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder;
/// <summary> /// <summary>
/// Атрибут обработчика данных Архивов Нефиса. /// Атрибут обработчика данных Архивов Нефиса.
/// </summary> /// </summary>
/// <param name="name">Название обработчика.</param>
/// <param name="type">Тип данных, которые обработчик обрабатывает.</param>
/// <param name="dependencies">Названия обработчиков, которые должны быть выполнены до запуска этого обработчика.</param>
/// <param name="includeDefaults">Флаг, указывающий, следует ли добавить важные общие зависимости для обработчка.</param>
[AttributeUsage(AttributeTargets.Class)] [AttributeUsage(AttributeTargets.Class)]
public class AonprdProcessorAttribute( public class AonprdProcessorAttribute : Attributes
string name,
string? type,
string[] dependencies,
bool includeDefaults = true
) : Attribute
{ {
/// <summary> /// <summary>
/// Важные общие зависимости для обработчка. /// Важные общие зависимости для обработчка.
/// </summary> /// </summary>
private static readonly string[] DefaultDependencies = ["trait", "source"]; private static readonly string[] DefaultDependencies = ["aonprd:trait", "aonprd:source"];
/// <summary>
/// Название обработчика.
/// </summary>
public readonly string Name = name;
/// <summary> /// <summary>
/// Тип данных, которые обработчик обрабатывает. /// Тип данных, которые обработчик обрабатывает.
/// </summary> /// </summary>
public readonly string? Type = type; public string Type { get; init; }
/// <summary>
/// Названия обработчиков, которые должны быть выполнены до запуска этого обработчика.
/// </summary>
public readonly IEnumerable<string> Dependencies =
[
.. includeDefaults ? DefaultDependencies : [],
.. dependencies,
];
/// <summary> /// <summary>
/// Конструктор класса <see cref="AonprdProcessorAttribute"/> со стандартными общими зависимости для обработчка. /// Конструктор класса <see cref="AonprdProcessorAttribute"/> со стандартными общими зависимости для обработчка.
@@ -62,4 +41,22 @@ public class AonprdProcessorAttribute(
/// <param name="type">Тип данных, которые обработчик обрабатывает.</param> /// <param name="type">Тип данных, которые обработчик обрабатывает.</param>
public AonprdProcessorAttribute(string name, string type) public AonprdProcessorAttribute(string name, string type)
: this(name, type, DefaultDependencies) { } : this(name, type, DefaultDependencies) { }
/// <summary>
/// Основной конструктор класса <see cref="AonprdProcessorAttribute"/>.
/// </summary>
/// <param name="name">Название обработчика.</param>
/// <param name="type">Тип данных, которые обработчик обрабатывает.</param>
/// <param name="dependencies">Названия обработчиков, которые должны быть выполнены до запуска этого обработчика.</param>
/// <param name="includeDefaults">Флаг, указывающий, следует ли добавить важные общие зависимости для обработчка.</param>
public AonprdProcessorAttribute(
string name,
string? type,
ICollection<string> dependencies,
bool includeDefaults = true
)
: base($"aonprd:{name}", [.. includeDefaults ? DefaultDependencies : [], .. dependencies])
{
Type = type;
}
} }
@@ -1,5 +1,5 @@
using System.Reflection; using System.Reflection;
using Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder.Processors; using Pf2eDataScrapper.Processors;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder;
@@ -1,5 +1,6 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder.Helpers; using Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder.Helpers;
using Pf2eDataScrapper.Processors;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder;
@@ -3,6 +3,7 @@ using System.Text;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Pf2eDataScrapper.Processors;
using Pf2eModel; using Pf2eModel;
using Pf2eModel.Model; using Pf2eModel.Model;
using Pf2eModel.Model.Base; using Pf2eModel.Model.Base;
@@ -1,49 +0,0 @@
using Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder.Helpers;
using Pf2eModel;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder.Processors;
/// <summary>
/// Абстрактный класс-обработчик данных, полученных из Архивов Нефиса.
/// Класс предназначен для обработки данных с помощью различных обработчиков,
/// для которых указан атрибут <see cref="AonprdProcessorAttribute"/>.
/// </summary>
/// <param name="input">Объект обработки вводимого JSON.</param>
/// <seealso cref="AonprdProcessorAttribute"/>
internal abstract class AbstractProcessor(AonprdProcessorInput input)
{
/// <summary>
/// Вспомогательный класс для работы с базой данных.
/// </summary>
protected readonly DatabaseHelper Helper = DatabaseHelper.Instance;
/// <summary>
/// Вспомогательный класс для работы со строками и форматированием.
/// </summary>
protected readonly StringHelper StringHelper = StringHelper.Instance;
/// <summary>
/// Вспомогательный класс для работы с глобальными настройками.
/// </summary>
protected readonly AonprdBuilderGlobals Globals = AonprdBuilderGlobals.Instance;
/// <summary>
/// Контекст базы данных для работы с данными.
/// </summary>
public readonly Pf2eDbContext Context = DatabaseHelper.DbContext;
/// <summary>
/// Объект обработки вводимого JSON.
/// </summary>
public readonly AonprdProcessorInput Input = input;
/// <summary>
/// Основной метод обработки данных.
/// </summary>
public abstract void Process();
/// <summary>
/// Метод для сохранения изменений в базе данных.
/// </summary>
public abstract void Save();
}
@@ -1,4 +1,5 @@
using Pf2eModel.Model.Base.Targets; using Pf2eDataScrapper.Processors;
using Pf2eModel.Model.Base.Targets;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder.Processors.Common; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder.Processors.Common;
@@ -1,4 +1,5 @@
using Pf2eModel.Model.Common; using Pf2eDataScrapper.Processors;
using Pf2eModel.Model.Common;
using Pf2eModel.Model.Common.I18n; using Pf2eModel.Model.Common.I18n;
using Pf2eModel.Model.Enum; using Pf2eModel.Model.Enum;
@@ -1,4 +1,5 @@
using Pf2eModel.Model.Base.Source; using Pf2eDataScrapper.Processors;
using Pf2eModel.Model.Base.Source;
using Pf2eModel.Model.Enum; using Pf2eModel.Model.Enum;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder.Processors.Common; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder.Processors.Common;
@@ -53,7 +53,7 @@ internal class FeatProcessor(AonprdProcessorInput input) : AbstractEntityProcess
Archetype[] archetypes = Context Archetype[] archetypes = Context
// ReSharper disable once EntityFramework.UnsupportedServerSideFunctionCall // ReSharper disable once EntityFramework.UnsupportedServerSideFunctionCall
.Archetypes.Where(a => Pf2eDbContext.LocaleSearchArray(a.Entity.Name, archetypeStrings)) .Archetypes.Where(a => Pf2eDbContext.LocaleSearchArray(a.Entity.Name, archetypeStrings))
// .Where(a => archetypeStrings.Contains(a.Entity.Name.ToString())) // .Where(a => archetypeStrings.Contains(a.Entity.Group.ToString()))
.ToArray(); .ToArray();
if (archetypes.Length != 0) if (archetypes.Length != 0)
@@ -6,6 +6,7 @@
// Ну а пока просто расставляем родительские и дочерние элементы. // Ну а пока просто расставляем родительские и дочерние элементы.
using System.Data; using System.Data;
using Pf2eDataScrapper.Processors;
using Pf2eModel.Model.Structure; using Pf2eModel.Model.Structure;
using Rule = Pf2eModel.Model.Structure.Rule; using Rule = Pf2eModel.Model.Structure.Rule;
@@ -2,6 +2,7 @@
using System.Web; using System.Web;
using AngleSharp; using AngleSharp;
using AngleSharp.Dom; using AngleSharp.Dom;
using Pf2eDataScrapper.Processors;
using Pf2eModel.Model.Base.Source; using Pf2eModel.Model.Base.Source;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder.Processors.Types.Rules; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder.Processors.Types.Rules;
@@ -83,6 +83,6 @@ internal class CompendiumFolder
/// <returns>Строковое представление данных о папки компендиума.</returns> /// <returns>Строковое представление данных о папки компендиума.</returns>
public override string ToString() public override string ToString()
{ {
return $"CompendiumFolder([Id={Id}][Type={Type}][Name={Name}][Path={Path}][Parent={Parent?.Name ?? "null"}])"; return $"CompendiumFolder([Id={Id}][Type={Type}][Group={Name}][Path={Path}][Parent={Parent?.Name ?? "null"}])";
} }
} }
@@ -2,6 +2,7 @@
using System.Text; using System.Text;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Pf2eDataScrapper.Processors;
using Pf2eModel; using Pf2eModel;
using Pf2eModel.Model; using Pf2eModel.Model;
using Pf2eModel.Model.Base; using Pf2eModel.Model.Base;
@@ -51,7 +52,7 @@ internal abstract class AbstractProcessor(ProcessorInput input)
EntityAlias alias = new() EntityAlias alias = new()
{ {
Type = LocaleType.English, Type = LocaleType.English,
Alias = Entity.Name, Alias = Entity.Group,
EntityId = Entity.Id, EntityId = Entity.Id,
Entity = Entity Entity = Entity
}; };
@@ -147,13 +148,13 @@ internal abstract class AbstractProcessor(ProcessorInput input)
/* /*
foreach (string name in names) foreach (string name in names)
{ {
if (result.Any(t => t.Entity.Name.Contains(name))) if (result.Any(t => t.Entity.Group.Contains(name)))
{ {
continue; continue;
} }
Trait trait = new() Trait trait = new()
{ {
Name = name, Group = name,
Slug = name, Slug = name,
Type = TraitType.Regular, Type = TraitType.Regular,
Description = "", Description = "",
@@ -2,13 +2,13 @@
using Konsole; using Konsole;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.Compendium; using Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.Compendium;
using Pf2eDataScrapper.Processors;
using Pf2eModel;
namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.DataBuilder; namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.DataBuilder;
internal class PF2eFoundryDataBuilder internal class PF2eFoundryDataBuilder
{ {
public Pf2eDatabase Database = Pf2eDatabase.Instance;
/// <summary> /// <summary>
/// Загрузчик данных. /// Загрузчик данных.
/// </summary> /// </summary>
@@ -51,7 +51,10 @@ internal class PF2eFoundryDataBuilder
{ {
Console.WriteLine("Truncating all old data..."); Console.WriteLine("Truncating all old data...");
Database.ExecuteCommand("SELECT f_truncate_tables();"); using Pf2eDbContext context = new();
string cmd = context.Entities.Truncate() + "\n" + context.TruncateAll();
Console.WriteLine(cmd);
Console.WriteLine("Truncation completed!"); Console.WriteLine("Truncation completed!");
} }
@@ -1,4 +1,5 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Pf2eDataScrapper.Processors;
using Pf2eModel.Model.Enum; using Pf2eModel.Model.Enum;
namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.DataBuilder.Processors; namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.DataBuilder.Processors;
@@ -1,4 +1,5 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Pf2eDataScrapper.Processors;
using Pf2eModel.Model.Enum; using Pf2eModel.Model.Enum;
using Pf2eModel.Model.Structure; using Pf2eModel.Model.Structure;
+4 -4
View File
@@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<LangVersion>13.0</LangVersion> <OutputType>WinExe</OutputType>
<OutputType>Exe</OutputType> <TargetFramework>net9.0-windows7.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AngleSharp" Version="1.3.0" /> <PackageReference Include="AngleSharp" Version="1.3.0" />
-152
View File
@@ -1,152 +0,0 @@
using System.Text.RegularExpressions;
using Npgsql;
namespace Pf2eDataScrapper;
/// <summary>
/// Main class for database connection and operations.
/// </summary>
public partial class Pf2eDatabase
{
/// <summary>
/// Подключение к базе данных.
/// </summary>
public readonly NpgsqlConnection Connection;
/// <summary>
/// Имя пользователя для подключения к базе данных.
/// </summary>
public readonly string Username;
/// <summary>
/// Название базы данных.
/// </summary>
public readonly string DatabaseName;
private Pf2eDatabase()
{
Username = UsernamePattern().Match(EnvSettings.DatabaseConnection).Groups[1].Value;
DatabaseName = DatabaseNamePattern().Match(EnvSettings.DatabaseConnection).Groups[1].Value;
Connection = new(EnvSettings.DatabaseConnection);
Connection.Open();
}
~Pf2eDatabase()
{
Connection.Close();
}
/// <summary>
/// Singleton instance of the database connection manager.
/// </summary>
public static Pf2eDatabase Instance { get; } = new();
/// <summary>
/// Current transaction associated with this database connection.
/// </summary>
public NpgsqlTransaction? Transaction { get; private set; }
/// <summary>
/// Begin a new transaction on the database connection.
/// </summary>
public void StartTransaction()
{
if (Transaction != null)
{
throw new InvalidOperationException("Transaction already started.");
}
Transaction = Connection.BeginTransaction();
}
/// <summary>
/// Commit the current transaction to the database.
/// </summary>
public void Commit()
{
if (Transaction == null)
{
throw new InvalidOperationException("No transaction started.");
}
Transaction.Commit();
}
/// <summary>
/// Execute a SQL command against the database.
/// </summary>
/// <param name="sql">The SQL command to execute</param>
public void ExecuteCommand(string sql)
{
NpgsqlCommand command = GetCommand(sql);
command.ExecuteNonQuery();
}
/// <summary>
/// Creates a new SQL command for the database connection.
/// </summary>
/// <param name="sql">The SQL command string</param>
/// <returns>A new NpgsqlCommand object</returns>
public NpgsqlCommand GetCommand(string sql)
{
return new(sql, Connection);
}
/*
public void RebuildDatabase()
{
// Закрываем текущее соединение
Connection.Close();
// Получение имён
var databaseNamePattern = DatabaseNamePattern();
var userConnection = databaseNamePattern.Replace(EnvSettings.DatabaseConnection, ";");
var databaseName = databaseNamePattern.Match(EnvSettings.DatabaseConnection).Groups[1].Value;
var username = UserNamePattern().Match(EnvSettings.DatabaseConnection).Groups[1].Value;
// Временное подключение к базе данных
NpgsqlConnection tmpConnection = new(userConnection);
tmpConnection.Open();
// Удаляем базу данных
NpgsqlCommand dropCommand = new($"DROP DATABASE \"{databaseName}\";", tmpConnection);
dropCommand.ExecuteNonQuery();
// Создаём новую базу данных
NpgsqlCommand createCommand = new($@"
CREATE DATABASE {databaseName}
WITH OWNER = {username}
ENCODING = 'UTF8'
CONNECTION LIMIT = -1;
", tmpConnection);
createCommand.ExecuteNonQuery();
string[] permissionCommands = [
$"GRANT TEMPORARY, CONNECT ON DATABASE {databaseName} TO PUBLIC;",
$"GRANT ALL ON DATABASE {databaseName} TO {username} WITH GRANT OPTION;",
$"GRANT ALL ON DATABASE {databaseName} TO postgres;"
];
foreach (var command in permissionCommands)
{
NpgsqlCommand permissionCommand = new(command, tmpConnection);
permissionCommand.ExecuteNonQuery();
}
// Закрываем временное соединение
tmpConnection.Close();
// Заново создаём соединение
Connection = new NpgsqlConnection(EnvSettings.DatabaseConnection);
}
*/
[GeneratedRegex(@"(?:^|;)Database=(.*?)(?:$|;)")]
private static partial Regex DatabaseNamePattern();
[GeneratedRegex(@"(?:^|;)User=(.*?)(?:$|;)")]
private static partial Regex UsernamePattern();
}
@@ -0,0 +1,44 @@
using Microsoft.Extensions.DependencyInjection;
using Pf2eDataScrapper.App;
using Pf2eModel;
namespace Pf2eDataScrapper.Processors;
/// <summary>
/// Абстрактный класс-обработчик данных, полученных из Архивов Нефиса.
/// Класс предназначен для обработки данных с помощью различных обработчиков,
/// для которых указан атрибут <see cref="ProcessAttribute"/>.
/// </summary>
/// <param name="serviceProvider">Провайдер сервисов.</param>
/// <param name="input">Провайдер сервисов.</param>
/// <seealso cref="ProcessAttribute"/>
internal abstract class AbstractInputProcessor(
IServiceProvider serviceProvider,
ProcessorInput input
) : IProcessor
{
/// <summary>
/// Провайдер сервисов.
/// </summary>
public IServiceProvider ServiceProvider { get; } = serviceProvider;
/// <summary>
/// Контекст базы данных.
/// </summary>
public Pf2eDbContext Context { get; } = serviceProvider.GetService<Pf2eDbContext>()!;
/// <summary>
/// Ввод.
/// </summary>
public ProcessorInput Input { get; } = input;
/// <summary>
/// Основной метод обработки данных.
/// </summary>
public abstract void Process();
/// <summary>
/// Метод для сохранения изменений в базе данных.
/// </summary>
public abstract void Save();
}
@@ -0,0 +1,35 @@
using Microsoft.Extensions.DependencyInjection;
using Pf2eDataScrapper.App;
using Pf2eModel;
namespace Pf2eDataScrapper.Processors;
/// <summary>
/// Абстрактный класс-обработчик данных, полученных из Архивов Нефиса.
/// Класс предназначен для обработки данных с помощью различных обработчиков,
/// для которых указан атрибут <see cref="ProcessAttribute"/>.
/// </summary>
/// <param name="serviceProvider">Провайдер сервисов.</param>
/// <seealso cref="ProcessAttribute"/>
internal abstract class AbstractProcessor(IServiceProvider serviceProvider) : IProcessor
{
/// <summary>
/// Провайдер сервисов.
/// </summary>
public IServiceProvider ServiceProvider { get; } = serviceProvider;
/// <summary>
/// Контекст базы данных.
/// </summary>
public Pf2eDbContext Context { get; } = serviceProvider.GetService<Pf2eDbContext>()!;
/// <summary>
/// Основной метод обработки данных.
/// </summary>
public abstract void Process();
/// <summary>
/// Метод для сохранения изменений в базе данных.
/// </summary>
public abstract void Save();
}
+21
View File
@@ -0,0 +1,21 @@
using Microsoft.Extensions.DependencyInjection;
namespace Pf2eDataScrapper.Processors;
public interface IProcessor
{
/// <summary>
/// Провайдер сервисов.
/// </summary>
IServiceProvider ServiceProvider { get; }
/// <summary>
/// Основной метод обработки данных.
/// </summary>
void Process();
/// <summary>
/// Метод для сохранения изменений в базе данных.
/// </summary>
void Save();
}
@@ -2,7 +2,7 @@
using NpgsqlTypes; using NpgsqlTypes;
using Pf2eModel.Model.Common.I18n; using Pf2eModel.Model.Common.I18n;
namespace Pf2eDataScrapper.Importers; namespace Pf2eDataScrapper.Processors;
/// <summary> /// <summary>
/// Класс для работы с JSON-объектом. /// Класс для работы с JSON-объектом.
+19 -14
View File
@@ -1,18 +1,23 @@
using Pf2eDataScrapper; namespace Pf2eDataScrapper;
using Pf2eDataScrapper.Importers.Implementations.Aonprd;
// Console.SetWindowSize(Console.LargestWindowWidth - 20, Console.LargestWindowHeight - 10); internal static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
// To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize();
EnvSettings.Load(); Application.SetHighDpiMode(HighDpiMode.SystemAware);
/* #pragma warning disable WFO5001
Pf2EFoundryImporter importer = new(); Application.SetColorMode(SystemColorMode.System);
*/ #pragma warning restore WFO5001
AonprdImporter importer = new(); Application.Run(new ImportApplication());
}
// importer.ProcessorNames = ["rules", "rules-order"]; }
importer.Prepare();
importer.Import();
// importer.BuildSchemas();
+97
View File
@@ -0,0 +1,97 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
namespace Pf2eModel;
/// <summary>
/// Класс, обеспецивающий общий способ для отчистки данных таблиц.
///
/// Источник: https://www.codeproject.com/Articles/5339402/Delete-All-Rows-in-Entity-Framework-Core-6
/// </summary>
public static class EfCleanHelper
{
/// <summary>
/// Определение полного названия таблицы по служебному классу.
/// </summary>
/// <param name="entityType">Служебный класс, определяющий таблицу базы данных.</param>
/// <param name="defaultSchemaName">Схема таблицы по умолчанию (<c>"public"</c> для PostgresSQL)</param>
/// <returns></returns>
private static string GetName(IEntityType entityType, string defaultSchemaName = "public")
{
object? schema = entityType.FindAnnotation("Relational:Schema")?.Value;
string tableName =
entityType.GetAnnotation("Relational:TableName")?.Value?.ToString()
?? throw new($"Не могу определить название таблицы для типа {entityType}");
string schemaName = schema?.ToString() ?? defaultSchemaName;
string name = $"\"{schemaName}\".\"{tableName}\"";
return name;
}
/// <summary>
/// Определение полного названия таблицы по типу сущности.
/// </summary>
/// <typeparam name="T">Тип искомой сущности.</typeparam>
/// <param name="dbContext">Контекст, в котором производится поиск таблицы.</param>
/// <returns>Строковое представление названия таблицы.</returns>
public static string TableName<T>(DbContext dbContext)
where T : class
{
IEntityType entityType =
dbContext.Model.FindEntityType(typeof(T))
?? throw new($"Не могу определить название таблицы для класса {typeof(T)}");
return GetName(entityType);
}
/// <summary>
/// Определение полного названия таблицы по типу сущности.
/// </summary>
/// <typeparam name="T">Тип искомой сущности.</typeparam>
/// <param name="dbSet">CLR-представление таблицы, название которой требуется определить.</param>
/// <returns>Строковое представление названия таблицы.</returns>
public static string TableName<T>(DbSet<T> dbSet)
where T : class
{
IEntityType entityType = dbSet.EntityType;
return GetName(entityType);
}
/// <summary>
/// Отчистка содержимого таблицы.
/// </summary>
/// <typeparam name="T">Тип сущности, таблицу которой необходимо отчистить.</typeparam>
/// <param name="dbSet">CLR-представление таблицы, которую требуется отчистить.</param>
/// <returns>Строковое представление выполненной SQL-команды.</returns>
public static string Truncate<T>(this DbSet<T> dbSet)
where T : class
{
string cmd = $"TRUNCATE TABLE {TableName(dbSet)} CASCADE;";
DbContext context = dbSet.GetService<ICurrentDbContext>().Context;
context.Database.ExecuteSqlRaw(cmd);
return cmd;
}
/// <summary>
/// Получение SQL-команды отчистки содержимого всех таблиц базы данных.
/// </summary>
/// <param name="context">Контекст базы данных, в которой необходимо отчистить все строки.</param>
/// <returns>Строковое представление выполненных SQL-команды.</returns>
public static string TruncateAll(this DbContext context)
{
IEnumerable<IEntityType> tables = context.Model.GetEntityTypes();
List<string> cmdList = [];
foreach (IEntityType entityType in tables)
{
string cmd = $"TRUNCATE TABLE {GetName(entityType)} CASCADE;";
context.Database.ExecuteSqlRaw(cmd);
cmdList.Add(cmd);
}
return string.Join('\n', cmdList);
}
}
@@ -12,25 +12,6 @@ public partial class Function : Migration
{ {
migrationBuilder.Sql( migrationBuilder.Sql(
""" """
CREATE OR REPLACE FUNCTION f_truncate_tables()
RETURNS void
LANGUAGE plpgsql AS
$func$
BEGIN
EXECUTE
(SELECT 'TRUNCATE TABLE '
|| string_agg(format('%I.%I', schemaname, tablename), ', ')
|| ' CASCADE;'
FROM pg_tables
WHERE schemaname = 'public'
AND tablename != '__EFMigrationsHistory'
);
END
$func$;
"""
);
migrationBuilder.Sql(
"""
CREATE OR REPLACE FUNCTION f_drop_tables() CREATE OR REPLACE FUNCTION f_drop_tables()
RETURNS void RETURNS void
LANGUAGE plpgsql AS LANGUAGE plpgsql AS
+6 -1
View File
@@ -52,7 +52,12 @@ public partial class Pf2eDbContext
private void BindEntity<T>(EntityTypeBuilder<T> entity) private void BindEntity<T>(EntityTypeBuilder<T> entity)
where T : class, IEntity where T : class, IEntity
{ {
entity.HasOne(e => e.Entity).WithOne().HasForeignKey<T>(e => e.Id).IsRequired(); entity
.HasOne(e => e.Entity)
.WithOne()
.HasForeignKey<T>(e => e.Id)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
} }
/// <summary> /// <summary>
-3
View File
@@ -26,7 +26,4 @@
<LastGenOutput>Settings.Designer.cs</LastGenOutput> <LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Resources\" />
</ItemGroup>
</Project> </Project>