From df18662f27c29ee96a7b29f11f79a5565a7d3d09 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 21 Nov 2025 09:42:56 +0100 Subject: [PATCH 01/18] Improves the NameTestData with last names / first names that contains digits. --- tests/People.Tests/NameTestData.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/People.Tests/NameTestData.cs b/tests/People.Tests/NameTestData.cs index 1a8b99a..664a615 100644 --- a/tests/People.Tests/NameTestData.cs +++ b/tests/People.Tests/NameTestData.cs @@ -27,6 +27,7 @@ public static class NameTestData "$$Jean", "Jean@$+Patrick", "Jean-Patrick.", + "Jean-Patrick1234", string.Empty, " ", " ", @@ -49,6 +50,7 @@ public static class NameTestData "$$Dupont", "Du@$+Pont", "Du-pont.", + "Dupont1234", string.Empty, " ", }; From ea4a28bfc705dba5a5415235b4be72d632487e51 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 21 Nov 2025 09:58:48 +0100 Subject: [PATCH 02/18] Add a note in the PosInformatique.Foundations.Text.Templating.Razor readme about the HTML encoding. --- src/Text.Templating.Razor/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Text.Templating.Razor/README.md b/src/Text.Templating.Razor/README.md index 533c3f5..e0613d3 100644 --- a/src/Text.Templating.Razor/README.md +++ b/src/Text.Templating.Razor/README.md @@ -155,6 +155,20 @@ Formatted data: @this.Formatter.Format(Model) As long as `IDateTimeProvider` and `IMyFormatter` are registered in the `IServiceCollection`, they are available during template rendering. +## HTML rendering and character encoding + +The output of Razor templates is standard HTML. This means that special characters (including accents) +are HTML-encoded by default when using expressions like `@Model.Name`. + +If you need to output already-encoded or raw HTML content from your model, you must explicitly disable HTML +encoding in your Razor template, for example: + +```razor +@Html.Raw(Model.Name) +``` + +Use this only when you are sure that the content is safe (to avoid XSS vulnerabilities). + ## Links - [NuGet package: Emailing.Templates.Razor](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Templates.Razor/) From 5ba596e317be71798b90a5216cf6238360a35d04 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 21 Nov 2025 10:02:02 +0100 Subject: [PATCH 03/18] Removes the prefix of PosInformatique.Foundations in the main README (fix: #3). --- README.md | 56 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index a269e78..7d04468 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,11 @@ PosInformatique.Foundations icon -PosInformatique.Foundations is a collection of small, focused .NET libraries that provide **simple, reusable building blocks** for your applications. +[PosInformatique.Foundations](https://github.com/PosInformatique/PosInformatique.Foundations) is a collection +of small, focused .NET libraries that provide **simple, reusable building blocks** for your applications. -The goal is to avoid shipping a monolithic framework by creating **modular NuGet packages**, each addressing a single responsibility. +The goal is to avoid shipping a monolithic framework by creating **modular NuGet packages**, +each addressing a single responsibility. ## ✨ Philosophy @@ -21,32 +23,32 @@ The goal is to avoid shipping a monolithic framework by creating **modular NuGet You can install any package using the .NET CLI or NuGet Package Manager. -| |Package | Description | NuGet | +| |Package (prefixed by PosInformatique.Foundations) | Description | NuGet | |--|---------|-------------|-------| -|PosInformatique.Foundations.EmailAddresses icon|[**PosInformatique.Foundations.EmailAddresses**](./src/EmailAddresses/README.md) | Strongly-typed value object representing an email address with validation and normalization as RFC 5322 compliant. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.EmailAddresses)](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses) | -|PosInformatique.Foundations.EmailAddresses.EntityFramework icon|[**PosInformatique.Foundations.EmailAddresses.EntityFramework**](./src/EmailAddresses.EntityFramework/README.md) | Entity Framework Core integration for the `EmailAddress` value object, including property configuration and value converter for seamless database persistence. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.EmailAddresses.EntityFramework)](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.EntityFramework) | -|PosInformatique.Foundations.EmailAddresses.FluentValidation icon|[**PosInformatique.Foundations.EmailAddresses.FluentValidation**](./src/EmailAddresses.FluentValidation/README.md) | FluentValidation integration for the `EmailAddress` value object, providing dedicated validators and rules to ensure RFC 5322 compliant email addresses. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.EmailAddresses.FluentValidation)](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.FluentValidation) | -|PosInformatique.Foundations.EmailAddresses.Json icon|[**PosInformatique.Foundations.EmailAddresses.Json**](./src/EmailAddresses.Json/README.md) | `System.Text.Json` converter for the `EmailAddress` value object, enabling seamless serialization and deserialization of RFC 5322 compliant email addresses. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.EmailAddresses.Json)](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.Json) | -|PosInformatique.Foundations.Emailing icon|[**PosInformatique.Foundations.Emailing**](./src/Emailing/README.md) | Template-based emailing infrastructure for .NET that lets you register strongly-typed email templates, create emails from models, and send them through pluggable providers. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Emailing)](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing) | -|PosInformatique.Foundations.Emailing.Azure icon|[**PosInformatique.Foundations.Emailing.Azure**](./src/Emailing.Azure/README.md) | `IEmailProvider` implementation for [PosInformatique.Foundations.Emailing](./src/Emailing/README.md) using **Azure Communication Service**. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Emailing.Azure)](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Azure) | -|PosInformatique.Foundations.Emailing.Graph icon|[**PosInformatique.Foundations.Emailing.Graph**](./src/Emailing.Graph/README.md) | `IEmailProvider` implementation for [PosInformatique.Foundations.Emailing](./src/Emailing/README.md) using **Microsoft Graph API**. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Emailing.Graph)](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Graph) | -|PosInformatique.Foundations.Emailing.Templates.Razor icon|[**PosInformatique.Foundations.Emailing.Templates.Razor**](./src/Emailing.Templates.Razor/README.md) | Helpers to build EmailTemplate instances from Razor components for subject and HTML body, supporting strongly-typed models and reusable layouts. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Emailing.Templates.Razor)](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Templates.Razor) | -|PosInformatique.Foundations.MediaTypes icon|[**PosInformatique.Foundations.MediaTypes**](./src/MediaTypes/README.md) | Immutable `MimeType` value object with well-known media types and helpers to map between media types and file extensions. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.MediaTypes)](https://www.nuget.org/packages/PosInformatique.Foundations.MediaTypes) | -|PosInformatique.Foundations.MediaTypes.EntityFramework icon|[**PosInformatique.Foundations.MediaTypes.EntityFramework**](./src/MediaTypes.EntityFramework/README.md) | Entity Framework Core integration for the `MimeType` value object, including property configuration and value converter for seamless database persistence. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.MediaTypes.EntityFramework)](https://www.nuget.org/packages/PosInformatique.Foundations.MediaTypes.EntityFramework) | -|PosInformatique.Foundations.MediaTypes.Json icon|[**PosInformatique.Foundations.MediaTypes.Json**](./src/MediaTypes.Json/README.md) | `System.Text.Json` converter for the `MimeType` value object, enabling seamless serialization and deserialization of MIME types within JSON documents. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.MediaTypes.Json)](https://www.nuget.org/packages/PosInformatique.Foundations.MediaTypes.Json) | -|PosInformatique.Foundations.People icon|[**PosInformatique.Foundations.People**](./src/People/README.md) | Strongly-typed value objects for first and last names with validation and normalization. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People)](https://www.nuget.org/packages/PosInformatique.Foundations.People) | -|PosInformatique.Foundations.People.DataAnnotations icon|[**PosInformatique.Foundations.People.DataAnnotations**](./src/People.DataAnnotations/README.md) | DataAnnotations attributes for `FirstName` and `LastName` value objects. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People.DataAnnotations)](https://www.nuget.org/packages/PosInformatique.Foundations.People.DataAnnotations) | -|PosInformatique.Foundations.People.EntityFramework icon|[**PosInformatique.Foundations.People.EntityFramework**](./src/People.EntityFramework/README.md) | Entity Framework Core integration for `FirstName` and `LastName` value objects, providing fluent property configuration and value converters. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People.EntityFramework)](https://www.nuget.org/packages/PosInformatique.Foundations.People.EntityFramework) | -|PosInformatique.Foundations.People.FluentAssertions icon|[**PosInformatique.Foundations.People.FluentAssertions**](./src/People.FluentAssertions/README.md) | [FluentAssertions](https://fluentassertions.com/) extensions for `FirstName` and `LastName` to avoid ambiguity and provide `Should().Be(string)` assertions (case-sensitive on normalized values). | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People.FluentAssertions)](https://www.nuget.org/packages/PosInformatique.Foundations.People.FluentAssertions) | -|PosInformatique.Foundations.People.FluentValidation icon|[**PosInformatique.Foundations.People.FluentValidation**](./src/People.FluentValidation/README.md) | [FluentValidation](https://fluentvalidation.net/) extensions for `FirstName` and `LastName` value objects. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People.FluentValidation)](https://www.nuget.org/packages/PosInformatique.Foundations.People.FluentValidation) | -|PosInformatique.Foundations.People.Json icon|[**PosInformatique.Foundations.People.Json**](./src/People.Json/README.md) | `System.Text.Json` converters for `FirstName` and `LastName`, with validation and easy registration via `AddPeopleConverters()`. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People.Json)](https://www.nuget.org/packages/PosInformatique.Foundations.People.Json) | -|PosInformatique.Foundations.PhoneNumbers icon|[**PosInformatique.Foundations.PhoneNumbers**](./src/PhoneNumbers/README.md) | Strongly-typed value object representing a phone number in E.164 format, with parsing (including region-aware local numbers), validation, comparison, and formatting helpers. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.PhoneNumbers)](https://www.nuget.org/packages/PosInformatique.Foundations.PhoneNumbers) | -|PosInformatique.Foundations.PhoneNumbers.EntityFramework icon|[**PosInformatique.Foundations.PhoneNumbers.EntityFramework**](./src/PhoneNumbers.EntityFramework/README.md) | Entity Framework Core integration for the `PhoneNumber` value object, mapping it to a SQL `PhoneNumber` column type backed by `VARCHAR(16)` using a dedicated value converter. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.PhoneNumbers.EntityFramework)](https://www.nuget.org/packages/PosInformatique.Foundations.PhoneNumbers.EntityFramework) | -|PosInformatique.Foundations.PhoneNumbers.FluentValidation icon|[**PosInformatique.Foundations.PhoneNumbers.FluentValidation**](./src/PhoneNumbers.FluentValidation/README.md) | FluentValidation integration for the `PhoneNumber` value object, providing dedicated validators and rules to ensure E.164 compliant phone numbers. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.PhoneNumbers.FluentValidation)](https://www.nuget.org/packages/PosInformatique.Foundations.PhoneNumbers.FluentValidation) | -|PosInformatique.Foundations.PhoneNumbers.Json icon|[**PosInformatique.Foundations.PhoneNumbers.Json**](./src/PhoneNumbers.Json/README.md) | `System.Text.Json` converter for the `PhoneNumber` value object, enabling seamless serialization and deserialization of E.164 compliant phone numbers. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.PhoneNumbers.Json)](https://www.nuget.org/packages/PosInformatique.Foundations.PhoneNumbers.Json) | -|PosInformatique.Foundations.Text.Templating icon|[**PosInformatique.Foundations.Text.Templating**](./src/Text.Templating/README.md) | Abstractions for text templating, including the `TextTemplate` base class and `ITextTemplateRenderContext` interface, to be used by concrete templating engine implementations such as Razor-based text templates. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Text.Templating)](https://www.nuget.org/packages/PosInformatique.Foundations.Text.Templating) | -|PosInformatique.Foundations.Text.Templating.Razor icon|[**PosInformatique.Foundations.Text.Templating.Razor**](./src/Text.Templating.Razor/README.md) | Razor-based text templating using Blazor components, allowing generation of text from Razor views with a strongly-typed Model parameter and full dependency injection integration. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Text.Templating.Razor)](https://www.nuget.org/packages/PosInformatique.Foundations.Text.Templating.Razor) | -|PosInformatique.Foundations.Text.Templating.Scriban icon|[**PosInformatique.Foundations.Text.Templating.Scriban**](./src/Text.Templating.Scriban/README.md) | Scriban-based text templating with mustache-style syntax, allowing generation of text from templates using a strongly-typed model and automatic property exposure. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Text.Templating.Scriban)](https://www.nuget.org/packages/PosInformatique.Foundations.Text.Templating.Scriban) | +|PosInformatique.Foundations.EmailAddresses icon|[**EmailAddresses**](./src/EmailAddresses/README.md) | Strongly-typed value object representing an email address with validation and normalization as RFC 5322 compliant. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.EmailAddresses)](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses) | +|PosInformatique.Foundations.EmailAddresses.EntityFramework icon|[**EmailAddresses.EntityFramework**](./src/EmailAddresses.EntityFramework/README.md) | Entity Framework Core integration for the `EmailAddress` value object, including property configuration and value converter for seamless database persistence. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.EmailAddresses.EntityFramework)](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.EntityFramework) | +|PosInformatique.Foundations.EmailAddresses.FluentValidation icon|[**EmailAddresses.FluentValidation**](./src/EmailAddresses.FluentValidation/README.md) | FluentValidation integration for the `EmailAddress` value object, providing dedicated validators and rules to ensure RFC 5322 compliant email addresses. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.EmailAddresses.FluentValidation)](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.FluentValidation) | +|PosInformatique.Foundations.EmailAddresses.Json icon|[**EmailAddresses.Json**](./src/EmailAddresses.Json/README.md) | `System.Text.Json` converter for the `EmailAddress` value object, enabling seamless serialization and deserialization of RFC 5322 compliant email addresses. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.EmailAddresses.Json)](https://www.nuget.org/packages/PosInformatique.Foundations.EmailAddresses.Json) | +|PosInformatique.Foundations.Emailing icon|[**Emailing**](./src/Emailing/README.md) | Template-based emailing infrastructure for .NET that lets you register strongly-typed email templates, create emails from models, and send them through pluggable providers. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Emailing)](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing) | +|PosInformatique.Foundations.Emailing.Azure icon|[**Emailing.Azure**](./src/Emailing.Azure/README.md) | `IEmailProvider` implementation for [PosInformatique.Foundations.Emailing](./src/Emailing/README.md) using **Azure Communication Service**. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Emailing.Azure)](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Azure) | +|PosInformatique.Foundations.Emailing.Graph icon|[**Emailing.Graph**](./src/Emailing.Graph/README.md) | `IEmailProvider` implementation for [PosInformatique.Foundations.Emailing](./src/Emailing/README.md) using **Microsoft Graph API**. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Emailing.Graph)](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Graph) | +|PosInformatique.Foundations.Emailing.Templates.Razor icon|[**Emailing.Templates.Razor**](./src/Emailing.Templates.Razor/README.md) | Helpers to build EmailTemplate instances from Razor components for subject and HTML body, supporting strongly-typed models and reusable layouts. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Emailing.Templates.Razor)](https://www.nuget.org/packages/PosInformatique.Foundations.Emailing.Templates.Razor) | +|PosInformatique.Foundations.MediaTypes icon|[**MediaTypes**](./src/MediaTypes/README.md) | Immutable `MimeType` value object with well-known media types and helpers to map between media types and file extensions. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.MediaTypes)](https://www.nuget.org/packages/PosInformatique.Foundations.MediaTypes) | +|PosInformatique.Foundations.MediaTypes.EntityFramework icon|[**MediaTypes.EntityFramework**](./src/MediaTypes.EntityFramework/README.md) | Entity Framework Core integration for the `MimeType` value object, including property configuration and value converter for seamless database persistence. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.MediaTypes.EntityFramework)](https://www.nuget.org/packages/PosInformatique.Foundations.MediaTypes.EntityFramework) | +|PosInformatique.Foundations.MediaTypes.Json icon|[**MediaTypes.Json**](./src/MediaTypes.Json/README.md) | `System.Text.Json` converter for the `MimeType` value object, enabling seamless serialization and deserialization of MIME types within JSON documents. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.MediaTypes.Json)](https://www.nuget.org/packages/PosInformatique.Foundations.MediaTypes.Json) | +|PosInformatique.Foundations.People icon|[**People**](./src/People/README.md) | Strongly-typed value objects for first and last names with validation and normalization. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People)](https://www.nuget.org/packages/PosInformatique.Foundations.People) | +|PosInformatique.Foundations.People.DataAnnotations icon|[**People.DataAnnotations**](./src/People.DataAnnotations/README.md) | DataAnnotations attributes for `FirstName` and `LastName` value objects. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People.DataAnnotations)](https://www.nuget.org/packages/PosInformatique.Foundations.People.DataAnnotations) | +|PosInformatique.Foundations.People.EntityFramework icon|[**People.EntityFramework**](./src/People.EntityFramework/README.md) | Entity Framework Core integration for `FirstName` and `LastName` value objects, providing fluent property configuration and value converters. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People.EntityFramework)](https://www.nuget.org/packages/PosInformatique.Foundations.People.EntityFramework) | +|PosInformatique.Foundations.People.FluentAssertions icon|[**People.FluentAssertions**](./src/People.FluentAssertions/README.md) | [FluentAssertions](https://fluentassertions.com/) extensions for `FirstName` and `LastName` to avoid ambiguity and provide `Should().Be(string)` assertions (case-sensitive on normalized values). | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People.FluentAssertions)](https://www.nuget.org/packages/PosInformatique.Foundations.People.FluentAssertions) | +|PosInformatique.Foundations.People.FluentValidation icon|[**People.FluentValidation**](./src/People.FluentValidation/README.md) | [FluentValidation](https://fluentvalidation.net/) extensions for `FirstName` and `LastName` value objects. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People.FluentValidation)](https://www.nuget.org/packages/PosInformatique.Foundations.People.FluentValidation) | +|PosInformatique.Foundations.People.Json icon|[**People.Json**](./src/People.Json/README.md) | `System.Text.Json` converters for `FirstName` and `LastName`, with validation and easy registration via `AddPeopleConverters()`. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.People.Json)](https://www.nuget.org/packages/PosInformatique.Foundations.People.Json) | +|PosInformatique.Foundations.PhoneNumbers icon|[**PhoneNumbers**](./src/PhoneNumbers/README.md) | Strongly-typed value object representing a phone number in E.164 format, with parsing (including region-aware local numbers), validation, comparison, and formatting helpers. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.PhoneNumbers)](https://www.nuget.org/packages/PosInformatique.Foundations.PhoneNumbers) | +|PosInformatique.Foundations.PhoneNumbers.EntityFramework icon|[**PhoneNumbers.EntityFramework**](./src/PhoneNumbers.EntityFramework/README.md) | Entity Framework Core integration for the `PhoneNumber` value object, mapping it to a SQL `PhoneNumber` column type backed by `VARCHAR(16)` using a dedicated value converter. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.PhoneNumbers.EntityFramework)](https://www.nuget.org/packages/PosInformatique.Foundations.PhoneNumbers.EntityFramework) | +|PosInformatique.Foundations.PhoneNumbers.FluentValidation icon|[**PhoneNumbers.FluentValidation**](./src/PhoneNumbers.FluentValidation/README.md) | FluentValidation integration for the `PhoneNumber` value object, providing dedicated validators and rules to ensure E.164 compliant phone numbers. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.PhoneNumbers.FluentValidation)](https://www.nuget.org/packages/PosInformatique.Foundations.PhoneNumbers.FluentValidation) | +|PosInformatique.Foundations.PhoneNumbers.Json icon|[**PhoneNumbers.Json**](./src/PhoneNumbers.Json/README.md) | `System.Text.Json` converter for the `PhoneNumber` value object, enabling seamless serialization and deserialization of E.164 compliant phone numbers. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.PhoneNumbers.Json)](https://www.nuget.org/packages/PosInformatique.Foundations.PhoneNumbers.Json) | +|PosInformatique.Foundations.Text.Templating icon|[**Text.Templating**](./src/Text.Templating/README.md) | Abstractions for text templating, including the `TextTemplate` base class and `ITextTemplateRenderContext` interface, to be used by concrete templating engine implementations such as Razor-based text templates. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Text.Templating)](https://www.nuget.org/packages/PosInformatique.Foundations.Text.Templating) | +|PosInformatique.Foundations.Text.Templating.Razor icon|[**Text.Templating.Razor**](./src/Text.Templating.Razor/README.md) | Razor-based text templating using Blazor components, allowing generation of text from Razor views with a strongly-typed Model parameter and full dependency injection integration. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Text.Templating.Razor)](https://www.nuget.org/packages/PosInformatique.Foundations.Text.Templating.Razor) | +|PosInformatique.Foundations.Text.Templating.Scriban icon|[**Text.Templating.Scriban**](./src/Text.Templating.Scriban/README.md) | Scriban-based text templating with mustache-style syntax, allowing generation of text from templates using a strongly-typed model and automatic property exposure. | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Foundations.Text.Templating.Scriban)](https://www.nuget.org/packages/PosInformatique.Foundations.Text.Templating.Scriban) | > Note: Most of the packages are completely independent. You install only what you need. From 9c990bf344156584753b94b0e33a47c0aa944792 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 21 Nov 2025 10:14:34 +0100 Subject: [PATCH 04/18] Fix the README for PosInformatique.Foundations.Text.Templating.Razor. --- src/Text.Templating.Razor/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Text.Templating.Razor/README.md b/src/Text.Templating.Razor/README.md index e0613d3..5261fcd 100644 --- a/src/Text.Templating.Razor/README.md +++ b/src/Text.Templating.Razor/README.md @@ -160,11 +160,11 @@ As long as `IDateTimeProvider` and `IMyFormatter` are registered in the `IServic The output of Razor templates is standard HTML. This means that special characters (including accents) are HTML-encoded by default when using expressions like `@Model.Name`. -If you need to output already-encoded or raw HTML content from your model, you must explicitly disable HTML -encoding in your Razor template, for example: +If you need to output already-encoded or raw HTML content from your model in a Razor Component (Blazor-style), +you must explicitly disable HTML encoding in your Razor template using the `MarkupString` class, for example: ```razor -@Html.Raw(Model.Name) +@((MarkupString)Model.Name) ``` Use this only when you are sure that the content is safe (to avoid XSS vulnerabilities). From b848c42bac1d24621a3bff2b4ae2c0e6b208672d Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 23 Jan 2026 12:19:28 +0100 Subject: [PATCH 05/18] Updates the version of .NET SDK which is include with VS 2026. --- PosInformatique.Foundations.slnx | 1 + global.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/PosInformatique.Foundations.slnx b/PosInformatique.Foundations.slnx index cd18ea0..5d20402 100644 --- a/PosInformatique.Foundations.slnx +++ b/PosInformatique.Foundations.slnx @@ -5,6 +5,7 @@ + diff --git a/global.json b/global.json index 9d34b15..e80aa8b 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.305", + "version": "9.0.308", "rollForward": "latestFeature" } } From fb0a5295de2869d93729ccc6744f85c6dac4ed09 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 23 Jan 2026 15:37:44 +0100 Subject: [PATCH 06/18] Improves the .editorconfig rules. --- .editorconfig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.editorconfig b/.editorconfig index 1117d16..b360236 100644 --- a/.editorconfig +++ b/.editorconfig @@ -22,6 +22,10 @@ indent_size = 2 [*.{cs,vb}] +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true + #### Naming styles #### tab_width = 4 indent_size = 4 @@ -81,6 +85,10 @@ dotnet_style_null_propagation = true:suggestion dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion dotnet_style_prefer_auto_properties = true:silent dotnet_style_object_initializer = true:suggestion +dotnet_style_qualification_for_event = true +dotnet_style_qualification_for_field = true +dotnet_style_qualification_for_method = true +dotnet_style_qualification_for_property = true dotnet_style_collection_initializer = true:suggestion dotnet_style_prefer_simplified_boolean_expressions = true:suggestion From 3cb036a5f3e4fca4fe8854ccf593244ca6f6eea9 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 23 Jan 2026 15:47:58 +0100 Subject: [PATCH 07/18] Add the supports of file attachments in the emails (fixes #5). --- src/Emailing.Azure/AzureEmailProvider.cs | 10 ++ src/Emailing.Graph/GraphEmailProvider.cs | 21 ++++ src/Emailing/Email.cs | 6 + src/Emailing/EmailAttachment.cs | 65 ++++++++++ src/Emailing/EmailManager.cs | 5 + src/Emailing/EmailMessage.cs | 6 + src/Emailing/Emailing.csproj | 1 + .../AzureEmailProviderTest.cs | 28 ++++- .../GraphEmailProviderTest.cs | 86 ++++++++++++- tests/Emailing.Tests/EmailAttachmentTest.cs | 117 ++++++++++++++++++ tests/Emailing.Tests/EmailManagerTest.cs | 21 ++++ tests/Emailing.Tests/EmailMessageTest.cs | 1 + tests/Emailing.Tests/EmailTest.cs | 1 + 13 files changed, 366 insertions(+), 2 deletions(-) create mode 100644 src/Emailing/EmailAttachment.cs create mode 100644 tests/Emailing.Tests/EmailAttachmentTest.cs diff --git a/src/Emailing.Azure/AzureEmailProvider.cs b/src/Emailing.Azure/AzureEmailProvider.cs index ca4fdf8..b1ad593 100644 --- a/src/Emailing.Azure/AzureEmailProvider.cs +++ b/src/Emailing.Azure/AzureEmailProvider.cs @@ -57,6 +57,16 @@ public async Task SendAsync(EmailMessage message, CancellationToken cancellation }, }; + foreach (var attachment in message.Attachments) + { + var attachmentContent = await BinaryData.FromStreamAsync(attachment.Content, cancellationToken); + + azureMessage.Attachments.Add(new global::Azure.Communication.Email.EmailAttachment( + attachment.FileName, + attachment.ContentType.ToString(), + attachmentContent)); + } + await this.client.SendAsync(global::Azure.WaitUntil.Started, azureMessage, cancellationToken); } } diff --git a/src/Emailing.Graph/GraphEmailProvider.cs b/src/Emailing.Graph/GraphEmailProvider.cs index bcfbc0c..15edcbe 100644 --- a/src/Emailing.Graph/GraphEmailProvider.cs +++ b/src/Emailing.Graph/GraphEmailProvider.cs @@ -66,6 +66,27 @@ public async Task SendAsync(EmailMessage message, CancellationToken cancellation ], }; + if (message.Attachments.Count > 0) + { + graphMessage.Attachments = new List(); + + foreach (var attachment in message.Attachments) + { + using var attachmentContent = new MemoryStream(); + + await attachment.Content.CopyToAsync(attachmentContent, cancellationToken); + + var graphAttachment = new FileAttachment() + { + Name = attachment.FileName, + ContentBytes = attachmentContent.ToArray(), + ContentType = attachment.ContentType.ToString(), + }; + + graphMessage.Attachments.Add(graphAttachment); + } + } + var body = new SendMailPostRequestBody() { Message = graphMessage, diff --git a/src/Emailing/Email.cs b/src/Emailing/Email.cs index 4fe4b81..a88fff5 100644 --- a/src/Emailing/Email.cs +++ b/src/Emailing/Email.cs @@ -24,10 +24,16 @@ public Email(EmailTemplate template) this.Template = template; + this.Attachments = []; this.Importance = EmailImportance.Normal; this.Recipients = []; } + /// + /// Gets the collection of attachments included with the email message. + /// + public Collection Attachments { get; } + /// /// Gets or sets the importance of the e-mail. /// diff --git a/src/Emailing/EmailAttachment.cs b/src/Emailing/EmailAttachment.cs new file mode 100644 index 0000000..655384f --- /dev/null +++ b/src/Emailing/EmailAttachment.cs @@ -0,0 +1,65 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Foundations.Emailing +{ + using PosInformatique.Foundations.MediaTypes; + + /// + /// Represents an email attachment of the . + /// + public class EmailAttachment + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the file. + /// The MIME type of the . + /// The content of the attachment. The stream will be read when the + /// is called. The stream will not be disposed by the e-mailing system it is the responsibility + /// of the caller to dispose it once the e-mail have been sent. + /// Thrown when the argument is . + /// Thrown when argument is empty or contains white spaces. + /// Thrown when the argument is . + /// Thrown when the argument is . + /// Thrown when the stream argument is not readable. + public EmailAttachment(string fileName, MimeType contentType, Stream content) + { + ArgumentException.ThrowIfNullOrWhiteSpace(fileName); + ArgumentNullException.ThrowIfNull(contentType); + ArgumentNullException.ThrowIfNull(content); + + if (!content.CanRead) + { + throw new ArgumentException("The content stream must be readable.", nameof(content)); + } + + this.FileName = fileName; + this.ContentType = contentType; + this.Content = content; + } + + /// + /// Gets the content of the attachment. + /// + /// + /// The stream will be read when the + /// is called. The will not be disposed by the e-mailing system it is the responsibility + /// of the caller to dispose it once the e-mail have been sent. + /// + public Stream Content { get; } + + /// + /// Gets the file name of the attachment. + /// + public string FileName { get; } + + /// + /// Gets the content type of the attachment. + /// + public MimeType ContentType { get; } + } +} \ No newline at end of file diff --git a/src/Emailing/EmailManager.cs b/src/Emailing/EmailManager.cs index 2975047..1d5ed81 100644 --- a/src/Emailing/EmailManager.cs +++ b/src/Emailing/EmailManager.cs @@ -78,6 +78,11 @@ public async Task SendAsync(Email email, CancellationToken cance Importance = email.Importance, }; + foreach (var attachment in email.Attachments) + { + message.Attachments.Add(attachment); + } + await this.provider.SendAsync(message, cancellationToken); } } diff --git a/src/Emailing/EmailMessage.cs b/src/Emailing/EmailMessage.cs index 1a5b583..afe7f96 100644 --- a/src/Emailing/EmailMessage.cs +++ b/src/Emailing/EmailMessage.cs @@ -34,9 +34,15 @@ public EmailMessage(EmailContact from, EmailContact to, string subject, string h this.Subject = subject; this.HtmlContent = htmlContent; + this.Attachments = []; this.Importance = EmailImportance.Normal; } + /// + /// Gets the collection of attachments included with the email message. + /// + public Collection Attachments { get; } + /// /// Gets the sender of the e-mail message. /// diff --git a/src/Emailing/Emailing.csproj b/src/Emailing/Emailing.csproj index b341ee0..2dc6fb9 100644 --- a/src/Emailing/Emailing.csproj +++ b/src/Emailing/Emailing.csproj @@ -22,6 +22,7 @@ + diff --git a/tests/Emailing.Azure.Tests/AzureEmailProviderTest.cs b/tests/Emailing.Azure.Tests/AzureEmailProviderTest.cs index 885c8a2..72c61a2 100644 --- a/tests/Emailing.Azure.Tests/AzureEmailProviderTest.cs +++ b/tests/Emailing.Azure.Tests/AzureEmailProviderTest.cs @@ -6,7 +6,9 @@ namespace PosInformatique.Foundations.Emailing.Azure.Tests { + using System.Reflection; using PosInformatique.Foundations.EmailAddresses; + using PosInformatique.Foundations.MediaTypes; public class AzureEmailProviderTest { @@ -33,8 +35,16 @@ public async Task SendSync(EmailImportance importance, string expectedXPriority, var from = new EmailContact(EmailAddress.Parse("sender@domain.com"), "Ignored"); var to = new EmailContact(EmailAddress.Parse("recipient@domain.com"), "The recipient"); + var attachment1 = new EmailAttachment("Attachment1", MimeTypes.Application.Pdf, new MemoryStream([1, 2])); + var attachment2 = new EmailAttachment("Attachment2", MimeTypes.Application.Docx, new MemoryStream([3, 4])); + var message = new EmailMessage(from, to, "The subject", "The HTML content") { + Attachments = + { + attachment1, + attachment2, + }, Importance = importance, }; @@ -42,7 +52,13 @@ public async Task SendSync(EmailImportance importance, string expectedXPriority, azureClient.Setup(c => c.SendAsync(global::Azure.WaitUntil.Started, It.IsAny(), cancellationToken)) .Callback((global::Azure.WaitUntil _, global::Azure.Communication.Email.EmailMessage m, CancellationToken _) => { - m.Attachments.Should().BeEmpty(); + m.Attachments.Should().HaveCount(2); + m.Attachments[0].Content.ToArray().Should().BeEquivalentTo(new byte[] { 1, 2 }); + m.Attachments[0].ContentType.Should().Be("application/pdf"); + m.Attachments[0].Name.Should().Be("Attachment1"); + m.Attachments[1].Content.ToArray().Should().BeEquivalentTo(new byte[] { 3, 4 }); + m.Attachments[1].ContentType.Should().Be("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); + m.Attachments[1].Name.Should().Be("Attachment2"); m.Headers.Should().HaveCount(2); m.Headers["X-Priority"].Should().Be(expectedXPriority); m.Headers["Importance"].Should().Be(expectedImportance); @@ -63,6 +79,9 @@ public async Task SendSync(EmailImportance importance, string expectedXPriority, await provider.SendAsync(message, cancellationToken); azureClient.VerifyAll(); + + IsOpen(attachment1.Content).Should().BeTrue(); + IsOpen(attachment2.Content).Should().BeTrue(); } [Fact] @@ -78,5 +97,12 @@ await provider.Invoking(p => p.SendAsync(null, default)) azureClient.VerifyAll(); } + + private static bool IsOpen(Stream stream) + { + var fieldIsOpen = typeof(MemoryStream).GetField("_isOpen", BindingFlags.NonPublic | BindingFlags.Instance); + + return (bool)fieldIsOpen.GetValue(stream); + } } } \ No newline at end of file diff --git a/tests/Emailing.Graph.Tests/GraphEmailProviderTest.cs b/tests/Emailing.Graph.Tests/GraphEmailProviderTest.cs index 706dd2c..9187821 100644 --- a/tests/Emailing.Graph.Tests/GraphEmailProviderTest.cs +++ b/tests/Emailing.Graph.Tests/GraphEmailProviderTest.cs @@ -6,12 +6,14 @@ namespace PosInformatique.Foundations.Emailing.Graph.Tests { + using System.Reflection; using Microsoft.Graph; using Microsoft.Graph.Models; using Microsoft.Graph.Users.Item.SendMail; using Microsoft.Kiota.Abstractions; using Microsoft.Kiota.Abstractions.Serialization; using Microsoft.Kiota.Serialization.Json; + using PosInformatique.Foundations.MediaTypes; public class GraphEmailProviderTest { @@ -31,7 +33,7 @@ public void Constructor_WithServiceClientArgumentNull() [InlineData(EmailImportance.Low, Importance.Low)] [InlineData(EmailImportance.Normal, Importance.Normal)] [InlineData(EmailImportance.High, Importance.High)] - public async Task SendAsync(EmailImportance importance, Importance expectedImportance) + public async Task SendAsync_WithNoAttachment(EmailImportance importance, Importance expectedImportance) { var cancellationToken = new CancellationTokenSource().Token; @@ -85,6 +87,81 @@ public async Task SendAsync(EmailImportance importance, Importance expectedImpor serializationWriterFactory.VerifyAll(); } + [Theory] + [InlineData(EmailImportance.Low, Importance.Low)] + [InlineData(EmailImportance.Normal, Importance.Normal)] + [InlineData(EmailImportance.High, Importance.High)] + public async Task SendAsync_WithAttachment(EmailImportance importance, Importance expectedImportance) + { + var cancellationToken = new CancellationTokenSource().Token; + + var serializationWriterFactory = new Mock(MockBehavior.Strict); + serializationWriterFactory.Setup(f => f.GetSerializationWriter("application/json")) + .Returns(new JsonSerializationWriter()); + + var requestAdapter = new Mock(MockBehavior.Strict); + requestAdapter.Setup(r => r.BaseUrl) + .Returns("http://base/url"); + requestAdapter.Setup(r => r.EnableBackingStore(null)); + requestAdapter.Setup(r => r.SerializationWriterFactory) + .Returns(serializationWriterFactory.Object); + requestAdapter.Setup(r => r.SendNoContentAsync(It.IsAny(), It.IsNotNull>>(), cancellationToken)) + .Callback((RequestInformation requestInfo, Dictionary> _, CancellationToken _) => + { + requestInfo.HttpMethod.Should().Be(Method.POST); + requestInfo.URI.Should().Be("http://base/url/users/sender%40domain.com/sendMail"); + + var jsonMessage = KiotaJsonSerializer.DeserializeAsync(requestInfo.Content).GetAwaiter().GetResult(); + + jsonMessage.Message.Attachments.Should().HaveCount(2); + jsonMessage.Message.Attachments[0].As().ContentBytes.Should().Equal([1, 2]); + jsonMessage.Message.Attachments[0].ContentType.Should().Be("application/pdf"); + jsonMessage.Message.Attachments[0].Name.Should().Be("Attachment1"); + jsonMessage.Message.Attachments[1].As().ContentBytes.Should().Equal([3, 4]); + jsonMessage.Message.Attachments[1].ContentType.Should().Be("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); + jsonMessage.Message.Attachments[1].Name.Should().Be("Attachment2"); + jsonMessage.Message.Body.Content.Should().Be("The HTML content"); + jsonMessage.Message.Body.ContentType.Should().Be(BodyType.Html); + jsonMessage.Message.BccRecipients.Should().BeNull(); + jsonMessage.Message.CcRecipients.Should().BeNull(); + jsonMessage.Message.Importance.Should().Be(expectedImportance); + jsonMessage.Message.ToRecipients.Should().HaveCount(1); + jsonMessage.Message.ToRecipients[0].EmailAddress.Address.Should().Be("recipient@domain.com"); + jsonMessage.Message.ToRecipients[0].EmailAddress.Name.Should().Be("The recipient"); + jsonMessage.SaveToSentItems.Should().BeFalse(); + }) + .Returns(Task.CompletedTask); + + var graphServiceClient = new Mock(MockBehavior.Strict, requestAdapter.Object, null); + + var client = new GraphEmailProvider(graphServiceClient.Object); + + var from = new EmailContact(EmailAddresses.EmailAddress.Parse("sender@domain.com"), "The sender"); + var to = new EmailContact(EmailAddresses.EmailAddress.Parse("recipient@domain.com"), "The recipient"); + + var attachment1 = new EmailAttachment("Attachment1", MimeTypes.Application.Pdf, new MemoryStream([1, 2])); + var attachment2 = new EmailAttachment("Attachment2", MimeTypes.Application.Docx, new MemoryStream([3, 4])); + + var message = new EmailMessage(from, to, "The subject", "The HTML content") + { + Attachments = + { + attachment1, + attachment2, + }, + Importance = importance, + }; + + await client.SendAsync(message, cancellationToken); + + graphServiceClient.VerifyAll(); + requestAdapter.VerifyAll(); + serializationWriterFactory.VerifyAll(); + + IsOpen(attachment1.Content).Should().BeTrue(); + IsOpen(attachment2.Content).Should().BeTrue(); + } + [Fact] public async Task SendSync_WithMessageArgumentNull() { @@ -104,5 +181,12 @@ await provider.Invoking(p => p.SendAsync(null, default)) requestAdapter.VerifyAll(); serviceClient.VerifyAll(); } + + private static bool IsOpen(Stream stream) + { + var fieldIsOpen = typeof(MemoryStream).GetField("_isOpen", BindingFlags.NonPublic | BindingFlags.Instance); + + return (bool)fieldIsOpen.GetValue(stream); + } } } \ No newline at end of file diff --git a/tests/Emailing.Tests/EmailAttachmentTest.cs b/tests/Emailing.Tests/EmailAttachmentTest.cs new file mode 100644 index 0000000..64639e3 --- /dev/null +++ b/tests/Emailing.Tests/EmailAttachmentTest.cs @@ -0,0 +1,117 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Foundations.Emailing.Tests +{ + using PosInformatique.Foundations.MediaTypes; + + public class EmailAttachmentTest + { + [Fact] + public void Constructor() + { + var content = new Mock(MockBehavior.Strict); + content.Setup(s => s.CanRead) + .Returns(true); + + var attachment = new EmailAttachment( + "File.pdf", + MimeTypes.Application.Pdf, + content.Object); + + attachment.Content.Should().BeSameAs(content.Object); + attachment.ContentType.Should().Be(MimeTypes.Application.Pdf); + attachment.FileName.Should().Be("File.pdf"); + + content.VerifyAll(); + } + + [Fact] + public void Constructor_WithContentNoReadable() + { + var content = new Mock(MockBehavior.Strict); + content.Setup(s => s.CanRead) + .Returns(false); + + var act = () => + { + new EmailAttachment( + "File.pdf", + MimeTypes.Application.Pdf, + content.Object); + }; + + act.Should().ThrowExactly() + .WithMessage("The content stream must be readable. (Parameter 'content')") + .WithParameterName("content"); + + content.VerifyAll(); + } + + [Fact] + public void Constructor_WithContentArgumentNull() + { + var act = () => + { + new EmailAttachment( + "File.pdf", + MimeTypes.Application.Pdf, + null); + }; + + act.Should().ThrowExactly() + .WithParameterName("content"); + } + + [Fact] + public void Constructor_WithContentTypeArgumentNull() + { + var act = () => + { + new EmailAttachment( + "File.pdf", + null, + default); + }; + + act.Should().ThrowExactly() + .WithParameterName("contentType"); + } + + [Fact] + public void Constructor_WithFileNameArgumentNull() + { + var act = () => + { + new EmailAttachment( + null, + default, + default); + }; + + act.Should().ThrowExactly() + .WithParameterName("fileName"); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void Constructor_WithFileNameWithWhitespaces(string fileName) + { + var act = () => + { + new EmailAttachment( + fileName, + default, + default); + }; + + act.Should().ThrowExactly() + .WithMessage("The value cannot be an empty string or composed entirely of whitespace. (Parameter 'fileName')") + .WithParameterName("fileName"); + } + } +} \ No newline at end of file diff --git a/tests/Emailing.Tests/EmailManagerTest.cs b/tests/Emailing.Tests/EmailManagerTest.cs index 20faabd..fd806cf 100644 --- a/tests/Emailing.Tests/EmailManagerTest.cs +++ b/tests/Emailing.Tests/EmailManagerTest.cs @@ -8,6 +8,7 @@ namespace PosInformatique.Foundations.Emailing.Tests { using Microsoft.Extensions.Options; using PosInformatique.Foundations.EmailAddresses; + using PosInformatique.Foundations.MediaTypes; using PosInformatique.Foundations.Text.Templating; public class EmailManagerTest @@ -129,8 +130,24 @@ public async Task SendAsync() var emailAddressRecipient1 = EmailAddress.Parse("email1@domain.com"); var emailAddressRecipient2 = EmailAddress.Parse("email2@domain.com"); + var content1 = new Mock(MockBehavior.Strict); + content1.Setup(c => c.CanRead) + .Returns(true); + + var content2 = new Mock(MockBehavior.Strict); + content2.Setup(c => c.CanRead) + .Returns(true); + + var attachment1 = new EmailAttachment("Attachment1", MimeTypes.Application.Pdf, content1.Object); + var attachment2 = new EmailAttachment("Attachment2", MimeTypes.Application.Docx, content2.Object); + var email = new Email(template) { + Attachments = + { + attachment1, + attachment2, + }, Importance = EmailImportance.High, Recipients = { @@ -148,6 +165,7 @@ public async Task SendAsync() provider.Setup(p => p.SendAsync(It.Is(m => m.To.Email == emailAddressRecipient1), cancellationToken)) .Callback((EmailMessage m, CancellationToken _) => { + m.Attachments.Should().Equal(attachment1, attachment2); m.From.Email.Should().BeSameAs(sender); m.From.DisplayName.Should().BeEmpty(); m.Importance.Should().Be(EmailImportance.High); @@ -159,6 +177,7 @@ public async Task SendAsync() provider.Setup(p => p.SendAsync(It.Is(m => m.To.Email == emailAddressRecipient2), cancellationToken)) .Callback((EmailMessage m, CancellationToken _) => { + m.Attachments.Should().Equal(attachment1, attachment2); m.From.Email.Should().BeSameAs(sender); m.From.DisplayName.Should().BeEmpty(); m.Importance.Should().Be(EmailImportance.High); @@ -172,6 +191,8 @@ public async Task SendAsync() await manager.SendAsync(email, cancellationToken); + content1.VerifyAll(); + content2.VerifyAll(); htmlBody.VerifyAll(); provider.VerifyAll(); subject.VerifyAll(); diff --git a/tests/Emailing.Tests/EmailMessageTest.cs b/tests/Emailing.Tests/EmailMessageTest.cs index a43cfd6..7bba5cb 100644 --- a/tests/Emailing.Tests/EmailMessageTest.cs +++ b/tests/Emailing.Tests/EmailMessageTest.cs @@ -22,6 +22,7 @@ public void Constructor() "The subject", "HTML content"); + emailMessage.Attachments.Should().BeEmpty(); emailMessage.From.Should().Be(from); emailMessage.Importance.Should().Be(EmailImportance.Normal); emailMessage.HtmlContent.Should().Be("HTML content"); diff --git a/tests/Emailing.Tests/EmailTest.cs b/tests/Emailing.Tests/EmailTest.cs index b5c3841..ab47057 100644 --- a/tests/Emailing.Tests/EmailTest.cs +++ b/tests/Emailing.Tests/EmailTest.cs @@ -20,6 +20,7 @@ public void Constructor() var email = new Email(template); + email.Attachments.Should().BeEmpty(); email.Importance.Should().Be(EmailImportance.Normal); email.Recipients.Should().BeEmpty(); email.Template.Should().BeSameAs(template); From 4ab9ff4c21ecf09fd8c6eb8c38b0e5da55ed6405 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 23 Jan 2026 17:33:00 +0100 Subject: [PATCH 08/18] Add the changelog. --- CHANGELOG.md | 41 ++++++++++++++++++++++++++++++++ PosInformatique.Foundations.slnx | 1 + README.md | 4 ++++ src/Emailing.Azure/CHANGELOG.md | 5 +++- src/Emailing.Graph/CHANGELOG.md | 5 +++- src/Emailing/CHANGELOG.md | 5 +++- 6 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..432036f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,41 @@ +# PosInformatique.Foundations.Emailing.Azure + +## Changelog + +### 1.1.0 + +#### PosInformatique.Foundations.Emailing +- Add the support to send emails with attachments. + +#### PosInformatique.Foundations.Emailing.Azure +- Add the support to send emails with attachments. + +#### PosInformatique.Foundations.Emailing.Graph +- Add the support to send emails with attachments. + +### 1.0.0 +- Initial version of the following packages: + - [PosInformatique.Foundations.EmailAddresses](./src/EmailAddresses/README.md) + - [PosInformatique.Foundations.EmailAddresses.EntityFramework](./src/EmailAddresses.EntityFramework/README.md) + - [PosInformatique.Foundations.EmailAddresses.FluentValidation](./src/EmailAddresses.FluentValidation/README.md) + - [PosInformatique.Foundations.EmailAddresses.Json](./src/EmailAddresses.Json/README.md) + - [PosInformatique.Foundations.Emailing](./src/Emailing/README.md) + - [PosInformatique.Foundations.Emailing.Azure](./src/Emailing.Azure/README.md) + - [PosInformatique.Foundations.Emailing.Graph](./src/Emailing.Graph/README.md) + - [PosInformatique.Foundations.Emailing.Templates.Razor](./src/Emailing.Templates.Razor/README.md) + - [PosInformatique.Foundations.MediaTypes](./src/MediaTypes/README.md) + - [PosInformatique.Foundations.MediaTypes.EntityFramework](./src/MediaTypes.EntityFramework/README.md) + - [PosInformatique.Foundations.MediaTypes.Json](./src/MediaTypes.Json/README.md) + - [PosInformatique.Foundations.People](./src/People/README.md) + - [PosInformatique.Foundations.People.DataAnnotations](./src/People.DataAnnotations/README.md) + - [PosInformatique.Foundations.People.EntityFramework](./src/People.EntityFramework/README.md) + - [PosInformatique.Foundations.People.FluentAssertions](./src/People.FluentAssertions/README.md) + - [PosInformatique.Foundations.People.FluentValidation](./src/People.FluentValidation/README.md) + - [PosInformatique.Foundations.People.Json](./src/People.Json/README.md) + - [PosInformatique.Foundations.PhoneNumbers](./src/PhoneNumbers/README.md) + - [PosInformatique.Foundations.PhoneNumbers.EntityFramework](./src/PhoneNumbers.EntityFramework/README.md) + - [PosInformatique.Foundations.PhoneNumbers.FluentValidation](./src/PhoneNumbers.FluentValidation/README.md) + - [PosInformatique.Foundations.PhoneNumbers.Json](./src/PhoneNumbers.Json/README.md) + - [PosInformatique.Foundations.Text.Templating](./src/Text.Templating/README.md) + - [PosInformatique.Foundations.Text.Templating.Razor](./src/Text.Templating.Razor/README.md) + - [PosInformatique.Foundations.Text.Templating.Scriban](./src/Text.Templating.Scriban/README.md) \ No newline at end of file diff --git a/PosInformatique.Foundations.slnx b/PosInformatique.Foundations.slnx index 5d20402..07ef3c9 100644 --- a/PosInformatique.Foundations.slnx +++ b/PosInformatique.Foundations.slnx @@ -2,6 +2,7 @@ + diff --git a/README.md b/README.md index 7d04468..83102ca 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,10 @@ You can install any package using the .NET CLI or NuGet Package Manager. - Get lightweight, modular libraries tailored to single responsibilities. - Add missing building blocks to your projects without introducing a heavyweight framework. +## 📜 Changelog +Each package maintains its own changelog in its respective `CHANGELOG.md` file located in the package's source directory, but +for your convenience, we also provide a consolidated [changelog here](./CHANGELOG.md). + ## 📌 .NET and dependency compatibility All [PosInformatique.Foundations](https://github.com/PosInformatique/PosInformatique.Foundations) packages are designed to be compatible with **.NET 8.0**, **.NET 9.0** and **.NET 10.0**. diff --git a/src/Emailing.Azure/CHANGELOG.md b/src/Emailing.Azure/CHANGELOG.md index bb7782e..135e53e 100644 --- a/src/Emailing.Azure/CHANGELOG.md +++ b/src/Emailing.Azure/CHANGELOG.md @@ -1,2 +1,5 @@ -1.0.0 +1.1.0 + - Add the support to send emails with attachments using Azure Communication Service Emailing provider. + +1.0.0 - Initial release with Azure Communication Service Emailing provider. diff --git a/src/Emailing.Graph/CHANGELOG.md b/src/Emailing.Graph/CHANGELOG.md index eb7a9ad..4e4324a 100644 --- a/src/Emailing.Graph/CHANGELOG.md +++ b/src/Emailing.Graph/CHANGELOG.md @@ -1,2 +1,5 @@ -1.0.0 +1.1.0 + - Add the support to send emails with attachments using Microsoft Graph Emailing provider. + +1.0.0 - Initial release with Microsoft Graph Emailing provider. diff --git a/src/Emailing/CHANGELOG.md b/src/Emailing/CHANGELOG.md index c76f51e..0fb6d28 100644 --- a/src/Emailing/CHANGELOG.md +++ b/src/Emailing/CHANGELOG.md @@ -1,2 +1,5 @@ -1.0.0 +1.1.0 + - Add the support to send emails with attachments. + +1.0.0 - Initial release with Emailing infrastructure. From dfaf060e7af9864b4d03e103b1d68d6590f1d22c Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 23 Jan 2026 20:04:05 +0100 Subject: [PATCH 09/18] Updates the documents. --- src/Emailing/README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Emailing/README.md b/src/Emailing/README.md index 53b1898..9b46bbd 100644 --- a/src/Emailing/README.md +++ b/src/Emailing/README.md @@ -44,6 +44,7 @@ and one of its concrete implementations (for example - Central `IEmailManager` to create and send emails. - Pluggable `IEmailProvider` to send the final `EmailMessage` (transport-agnostic design). - Support of the *Importance* of the e-mails (**Low, Normal and High). +- Support for sending emails with attachments. ## Basic concepts @@ -206,7 +207,22 @@ invitationEmail.Recipients.Add( }); ``` -### 3. Send the email +### 3. Add attachments (optional) + +You can add attachments to the email by using the `Attachments` collection +of the `EmailMessage` class: + +```csharp +using var fileContent = File.OpenRead("document.pdf"); + +invitationEmail.Attachments.Add(new EmailAttachment("MyFile.pdf", "MimeTypes.Pdf", fileContent)); +``` + +> **Note:** The `IEmailManager.SendAsync()` method will read the content of the attachment stream +> but will not dispose of it. Make sure to manage the `Stream` lifetime appropriately +> by calling the `Dispose()` method on the `Stream` instance after the email has been sent. + +### 4. Send the email Once the email and its recipients are configured, you ask the `IEmailManager` to send it: @@ -224,6 +240,8 @@ Under the hood: - The configured sender (`EmailingOptions.SenderEmailAddress`). - The recipient address and display name. - The generated subject and HTML content. + - The importance. + - The associated attachments. 4. It calls `IEmailProvider.SendAsync(...)` to actually send the message. The provider implementation is responsible for the technical details (SMTP, Azure Communication Service, etc.). From 7e62f7b98973b704588b44ff159b49d3c4e9642c Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 23 Jan 2026 20:04:45 +0100 Subject: [PATCH 10/18] Upgrade to the version 1.1.0 --- .github/workflows/github-actions-release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-release.yaml b/.github/workflows/github-actions-release.yaml index c196d88..0be6c57 100644 --- a/.github/workflows/github-actions-release.yaml +++ b/.github/workflows/github-actions-release.yaml @@ -7,7 +7,7 @@ on: type: string description: The version of the library required: true - default: 1.0.0 + default: 1.1.0 VersionSuffix: type: string description: The version suffix of the library (for example rc.1) From e57dc36791e380fef07a3282989454ffef5a1e0d Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Mon, 26 Jan 2026 11:45:57 +0100 Subject: [PATCH 11/18] Add an overload of the EmailRecipientCollection.Add() method (fixes: #6). --- src/Emailing/EmailRecipientCollection.cs | 13 ++++++ .../EmailRecipientCollectionTest.cs | 44 +++++++++++++++++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/Emailing/EmailRecipientCollection.cs b/src/Emailing/EmailRecipientCollection.cs index c2a1f30..3aad337 100644 --- a/src/Emailing/EmailRecipientCollection.cs +++ b/src/Emailing/EmailRecipientCollection.cs @@ -15,6 +15,19 @@ namespace PosInformatique.Foundations.Emailing /// Data model injected to the to generate the e-mail for each recipient. public class EmailRecipientCollection : Collection> { + /// + /// Creates and add new in the . + /// + /// E-mail address of the recipient. + /// Data model to apply on the to generate the e-mail for the recipient. + /// The created and added. + /// Thrown when the argument is . + /// Thrown when the argument is . + public EmailRecipient Add(EmailAddress address, TModel model) + { + return this.Add(address, string.Empty, model); + } + /// /// Creates and add new in the . /// diff --git a/tests/Emailing.Tests/EmailRecipientCollectionTest.cs b/tests/Emailing.Tests/EmailRecipientCollectionTest.cs index 7a48f84..554038f 100644 --- a/tests/Emailing.Tests/EmailRecipientCollectionTest.cs +++ b/tests/Emailing.Tests/EmailRecipientCollectionTest.cs @@ -25,6 +25,44 @@ public void Add() var collection = new EmailRecipientCollection(); + var result = collection.Add(EmailAddress.Parse("name@domain.com"), model); + + collection.Should().Equal(result); + + result.Address.Should().Be(EmailAddress.Parse("name@domain.com")); + result.DisplayName.Should().BeEmpty(); + result.Model.Should().BeSameAs(model); + } + + [Fact] + public void Add_WithNullAddress() + { + var collection = new EmailRecipientCollection(); + + collection.Invoking(c => c.Add(null, default)) + .Should().ThrowExactly() + .WithParameterName("address"); + } + + [Fact] + public void Add_WithNullModel() + { + var address = EmailAddress.Parse("email@domain.com"); + + var collection = new EmailRecipientCollection(); + + collection.Invoking(c => c.Add(address, null)) + .Should().ThrowExactly() + .WithParameterName("model"); + } + + [Fact] + public void Add_WithDisplayName() + { + var model = new Model(); + + var collection = new EmailRecipientCollection(); + var result = collection.Add(EmailAddress.Parse("name@domain.com"), "The display name", model); collection.Should().Equal(result); @@ -35,7 +73,7 @@ public void Add() } [Fact] - public void Add_WithNullAddress() + public void Add_WithDisplayName_WithNullAddress() { var collection = new EmailRecipientCollection(); @@ -45,7 +83,7 @@ public void Add_WithNullAddress() } [Fact] - public void Add_WithNullDisplayName() + public void Add_WithDisplayName_WithNullDisplayName() { var address = EmailAddress.Parse("email@domain.com"); @@ -57,7 +95,7 @@ public void Add_WithNullDisplayName() } [Fact] - public void Add_WithNullModel() + public void Add_WithDisplayName_WithNullModel() { var address = EmailAddress.Parse("email@domain.com"); From 2e01be4f8cdb9785fb1b44213610a5a0cd6153dc Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Mon, 26 Jan 2026 14:17:04 +0100 Subject: [PATCH 12/18] Fix a bug to read the attachments in the memory to send to the provider. --- src/Emailing/EmailManager.cs | 75 ++++++++++++++++-------- tests/Emailing.Tests/EmailManagerTest.cs | 57 ++++++++++++++---- 2 files changed, 96 insertions(+), 36 deletions(-) diff --git a/src/Emailing/EmailManager.cs b/src/Emailing/EmailManager.cs index 1d5ed81..4dc8870 100644 --- a/src/Emailing/EmailManager.cs +++ b/src/Emailing/EmailManager.cs @@ -49,41 +49,70 @@ public async Task SendAsync(Email email, CancellationToken cance var senderEmailAddress = this.options.Value.SenderEmailAddress!; - foreach (var recipient in email.Recipients) + // Copy the attachments in memory. + var attachmentsContent = new List(email.Attachments.Count); + + foreach (var attachment in email.Attachments) { - // Render the subject - using var subjectOutputWriter = new StringWriter(); + var attachementStream = new MemoryStream(); + + await attachment.Content.CopyToAsync(attachementStream, cancellationToken); + + attachementStream.Position = 0; + + attachmentsContent.Add(attachementStream); + } - var textTemplateRenderContext = new TextTemplateRenderContext(this.serviceProvider); + try + { + foreach (var recipient in email.Recipients) + { + // Render the subject + using var subjectOutputWriter = new StringWriter(); - await email.Template.Subject.RenderAsync(recipient.Model, subjectOutputWriter, textTemplateRenderContext, cancellationToken); + var textTemplateRenderContext = new TextTemplateRenderContext(this.serviceProvider); - var subject = subjectOutputWriter.ToString(); + await email.Template.Subject.RenderAsync(recipient.Model, subjectOutputWriter, textTemplateRenderContext, cancellationToken); - // Render the HTML content - using var htmlContentOutputWriter = new StringWriter(); + var subject = subjectOutputWriter.ToString(); - textTemplateRenderContext = new TextTemplateRenderContext(this.serviceProvider); + // Render the HTML content + using var htmlContentOutputWriter = new StringWriter(); - await email.Template.HtmlBody.RenderAsync(recipient.Model, htmlContentOutputWriter, textTemplateRenderContext, cancellationToken); + textTemplateRenderContext = new TextTemplateRenderContext(this.serviceProvider); - var htmlContent = htmlContentOutputWriter.ToString(); + await email.Template.HtmlBody.RenderAsync(recipient.Model, htmlContentOutputWriter, textTemplateRenderContext, cancellationToken); - var message = new EmailMessage( - new EmailContact(senderEmailAddress, string.Empty), - new EmailContact(recipient.Address, recipient.DisplayName), - subject, - htmlContent) - { - Importance = email.Importance, - }; + var htmlContent = htmlContentOutputWriter.ToString(); - foreach (var attachment in email.Attachments) + var message = new EmailMessage( + new EmailContact(senderEmailAddress, string.Empty), + new EmailContact(recipient.Address, recipient.DisplayName), + subject, + htmlContent) + { + Importance = email.Importance, + }; + + for (int i = 0; i < email.Attachments.Count; i++) + { + attachmentsContent[i].Position = 0; + + var emailAttachement = email.Attachments[i]; + var newEmailAttachement = new EmailAttachment(emailAttachement.FileName, emailAttachement.ContentType, attachmentsContent[i]); + + message.Attachments.Add(newEmailAttachement); + } + + await this.provider.SendAsync(message, cancellationToken); + } + } + finally + { + foreach (var attachmentContent in attachmentsContent) { - message.Attachments.Add(attachment); + await attachmentContent.DisposeAsync(); } - - await this.provider.SendAsync(message, cancellationToken); } } diff --git a/tests/Emailing.Tests/EmailManagerTest.cs b/tests/Emailing.Tests/EmailManagerTest.cs index fd806cf..8de1ef7 100644 --- a/tests/Emailing.Tests/EmailManagerTest.cs +++ b/tests/Emailing.Tests/EmailManagerTest.cs @@ -6,6 +6,7 @@ namespace PosInformatique.Foundations.Emailing.Tests { + using System.Reflection; using Microsoft.Extensions.Options; using PosInformatique.Foundations.EmailAddresses; using PosInformatique.Foundations.MediaTypes; @@ -130,16 +131,11 @@ public async Task SendAsync() var emailAddressRecipient1 = EmailAddress.Parse("email1@domain.com"); var emailAddressRecipient2 = EmailAddress.Parse("email2@domain.com"); - var content1 = new Mock(MockBehavior.Strict); - content1.Setup(c => c.CanRead) - .Returns(true); + var content1 = new MemoryStream([1, 2]); + var content2 = new MemoryStream([3, 4]); - var content2 = new Mock(MockBehavior.Strict); - content2.Setup(c => c.CanRead) - .Returns(true); - - var attachment1 = new EmailAttachment("Attachment1", MimeTypes.Application.Pdf, content1.Object); - var attachment2 = new EmailAttachment("Attachment2", MimeTypes.Application.Docx, content2.Object); + var attachment1 = new EmailAttachment("Attachment1", MimeTypes.Application.Pdf, content1); + var attachment2 = new EmailAttachment("Attachment2", MimeTypes.Application.Docx, content2); var email = new Email(template) { @@ -161,29 +157,51 @@ public async Task SendAsync() var options = new EmailingOptions(); options.SenderEmailAddress = sender; + var contentStreams = new List(); + var provider = new Mock(MockBehavior.Strict); provider.Setup(p => p.SendAsync(It.Is(m => m.To.Email == emailAddressRecipient1), cancellationToken)) .Callback((EmailMessage m, CancellationToken _) => { - m.Attachments.Should().Equal(attachment1, attachment2); + m.Attachments[0].Content.As().Position.Should().Be(0); + m.Attachments[0].Content.As().ToArray().Should().Equal(1, 2); + m.Attachments[0].ContentType.Should().Be(MimeTypes.Application.Pdf); + m.Attachments[0].FileName.Should().Be("Attachment1"); + m.Attachments[1].Content.As().Position.Should().Be(0); + m.Attachments[1].Content.As().ToArray().Should().Equal(3, 4); + m.Attachments[1].ContentType.Should().Be(MimeTypes.Application.Docx); + m.Attachments[1].FileName.Should().Be("Attachment2"); m.From.Email.Should().BeSameAs(sender); m.From.DisplayName.Should().BeEmpty(); m.Importance.Should().Be(EmailImportance.High); m.Subject.Should().Be("Subject 1"); m.HtmlContent.Should().Be("HTML Content 1"); m.To.DisplayName.Should().Be("The display name 1"); + + contentStreams.Add(m.Attachments[0].Content); + contentStreams.Add(m.Attachments[1].Content); }) .Returns(Task.CompletedTask); provider.Setup(p => p.SendAsync(It.Is(m => m.To.Email == emailAddressRecipient2), cancellationToken)) .Callback((EmailMessage m, CancellationToken _) => { - m.Attachments.Should().Equal(attachment1, attachment2); + m.Attachments[0].Content.As().Position.Should().Be(0); + m.Attachments[0].Content.As().ToArray().Should().Equal(1, 2); + m.Attachments[0].ContentType.Should().Be(MimeTypes.Application.Pdf); + m.Attachments[0].FileName.Should().Be("Attachment1"); + m.Attachments[1].Content.As().Position.Should().Be(0); + m.Attachments[1].Content.As().ToArray().Should().Equal(3, 4); + m.Attachments[1].ContentType.Should().Be(MimeTypes.Application.Docx); + m.Attachments[1].FileName.Should().Be("Attachment2"); m.From.Email.Should().BeSameAs(sender); m.From.DisplayName.Should().BeEmpty(); m.Importance.Should().Be(EmailImportance.High); m.Subject.Should().Be("Subject 2"); m.HtmlContent.Should().Be("HTML Content 2"); m.To.DisplayName.Should().Be("The display name 2"); + + contentStreams.Add(m.Attachments[0].Content); + contentStreams.Add(m.Attachments[1].Content); }) .Returns(Task.CompletedTask); @@ -191,8 +209,14 @@ public async Task SendAsync() await manager.SendAsync(email, cancellationToken); - content1.VerifyAll(); - content2.VerifyAll(); + foreach (var contentStream in contentStreams) + { + IsOpen(contentStream).Should().BeFalse(); + } + + IsOpen(email.Attachments[0].Content).Should().BeTrue(); + IsOpen(email.Attachments[1].Content).Should().BeTrue(); + htmlBody.VerifyAll(); provider.VerifyAll(); subject.VerifyAll(); @@ -211,6 +235,13 @@ await manager.Invoking(m => m.SendAsync(null, default)) .WithParameterName("email"); } + private static bool IsOpen(Stream stream) + { + var fieldIsOpen = typeof(MemoryStream).GetField("_isOpen", BindingFlags.NonPublic | BindingFlags.Instance); + + return (bool)fieldIsOpen.GetValue(stream); + } + internal sealed class Model { } From 7a8ea4eae23eaaf5c4226fbd3f4a3559a6539257 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Mon, 26 Jan 2026 14:32:03 +0100 Subject: [PATCH 13/18] Fix documentation. --- CHANGELOG.md | 1 + src/Emailing/CHANGELOG.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 432036f..43129fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ #### PosInformatique.Foundations.Emailing - Add the support to send emails with attachments. +- Add a new overload method `EmailRecipientCollection.Add(EmailAddress, TModel)`. #### PosInformatique.Foundations.Emailing.Azure - Add the support to send emails with attachments. diff --git a/src/Emailing/CHANGELOG.md b/src/Emailing/CHANGELOG.md index 0fb6d28..70485ef 100644 --- a/src/Emailing/CHANGELOG.md +++ b/src/Emailing/CHANGELOG.md @@ -1,5 +1,6 @@ 1.1.0 - Add the support to send emails with attachments. + - Add a new overload method `EmailRecipientCollection.Add(EmailAddress, TModel)`. 1.0.0 - Initial release with Emailing infrastructure. From 813bea8a82bf32ae577cb69df38aff2c4c7f3da3 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 27 Mar 2026 11:06:29 +0100 Subject: [PATCH 14/18] Upgrade to .NET 10.0 --- CHANGELOG.md | 2 ++ Directory.Packages.props | 9 +++++---- global.json | 2 +- src/Directory.Build.props | 2 +- tests/Directory.Build.props | 2 +- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43129fa..f3679a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### 1.1.0 +- Add the support of .NET 10.0 + #### PosInformatique.Foundations.Emailing - Add the support to send emails with attachments. - Add a new overload method `EmailRecipientCollection.Add(EmailAddress, TModel)`. diff --git a/Directory.Packages.props b/Directory.Packages.props index 6415f3a..ef6856b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,10 +4,11 @@ 8.0.0 9.0.0 + 10.0.0 - + @@ -23,12 +24,12 @@ - + - + - + \ No newline at end of file diff --git a/global.json b/global.json index e80aa8b..8912ad6 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.308", + "version": "10.0.201", "rollForward": "latestFeature" } } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index a6e1a64..1f93a14 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -4,7 +4,7 @@ - net8.0;net9.0 + net8.0;net9.0;net10.0 enable true diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 94f80d6..cf532c5 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -24,7 +24,7 @@ - net8.0;net9.0 + net8.0;net9.0;net10.0 $(SolutionDir)\CodeCoverage.runsettings From 433098402cf7be5e568777c3bc0984a20105de95 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 27 Mar 2026 11:18:06 +0100 Subject: [PATCH 15/18] Upgrade Scriban to the version 7.0.0 --- CHANGELOG.md | 5 ++++- Directory.Packages.props | 2 +- src/Text.Templating.Scriban/CHANGELOG.md | 9 ++++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3679a3..8b4e935 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### 1.1.0 -- Add the support of .NET 10.0 +- Add the support of .NET 10.0 for all the packages. #### PosInformatique.Foundations.Emailing - Add the support to send emails with attachments. @@ -16,6 +16,9 @@ #### PosInformatique.Foundations.Emailing.Graph - Add the support to send emails with attachments. +#### PosInformatique.Foundations.Text.Templating.Scriban +- Upgrade the Scriban dependency to version 7.0.0 to fix security vulnerabilities. + ### 1.0.0 - Initial version of the following packages: - [PosInformatique.Foundations.EmailAddresses](./src/EmailAddresses/README.md) diff --git a/Directory.Packages.props b/Directory.Packages.props index ef6856b..9c73f34 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,7 +26,7 @@ - + diff --git a/src/Text.Templating.Scriban/CHANGELOG.md b/src/Text.Templating.Scriban/CHANGELOG.md index 11422a3..4da8a06 100644 --- a/src/Text.Templating.Scriban/CHANGELOG.md +++ b/src/Text.Templating.Scriban/CHANGELOG.md @@ -1,2 +1,9 @@ -1.0.0 +1.1.0 + - Upgrade the Scriban dependency to version 7.0.0 to fix security vulnerabilities. + - [Scriban Affected by Memory Exhaustion (OOM) via Unbounded String Generation (Denial of Service)](https://github.com/advisories/GHSA-5rpf-x9jg-8j5p) + - [Scriban: Sandbox escape due to TypedObjectAccessorcache bypassing MemberFilter after TemplateContext reuse](https://github.com/advisories/GHSA-5wr9-m6jw-xx44) + - [Scriban: Denial of Service via Unbounded Cumulative Template Output Bypassing LimitToString](https://github.com/advisories/GHSA-m2p3-hwv5-xpqw) + - [Scriban has Multiple Denial-of-Service Vectors via Unbounded Resource Consumption During Expression Evaluation](https://github.com/advisories/GHSA-xw6w-9jjh-p9cr) + +1.0.0 - Initial release with the Scriban Text Templating feature. From bf1fe371f5f56676c305cb518607e88f68295f90 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 27 Mar 2026 11:19:46 +0100 Subject: [PATCH 16/18] Disable the warnings EnableGenerateDocumentationFile for the tests projects. --- tests/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index cf532c5..18bf08c 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -33,7 +33,7 @@ false - $(NoWarn);SA0001 + $(NoWarn);SA0001;EnableGenerateDocumentationFile From 33951826084818e870cb75033dd4685395e802f3 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 27 Mar 2026 11:21:39 +0100 Subject: [PATCH 17/18] Upgrades the NuGet packages related to the unit tests. --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 9c73f34..029354f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -22,12 +22,12 @@ - + - + From c23c69e090b783688a5fcd2aa3d0216531d7cfb6 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 27 Mar 2026 11:59:31 +0100 Subject: [PATCH 18/18] Upgrades the NuGet package Microsoft.Extensions.Azure to the version 1.13.1. --- CHANGELOG.md | 3 ++- Directory.Packages.props | 8 +++++--- README.md | 6 +++++- src/Emailing.Azure/CHANGELOG.md | 3 +++ src/Text.Templating.Scriban/CHANGELOG.md | 2 +- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b4e935..4a4a215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,13 @@ #### PosInformatique.Foundations.Emailing.Azure - Add the support to send emails with attachments. +- Upgrade the [Microsoft.Extensions.Azure](https://www.nuget.org/packages/Microsoft.Extensions.Azure) dependency to version 1.13.1 to fix security vulnerabilities. #### PosInformatique.Foundations.Emailing.Graph - Add the support to send emails with attachments. #### PosInformatique.Foundations.Text.Templating.Scriban -- Upgrade the Scriban dependency to version 7.0.0 to fix security vulnerabilities. +- Upgrade the [Scriban](https://www.nuget.org/packages/Scriban) dependency to version 7.0.0 to fix security vulnerabilities. ### 1.0.0 - Initial version of the following packages: diff --git a/Directory.Packages.props b/Directory.Packages.props index 029354f..9003ee3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,9 +14,11 @@ - - - + + + + + diff --git a/README.md b/README.md index 83102ca..75bdd9e 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ All [PosInformatique.Foundations](https://github.com/PosInformatique/PosInformat To maximize backward compatibility with existing projects, dependencies on external libraries (such as `Microsoft.Graph`, etc.) intentionally target **relatively old versions**. This avoids forcing you to update your entire solution to the -latest versions used internally by PosInformatique.Foundations. +latest versions used internally by [PosInformatique.Foundations](https://github.com/PosInformatique/PosInformatique.Foundations). > Important: It is the responsibility of the application developer to explicitly reference and update any **transitive dependencies** in their own project if they want to use newer versions. @@ -98,6 +98,10 @@ while still consuming [PosInformatique.Foundations.Emailing.Graph](https://www.nuget.org/packages/[PosInformatique.Foundations.Emailing.Graph/). This is **recommended**, especially to benefit from the latest security updates and bug fixes of the underlying dependencies. +> The next versions of [PosInformatique.Foundations](https://github.com/PosInformatique/PosInformatique.Foundations) packages +> will updated their dependencies to newer versions, if there is security vulnerabilities reported by NuGet or GitHub advisories. +> This to force also developers to avoid using vulnerable versions of the dependencies when upgrading to the new versions of the packages. + ## 📄 License Licensed under the [MIT License](./LICENSE). diff --git a/src/Emailing.Azure/CHANGELOG.md b/src/Emailing.Azure/CHANGELOG.md index 135e53e..8bdeff2 100644 --- a/src/Emailing.Azure/CHANGELOG.md +++ b/src/Emailing.Azure/CHANGELOG.md @@ -1,5 +1,8 @@ 1.1.0 - Add the support to send emails with attachments using Azure Communication Service Emailing provider. + - Upgrade the [Microsoft.Extensions.Azure](https://www.nuget.org/packages/Microsoft.Extensions.Azure) dependency to version 1.13.1 to fix security vulnerabilities. + - [Azure Identity Libraries and Microsoft Authentication Library Elevation of Privilege Vulnerability](https://github.com/advisories/GHSA-m5vv-6r4h-3vj9) + - [Azure Identity Library for .NET Information Disclosure Vulnerability](https://github.com/advisories/GHSA-wvxc-855f-jvrv) 1.0.0 - Initial release with Azure Communication Service Emailing provider. diff --git a/src/Text.Templating.Scriban/CHANGELOG.md b/src/Text.Templating.Scriban/CHANGELOG.md index 4da8a06..60151b9 100644 --- a/src/Text.Templating.Scriban/CHANGELOG.md +++ b/src/Text.Templating.Scriban/CHANGELOG.md @@ -1,5 +1,5 @@ 1.1.0 - - Upgrade the Scriban dependency to version 7.0.0 to fix security vulnerabilities. + - Upgrade the [Scriban](https://www.nuget.org/packages/Scriban) dependency to version 7.0.0 to fix security vulnerabilities. - [Scriban Affected by Memory Exhaustion (OOM) via Unbounded String Generation (Denial of Service)](https://github.com/advisories/GHSA-5rpf-x9jg-8j5p) - [Scriban: Sandbox escape due to TypedObjectAccessorcache bypassing MemberFilter after TemplateContext reuse](https://github.com/advisories/GHSA-5wr9-m6jw-xx44) - [Scriban: Denial of Service via Unbounded Cumulative Template Output Bypassing LimitToString](https://github.com/advisories/GHSA-m2p3-hwv5-xpqw)