diff --git a/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/CollectionFiltersTest.cs b/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/CollectionFiltersTest.cs index 6aaf5e836..84f512e7b 100644 --- a/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/CollectionFiltersTest.cs +++ b/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/CollectionFiltersTest.cs @@ -52,20 +52,30 @@ public void BatchRenderTests() maxIterations: 0, formatProvider: CultureInfo.InvariantCulture, cancellationToken: CancellationToken.None); + var collection = new List { 1, 2, 3 }; Assert.Equal("1 ,2 ,3 ,", Filters.BatchRender(context, collection, "foo", "i")); + var result = Filters.BatchRenderParallel(context, collection, "foo", "i"); + Assert.Contains("1 ", result); + Assert.Contains("2 ", result); + Assert.Contains("3 ", result); // Valid template file system but null collection Assert.Equal(string.Empty, Filters.BatchRender(context, null, "foo", "i")); + Assert.Equal(string.Empty, Filters.BatchRenderParallel(context, null, "foo", "i")); // No template file system context = new Context(CultureInfo.InvariantCulture); var exception = Assert.Throws(() => Filters.BatchRender(context, null, "foo", "bar")); Assert.Equal(FhirConverterErrorCode.TemplateNotFound, exception.FhirConverterErrorCode); + exception = Assert.Throws(() => Filters.BatchRenderParallel(context, null, "foo", "bar")); + Assert.Equal(FhirConverterErrorCode.TemplateNotFound, exception.FhirConverterErrorCode); // Valid template file system but non-existing template exception = Assert.Throws(() => Filters.BatchRender(context, collection, "bar", "i")); Assert.Equal(FhirConverterErrorCode.TemplateNotFound, exception.FhirConverterErrorCode); + exception = Assert.Throws(() => Filters.BatchRenderParallel(context, collection, "bar", "i")); + Assert.Equal(FhirConverterErrorCode.TemplateNotFound, exception.FhirConverterErrorCode); } } } diff --git a/src/Microsoft.Health.Fhir.Liquid.Converter/Filters/CollectionFilters.cs b/src/Microsoft.Health.Fhir.Liquid.Converter/Filters/CollectionFilters.cs index 4cf12a73e..53b1023e0 100644 --- a/src/Microsoft.Health.Fhir.Liquid.Converter/Filters/CollectionFilters.cs +++ b/src/Microsoft.Health.Fhir.Liquid.Converter/Filters/CollectionFilters.cs @@ -7,6 +7,8 @@ using System.Globalization; using System.Linq; using System.Text; +using System.Threading; +using System.Threading.Tasks; using DotLiquid; using Microsoft.Health.Fhir.Liquid.Converter.DotLiquids; using Microsoft.Health.Fhir.Liquid.Converter.Exceptions; @@ -35,15 +37,9 @@ public static List Concat(List l1, List l2) return new List().Concat(l1 ?? new List()).Concat(l2 ?? new List()).ToList(); } - public static string BatchRender(Context context, List collection, string templateName, string variableName) + public static string BatchRender(Context context, List collection, string templateName, string variableName, string collectionVarName = null) { - var templateFileSystem = context.Registers["file_system"] as IFhirConverterTemplateFileSystem; - var template = templateFileSystem?.GetTemplate(templateName, context[TemplateUtility.RootTemplateParentPathScope]?.ToString()); - - if (template == null) - { - throw new RenderException(FhirConverterErrorCode.TemplateNotFound, string.Format(Resources.TemplateNotFound, templateName)); - } + var template = GetTemplate(context, templateName); var sb = new StringBuilder(); context.Stack(() => @@ -51,12 +47,74 @@ public static string BatchRender(Context context, List collection, strin collection?.ForEach(entry => { context[variableName] = entry; - sb.Append(template.Render(RenderParameters.FromContext(context, CultureInfo.InvariantCulture))); + if (!string.IsNullOrWhiteSpace(collectionVarName)) + { + context[collectionVarName] = collection; + } + + var result = template.Render(RenderParameters.FromContext(context, CultureInfo.InvariantCulture)); + + sb.Append(result); sb.Append(','); }); }); return sb.ToString(); } + + public static string BatchRenderParallel(Context context, List collection, string templateName, string variableName, string collectionVarName = null) + { + var template = GetTemplate(context, templateName); + + var sb = new StringBuilder(); + context.Stack(() => + { + if (collection != null && collection.Any()) + { + Parallel.ForEach(collection, entry => + { + // Create a new context for each parallel task to avoid race conditions + var localContext = new Context(context.Environments, new Hash(), context.Registers, ErrorsOutputMode.Rethrow, context.MaxIterations, CultureInfo.InvariantCulture, CancellationToken.None); + + foreach (var scope in context.Scopes) + { + foreach (var key in scope.Keys) + { + localContext[key] = scope[key]; + } + } + + localContext[variableName] = entry; + if (!string.IsNullOrWhiteSpace(collectionVarName)) + { + localContext[collectionVarName] = collection; + } + + var result = template.Render(RenderParameters.FromContext(localContext, CultureInfo.InvariantCulture)); + + lock (sb) + { + sb.Append(result); + sb.Append(','); + } + }); + } + }); + + return sb.ToString(); + } + + private static Template GetTemplate(Context context, string templateName) + { + var templateFileSystem = context.Registers["file_system"] as IFhirConverterTemplateFileSystem; + var template = templateFileSystem?.GetTemplate(templateName, context[TemplateUtility.RootTemplateParentPathScope]?.ToString()); + + if (template == null) + { + throw new RenderException(FhirConverterErrorCode.TemplateNotFound, string.Format(Resources.TemplateNotFound, templateName)); + } + + return template; + } } }