Refactor Naming Templates

This commit is contained in:
Michael Bucari-Tovo 2023-02-04 12:49:48 -07:00
parent 6f490b4491
commit a672174a9b
7 changed files with 215 additions and 132 deletions

View File

@ -61,6 +61,6 @@ public class ConditionalTagClass<TClass> : TagCollection
return false; return false;
} }
protected override Expression GetTagExpression(string exactName, string formatter) => formatter == "!" ? Expression.Not(ExpressionValue) : ExpressionValue; protected override Expression GetTagExpression(string exactName, string formatter) => formatter == "!" ? Expression.Not(ValueExpression) : ValueExpression;
} }
} }

View File

@ -27,7 +27,7 @@ public class NamingTemplate
/// <summary> /// <summary>
/// Invoke the <see cref="NamingTemplate"/> to /// Invoke the <see cref="NamingTemplate"/> to
/// </summary> /// </summary>
/// <param name="propertyClasses">Instances of the TClass used in <see cref="PropertyTagClass{TClass}"/> and <see cref="ConditionalTagClass{TClass}"/></param> /// <param name="propertyClasses">Instances of the TClass used in <see cref="PropertyTagCollection{TClass}"/> and <see cref="ConditionalTagClass{TClass}"/></param>
/// <returns></returns> /// <returns></returns>
public TemplatePart Evaluate(params object[] propertyClasses) public TemplatePart Evaluate(params object[] propertyClasses)
{ {

View File

@ -1,88 +0,0 @@
using System;
using System.Linq.Expressions;
using System.Reflection;
using System.Text.RegularExpressions;
namespace FileManager.NamingTemplate;
public delegate string PropertyFormatter<T>(ITemplateTag templateTag, T value, string formatString);
public class PropertyTagClass<TClass> : TagCollection
{
public PropertyTagClass(bool caseSensative = true) : base(typeof(TClass), caseSensative) { }
/// <summary>
/// Register a nullable value type property.
/// </summary>
/// <typeparam name="U">Type of the property from <see cref="TClass"/></typeparam>
/// <param name="propertyGetter">A Func to get the property value from <see cref="TClass"/></param>
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="U"/> property and a formatting string and returnes the value formatted to string</param>
public void Add<U>(ITemplateTag templateTag, Func<TClass, U?> propertyGetter, PropertyFormatter<U> formatter = null)
where U : struct
=> RegisterProperty(templateTag, propertyGetter, formatter);
/// <summary>
/// Register a non-nullable value type property
/// </summary>
/// <typeparam name="U">Type of the property from <see cref="TClass"/></typeparam>
/// <param name="propertyGetter">A Func to get the property value from <see cref="TClass"/></param>
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="U"/> property and a formatting string and returnes the value formatted to string</param>
public void Add<U>(ITemplateTag templateTag, Func<TClass, U> propertyGetter, PropertyFormatter<U> formatter = null)
where U : struct
=> RegisterProperty(templateTag, propertyGetter, formatter);
/// <summary>
/// Register a string type property.
/// </summary>
/// <param name="propertyGetter">A Func to get the string property from <see cref="TClass"/></param>
/// <param name="formatter">Optional formatting function that accepts the string property and a formatting string and returnes the value formatted to string</param>
public void Add(ITemplateTag templateTag, Func<TClass, string> propertyGetter, PropertyFormatter<string> formatter = null)
=> RegisterProperty(templateTag, propertyGetter, formatter);
private void RegisterProperty(ITemplateTag templateTag, Delegate propertyGetter, Delegate formatter)
{
if (formatter?.Target is not null)
throw new ArgumentException($"{nameof(formatter)} must be a static method");
var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter);
AddPropertyTag(new PropertyTag(templateTag, Options, expr, formatter?.Method));
}
private class PropertyTag : TagBase
{
private readonly Func<Expression, Type, string, Expression> createToStringExpression;
public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyExpression, MethodInfo formatter)
: base(templateTag, propertyExpression)
{
var regexStr = formatter is null ? @$"^<{TemplateTag.TagName}>" : @$"^<{TemplateTag.TagName.Replace(" ", "\\s*?")}\s*?(?:\[([^\[\]]*?)\]\s*?)?>";
NameMatcher = new Regex(regexStr, options);
//Create the ToString() expression for the TagBase.ExpressionValue's type.
//If a formatter delegate was registered for this property, use that.
//Otherwise use the object.Tostring() method.
createToStringExpression
= formatter is null
? (expValue, retTyp, format) => Expression.Call(expValue, retTyp.GetMethod(nameof(object.ToString), Array.Empty<Type>()))
: (expValue, retTyp, format) => Expression.Call(null, formatter, Expression.Constant(templateTag), expValue, Expression.Constant(format));
}
protected override Expression GetTagExpression(string exactName, string formatString)
{
var underlyingType = Nullable.GetUnderlyingType(ReturnType);
Expression toStringExpression
= ReturnType == typeof(string)
? createToStringExpression(Expression.Coalesce(ExpressionValue, Expression.Constant("")), ReturnType, formatString)
: underlyingType is null
? createToStringExpression(ExpressionValue, ReturnType, formatString)
: Expression.Condition(
Expression.PropertyOrField(ExpressionValue, "HasValue"),
createToStringExpression(Expression.PropertyOrField(ExpressionValue, "Value"), underlyingType, formatString),
Expression.Constant(""));
return Expression.TryCatch(toStringExpression, Expression.Catch(typeof(Exception), Expression.Constant(exactName)));
}
}
}

View File

@ -0,0 +1,168 @@
using Dinah.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
namespace FileManager.NamingTemplate;
public delegate string PropertyFormatter<T>(ITemplateTag templateTag, T value, string formatString);
public class PropertyTagCollection<TClass> : TagCollection
{
private readonly Dictionary<Type, Delegate> defaultFormatters = new();
public PropertyTagCollection(bool caseSensative = true, params Delegate[] defaultFormatters) : base(typeof(TClass), caseSensative)
{
foreach (var formatter in defaultFormatters)
{
var parameters = formatter.Method.GetParameters();
if (formatter.Method.ReturnType != typeof(string)
|| parameters.Length != 3
|| parameters[0].ParameterType != typeof(ITemplateTag)
|| parameters[2].ParameterType != typeof(string))
throw new ArgumentException($"{nameof(defaultFormatters)} must have a signature of [{nameof(String)} PropertyFormatter<T>({nameof(ITemplateTag)}, T, {nameof(String)})]");
this.defaultFormatters[parameters[1].ParameterType] = formatter;
}
}
/// <summary>
/// Register a nullable value type property.
/// </summary>
/// <typeparam name="U">Type of the property from <see cref="TClass"/></typeparam>
/// <param name="propertyGetter">A Func to get the property value from <see cref="TClass"/></param>
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="U"/> property
/// and a formatting string and returnes the value the formatted string. If <see cref="null"/>, use the default
/// <typeparamref name="U"/> formatter if present, or <see cref="object.ToString"/></param>
public void Add<U>(ITemplateTag templateTag, Func<TClass, U?> propertyGetter, PropertyFormatter<U> formatter = null)
where U : struct
=> RegisterWithFormatter(templateTag, propertyGetter, formatter);
/// <summary>
/// Register a nullable value type property.
/// </summary>
/// <param name="propertyGetter">A Func to get the string property from <see cref="TClass"/></param>
/// <param name="toString">ToString function that accepts the <typeparamref name="U"/> property and returnes a string</param>
public void Add<U>(ITemplateTag templateTag, Func<TClass, U?> propertyGetter, Func<U, string> toString)
where U : struct
=> RegisterWithToString(templateTag, propertyGetter, toString);
/// <summary>
/// Register a non-nullable value type property
/// </summary>
/// <typeparam name="U">Type of the property from <see cref="TClass"/></typeparam>
/// <param name="propertyGetter">A Func to get the property value from <see cref="TClass"/></param>
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="U"/> property
/// and a formatting string and returnes the value formatted to string. If <see cref="null"/>, use the default
/// <typeparamref name="U"/> formatter if present, or <see cref="object.ToString"/></param>
public void Add<U>(ITemplateTag templateTag, Func<TClass, U> propertyGetter, PropertyFormatter<U> formatter = null)
where U : struct
=> RegisterWithFormatter(templateTag, propertyGetter, formatter);
/// <summary>
/// Register a non-nullable value type property.
/// </summary>
/// <param name="propertyGetter">A Func to get the string property from <see cref="TClass"/></param>
/// <param name="toString">ToString function that accepts the <typeparamref name="U"/> property and returnes a string</param>
public void Add<U>(ITemplateTag templateTag, Func<TClass, U> propertyGetter, Func<U, string> toString)
where U : struct
=> RegisterWithToString(templateTag, propertyGetter, toString);
/// <summary>
/// Register a string type property
/// </summary>
/// <param name="propertyGetter">A Func to get the string property from <see cref="TClass"/></param>
/// <param name="formatter">Optional formatting function that accepts the string property and a formatting
/// string and returnes the value formatted to string. If <see cref="null"/>, use the default <see cref="string"/>
/// formatter if present, or <see cref="string.ToString"/></param>
public void Add(ITemplateTag templateTag, Func<TClass, string> propertyGetter, PropertyFormatter<string> formatter = null)
=> RegisterWithFormatter(templateTag, propertyGetter, formatter);
/// <summary>
/// Register a string type property.
/// </summary>
/// <param name="propertyGetter">A Func to get the string property from <see cref="TClass"/></param>
/// <param name="toString">ToString function that accepts the string property and returnes a string</param>
public void Add(ITemplateTag templateTag, Func<TClass, string> propertyGetter, Func<string, string> toString)
=> RegisterWithToString(templateTag, propertyGetter, toString);
private void RegisterWithFormatter<T,U>(ITemplateTag templateTag, Func<TClass, T> propertyGetter, PropertyFormatter<U> formatter)
{
static string ToStringFunc(U value) => value is string str ? str : value.ToString();
ArgumentValidator.EnsureNotNull(templateTag, nameof(templateTag));
ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter));
var formatDelegate = formatter ?? defaultFormatters.FirstOrDefault(kvp => kvp.Key == typeof(U)).Value;
var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter);
if (formatDelegate is null)
AddPropertyTag(PropertyTag.CreateWithToString(templateTag, Options, expr, ToStringFunc));
else
AddPropertyTag(PropertyTag.CreateWithFormatter(templateTag, Options, expr, formatDelegate));
}
private void RegisterWithToString<T,U>(ITemplateTag templateTag, Func<TClass, T> propertyGetter, Func<U, string> toString)
{
static string ToStringFunc(U value) => value is string str ? str : value.ToString();
ArgumentValidator.EnsureNotNull(templateTag, nameof(templateTag));
ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter));
toString ??= ToStringFunc;
var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter);
AddPropertyTag(PropertyTag.CreateWithToString(templateTag, Options, expr, toString));
}
private class PropertyTag : TagBase
{
private Func<Expression, string, Expression> CreateToStringExpression { get; init; }
private PropertyTag(ITemplateTag templateTag, Expression propertyExpression) : base(templateTag, propertyExpression) { }
public static PropertyTag CreateWithFormatter(ITemplateTag templateTag, RegexOptions options, Expression propertyExpression, Delegate formatter)
{
return new PropertyTag(templateTag, propertyExpression)
{
NameMatcher = new Regex(@$"^<{templateTag.TagName.Replace(" ", "\\s*?")}\s*?(?:\[([^\[\]]*?)\]\s*?)?>", options),
CreateToStringExpression = (expVal, format) =>
Expression.Call(
formatter.Target is null ? null : Expression.Constant(formatter.Target),
formatter.Method,
Expression.Constant(templateTag),
expVal,
Expression.Constant(format))
};
}
public static PropertyTag CreateWithToString(ITemplateTag templateTag, RegexOptions options, Expression propertyExpression, Delegate toString)
{
return new PropertyTag(templateTag, propertyExpression)
{
NameMatcher = new Regex(@$"^<{templateTag.TagName}>", options),
CreateToStringExpression = (expVal, _) =>
Expression.Call(
toString.Target is null ? null : Expression.Constant(toString.Target),
toString.Method,
expVal)
};
}
protected override Expression GetTagExpression(string exactName, string formatString)
{
Expression toStringExpression
= ReturnType == typeof(string)
? CreateToStringExpression(Expression.Coalesce(ValueExpression, Expression.Constant("")), formatString)
: Nullable.GetUnderlyingType(ReturnType) is null
? CreateToStringExpression(ValueExpression, formatString)
: Expression.Condition(
Expression.PropertyOrField(ValueExpression, "HasValue"),
CreateToStringExpression(Expression.PropertyOrField(ValueExpression, "Value"), formatString),
Expression.Constant(""));
return Expression.TryCatch(toStringExpression, Expression.Catch(typeof(Exception), Expression.Constant(exactName)));
}
}
}

View File

@ -29,13 +29,13 @@ internal abstract class TagBase : IPropertyTag
{ {
public ITemplateTag TemplateTag { get; } public ITemplateTag TemplateTag { get; }
public Regex NameMatcher { get; protected init; } public Regex NameMatcher { get; protected init; }
public Type ReturnType => ExpressionValue.Type; public Type ReturnType => ValueExpression.Type;
protected Expression ExpressionValue { get; } protected Expression ValueExpression { get; }
protected TagBase(ITemplateTag templateTag, Expression propertyExpression) protected TagBase(ITemplateTag templateTag, Expression propertyExpression)
{ {
TemplateTag = templateTag; TemplateTag = templateTag;
ExpressionValue = propertyExpression; ValueExpression = propertyExpression;
} }
/// <summary>Create an <see cref="Expression"/> that returns the property's value.</summary> /// <summary>Create an <see cref="Expression"/> that returns the property's value.</summary>

View File

@ -12,7 +12,7 @@ namespace LibationFileManager
public interface ITemplate public interface ITemplate
{ {
static abstract string DefaultTemplate { get; } static abstract string DefaultTemplate { get; }
static abstract IEnumerable<TagClass> TagClass { get; } static abstract IEnumerable<TagCollection> TagClass { get; }
} }
public abstract class Templates public abstract class Templates
@ -191,45 +191,48 @@ namespace LibationFileManager
#region Registered Template Properties #region Registered Template Properties
private static readonly PropertyTagClass<LibraryBookDto> filePropertyTags = new() private static readonly PropertyTagCollection<LibraryBookDto> filePropertyTags =
new(caseSensative: true, StringFormatter, DateTimeFormatter, IntegerFormatter)
{ {
{ TemplateTags.Id, lb => lb.AudibleProductId }, //Don't allow formatting of Id
{ TemplateTags.Title, lb => lb.Title, StringFormatter }, { TemplateTags.Id, lb => lb.AudibleProductId, v => v },
{ TemplateTags.TitleShort, lb => lb.Title.IndexOf(':') < 1 ? lb.Title : lb.Title.Substring(0, lb.Title.IndexOf(':')), StringFormatter }, { TemplateTags.Title, lb => lb.Title },
{ TemplateTags.Author, lb => lb.AuthorNames, StringFormatter }, { TemplateTags.TitleShort, lb => lb.Title.IndexOf(':') < 1 ? lb.Title : lb.Title.Substring(0, lb.Title.IndexOf(':')) },
{ TemplateTags.FirstAuthor, lb => lb.FirstAuthor, StringFormatter }, { TemplateTags.Author, lb => lb.AuthorNames },
{ TemplateTags.Narrator, lb => lb.NarratorNames, StringFormatter }, { TemplateTags.FirstAuthor, lb => lb.FirstAuthor },
{ TemplateTags.FirstNarrator, lb => lb.FirstNarrator, StringFormatter }, { TemplateTags.Narrator, lb => lb.NarratorNames },
{ TemplateTags.Series, lb => lb.SeriesName, StringFormatter }, { TemplateTags.FirstNarrator, lb => lb.FirstNarrator },
{ TemplateTags.SeriesNumber, lb => lb.SeriesNumber, IntegerFormatter }, { TemplateTags.Series, lb => lb.SeriesName },
{ TemplateTags.Language, lb => lb.Language, StringFormatter }, { TemplateTags.SeriesNumber, lb => lb.SeriesNumber },
{ TemplateTags.LanguageShort, lb => getLanguageShort(lb.Language), StringFormatter }, { TemplateTags.Language, lb => lb.Language },
{ TemplateTags.Bitrate, lb => lb.BitRate, IntegerFormatter }, //Don't allow formatting of LanguageShort
{ TemplateTags.SampleRate, lb => lb.SampleRate, IntegerFormatter }, { TemplateTags.LanguageShort, lb =>lb.Language, getLanguageShort },
{ TemplateTags.Channels, lb => lb.Channels, IntegerFormatter }, { TemplateTags.Bitrate, lb => lb.BitRate },
{ TemplateTags.Account, lb => lb.Account, StringFormatter }, { TemplateTags.SampleRate, lb => lb.SampleRate },
{ TemplateTags.Locale, lb => lb.Locale, StringFormatter }, { TemplateTags.Channels, lb => lb.Channels },
{ TemplateTags.YearPublished, lb => lb.YearPublished, IntegerFormatter }, { TemplateTags.Account, lb => lb.Account },
{ TemplateTags.DatePublished, lb => lb.DatePublished, DateTimeFormatter }, { TemplateTags.Locale, lb => lb.Locale },
{ TemplateTags.DateAdded, lb => lb.DateAdded, DateTimeFormatter }, { TemplateTags.YearPublished, lb => lb.YearPublished },
{ TemplateTags.FileDate, lb => lb.FileDate, DateTimeFormatter }, { TemplateTags.DatePublished, lb => lb.DatePublished },
{ TemplateTags.DateAdded, lb => lb.DateAdded },
{ TemplateTags.FileDate, lb => lb.FileDate },
}; };
private static readonly List<TagClass> chapterPropertyTags = new() private static readonly List<TagCollection> chapterPropertyTags = new()
{ {
new PropertyTagClass<LibraryBookDto>() new PropertyTagCollection<LibraryBookDto>(caseSensative: true, StringFormatter)
{ {
{ TemplateTags.Title, lb => lb.Title, StringFormatter }, { TemplateTags.Title, lb => lb.Title },
{ TemplateTags.TitleShort, lb => lb?.Title?.IndexOf(':') > 0 ? lb.Title.Substring(0, lb.Title.IndexOf(':')) : lb.Title, StringFormatter }, { TemplateTags.TitleShort, lb => lb ?.Title ?.IndexOf(':') > 0 ? lb.Title.Substring(0, lb.Title.IndexOf(':')) : lb.Title },
{ TemplateTags.Series, lb => lb.SeriesName, StringFormatter }, { TemplateTags.Series, lb => lb.SeriesName },
}, },
new PropertyTagClass<MultiConvertFileProperties>() new PropertyTagCollection<MultiConvertFileProperties>(caseSensative: true, StringFormatter, IntegerFormatter, DateTimeFormatter)
{ {
{ TemplateTags.ChCount, m => m.PartsTotal, IntegerFormatter }, { TemplateTags.ChCount, m => m.PartsTotal },
{ TemplateTags.ChNumber, m => m.PartsPosition, IntegerFormatter }, { TemplateTags.ChNumber, m => m.PartsPosition },
{ TemplateTags.ChNumber0, m => m.PartsPosition.ToString("D" + ((int)Math.Log10(m.PartsTotal) + 1)) }, { TemplateTags.ChNumber0, m => m.PartsPosition.ToString("D" + ((int)Math.Log10(m.PartsTotal) + 1)) },
{ TemplateTags.ChTitle, m => m.Title, StringFormatter }, { TemplateTags.ChTitle, m => m.Title },
{ TemplateTags.FileDate, m => m.FileDate, DateTimeFormatter } { TemplateTags.FileDate, m => m.FileDate }
} }
}; };
@ -283,7 +286,7 @@ namespace LibationFileManager
public override string Name => "Folder Template"; public override string Name => "Folder Template";
public override string Description => Configuration.GetDescription(nameof(Configuration.FolderTemplate)); public override string Description => Configuration.GetDescription(nameof(Configuration.FolderTemplate));
public static string DefaultTemplate { get; } = "<title short> [<id>]"; public static string DefaultTemplate { get; } = "<title short> [<id>]";
public static IEnumerable<TagClass> TagClass => new TagClass[] { filePropertyTags, conditionalTags }; public static IEnumerable<TagCollection> TagClass => new TagCollection[] { filePropertyTags, conditionalTags };
public override IEnumerable<string> Errors public override IEnumerable<string> Errors
=> TemplateText?.Length >= 2 && Path.IsPathFullyQualified(TemplateText) ? base.Errors.Append(ERROR_FULL_PATH_IS_INVALID) : base.Errors; => TemplateText?.Length >= 2 && Path.IsPathFullyQualified(TemplateText) ? base.Errors.Append(ERROR_FULL_PATH_IS_INVALID) : base.Errors;
@ -302,7 +305,7 @@ namespace LibationFileManager
public override string Name => "File Template"; public override string Name => "File Template";
public override string Description => Configuration.GetDescription(nameof(Configuration.FileTemplate)); public override string Description => Configuration.GetDescription(nameof(Configuration.FileTemplate));
public static string DefaultTemplate { get; } = "<title> [<id>]"; public static string DefaultTemplate { get; } = "<title> [<id>]";
public static IEnumerable<TagClass> TagClass { get; } = new TagClass[] { filePropertyTags, conditionalTags }; public static IEnumerable<TagCollection> TagClass { get; } = new TagCollection[] { filePropertyTags, conditionalTags };
} }
public class ChapterFileTemplate : Templates, ITemplate public class ChapterFileTemplate : Templates, ITemplate
@ -310,7 +313,7 @@ namespace LibationFileManager
public override string Name => "Chapter File Template"; public override string Name => "Chapter File Template";
public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate)); public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate));
public static string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>"; public static string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>";
public static IEnumerable<TagClass> TagClass { get; } = chapterPropertyTags.Append(filePropertyTags).Append(conditionalTags); public static IEnumerable<TagCollection> TagClass { get; } = chapterPropertyTags.Append(filePropertyTags).Append(conditionalTags);
public override IEnumerable<string> Warnings public override IEnumerable<string> Warnings
=> Template.TagsInUse.Any(t => t.TagName.In(TemplateTags.ChNumber.TagName, TemplateTags.ChNumber0.TagName)) => Template.TagsInUse.Any(t => t.TagName.In(TemplateTags.ChNumber.TagName, TemplateTags.ChNumber0.TagName))
@ -323,7 +326,7 @@ namespace LibationFileManager
public override string Name => "Chapter Title Template"; public override string Name => "Chapter Title Template";
public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate)); public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate));
public static string DefaultTemplate => "<ch#> - <title short>: <ch title>"; public static string DefaultTemplate => "<ch#> - <title short>: <ch title>";
public static IEnumerable<TagClass> TagClass { get; } = chapterPropertyTags.Append(conditionalTags); public static IEnumerable<TagCollection> TagClass { get; } = chapterPropertyTags.Append(conditionalTags);
protected override IEnumerable<string> GetTemplatePartsStrings(List<TemplatePart> parts, ReplacementCharacters replacements) protected override IEnumerable<string> GetTemplatePartsStrings(List<TemplatePart> parts, ReplacementCharacters replacements)
=> parts.Select(p => p.Value); => parts.Select(p => p.Value);

View File

@ -41,21 +41,21 @@ namespace NamingTemplateTests
[TestClass] [TestClass]
public class GetPortionFilename public class GetPortionFilename
{ {
PropertyTagClass<PropertyClass1> props1 = new() PropertyTagCollection<PropertyClass1> props1 = new()
{ {
{ new TemplateTag { TagName = "item1" }, i => i.Item1 }, { new TemplateTag { TagName = "item1" }, i => i.Item1 },
{ new TemplateTag { TagName = "item2" }, i => i.Item2 }, { new TemplateTag { TagName = "item2" }, i => i.Item2 },
{ new TemplateTag { TagName = "item3" }, i => i.Item3 } { new TemplateTag { TagName = "item3" }, i => i.Item3 }
}; };
PropertyTagClass<PropertyClass2> props2 = new() PropertyTagCollection<PropertyClass2> props2 = new()
{ {
{ new TemplateTag { TagName = "item1" }, i => i.Item1 }, { new TemplateTag { TagName = "item1" }, i => i.Item1 },
{ new TemplateTag { TagName = "item2" }, i => i.Item2 }, { new TemplateTag { TagName = "item2" }, i => i.Item2 },
{ new TemplateTag { TagName = "item3" }, i => i.Item3 }, { new TemplateTag { TagName = "item3" }, i => i.Item3 },
{ new TemplateTag { TagName = "item4" }, i => i.Item4 }, { new TemplateTag { TagName = "item4" }, i => i.Item4 },
}; };
PropertyTagClass<PropertyClass3> props3 = new() PropertyTagCollection<PropertyClass3> props3 = new()
{ {
{ new TemplateTag { TagName = "item3_1" }, i => i.Item1 }, { new TemplateTag { TagName = "item3_1" }, i => i.Item1 },
{ new TemplateTag { TagName = "item3_2" }, i => i.Item2 }, { new TemplateTag { TagName = "item3_2" }, i => i.Item2 },