From 18f69bc73de8aeba1a22dab2d79d505fcf2b60c6 Mon Sep 17 00:00:00 2001 From: Mbucari Date: Mon, 6 Feb 2023 15:24:18 -0700 Subject: [PATCH] Refactor Naming Template --- .../NamingTemplate/NamingTemplate.cs | 48 ++++++------- .../PropertyTagCollection[TClass].cs | 70 ++++++++----------- .../Dialogs/EditTemplateDialog.axaml.cs | 6 +- .../LibationFileManager/TemplateEditor[T].cs | 13 +++- Source/LibationFileManager/Templates.cs | 44 ++++++------ .../Dialogs/EditTemplateDialog.cs | 4 +- .../FileNamingTemplateTests.cs | 4 +- 7 files changed, 91 insertions(+), 98 deletions(-) diff --git a/Source/FileManager/NamingTemplate/NamingTemplate.cs b/Source/FileManager/NamingTemplate/NamingTemplate.cs index 8e4a3d0c..acd51bb1 100644 --- a/Source/FileManager/NamingTemplate/NamingTemplate.cs +++ b/Source/FileManager/NamingTemplate/NamingTemplate.cs @@ -9,14 +9,14 @@ public class NamingTemplate { public string TemplateText { get; private set; } public IEnumerable TagsInUse => _tagsInUse; - public IEnumerable TagsRegistered => Classes.SelectMany(t => t).DistinctBy(t => t.TagName); + public IEnumerable TagsRegistered => TagCollections.SelectMany(t => t).DistinctBy(t => t.TagName); public IEnumerable Warnings => errors.Concat(warnings); public IEnumerable Errors => errors; private Delegate templateToString; private readonly List warnings = new(); private readonly List errors = new(); - private readonly IEnumerable Classes; + private readonly IEnumerable TagCollections; private readonly List _tagsInUse = new(); public const string ERROR_NULL_IS_INVALID = "Null template is invalid."; @@ -25,21 +25,18 @@ public class NamingTemplate public const string WARNING_NO_TAGS = "Should use tags. Eg: "; /// <summary> - /// Invoke the <see cref="NamingTemplate"/> to + /// Invoke the <see cref="NamingTemplate"/> /// </summary> /// <param name="propertyClasses">Instances of the TClass used in <see cref="PropertyTagCollection{TClass}"/> and <see cref="ConditionalTagCollection{TClass}"/></param> - /// <returns></returns> public TemplatePart Evaluate(params object[] propertyClasses) { - //Match propertyClasses to the arguments required by templateToString.DynamicInvoke() - var delegateArgTypes = templateToString.GetType().GenericTypeArguments[..^1]; + // Match propertyClasses to the arguments required by templateToString.DynamicInvoke(). + // First parameter is "this", so ignore it. + var delegateArgTypes = templateToString.Method.GetParameters().Skip(1); - object[] args = new object[delegateArgTypes.Length]; - - for (int i = 0; i < delegateArgTypes.Length; i++) - args[i] = propertyClasses.First(o => o.GetType() == delegateArgTypes[i]); - - if (args.Any(a => a is null)) + object[] args = delegateArgTypes.Join(propertyClasses, o => o.ParameterType, i => i.GetType(), (_, i) => i).ToArray(); + + if (args.Length != delegateArgTypes.Count()) throw new ArgumentException($"This instance of {nameof(NamingTemplate)} requires the following arguments: {string.Join(", ", delegateArgTypes.Select(t => t.Name).Distinct())}"); return ((TemplatePart)templateToString.DynamicInvoke(args)).FirstPart; @@ -47,22 +44,17 @@ public class NamingTemplate /// <summary>Parse a template string to a <see cref="NamingTemplate"/></summary> /// <param name="template">The template string to parse</param> - /// <param name="tagClasses">A collection of <see cref="TagCollection"/> with + /// <param name="tagCollections">A collection of <see cref="TagCollection"/> with /// properties registered to match to the <paramref name="template"/></param> - public static NamingTemplate Parse(string template, IEnumerable<TagCollection> tagClasses) + public static NamingTemplate Parse(string template, IEnumerable<TagCollection> tagCollections) { - var namingTemplate = new NamingTemplate(tagClasses); + var namingTemplate = new NamingTemplate(tagCollections); try { BinaryNode intermediate = namingTemplate.IntermediateParse(template); Expression evalTree = GetExpressionTree(intermediate); - List<ParameterExpression> parameters = new(); - - foreach (var tagclass in tagClasses) - parameters.Add(tagclass.Parameter); - - namingTemplate.templateToString = Expression.Lambda(evalTree, parameters).Compile(); + namingTemplate.templateToString = Expression.Lambda(evalTree, tagCollections.Select(tc => tc.Parameter)).Compile(); } catch(Exception ex) { @@ -73,7 +65,7 @@ public class NamingTemplate private NamingTemplate(IEnumerable<TagCollection> properties) { - Classes = properties; + TagCollections = properties; } /// <summary>Builds an <see cref="Expression"/> tree that will evaluate to a <see cref="TemplatePart"/></summary> @@ -84,7 +76,7 @@ public class NamingTemplate else if (node.IsConditional) return Expression.Condition(node.Expression, concatExpression(node), TemplatePart.Blank); else return concatExpression(node); - Expression concatExpression(BinaryNode node) + static Expression concatExpression(BinaryNode node) => TemplatePart.CreateConcatenation(GetExpressionTree(node.LeftChild), GetExpressionTree(node.RightChild)); } @@ -100,8 +92,8 @@ public class NamingTemplate TemplateText = templateString; - BinaryNode currentNode = BinaryNode.CreateRoot(); - BinaryNode topNode = currentNode; + BinaryNode topNode = BinaryNode.CreateRoot(); + BinaryNode currentNode = topNode; List<char> literalChars = new(); while (templateString.Length > 0) @@ -170,7 +162,7 @@ public class NamingTemplate { if (literalChars.Count != 0) { - currentNode = currentNode.AddNewNode(BinaryNode.CreateValue(new string(literalChars.ToArray()))); + currentNode = currentNode.AddNewNode(BinaryNode.CreateValue(string.Concat(literalChars))); literalChars.Clear(); } } @@ -178,7 +170,7 @@ public class NamingTemplate private bool StartsWith(string template, out string exactName, out IPropertyTag propertyTag, out Expression valueExpression) { - foreach (var pc in Classes) + foreach (var pc in TagCollections) { if (pc.StartsWith(template, out exactName, out propertyTag, out valueExpression)) return true; @@ -192,7 +184,7 @@ public class NamingTemplate private bool StartsWithClosing(string template, out string exactName, out IClosingPropertyTag closingPropertyTag) { - foreach (var pc in Classes) + foreach (var pc in TagCollections) { if (pc.StartsWithClosing(template, out exactName, out closingPropertyTag)) return true; diff --git a/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs b/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs index 58683634..118956a7 100644 --- a/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs +++ b/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs @@ -44,6 +44,7 @@ public class PropertyTagCollection<TClass> : TagCollection /// <summary> /// Register a nullable value type <typeparamref name="TClass"/> property. /// </summary> + /// <typeparam name="TProperty">Type of the property from <see cref="TClass"/></typeparam> /// <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="TProperty"/> property and returnes a string</param> public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty?> propertyGetter, Func<TProperty, string> toString) @@ -64,6 +65,7 @@ public class PropertyTagCollection<TClass> : TagCollection /// <summary> /// Register a <typeparamref name="TClass"/> property. /// </summary> + /// <typeparam name="TProperty">Type of the property from <see cref="TClass"/></typeparam> /// <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="TProperty"/> property and returnes a string</param> public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, Func<TProperty, string> toString) @@ -75,17 +77,26 @@ public class PropertyTagCollection<TClass> : TagCollection ArgumentValidator.EnsureNotNull(templateTag, nameof(templateTag)); ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter)); - formatter ??= GetDefaultFormatter<TPropertyValue>(); + var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter); - if (formatter is null) - RegisterWithToString<TProperty, TPropertyValue>(templateTag, propertyGetter, null); + if ((formatter ??= GetDefaultFormatter<TPropertyValue>()) is null) + AddPropertyTag(new PropertyTag<TPropertyValue>(templateTag, Options, expr, ToStringFunc)); else - { - var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter); - AddPropertyTag(PropertyTag.Create(templateTag, Options, expr, formatter)); - } + AddPropertyTag(new PropertyTag<TPropertyValue>(templateTag, Options, expr, formatter)); } + private void RegisterWithToString<TProperty, TPropertyValue> + (ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, Func<TPropertyValue, string> toString) + { + ArgumentValidator.EnsureNotNull(templateTag, nameof(templateTag)); + ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter)); + + var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter); + AddPropertyTag(new PropertyTag<TPropertyValue>(templateTag, Options, expr, toString ?? ToStringFunc)); + } + + private static string ToStringFunc<T>(T propertyValue) => propertyValue?.ToString() ?? ""; + private PropertyFormatter<T> GetDefaultFormatter<T>() { try @@ -93,54 +104,35 @@ public class PropertyTagCollection<TClass> : TagCollection var del = defaultFormatters.FirstOrDefault(kvp => kvp.Key == typeof(T)).Value; return del is null ? null : Delegate.CreateDelegate(typeof(PropertyFormatter<T>), del.Target, del.Method) as PropertyFormatter<T>; } - catch - { - return null; - } + catch { return null; } } - private void RegisterWithToString<TProperty, TPropertyValue> - (ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, Func<TPropertyValue, string> toString) + private class PropertyTag<TPropertyValue> : TagBase { - static string ToStringFunc(TPropertyValue value) => value?.ToString() ?? ""; - ArgumentValidator.EnsureNotNull(templateTag, nameof(templateTag)); - ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter)); + private Func<Expression, string, Expression> CreateToStringExpression { get; } - var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter); - AddPropertyTag(PropertyTag.Create(templateTag, Options, expr, toString ?? ToStringFunc)); - } - - private class PropertyTag : TagBase - { - private Func<Expression, string, Expression> CreateToStringExpression { get; init; } - private PropertyTag(ITemplateTag templateTag, Expression propertyGetter) : base(templateTag, propertyGetter) { } - - public static PropertyTag Create<TPropertyValue>(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, PropertyFormatter<TPropertyValue> formatter) + public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, PropertyFormatter<TPropertyValue> formatter) + : base(templateTag, propertyGetter) { - return new PropertyTag(templateTag, propertyGetter) - { - NameMatcher = new Regex(@$"^<{templateTag.TagName.Replace(" ", "\\s*?")}\s*?(?:\[([^\[\]]*?)\]\s*?)?>", options), - CreateToStringExpression = (expVal, format) => + 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)) - }; + Expression.Constant(format)); } - public static PropertyTag Create<TPropertyValue>(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, Func<TPropertyValue, string> toString) + public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, Func<TPropertyValue, string> toString) + : base(templateTag, propertyGetter) { - return new PropertyTag(templateTag, propertyGetter) - { - NameMatcher = new Regex(@$"^<{templateTag.TagName}>", options), - CreateToStringExpression = (expVal, _) => + NameMatcher = new Regex(@$"^<{templateTag.TagName.Replace(" ", "\\s*?")}>", options); + CreateToStringExpression = (expVal, _) => Expression.Call( toString.Target is null ? null : Expression.Constant(toString.Target), toString.Method, - expVal) - }; + expVal); } protected override Expression GetTagExpression(string exactName, string formatString) diff --git a/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs index 493a16eb..57312389 100644 --- a/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs +++ b/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs @@ -29,7 +29,7 @@ namespace LibationAvalonia.Dialogs var editor = TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(Configuration.Instance.Books, Configuration.Instance.FileTemplate); _viewModel = new(Configuration.Instance, editor); _viewModel.resetTextBox(editor.EditingTemplate.TemplateText); - Title = $"Edit {editor.EditingTemplate.Name}"; + Title = $"Edit {editor.TemplateName}"; DataContext = _viewModel; } } @@ -40,7 +40,7 @@ namespace LibationAvalonia.Dialogs _viewModel = new EditTemplateViewModel(Configuration.Instance, templateEditor); _viewModel.resetTextBox(templateEditor.EditingTemplate.TemplateText); - Title = $"Edit {templateEditor.EditingTemplate.Name}"; + Title = $"Edit {templateEditor.TemplateName}"; DataContext = _viewModel; } @@ -82,7 +82,7 @@ namespace LibationAvalonia.Dialogs { config = configuration; TemplateEditor = templates; - Description = templates.EditingTemplate.Description; + Description = templates.TemplateDescription; ListItems = new AvaloniaList<Tuple<string, string, string>>( TemplateEditor diff --git a/Source/LibationFileManager/TemplateEditor[T].cs b/Source/LibationFileManager/TemplateEditor[T].cs index a7226de2..e704e74d 100644 --- a/Source/LibationFileManager/TemplateEditor[T].cs +++ b/Source/LibationFileManager/TemplateEditor[T].cs @@ -12,6 +12,8 @@ namespace LibationFileManager bool IsFilePath { get; } LongPath BaseDirectory { get; } string DefaultTemplate { get; } + string TemplateName { get; } + string TemplateDescription { get; } Templates Folder { get; } Templates File { get; } Templates Name { get; } @@ -28,6 +30,8 @@ namespace LibationFileManager public bool IsFilePath => EditingTemplate is not Templates.ChapterTitleTemplate; public LongPath BaseDirectory { get; private init; } public string DefaultTemplate { get; private init; } + public string TemplateName { get; private init; } + public string TemplateDescription { get; private init; } public Templates Folder { get; private set; } public Templates File { get; private set; } public Templates Name { get; private set; } @@ -99,7 +103,10 @@ namespace LibationFileManager { _editingTemplate = template, BaseDirectory = baseDir, - DefaultTemplate = T.DefaultTemplate + DefaultTemplate = T.DefaultTemplate, + TemplateName = T.Name, + TemplateDescription = T.Description + }; if (!templateEditor.IsFolder && !templateEditor.IsFilePath) @@ -118,7 +125,9 @@ namespace LibationFileManager var templateEditor = new TemplateEditor<T> { _editingTemplate = nameTemplate, - DefaultTemplate = T.DefaultTemplate + DefaultTemplate = T.DefaultTemplate, + TemplateName = T.Name, + TemplateDescription = T.Description }; if (templateEditor.IsFolder || templateEditor.IsFilePath) diff --git a/Source/LibationFileManager/Templates.cs b/Source/LibationFileManager/Templates.cs index df672814..01b1cf64 100644 --- a/Source/LibationFileManager/Templates.cs +++ b/Source/LibationFileManager/Templates.cs @@ -11,6 +11,8 @@ namespace LibationFileManager { public interface ITemplate { + static abstract string Name { get; } + static abstract string Description { get; } static abstract string DefaultTemplate { get; } static abstract IEnumerable<TagCollection> TagCollections { get; } } @@ -42,12 +44,12 @@ namespace LibationFileManager { var namingTemplate = NamingTemplate.Parse(templateText, T.TagCollections); - template = new() { Template = namingTemplate }; + template = new() { NamingTemplate = namingTemplate }; return !namingTemplate.Errors.Any(); } private static T GetDefaultTemplate<T>() where T : Templates, ITemplate, new() - => new() { Template = NamingTemplate.Parse(T.DefaultTemplate, T.TagCollections) }; + => new() { NamingTemplate = NamingTemplate.Parse(T.DefaultTemplate, T.TagCollections) }; static Templates() { @@ -72,21 +74,19 @@ namespace LibationFileManager #region Template Properties - public IEnumerable<TemplateTags> TagsRegistered => Template.TagsRegistered.Cast<TemplateTags>(); - public IEnumerable<TemplateTags> TagsInUse => Template.TagsInUse.Cast<TemplateTags>(); - public abstract string Name { get; } - public abstract string Description { get; } - public string TemplateText => Template.TemplateText; - protected NamingTemplate Template { get; private set; } + public IEnumerable<TemplateTags> TagsRegistered => NamingTemplate.TagsRegistered.Cast<TemplateTags>(); + public IEnumerable<TemplateTags> TagsInUse => NamingTemplate.TagsInUse.Cast<TemplateTags>(); + public string TemplateText => NamingTemplate.TemplateText; + protected NamingTemplate NamingTemplate { get; private set; } #endregion #region validation - public virtual IEnumerable<string> Errors => Template.Errors; + public virtual IEnumerable<string> Errors => NamingTemplate.Errors; public bool IsValid => !Errors.Any(); - public virtual IEnumerable<string> Warnings => Template.Warnings; + public virtual IEnumerable<string> Warnings => NamingTemplate.Warnings; public bool HasWarnings => Warnings.Any(); #endregion @@ -97,7 +97,7 @@ namespace LibationFileManager { ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto)); ArgumentValidator.EnsureNotNull(multiChapProps, nameof(multiChapProps)); - return string.Join("", Template.Evaluate(libraryBookDto, multiChapProps).Select(p => p.Value)); + return string.Concat(NamingTemplate.Evaluate(libraryBookDto, multiChapProps).Select(p => p.Value)); } public LongPath GetFilename(LibraryBookDto libraryBookDto, string baseDir, string fileExtension, ReplacementCharacters replacements = null, bool returnFirstExisting = false) @@ -128,7 +128,7 @@ namespace LibationFileManager { fileExtension = FileUtility.GetStandardizedExtension(fileExtension); - var parts = Template.Evaluate(dtos).ToList(); + var parts = NamingTemplate.Evaluate(dtos).ToList(); var pathParts = GetPathParts(GetTemplatePartsStrings(parts, replacements)); //Remove 1 character from the end of the longest filename part until @@ -154,7 +154,7 @@ namespace LibationFileManager } } - var fullPath = Path.Combine(pathParts.Select(fileParts => string.Join("", fileParts)).Prepend(baseDir).ToArray()); + var fullPath = Path.Combine(pathParts.Select(fileParts => string.Concat(fileParts)).Prepend(baseDir).ToArray()); return FileUtility.GetValidFilename(fullPath, replacements, fileExtension, returnFirstExisting); } @@ -286,8 +286,8 @@ namespace LibationFileManager public class FolderTemplate : Templates, ITemplate { - public override string Name => "Folder Template"; - public override string Description => Configuration.GetDescription(nameof(Configuration.FolderTemplate)); + public static string Name { get; }= "Folder Template"; + public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate)); public static string DefaultTemplate { get; } = "<title short> [<id>]"; public static IEnumerable<TagCollection> TagCollections => new TagCollection[] { filePropertyTags, conditionalTags }; @@ -305,29 +305,29 @@ namespace LibationFileManager public class FileTemplate : Templates, ITemplate { - public override string Name => "File Template"; - public override string Description => Configuration.GetDescription(nameof(Configuration.FileTemplate)); + public static string Name { get; } = "File Template"; + public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate)); public static string DefaultTemplate { get; } = "<title> [<id>]"; public static IEnumerable<TagCollection> TagCollections { get; } = new TagCollection[] { filePropertyTags, conditionalTags }; } public class ChapterFileTemplate : Templates, ITemplate { - public override string Name => "Chapter File Template"; - public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate)); + public static string Name { get; } = "Chapter File Template"; + public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate)); public static string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>"; public static IEnumerable<TagCollection> TagCollections { get; } = chapterPropertyTags.Append(filePropertyTags).Append(conditionalTags); public override IEnumerable<string> Warnings - => Template.TagsInUse.Any(t => t.TagName.In(TemplateTags.ChNumber.TagName, TemplateTags.ChNumber0.TagName)) + => NamingTemplate.TagsInUse.Any(t => t.TagName.In(TemplateTags.ChNumber.TagName, TemplateTags.ChNumber0.TagName)) ? base.Warnings : base.Warnings.Append(WARNING_NO_CHAPTER_NUMBER_TAG); } public class ChapterTitleTemplate : Templates, ITemplate { - public override string Name => "Chapter Title Template"; - public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate)); + public static string Name { get; } = "Chapter Title Template"; + public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate)); public static string DefaultTemplate => "<ch#> - <title short>: <ch title>"; public static IEnumerable<TagCollection> TagCollections { get; } = chapterPropertyTags.Append(conditionalTags); diff --git a/Source/LibationWinForms/Dialogs/EditTemplateDialog.cs b/Source/LibationWinForms/Dialogs/EditTemplateDialog.cs index 12f42c6a..93cec08c 100644 --- a/Source/LibationWinForms/Dialogs/EditTemplateDialog.cs +++ b/Source/LibationWinForms/Dialogs/EditTemplateDialog.cs @@ -37,9 +37,9 @@ namespace LibationWinForms.Dialogs warningsLbl.Text = ""; - this.Text = $"Edit {templateEditor.EditingTemplate.Name}"; + this.Text = $"Edit {templateEditor.TemplateName}"; - this.templateLbl.Text = templateEditor.EditingTemplate.Description; + this.templateLbl.Text = templateEditor.TemplateDescription; resetTextBox(templateEditor.EditingTemplate.TemplateText); // populate list view diff --git a/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs b/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs index 4470436b..fccffc91 100644 --- a/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs +++ b/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs @@ -131,7 +131,7 @@ namespace NamingTemplateTests template.Warnings.Should().HaveCount(numTags > 0 ? 0 : 1); template.Errors.Should().HaveCount(0); - var templateText = string.Join("", template.Evaluate(propertyClass3, propertyClass2, propertyClass1).Select(v => v.Value)); + var templateText = string.Concat(template.Evaluate(propertyClass3, propertyClass2, propertyClass1).Select(v => v.Value)); templateText.Should().Be(outStr); } @@ -186,7 +186,7 @@ namespace NamingTemplateTests template.Warnings.Should().HaveCount(0); template.Errors.Should().HaveCount(0); - var templateText = string.Join("", template.Evaluate(propertyClass3, propertyClass2, propertyClass1).Select(v => v.Value)); + var templateText = string.Concat(template.Evaluate(propertyClass3, propertyClass2, propertyClass1).Select(v => v.Value)); templateText.Should().Be(outStr);