2023-02-11 20:22:25 +00:00
|
|
|
|
using System.Linq.Expressions;
|
|
|
|
|
using System.Text.Json;
|
2022-11-14 19:32:41 +00:00
|
|
|
|
using System.Text.Json.Serialization;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
2022-06-12 15:15:30 +00:00
|
|
|
|
|
|
|
|
|
namespace DataModel.Database.Common;
|
|
|
|
|
|
2022-08-17 19:00:37 +00:00
|
|
|
|
public sealed class DataContext : DbContext, IDataContext
|
2022-06-12 15:15:30 +00:00
|
|
|
|
{
|
2022-07-09 13:03:50 +00:00
|
|
|
|
public DbSet<Setting> Settings { get; set; }
|
2022-06-12 15:15:30 +00:00
|
|
|
|
|
2022-07-09 13:03:50 +00:00
|
|
|
|
public DbSet<Section> Sections { get; set; }
|
2022-06-12 19:42:47 +00:00
|
|
|
|
|
2022-07-09 13:03:50 +00:00
|
|
|
|
public DbSet<TextElement> TextElements { get; set; }
|
2022-06-12 19:42:47 +00:00
|
|
|
|
|
2022-07-09 13:03:50 +00:00
|
|
|
|
public DbSet<Translation> Translations { get; set; }
|
2022-06-12 19:42:47 +00:00
|
|
|
|
|
2022-06-12 15:15:30 +00:00
|
|
|
|
public DataContext(DbContextOptions<DataContext> contextOptions) : base(contextOptions)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
|
|
|
{
|
|
|
|
|
base.OnModelCreating(modelBuilder);
|
|
|
|
|
|
2022-06-12 19:42:47 +00:00
|
|
|
|
#region Settings
|
2022-06-12 15:15:30 +00:00
|
|
|
|
|
|
|
|
|
modelBuilder.Entity<Setting>().HasIndex(n => n.Id);
|
2022-07-25 17:03:49 +00:00
|
|
|
|
modelBuilder.Entity<Setting>().HasIndex(n => n.Code).IsUnique();
|
2022-06-12 15:15:30 +00:00
|
|
|
|
modelBuilder.Entity<Setting>().HasIndex(n => n.BoolValue);
|
|
|
|
|
modelBuilder.Entity<Setting>().HasIndex(n => n.GuidValue);
|
|
|
|
|
modelBuilder.Entity<Setting>().HasIndex(n => n.IntegerValue);
|
|
|
|
|
modelBuilder.Entity<Setting>().HasIndex(n => n.TextValue);
|
|
|
|
|
|
|
|
|
|
#endregion
|
2022-06-12 19:42:47 +00:00
|
|
|
|
|
|
|
|
|
#region Sections
|
|
|
|
|
|
|
|
|
|
modelBuilder.Entity<Section>().HasIndex(n => n.Id);
|
|
|
|
|
modelBuilder.Entity<Section>().HasIndex(n => n.Name);
|
2022-07-09 13:03:18 +00:00
|
|
|
|
modelBuilder.Entity<Section>().HasIndex(n => n.Depth);
|
|
|
|
|
modelBuilder.Entity<Section>().HasIndex(n => n.DataKey);
|
2022-07-16 20:28:40 +00:00
|
|
|
|
// modelBuilder.Entity<Section>().Navigation(n => n.Parent).AutoInclude(); // Cycle-reference, does not work, though.
|
2022-07-13 18:04:26 +00:00
|
|
|
|
modelBuilder.Entity<Section>().Navigation(n => n.TextElements).AutoInclude();
|
2022-06-12 19:42:47 +00:00
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region TextElements
|
|
|
|
|
|
|
|
|
|
modelBuilder.Entity<TextElement>().HasIndex(n => n.Id);
|
|
|
|
|
modelBuilder.Entity<TextElement>().HasIndex(n => n.Code);
|
2022-07-10 17:30:22 +00:00
|
|
|
|
modelBuilder.Entity<TextElement>().HasIndex(n => n.Name);
|
2022-09-17 17:23:11 +00:00
|
|
|
|
modelBuilder.Entity<TextElement>().HasIndex(n => n.IsMultiLine);
|
2022-07-13 18:04:26 +00:00
|
|
|
|
modelBuilder.Entity<TextElement>().Navigation(n => n.Section).AutoInclude();
|
2022-09-17 19:46:48 +00:00
|
|
|
|
modelBuilder.Entity<TextElement>().Navigation(n => n.Translations).AutoInclude();
|
2022-06-12 19:42:47 +00:00
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Translations
|
|
|
|
|
|
|
|
|
|
modelBuilder.Entity<Translation>().HasIndex(n => n.Id);
|
|
|
|
|
modelBuilder.Entity<Translation>().HasIndex(n => n.Culture);
|
|
|
|
|
modelBuilder.Entity<Translation>().HasIndex(n => n.Text);
|
2022-09-21 20:52:17 +00:00
|
|
|
|
modelBuilder.Entity<Translation>().HasIndex(n => n.TranslateManual);
|
2022-07-13 18:04:26 +00:00
|
|
|
|
modelBuilder.Entity<Translation>().Navigation(n => n.TextElement).AutoInclude();
|
2022-06-12 19:42:47 +00:00
|
|
|
|
|
|
|
|
|
#endregion
|
2022-06-12 15:15:30 +00:00
|
|
|
|
}
|
2022-11-14 19:32:41 +00:00
|
|
|
|
|
2023-01-02 19:50:11 +00:00
|
|
|
|
#region Export and import
|
2022-11-14 19:32:41 +00:00
|
|
|
|
|
|
|
|
|
private readonly record struct JsonData(
|
2023-01-18 19:00:25 +00:00
|
|
|
|
IList<JsonSetting> Settings,
|
|
|
|
|
IList<JsonSection> Sections,
|
|
|
|
|
IList<JsonTextElement> TextElements,
|
|
|
|
|
IList<JsonTranslation> Translations
|
2022-11-14 19:32:41 +00:00
|
|
|
|
);
|
|
|
|
|
|
2023-01-18 19:00:03 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Represents a unique identifier for a JSON export and import.
|
|
|
|
|
/// </summary>
|
2022-11-14 19:32:41 +00:00
|
|
|
|
internal readonly record struct JsonUniqueId(string Code, Guid UniqueId, string Prefix = "")
|
|
|
|
|
{
|
|
|
|
|
public override string ToString() => string.IsNullOrWhiteSpace(this.Prefix) ? $"{this.Code}::{this.UniqueId}" : $"{this.Prefix}::{this.Code}::{this.UniqueId}";
|
|
|
|
|
|
|
|
|
|
public static implicit operator string(JsonUniqueId id) => id.ToString();
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-18 19:00:03 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// A JSON converter to serialize and deserialize JsonUniqueId instances.
|
|
|
|
|
/// </summary>
|
2022-11-14 19:32:41 +00:00
|
|
|
|
private sealed class JsonUniqueIdConverter : JsonConverter<JsonUniqueId>
|
|
|
|
|
{
|
|
|
|
|
public override JsonUniqueId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
|
|
|
{
|
|
|
|
|
var json = reader.GetString();
|
|
|
|
|
var parts = json?.Split("::");
|
|
|
|
|
return parts?.Length switch
|
|
|
|
|
{
|
|
|
|
|
2 => new JsonUniqueId(parts[0], Guid.Parse(parts[1])),
|
|
|
|
|
3 => new JsonUniqueId(parts[1], Guid.Parse(parts[2]), parts[0]),
|
|
|
|
|
|
|
|
|
|
_ => throw new JsonException($"Invalid format of JsonUniqueId: {json}")
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void Write(Utf8JsonWriter writer, JsonUniqueId value, JsonSerializerOptions options)
|
|
|
|
|
{
|
|
|
|
|
writer.WriteStringValue(value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal readonly record struct JsonSetting(
|
|
|
|
|
JsonUniqueId UniqueId,
|
|
|
|
|
string Code,
|
|
|
|
|
string TextValue,
|
|
|
|
|
int IntegerValue,
|
|
|
|
|
bool BoolValue,
|
|
|
|
|
Guid GuidValue
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
internal readonly record struct JsonSection(
|
|
|
|
|
JsonUniqueId UniqueId,
|
|
|
|
|
string Name,
|
|
|
|
|
string DataKey,
|
|
|
|
|
int Depth,
|
|
|
|
|
JsonUniqueId ParentUniqueId,
|
|
|
|
|
List<JsonUniqueId> TextElements
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
internal readonly record struct JsonTextElement(
|
|
|
|
|
JsonUniqueId UniqueId,
|
|
|
|
|
string Code,
|
|
|
|
|
string Name,
|
|
|
|
|
bool IsMultiLine,
|
|
|
|
|
JsonUniqueId SectionUniqueId,
|
|
|
|
|
List<JsonUniqueId> Translations
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
internal readonly record struct JsonTranslation(
|
|
|
|
|
JsonUniqueId UniqueId,
|
|
|
|
|
string Culture,
|
|
|
|
|
string Text,
|
|
|
|
|
bool TranslateManual,
|
|
|
|
|
JsonUniqueId TextElementUniqueId
|
|
|
|
|
);
|
2023-01-24 19:58:42 +00:00
|
|
|
|
|
2023-01-18 19:00:03 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Exports this database to a JSON file.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="path">The path to the JSON file.</param>
|
2023-01-24 19:58:42 +00:00
|
|
|
|
/// <param name="includeSensitiveData">When false, exclude sensitive data from export.</param>
|
|
|
|
|
public async Task ExportAsync(string path, bool includeSensitiveData = false)
|
2022-11-14 19:32:41 +00:00
|
|
|
|
{
|
2023-02-11 20:22:25 +00:00
|
|
|
|
Console.WriteLine("Exporting database to JSON file...");
|
2022-11-14 19:32:41 +00:00
|
|
|
|
var jsonSettings = new JsonSerializerOptions
|
|
|
|
|
{
|
|
|
|
|
WriteIndented = true,
|
|
|
|
|
Converters = { new JsonUniqueIdConverter() },
|
|
|
|
|
};
|
2023-01-24 19:58:42 +00:00
|
|
|
|
|
|
|
|
|
// Maintained list of sensitive data to be removed from the export:
|
|
|
|
|
var sensitiveSettingCodes = new HashSet<string>
|
|
|
|
|
{
|
|
|
|
|
SettingNames.DEEPL_API_KEY,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// A local filter function to remove sensitive data from the export.
|
|
|
|
|
// Removing just the sensitive values instead of the entire setting.
|
|
|
|
|
IEnumerable<JsonSetting> FilterSensitiveSettings(IEnumerable<JsonSetting> settings)
|
|
|
|
|
{
|
|
|
|
|
foreach (var setting in settings)
|
|
|
|
|
{
|
|
|
|
|
if (sensitiveSettingCodes!.Contains(setting.Code))
|
|
|
|
|
yield return new JsonSetting(setting.UniqueId, setting.Code, string.Empty, 0, false, Guid.Empty);
|
|
|
|
|
else
|
|
|
|
|
yield return setting;
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-11-14 19:32:41 +00:00
|
|
|
|
|
2023-02-11 20:22:25 +00:00
|
|
|
|
// Use a local reference to the database to use it in the lambda expression trees below:
|
|
|
|
|
// (we cannot use "this" in a lambda expression tree; yields an exception at runtime)
|
|
|
|
|
var db = this;
|
|
|
|
|
|
2022-11-14 19:32:41 +00:00
|
|
|
|
await using var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
|
|
|
|
|
await JsonSerializer.SerializeAsync(fileStream,
|
|
|
|
|
new JsonData
|
|
|
|
|
{
|
2023-02-11 20:22:25 +00:00
|
|
|
|
// Settings don't have references to other entities; we can just use them here:
|
2023-01-24 19:58:42 +00:00
|
|
|
|
Settings = includeSensitiveData ?
|
|
|
|
|
// Include all settings, including sensitive data:
|
|
|
|
|
this.Settings.OrderBy(n => n.UniqueId).Select(n => n.ToJsonSetting()).ToList() :
|
|
|
|
|
// Exclude sensitive data:
|
|
|
|
|
FilterSensitiveSettings(this.Settings.OrderBy(n => n.UniqueId).Select(n => n.ToJsonSetting()).AsEnumerable()).ToList(),
|
2023-02-11 20:22:25 +00:00
|
|
|
|
|
|
|
|
|
// Warning: the parents cannot pre-loaded, thus, we must load them now inside the lambda expression tree:
|
|
|
|
|
Sections = this.Sections.OrderBy(n => n.UniqueId).Select(n => n.ToJsonSection(db)).ToList(),
|
|
|
|
|
|
|
|
|
|
// All text elements references are pre-loaded, so we can use them here:
|
2023-01-22 20:58:01 +00:00
|
|
|
|
TextElements = this.TextElements.OrderBy(n => n.UniqueId).Select(n => n.ToJsonTextElement()).ToList(),
|
2023-02-11 20:22:25 +00:00
|
|
|
|
|
|
|
|
|
// All translation references are pre-loaded, so we can use them here:
|
2023-01-22 20:58:01 +00:00
|
|
|
|
Translations = this.Translations.OrderBy(n => n.UniqueId).Select(n => n.ToJsonTranslation()).ToList(),
|
2022-11-14 19:32:41 +00:00
|
|
|
|
}, jsonSettings);
|
2023-02-11 20:22:25 +00:00
|
|
|
|
|
|
|
|
|
Console.WriteLine("Export complete.");
|
2022-11-14 19:32:41 +00:00
|
|
|
|
}
|
2023-01-02 19:50:11 +00:00
|
|
|
|
|
2023-01-22 18:35:57 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Stores data needed to resolve a parent-child relationship.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="ParentId">The parent id we want to resolve.</param>
|
|
|
|
|
/// <param name="Entity">The entity for which we want to resolve the parent.</param>
|
|
|
|
|
/// <typeparam name="T">The type of the entity.</typeparam>
|
|
|
|
|
private readonly record struct TreeResolver<T>(Guid ParentId, T Entity);
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Imports data from a JSON file into an empty database.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="path">The path to the JSON export.</param>
|
|
|
|
|
/// <exception cref="InvalidOperationException">When the database is not empty.</exception>
|
|
|
|
|
public async Task ImportAsync(string path)
|
2023-01-02 19:56:06 +00:00
|
|
|
|
{
|
2023-01-22 18:35:57 +00:00
|
|
|
|
if(await this.Settings.AnyAsync() ||
|
|
|
|
|
await this.Sections.AnyAsync() ||
|
|
|
|
|
await this.TextElements.AnyAsync() ||
|
|
|
|
|
await this.Translations.AnyAsync())
|
|
|
|
|
throw new InvalidOperationException("The database is not empty. In order to import data, the database must be empty.");
|
2023-02-11 20:22:25 +00:00
|
|
|
|
|
|
|
|
|
Console.WriteLine("Start importing data from JSON file...");
|
2023-01-22 18:35:57 +00:00
|
|
|
|
|
|
|
|
|
// Start a transaction:
|
|
|
|
|
await using var transaction = await this.Database.BeginTransactionAsync();
|
|
|
|
|
|
|
|
|
|
// Configure the JSON serializer:
|
|
|
|
|
var jsonSettings = new JsonSerializerOptions
|
|
|
|
|
{
|
|
|
|
|
Converters = { new JsonUniqueIdConverter() },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
|
|
|
|
var jsonData = await JsonSerializer.DeserializeAsync<JsonData>(fileStream, jsonSettings);
|
|
|
|
|
|
|
|
|
|
// --------------------
|
|
|
|
|
// Import the settings:
|
|
|
|
|
// --------------------
|
|
|
|
|
foreach (var setting in jsonData.Settings)
|
|
|
|
|
this.Settings.Add(Setting.FromJsonSetting(setting));
|
|
|
|
|
|
|
|
|
|
// --------------------
|
|
|
|
|
// Import the sections:
|
|
|
|
|
// --------------------
|
|
|
|
|
|
|
|
|
|
// We must store the intermediate data in a list, because we need to resolve
|
|
|
|
|
// the parent-child relationships in a second step.
|
|
|
|
|
var allSections = new Dictionary<Guid, TreeResolver<Section>>();
|
|
|
|
|
var sectionToTextElements = new Dictionary<Guid, List<Guid>>();
|
|
|
|
|
|
|
|
|
|
// Read the data from the JSON file:
|
|
|
|
|
foreach (var section in jsonData.Sections)
|
|
|
|
|
{
|
|
|
|
|
// Convert the next element:
|
|
|
|
|
var nextSection = Section.FromJsonSection(section);
|
2023-02-11 20:22:25 +00:00
|
|
|
|
|
|
|
|
|
// Notice: the parent id is not yet resolved.
|
2023-01-22 18:35:57 +00:00
|
|
|
|
|
|
|
|
|
// Store the element:
|
|
|
|
|
allSections.Add(nextSection.UniqueId, new (section.ParentUniqueId.UniqueId, nextSection));
|
|
|
|
|
sectionToTextElements.Add(nextSection.UniqueId, section.TextElements.Select(n => n.UniqueId).ToList());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now, resolve the parent-child relationships for the sections:
|
|
|
|
|
foreach (var (uniqueId, (parentId, section)) in allSections)
|
2023-02-11 20:22:25 +00:00
|
|
|
|
{
|
|
|
|
|
if(parentId == Guid.Empty)
|
|
|
|
|
{
|
|
|
|
|
if(section.Depth != 0)
|
|
|
|
|
Console.WriteLine(@$"Section {uniqueId} ""{section.Name}"" has no parent.");
|
|
|
|
|
else
|
|
|
|
|
Console.WriteLine(@$"Section {uniqueId} ""{section.Name}"" is a root section, thus, has no parent.");
|
|
|
|
|
|
|
|
|
|
section.Parent = null;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(allSections.TryGetValue(parentId, out var parent))
|
|
|
|
|
section.Parent = parent.Entity;
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine(@$"Parent of section {uniqueId} ""{section.Name}"" was not found.");
|
|
|
|
|
section.Parent = null;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-22 18:35:57 +00:00
|
|
|
|
// -------------------------
|
|
|
|
|
// Import the text elements:
|
|
|
|
|
// -------------------------
|
|
|
|
|
|
|
|
|
|
// We must store the intermediate data in a list, because we need to resolve
|
|
|
|
|
// the parent-child relationships in a second step.
|
|
|
|
|
var allTextElements = new Dictionary<Guid, TextElement>();
|
|
|
|
|
var textElementToTranslations = new Dictionary<Guid, List<Guid>>();
|
|
|
|
|
|
|
|
|
|
// Read the data from the JSON file:
|
|
|
|
|
foreach (var textElement in jsonData.TextElements)
|
|
|
|
|
{
|
|
|
|
|
// Convert the next element:
|
|
|
|
|
var nextTextElement = TextElement.FromJsonTextElement(textElement);
|
|
|
|
|
|
|
|
|
|
// We know that the section is already imported, because we imported the sections first:
|
|
|
|
|
nextTextElement.Section = allSections[textElement.SectionUniqueId.UniqueId].Entity;
|
|
|
|
|
|
|
|
|
|
// Store the element in the list:
|
|
|
|
|
allTextElements.Add(nextTextElement.UniqueId, nextTextElement);
|
|
|
|
|
textElementToTranslations.Add(nextTextElement.UniqueId, textElement.Translations.Select(n => n.UniqueId).ToList());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now, resolve the parent-child relationships for the text elements to the sections:
|
|
|
|
|
foreach (var (sectionUniqueId, textElementsIds) in sectionToTextElements)
|
|
|
|
|
{
|
|
|
|
|
var section = allSections[sectionUniqueId].Entity;
|
|
|
|
|
section.TextElements.AddRange(textElementsIds.Select(n => allTextElements[n]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Free the memory:
|
|
|
|
|
sectionToTextElements.Clear();
|
|
|
|
|
|
|
|
|
|
// ------------------------
|
|
|
|
|
// Import the translations:
|
|
|
|
|
// ------------------------
|
|
|
|
|
|
|
|
|
|
// We must store the intermediate data in a list, because we need to resolve
|
|
|
|
|
// the parent-child relationships in a second step.
|
|
|
|
|
var allTranslations = new Dictionary<Guid, Translation>();
|
|
|
|
|
|
|
|
|
|
// Read the data from the JSON file:
|
|
|
|
|
foreach (var translation in jsonData.Translations)
|
|
|
|
|
{
|
|
|
|
|
// Convert the next element:
|
|
|
|
|
var nextTranslation = Translation.FromJsonTranslation(translation);
|
|
|
|
|
|
|
|
|
|
// We know that the text element is already imported, because we imported the text elements first:
|
|
|
|
|
nextTranslation.TextElement = allTextElements[translation.TextElementUniqueId.UniqueId];
|
|
|
|
|
|
|
|
|
|
// Store the element in the list:
|
|
|
|
|
allTranslations.Add(nextTranslation.UniqueId, nextTranslation);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now, resolve the parent-child relationships for the translations to the text elements:
|
|
|
|
|
foreach (var (textElementUniqueId, translationsIds) in textElementToTranslations)
|
|
|
|
|
{
|
|
|
|
|
var textElement = allTextElements[textElementUniqueId];
|
|
|
|
|
textElement.Translations.AddRange(translationsIds.Select(n => allTranslations[n]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Free the memory:
|
|
|
|
|
textElementToTranslations.Clear();
|
|
|
|
|
|
|
|
|
|
// ---------------------------------
|
|
|
|
|
// Add all the data to the database:
|
|
|
|
|
// ---------------------------------
|
|
|
|
|
this.Sections.AddRange(allSections.Values.Select(n => n.Entity));
|
|
|
|
|
this.TextElements.AddRange(allTextElements.Values);
|
|
|
|
|
this.Translations.AddRange(allTranslations.Values);
|
|
|
|
|
|
|
|
|
|
// Save the changes:
|
|
|
|
|
await this.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
// Commit the transaction:
|
|
|
|
|
await transaction.CommitAsync();
|
2023-02-11 20:22:25 +00:00
|
|
|
|
|
|
|
|
|
Console.WriteLine("Finished importing data from JSON file.");
|
2023-01-02 19:56:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-01-02 19:50:11 +00:00
|
|
|
|
#endregion
|
2023-02-11 20:22:25 +00:00
|
|
|
|
|
|
|
|
|
#region Tools
|
|
|
|
|
|
|
|
|
|
internal Task LoadElementsAsync<TEntry, TProp>(TEntry entry, Expression<Func<TEntry, TProp?>> selector) where TEntry : class where TProp : class => this.Entry<TEntry>(entry).Reference(selector).LoadAsync();
|
|
|
|
|
|
|
|
|
|
#endregion
|
2022-06-12 15:15:30 +00:00
|
|
|
|
}
|