{"id":16534022,"url":"https://github.com/martindisch/csharp11features","last_synced_at":"2026-06-08T19:30:57.387Z","repository":{"id":75443142,"uuid":"568555275","full_name":"martindisch/CSharp11Features","owner":"martindisch","description":"Demonstrations of my favorite C# 11/.NET 7 features","archived":false,"fork":false,"pushed_at":"2022-12-05T21:46:23.000Z","size":194,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-12T07:58:03.437Z","etag":null,"topics":["csharp","csharp-11","dotnet-framework","dotnet7"],"latest_commit_sha":null,"homepage":"https://martindisch.github.io/CSharp11Features/","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/martindisch.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-11-20T22:04:44.000Z","updated_at":"2022-12-05T21:46:01.000Z","dependencies_parsed_at":"2023-06-03T04:10:17.543Z","dependency_job_id":null,"html_url":"https://github.com/martindisch/CSharp11Features","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/martindisch/CSharp11Features","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/martindisch%2FCSharp11Features","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/martindisch%2FCSharp11Features/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/martindisch%2FCSharp11Features/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/martindisch%2FCSharp11Features/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/martindisch","download_url":"https://codeload.github.com/martindisch/CSharp11Features/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/martindisch%2FCSharp11Features/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34078019,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-08T02:00:07.615Z","response_time":111,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["csharp","csharp-11","dotnet-framework","dotnet7"],"created_at":"2024-10-11T18:16:36.087Z","updated_at":"2026-06-08T19:30:57.365Z","avatar_url":"https://github.com/martindisch.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# My favorite C# 11/.NET 7 features\n\nAlso available as [slides](https://martindisch.github.io/CSharp11Features/).\n\n## Raw string literals\n\nLuckily I don't have to do a lot of manual formatting of semi-structured data,\nbut whenever I do, especially if the text contains quotes or curly braces, it\ngets ugly very quickly. Not anymore!\n\n```csharp\nvar id = 25;\n\nvar meep = $\"\"\"\n\u003cperson id=\"{id}\"\u003e\n    \u003cname\u003eJohn Doe\u003c/name\u003e\n    \u003caddress kind=\"home\"\u003eMarket Street 45\u003c/address\u003e\n\u003c/person\u003e\n\"\"\";\n```\n\nNot only does string interpolation still work (you can even include raw curly\nbraces without having to escape them by increasing the number of dollar signs\nat the start), it also removes the newlines after the opening quote and the\nsame plus any whitespace before the closing quote so you have a decent chance\nof ending up with what you intended.\n\n## List pattern\n\nThis finally gives C# pattern matching capabilities on par with\n[the best programming languages](https://adventures.michaelfbryan.com/posts/daily/slice-patterns/#checking-for-palindromes)\nout there. You can start writing some truly beautiful stuff.\n\n```csharp\nbool IsPalindrome(char[] characters) =\u003e characters switch\n{\n    [var first, .. var middle, var last] =\u003e first == last \u0026\u0026 IsPalindrome(middle),\n    [] or [_] =\u003e true,\n};\n```\n\n## Generic parse/static interface members\n\nThanks to some fancy type system features, the long-standing convention of the\nstatic `Parse` factory method can be formalized through the `IParsable\u003cTSelf\u003e`\ninterface. It's now possible to have interfaces with static members, which\nopens way more cool possibilities than just this.\n\nGiven an implementation on your type like this one\n\n```csharp\nrecord PoorCulture(string Language, string Country) : IParsable\u003cPoorCulture\u003e\n{\n    public static PoorCulture Parse(string s, IFormatProvider? provider)\n    {\n        var parts = s.Split('-');\n        if (parts.Length != 2)\n        {\n            throw new FormatException(\"Oh noes\");\n        }\n\n        return new(parts[0], parts[1]);\n    }\n\n    // TryParse omitted for brevity\n}\n```\n\nyou can use this now standardized implementation of `Parse` in a function like\n\n```csharp\nstatic T GenericParse\u003cT\u003e(string input) where T : IParsable\u003cT\u003e\n{\n    return T.Parse(input, null);\n}\n```\n\nwhich accepts all other parsable types like integers as well.\n\n## Required modifier\n\nThe object initializer syntax is pretty sweet. But for a long time, using it\nhas been extremely unsafe. Consider the following:\n\n```csharp\nclass InsanePerson\n{\n    public int Age { get; init; }\n    public string FirstName { get; init; } = null!;\n    public string? MiddleName { get; init; }\n}\n\n// We just made a mistake. Is it obvious? No.\nvar invalidPerson = new InsanePerson { MiddleName = \"None\" };\n```\n\nWith the type receiving the dreaded default constructor, even the compiler\nnotices that `FirstName` for example could be left uninitialized, so in our\ninfinite wisdom we decide to shut it up with the \"trust me\"-operator. This is\ncompletely our fault, but a pattern we see frequently. Now for the value type\n`Age` the language shows us no such kindness, as it will silently\nauto-initialize to the default value of 0 if we forget to set it. Just great.\n\nThe solution is to create constructors on all types to deny the dumb default\nconstructor. But constructors are so verbose! Luckily, C# 9 graced us with its\nextremely concise positional record syntax, allowing us to define a type and\nhave its full constructor implemented at the same time on a single line.\n\n```csharp\nrecord PositionalPerson(int Age, string FirstName, string? MiddleName = null);\n\n// Obviously fails to compile, as it should\nvar invalidPerson = new PositionalPerson(\"None\");\n```\n\nThis is nice and all, but what if you can't or won't use records and are stuck\nwith classes? C# 11 comes to the rescue with `required` members.\n\n```csharp\nclass RequiredPerson\n{\n    public required int Age { get; init; }\n    public required string FirstName { get; init; }\n    public string? MiddleName { get; init; }\n}\n\n// This time the compiler will stop us from shooting ourselves in the foot\nvar invalidPerson = new RequiredPerson { MiddleName = \"None\" };\n```\n\nBeautiful!\n\n## Bonus: IAsyncEnumerable support in Dataflow TransformManyBlock\n\nAdmittedly, this is a rather niche thing that I'm pretty sure barely anybody\nknows about. But those who do, have wanted it badly. If you haven't heard of\n[TPL Dataflow](https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/dataflow-task-parallel-library)\nyet, I highly recommend reading up on it. It's a hidden gem of the .NET\nFramework.\n\nTo demonstrate the new superpower that Dataflow has gained with .NET 7, let's\nimagine an example application. It's a system that sends notifications with\nnews about certain topics to users that have subscribed to those topics. Our\npipeline therefore has two stages/blocks:\n\n- A notification producer, which takes a topic for which we have news (a simple\n  string with the topic name) and returns a `Notification`. This is merely a\n  tuple containing the topic name and IDs of all users that are subscribed to\n  it and should therefore receive a notification. It's modeled as\n  `record Notification(string Topic, List\u003cint\u003e UserIds)`. This notification\n  producer loads the IDs of all users that have subscribed to a certain topic\n  from the database.\n- A notification sender, which accepts such a `Notification` and notifies the\n  users in it about the topic.\n\nOur pipeline is then defined as\n\n```csharp\nvar notificationProducer = new TransformBlock\u003cstring, Notification\u003e(\n    GenerateNotificationAsync,\n    new ExecutionDataflowBlockOptions { BoundedCapacity = 1 });\nvar notificationSender = new ActionBlock\u003cNotification\u003e(\n    SendNotificationsAsync,\n    new ExecutionDataflowBlockOptions { BoundedCapacity = 1 });\n\nnotificationProducer.LinkTo(\n    notificationSender,\n    new DataflowLinkOptions { PropagateCompletion = true });\n```\n\nThe `BoundedCapacity` is there to provide backpressure, guaranteeing that we\ndon't load users from the database faster than we can send notifications. This\nprevents unnecessary load on the database and potentially high memory usage for\nbuffering all the accumulated notifications, in case loading them from the\ndatabase is faster than sending notifications.\n\nThis is fine and dandy, as long as `GenerateNotificationAsync` can load all\nusers for a topic at once to return a `Notification` and pass them on to the\nnext block. However, that is quite unlikely. You might have many hundreds of\nthousands of users, meaning that you would most likely want to iterate over\nyour database in a paging fashion to produce batches of user IDs instead. Easy\nenough! Why don't we rename `Notification` to\n`record NotificationBatch(string Topic, List\u003cint\u003e UserIds)` and turn the first\nblock into a `TransformManyBlock`?\n\n```csharp\nvar notificationProducer = new TransformManyBlock\u003cstring, NotificationBatch\u003e(\n    GenerateNotificationBatchesAsync,\n    new ExecutionDataflowBlockOptions { BoundedCapacity = 1 });\n```\n\nLike its `SelectMany` counterpart in LINQ, the function in this block now\naccepts a single topic and produces many batches of notifications. The trouble\nstarts when thinking about how to implement this. If we still have to write an\nasync function that takes a single string and returns a list of\n`NotificationBatch`, don't we have the same problem as before? That we have to\nload all user IDs for the topic at once and then return them, this time in a\nlist of smaller `NotificationBatch` chunks? It may look this way, but remember\nthat the signature of the function we provide for the `TransformManyBlock` is\n`Func\u003cTInput, Task\u003cIEnumerable\u003cTOutput\u003e\u003e\u003e`. Since we can return an enumerable,\ncouldn't we use an iterator method that does\n`yield return new NotificationBatch(...)` after having loaded the next batch of\nuser IDs for the topic from the database? That would produce a nice, steady\nstream of batches that would flow into the next block as they become available.\n\nWe can, but only if our database access is synchronous, as it's of course not\npossible to await within an iterator method returning `IEnumerable`. And that's\nexactly the problem described in\n[this issue](https://github.com/dotnet/runtime/issues/30863)\nthat has been solved in .NET 7. It's now possible to to provide a\ntransformation `Func\u003cTInput, IAsyncEnumerable\u003cTOutput\u003e\u003e` to achieve this. Now\nwe can implement our async iterator method for the block:\n\n```csharp\nasync IAsyncEnumerable\u003cNotificationBatch\u003e GenerateNotificationBatchesAsync(string topic)\n{\n    int? after = null;\n\n    while (true)\n    {\n        var userIds = await simulatedDb.GetNextSubscribedUsersBatchAsync(topic, after);\n        if (userIds.Count == 0)\n        {\n            yield break;\n        }\n\n        after = userIds.Last();\n        yield return new(topic, userIds);\n    }\n}\n```\n\nIt uses a paging mechanism on the database to load the next batch of user IDs\nfor the topic, providing a reference to the last entry of the previous batch so\nit knows where to continue. If no more users are found it terminates, otherwise\nit yields the current batch before continuing with the next one. As a result,\nit will only begin loading the next batch after it has been able to yield the\ncurrent one, which it will only be able to do if the subsequent block\n(notification sender) accepts the next input, which it in turn will only do\nonce it's ready to send another batch of notifications. In this way we have\nperfect backpressure throughout the pipeline, where the slowest block is the\ndeciding factor for the rate at which items will be processed.\n\nIf we log whenever a `NotificationBatch` has been sent in our toy example,\nconfigure it such that\n\n- Loading the next batch of user IDs for a topic takes 1 second (batch size 5)\n- Sending a `NotificationBatch` takes 2 seconds\n\nand push the following topics into the first block\n\n```csharp\nvar notificationTopics = new[] { \"Gaming\", \"Furniture\", \"Smartphones\", \"Fashion\" };\nforeach (var topic in notificationTopics)\n{\n    await notificationProducer.SendAsync(topic);\n}\n\nnotificationProducer.Complete();\nawait notificationSender.Completion;\n```\n\nwe might see something like the following.\n\n```console\n$ dotnet run --project AsyncEnumerableDataflow\n[1:38:12PM] Start processing topics\n[1:38:15PM] Sent notification for topic Gaming to 5 users\n[1:38:17PM] Sent notification for topic Gaming to 5 users\n[1:38:19PM] Sent notification for topic Gaming to 5 users\n[1:38:21PM] Sent notification for topic Furniture to 5 users\n[1:38:23PM] Sent notification for topic Furniture to 5 users\n[1:38:25PM] Sent notification for topic Furniture to 5 users\n[1:38:27PM] Sent notification for topic Furniture to 5 users\n[1:38:29PM] Sent notification for topic Furniture to 5 users\n[1:38:31PM] Sent notification for topic Furniture to 5 users\n[1:38:33PM] Sent notification for topic Furniture to 5 users\n[1:38:35PM] Sent notification for topic Smartphones to 5 users\n[1:38:37PM] Sent notification for topic Smartphones to 5 users\n[1:38:39PM] Sent notification for topic Fashion to 5 users\n[1:38:41PM] Sent notification for topic Fashion to 5 users\n[1:38:43PM] Sent notification for topic Fashion to 5 users\n```\n\nIt takes the first batch 3 seconds to pass through the pipeline, because cycle\ntime is 1 + 2 seconds. But after that we are able to fully process a batch\nevery 2 seconds, which is the time the bottleneck (sending notifications)\ntakes.\n\n## License\n\nLicensed under [MIT license](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmartindisch%2Fcsharp11features","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmartindisch%2Fcsharp11features","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmartindisch%2Fcsharp11features/lists"}