Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
114 commits
Select commit Hold shift + click to select a range
176f582
Target Standard1.2 and 2.0, no more FX4.5
jeffijoe Dec 4, 2020
5a2c600
Attempt at build with GH Actions
jeffijoe Dec 4, 2020
65c67de
Add MinVer
jeffijoe Dec 4, 2020
cf4e529
Simplify AssemblyInfo
jeffijoe Dec 4, 2020
f5e24b7
More cleanup
jeffijoe Dec 4, 2020
b285778
Add publish step
jeffijoe Dec 4, 2020
52f0f81
Merge pull request #21 from jeffijoe/chore/modernize
jeffijoe Dec 5, 2020
4dbf9a0
Add pack step to build
jeffijoe Dec 5, 2020
2582af4
Add working-directory option to publish step
jeffijoe Dec 5, 2020
d595c50
I SAID ONLY ON RELEASE!
jeffijoe Dec 5, 2020
0ac587c
Update readme
jeffijoe Dec 5, 2020
446c27b
Add source generator. Parse simple rules with absolute value and visi…
kostya9 Mar 30, 2021
e611ed6
Fix assembly name. Refactor rule parsing to a class
kostya9 Mar 31, 2021
e2afb05
Add 'and' rule parsing
kostya9 Mar 31, 2021
05ca430
Add modulo support
kostya9 Mar 31, 2021
b21c0a8
Add support for range operators in the right part
kostya9 Mar 31, 2021
df8bec3
Add source generation for a single rule
kostya9 Mar 31, 2021
662768d
Add code generation
kostya9 Mar 31, 2021
1f75545
Make locale functions public
kostya9 Apr 1, 2021
d543377
Extract helpers for indents
kostya9 Apr 1, 2021
afe7e0e
Move equals logic to left operand
kostya9 Apr 1, 2021
3942a3f
Add test for full source code
kostya9 Apr 1, 2021
cec254a
Bump SDK version
kostya9 Apr 1, 2021
0f56f02
Update test project version
kostya9 Apr 1, 2021
77915c9
Remove not used code
kostya9 Apr 2, 2021
b787534
Use proper null character for EOF, use appropriate FormatException fo…
kostya9 Apr 3, 2021
cec16c3
Remove duplicate logic for IsEnd check, as PeekCurrentChar does absol…
kostya9 Apr 3, 2021
49b4652
Address feedback
kostya9 Apr 8, 2021
fa5b60b
Add locale-specific tests to verify plural rules are there
kostya9 Apr 17, 2021
6e1fb68
Minor rename - indicate that test tests Plural rules
kostya9 Apr 17, 2021
1a4c586
Support all rules except exponent via introducing PluralContext
kostya9 Apr 25, 2021
0152076
Remove double parsing
kostya9 Apr 26, 2021
21c0d3c
Fix culture assumption
kostya9 Apr 26, 2021
7ac0c61
Add special case for double in context
kostya9 Apr 26, 2021
4249738
Search for dot character, not string
kostya9 Apr 26, 2021
938bc4e
Remove useless cast in generated code
kostya9 Apr 26, 2021
cb19c33
Remove bunch of ToArrays in SourceGenerator
kostya9 Apr 26, 2021
49dac1a
Make plural metadata internal
kostya9 Apr 26, 2021
c956c09
Merge pull request #24 from kostya9/parse_pluralrules_from_cldr
jeffijoe Apr 26, 2021
18e6420
Add signing to new MetadataGenerator project
jeffijoe Apr 26, 2021
83d95a2
Update README
jeffijoe Apr 26, 2021
ac64cc0
Merge pull request #25 from jeffijoe/metadatagenerator-signing
jeffijoe Apr 26, 2021
ee224ab
Remove ToArray -> the requests are already cloned
kostya9 Apr 27, 2021
ff4e0e1
Remove allocation in formatter get
kostya9 Apr 27, 2021
753fa1e
Pool StringBuilders
kostya9 Apr 27, 2021
8649a61
Literal InnerText to string
kostya9 Apr 27, 2021
a2774d7
Fix tests after StringBuilder changes
kostya9 Apr 27, 2021
23ea38d
Do not allocate twice on subsstring+trim for net5.0
kostya9 Apr 27, 2021
35e6bc1
Minor indent fix
kostya9 Apr 27, 2021
f346cf9
Minor formatting change - move stuff out of try/finally
kostya9 Apr 27, 2021
1ea6acd
Do not unescape if nothing to escape
kostya9 Apr 27, 2021
1c1ca78
Remove outdated comment
kostya9 Apr 27, 2021
4989ff6
Fix nullability warning
kostya9 Apr 27, 2021
33f5330
Merge pull request #26 from kostya9/reduce_allocations
jeffijoe Apr 27, 2021
01a7efb
Add .NET 6 support
jeffijoe Jan 7, 2022
104d009
Use correct offset when parsing formatter arguments
jeffijoe Jan 14, 2022
facedda
100% coverage, remove Moq and delete unreachable code
jeffijoe Jan 15, 2022
81d26f9
Merge pull request #28 from jeffijoe/feature/lenient-whitespace
jeffijoe Jan 15, 2022
8f80ee9
Dotnet 7 support + allow IReadOnlyDictionary as args
jeffijoe Dec 23, 2022
c3330e2
Merge pull request #30 from jeffijoe/fix/readonlydict
jeffijoe Dec 23, 2022
1a0bc29
Fix `IDictionary` support
jeffijoe Jan 1, 2023
a2b40ba
Merge pull request #32 from jeffijoe/fix/dictionary
jeffijoe Jan 1, 2023
4967ef4
Fix issue where newlines were being trimmed
jeffijoe Aug 22, 2023
71c285f
Update packages
jeffijoe Aug 22, 2023
f5db9c6
Merge pull request #35 from jeffijoe/fix/newlines
jeffijoe Aug 22, 2023
4c2a79e
Add basic support for date. time and number formatting
jeffijoe Jan 1, 2023
4bb8c5f
Update plurals.xml from latest CLDR release
jeffijoe Oct 11, 2023
87f2d9b
Merge pull request #33 from jeffijoe/feat/value-formatters
jeffijoe Oct 11, 2023
aec7e82
Update README - fix Link
BrunoJuchli Jan 5, 2024
14e9a9d
Merge pull request #40 from BrunoJuchli/patch-1
jeffijoe Sep 28, 2024
0773a4a
Fix source generator
Frooxius Nov 6, 2023
c6fb960
Fix DateFormatter_Custom
Frooxius Nov 6, 2023
935e36b
Fix NumberFormatter_Decimal_CustomFormat
Frooxius Nov 6, 2023
a61d87b
Fix NumberFormatter_Integer
Frooxius Nov 6, 2023
e85b2e6
Fix ParseLiterals_position_and_inner_text
Frooxius Nov 6, 2023
40fc8b0
Fix target framework
jeffijoe Sep 28, 2024
08b468f
Update to .NET 8 + upgrade packages
jeffijoe Sep 28, 2024
13ce061
Merge pull request #44 from jeffijoe/net8
jeffijoe Sep 28, 2024
6ac96e5
Fix whitespace normalization in tests
jeffijoe Oct 14, 2024
b59363b
Fix issue where urls were parsed as offsets
jeffijoe Oct 14, 2024
0500f57
Use file-scoped namespaces in solution
jeffijoe Oct 14, 2024
3b2c2d0
Merge pull request #46 from jeffijoe/fix/url
jeffijoe Oct 14, 2024
d398f62
Add PolySharp to polyfill attributes
Cheesebaron Mar 3, 2025
1117a4a
Mark ToDictionary and GetProperties methods with RequiresUnreferenced…
Cheesebaron Mar 3, 2025
f00da59
Lower extension method overloads for signature with object lower
Cheesebaron Mar 3, 2025
79da44e
Merge pull request #50 from Cheesebaron/feature/gh-49-overload-and-re…
jeffijoe Mar 3, 2025
f2f23dd
Sync CLDR and add ordinals.xml + README
sfuqua Oct 18, 2025
a05bfe6
Update CLDR to 48.1
sfuqua Feb 15, 2026
b18aad8
Add plurals xml to metadata csproj
sfuqua Feb 15, 2026
f707fec
xmldoc for AST types
sfuqua Feb 15, 2026
744e761
parsing basics
sfuqua Feb 15, 2026
d177f40
Add some documentation to LMDL types
sfuqua Feb 15, 2026
03a621c
Codegen likely happy now
sfuqua Feb 15, 2026
c6fff54
Building and tests passing
sfuqua Feb 16, 2026
eff20d1
Add new README test for selectordinal
sfuqua Feb 16, 2026
22c151b
Fix bad test
sfuqua Feb 16, 2026
550fa53
readonly record struct
sfuqua Feb 16, 2026
fec0a92
More tests
sfuqua Feb 16, 2026
8b7ebb6
PR feedback
sfuqua Feb 16, 2026
23cb463
Remove inbox pluralizers; add root locale support; refactor codegen
sfuqua Feb 17, 2026
e9cea37
Locale fallback
sfuqua Feb 17, 2026
e464a9a
Cleanup formatting
sfuqua Feb 17, 2026
13c6ae2
Rename properties
sfuqua Feb 17, 2026
f587a11
Revert redundant Clear() calls
sfuqua Feb 17, 2026
32b446c
Merge pull request #52 from sfuqua/sfuqua/ordinals
jeffijoe Feb 18, 2026
3197fe2
Remove .NET 6 target, add .NET 10 target
jeffijoe Feb 21, 2026
12bf834
Use `CultureInfo` in the API instead of a string locale + default to …
jeffijoe Feb 21, 2026
5e70b7b
Add `culture` override to `FormatMessage`
jeffijoe Feb 21, 2026
bf49999
Upgrade packages and migrate to incremental generator
jeffijoe Feb 21, 2026
0a9f9ff
Remove unused build property
jeffijoe Feb 21, 2026
eac9acd
Update CI to use .NET 10
jeffijoe Feb 21, 2026
b656069
Merge pull request #54 from jeffijoe/feat/cultureinfo
jeffijoe Feb 23, 2026
6798a5b
Replace `Microsoft.Extensions.ObjectPool` with built-in Roslyn-based …
jeffijoe Feb 21, 2026
dae6f0b
Merge pull request #55 from jeffijoe/perf/pool
jeffijoe Feb 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Build, Test and Release

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
release:
types: [ released ]

env:
DOTNET_NOLOGO: true
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 100

- name: Setup .NET
uses: actions/setup-dotnet@v5.1.0
with:
dotnet-version: 10.0.x

- name: Install dependencies
working-directory: ./src
run: dotnet restore

- name: Build
working-directory: ./src
run: |
dotnet build --configuration Release --no-restore
dotnet pack -c Release Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj

- name: Test
working-directory: ./src
run: dotnet test --no-restore --verbosity normal

- name: Publish to NuGet
if: github.event_name == 'release' # Only publish on Release creation.
working-directory: ./src
run: |
dotnet nuget push Jeffijoe.MessageFormat/**/*.nupkg --api-key ${{ secrets.NUGET_JEFFIJOE_KEY }} --source https://api.nuget.org/v3/index.json
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ obj
bin
imagecache
/src/.vs
src/.idea/.idea.MessageFormat/.idea/workspace.xml
.DS_Store
BenchmarkDotNet.Artifacts/
92 changes: 68 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@

#### - better UI strings.

[![Build status](https://ci.appveyor.com/api/projects/status/9g7dplst1vyibc3e?svg=true)](https://ci.appveyor.com/project/jeffijoe/messageformat-net) [![Join the chat at https://gitter.im/jeffijoe/messageformat.net](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/jeffijoe/messageformat.net?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Build & Test](https://github.com/jeffijoe/messageformat.net/actions/workflows/ci.yml/badge.svg)](https://github.com/jeffijoe/messageformat.net/actions/workflows/ci.yml)

This is an implementation of the ICU Message Format in .NET. For official information about the format, go to:
http://userguide.icu-project.org/formatparse/messages
https://unicode-org.github.io/icu/userguide/format_parse/messages/

## Quickstart

````csharp
var mf = new MessageFormatter();

var str = @"You have {notifications, plural,
zero {no notifications}
=0 {no notifications}
one {one notification}
=42 {a universal amount of notifications}
other {# notifications}
Expand Down Expand Up @@ -60,20 +60,19 @@ Install-Package MessageFormat
## Features

* **It's fast.** Everything is hand-written; no parser-generators, *not even regular expressions*.
* **It's portable.** The library is a PCL, and has just a single dependency ([Portable.ConcurrentDictionary](https://www.nuget.org/packages/Portable.ConcurrentDictionary/) for thread safety) - other than that the only reference is the standard `.NET` in PCL's.
* **It's compatible with other implementations.** I've been peeking a bit at the [MessageFormat.js][0] library to make sure
the results would be the same.
* **It's portable.** The library is targeting **.NET Standard 2.0**.
* **It's (relatively) small**. For a .NET library, ~25kb is not a lot.
* **It's very white-space tolerant.** You can structure your blocks so they are more readable - look at the example above.
* **Nesting is supported.** You can nest your blocks as you please, there's no special structure required to do this, just ensure your braces match.
* **Adding your own formatters.** I don't know why you would need to, but if you want, you can add your own formatters, and
take advantage of the code in my base classes to help you parse patterns. Look at the source, this is how I implemented the built-in formatters.
* **Exceptions make atleast a little sense.** When exceptions are thrown due to a bad pattern, the exception should include useful information.
* **Exceptions make at least a little sense.** When exceptions are thrown due to a bad pattern, the exception should include useful information.
* **There are unit tests.** Run them yourself if you want, they're using XUnit.
* **Built-in cache.** If you are formatting messages in a tight loop, with different data for each iteration,
and if you are reusing the same instance of `MessageFormatter`, the formatter will cache the tokens of each pattern (nested, too),
so it won't have to spend CPU time to parse out literals every time. I benchmarked it, and on my monster machine,
it didn't make much of a difference (10000 iterations).
* **Built-in pluralization formatters**. Generated from the [CLDR pluralization rule data][plural-cldr].

## Performance

Expand All @@ -84,21 +83,73 @@ and about 3 seconds (3236ms) without it. **These results are with a debug build,

## Supported formats

Basically, it should be able to do anything that [MessageFormat.js][0] can do.
MessageFormat.NET supports the most commonly used formats:

* Select Format: `{gender, select, male{He likes} female{She likes} other{They like}} cheeseburgers`
* Plural Format: `There {msgCount, plural, zero {are no unread messages} one {is 1 unread message} other{are # unread messages}}.` (where `#` is the actual number, with the offset (if any) subtracted).
* Plural Format: `There {msgCount, plural, =0 {are no unread messages} one {is 1 unread message} other{are # unread messages}}.` (where `#` is the actual number, with the offset (if any) subtracted).
* Ordinal Format: `You are the {position, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} person in line.`
* Simple variable replacement: `Your name is {name}`
* Numbers: `Your age is {age, number}`
* Dates: `You were born {birthday, date}`
* Time: `The time is {now, time}`

You can also specify a _predefined style_, for example `{birthday, date, short}`. The supported predefined styles are:

* For the `number` format: `integer`, `currency`, `percent`
* For the `date` format: `short`, `full`
* For the `time` format: `short`, `medium`

These are currently mapped to the built-in .NET format specifiers. This package does not ship with
any locale data beyond the pluralizers that are generated based on [CLDR data][plural-cldr], so if you wish
to provide your own localized formatting, read the section below.

## Customize formatting

If you wish to control exactly how `number`, `date` and `time` are formatted, you can either:
* Derive `CustomValueFormatter` and override the format methods
* Instantiate a `CustomValueFormatters` and assign a lambda to the desired properties
Then pass it in as the `customValueFormatter` parameter to `new MessageFormatter`.

**Example**: A custom formatter that allows the use of .NET's formatting tokens. This is for illustration purposes only and
is not recommended for use in real apps.

```csharp
// This is using the lambda-based approach.
var custom = new CustomValueFormatters
{
// The formatter must set the `formatted` out parameter and return `true`
// If the formatter returns `false`, the built-in formatting is used.
Number = (CultureInfo _, object? value, string? style, out string? formatted) =>
{
formatted = string.Format($"{{0:{style}}}", value);
return true;
}
};

// Create a MessageFormatter with the custom value formatter.
var formatter = new MessageFormatter(customValueFormatter: custom);

// Format a message, passing the culture to FormatMessage.
var message = formatter.FormatMessage("{value, number, $0.0}", new { value = 23 },
CultureInfo.GetCultureInfo("en-US"));
// "$23.0"
```

## Adding your own pluralizer functions

> Since MessageFormat 5.0, pluralizers based on the [official CLDR data][plural-cldr] ship
> with the package, so this is no longer needed except when overriding specific custom locales.

Same thing as with [MessageFormat.js][0], you can add your own pluralizer function.
The `Pluralizers` property is a `IDictionary<string, Pluralizer>`, so you can remove the built-in
ones if you want.
The `CardinalPluralizers` property is a `IDictionary<string, Pluralizer>` that starts empty, along
with `OrdinalPluralizers` for ordinal numbers.

Adding to these Dictionaries will take precedence over the CLDR data for exact matches on
the input locales.

````csharp
var mf = new MessageFormatter();
mf.Pluralizers.Add("<locale>", n => {
mf.CardinalPluralizers.Add("<locale>", n => {
// ´n´ is the number being pluralized.
if(n == 0)
return "zero";
Expand All @@ -108,16 +159,14 @@ mf.Pluralizers.Add("<locale>", n => {
});
````

There's no restrictions on what strings you may return, nor what strings
There are no restrictions on what strings you may return, nor what strings
you may use in your pluralization block.

````csharp
var mf = new MessageFormatter(true, "en"); // true = use cache
mf.Pluralizers["en"] = n =>
var mf = new MessageFormatter(); // uses cache by default
mf.CardinalPluralizers["en"] = n =>
{
// ´n´ is the number being pluralized.
if (n == 0)
return "zero";
if (n == 1)
return "one";
if (n > 1000)
Expand All @@ -127,7 +176,7 @@ mf.Pluralizers["en"] = n =>

mf.FormatMessage("You have {number, plural, thatsalot {a shitload of notifications} other {# notifications}}", new Dictionary<string, object>{
{"number", 1001}
});
}, CultureInfo.GetCultureInfo("en"));
````

## Escaping literals
Expand All @@ -149,12 +198,6 @@ be (somewhat) compatible with his.
If you have issues with the library, and the exception makes no sense, please open an issue
and include your message, as well as the data you used.

# Todo

* Built-in locales (currently only `en` is added per default).

Don't expect this in the near future - you're welcome to submit a PR. :)

# Author

I'm Jeff Hansen, a software developer who likes to fiddle with string parsing when it is not too difficult.
Expand All @@ -164,3 +207,4 @@ You can find me on Twitter: [@jeffijoe][1].

[0]: https://github.com/SlexAxton/messageformat.js
[1]: https://twitter.com/jeffijoe
[plural-cldr]: https://cldr.unicode.org/index/downloads
1 change: 1 addition & 0 deletions src/.idea/.idea.MessageFormat/.idea/.name

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/.idea/.idea.MessageFormat/.idea/encodings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/.idea/.idea.MessageFormat/.idea/indexLayout.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/.idea/.idea.MessageFormat/.idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<SignAssembly>True</SignAssembly>
<AssemblyOriginatorKeyFile>MessageFormat.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Jeffijoe.MessageFormat\Jeffijoe.MessageFormat.csproj" />
</ItemGroup>

</Project>
Binary file not shown.
104 changes: 104 additions & 0 deletions src/Jeffijoe.MessageFormat.Benchmarks/MessageFormatterBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using Jeffijoe.MessageFormat;

namespace Jeffijoe.MessageFormat.Benchmarks;

[MemoryDiagnoser]
public class MessageFormatterBenchmarks
{
private MessageFormatter _formatter = null!;

private readonly Dictionary<string, object?> _simpleArgs = new() { ["name"] = "World" };

private readonly Dictionary<string, object?> _pluralSimpleArgs = new() { ["count"] = 5 };

private readonly Dictionary<string, object?> _selectSimpleArgs = new() { ["gender"] = "male" };

private readonly Dictionary<string, object?> _pluralOffsetArgs = new() { ["count"] = 3 };

private readonly Dictionary<string, object?> _nested2Args = new() { ["gender"] = "female", ["count"] = 1 };

private readonly Dictionary<string, object?> _nested3Args = new()
{
["gender"] = "male",
["count"] = 2,
["total"] = 10
};

[GlobalSetup]
public void Setup()
{
_formatter = new MessageFormatter();
}

[Benchmark]
public string SimpleSubstitution()
{
return _formatter.FormatMessage("{name}", _simpleArgs);
}

[Benchmark]
public string PluralSimple()
{
return _formatter.FormatMessage(
"{count, plural, one {1 thing} other {# things}}",
_pluralSimpleArgs);
}

[Benchmark]
public string SelectSimple()
{
return _formatter.FormatMessage(
"{gender, select, male {He} female {She} other {They}}",
_selectSimpleArgs);
}

[Benchmark]
public string PluralWithOffset()
{
return _formatter.FormatMessage(
"{count, plural, offset:1 =0 {Nobody} one {You and one other} other {You and # others}}",
_pluralOffsetArgs);
}

[Benchmark]
public string Nested2Levels()
{
return _formatter.FormatMessage(
"{gender, select, male {{count, plural, one {He has 1 item} other {He has # items}}} female {{count, plural, one {She has 1 item} other {She has # items}}} other {{count, plural, one {They have 1 item} other {They have # items}}}}",
_nested2Args);
}

[Benchmark]
public string Nested3Levels()
{
return _formatter.FormatMessage(
"{gender, select, male {{count, plural, one {He has 1 of {total} items} other {He has # of {total} items}}} female {{count, plural, one {She has 1 of {total} items} other {She has # of {total} items}}} other {{count, plural, one {They have 1 of {total} items} other {They have # of {total} items}}}}",
_nested3Args);
}

[Params(1, 2, 4, 8)]
public int ThreadCount { get; set; }

[Benchmark]
public void MultiThreadFormatMessage()
{
var args = new Dictionary<string, object?> { ["count"] = 5 };
var pattern = "{count, plural, one {1 thing} other {# things}}";

var tasks = new Task[ThreadCount];
for (var t = 0; t < ThreadCount; t++)
{
tasks[t] = Task.Run(() =>
{
for (var i = 0; i < 1000; i++)
{
_formatter.FormatMessage(pattern, args);
}
});
}

Task.WaitAll(tasks);
}
}
Loading